OSM-Gebäude
Ein 3D-Gebäudebetrachter für OSM (OpenStreetMap) Gebäudekartendaten.
Übersicht
Diese Anwendung demonstriert, wie 3D-Gebäudegeometrie für die Anzeige auf einer Karte mit Daten von OpenStreetMap (OSM)-Servern oder einem lokal begrenzten Datensatz erstellt werden kann, wenn der Server nicht verfügbar ist.
Behandlung von Warteschlangen
Die Anwendung verwendet eine Warteschlange zur Verarbeitung gleichzeitiger Anfragen, um den Ladeprozess von Karten und Gebäudedaten zu beschleunigen.
OSMRequest::OSMRequest(QObject *parent) : QObject{parent} { connect( &m_queuesTimer, &QTimer::timeout, this, [this](){ if ( m_buildingsQueue.isEmpty() && m_mapsQueue.isEmpty() ) { m_queuesTimer.stop(); } else { #ifdef QT_DEBUG const int numConcurrentRequests = 1; #else const int numConcurrentRequests = 6; #endif if ( !m_buildingsQueue.isEmpty() && m_buildingsNumberOfRequestsInFlight < numConcurrentRequests ) { getBuildingsDataRequest(m_buildingsQueue.dequeue()); ++m_buildingsNumberOfRequestsInFlight; } if ( !m_mapsQueue.isEmpty() && m_mapsNumberOfRequestsInFlight < numConcurrentRequests ) { getMapsDataRequest(m_mapsQueue.dequeue()); ++m_mapsNumberOfRequestsInFlight; } } }); m_queuesTimer.setInterval(0);
Abrufen und Parsen von Daten
Für den Abruf der Daten von den OSM-Gebäude- und Kartenservern ist eine eigene Request-Handler-Klasse implementiert.
void OSMRequest::getBuildingsData(const QQueue<OSMTileData> &buildingsQueue) { if ( buildingsQueue.isEmpty() ) return; m_buildingsQueue = buildingsQueue; if ( !m_queuesTimer.isActive() ) m_queuesTimer.start(); }void OSMRequest::getBuildingsDataRequest(const OSMTileData &tile) { const QString fileName = "data/"_L1 + tileKey(tile) + ".json"_L1; QFileInfo file(fileName); if ( file.size() > 0 ) { QFile file(Dateiname); if (file.open(QFile::ReadOnly)){ QByteArray data = file.readAll(); file.close(); emit buildingsDataReady( importGeoJson(QJsonDocument::fromJson( data )), tile.TileX, tile.TileY, tile.ZoomLevel ); --m_buildingsNumberOfRequestsInFlight; return; } QUrl url = QUrl(QString(URL_OSMB_JSON).arg(QString::number(tile.ZoomLevel), QString::number(tile.TileX), QString::Zahl(tile.TileY),m_token)); QNetworkReply * reply = m_networkAccessManager.get( QNetworkRequest(url)); connect( reply, &QNetworkReply::finished, this, [this, reply, tile](){ reply->deleteLater(); if ( reply->error() == QNetworkReply::NoError ) { QByteArray data = reply->readAll(); emit buildingsDataReady( importGeoJson(QJsonDocument::fromJson( data )), tile.TileX, tile.TileY, tile.ZoomLevel ); } else { const QByteArray message = reply->readAll(); static QByteArray lastMessage; if (message != lastMessage) { lastMessage = message; qWarning().noquote() << "OSMRequest::getBuildingsData " << reply->error() << reply->url()<< message; } } --m_buildingsNumberOfRequestsInFlight; } );void OSMRequest::getMapsData(const QQueue<OSMTileData> &mapsQueue) { if ( mapsQueue.isEmpty() ) return; m_mapsQueue = mapsQueue; if ( !m_queuesTimer.isActive() ) m_queuesTimer.start(); }void OSMRequest::getMapsDataRequest(const OSMTileData &tile) { const QString fileName = "data/"_L1 + tileKey(tile) + ".png"_L1; QFileInfo file(Dateiname); if ( file.size() > 0) { QFile file(Dateiname); if (file.open(QFile::ReadOnly)){ QByteArray data = file.readAll(); file.close(); emit mapsDataReady( data, tile.TileX, tile.TileY, tile.ZoomLevel ); --m_mapsNumberOfRequestsInFlight; return; } QUrl url = QUrl(QString(URL_OSMB_MAP).arg(QString::number(tile.ZoomLevel), QString::number(tile.TileX), QString::Zahl(tile.TileY))); QNetworkReply * reply = m_networkAccessManager.get( QNetworkRequest(url)); connect( reply, &QNetworkReply::finished, this, [this, reply, tile](){ reply->deleteLater(); if ( reply->error() == QNetworkReply::NoError ) { QByteArray data = reply->readAll(); emit mapsDataReady( data, tile.TileX, tile.TileY, tile.ZoomLevel ); } else { const QByteArray message = reply->readAll(); static QByteArray lastMessage; if (message != lastMessage) { lastMessage = message; qWarning().noquote() << "OSMRequest::getMapsDataRequest" << reply->error() << reply->url()<< message; } } --m_mapsNumberOfRequestsInFlight; } );
Die Anwendung parst die Online-Daten, um sie in eine QVariant Liste von Schlüsseln und Werten in Geoformaten wie QGeoPolygon umzuwandeln.
emit buildingsDataReady( importGeoJson(QJsonDocument::fromJson( data )), tile.TileX, tile.TileY, tile.ZoomLevel ); --m_buildingsNumberOfRequestsInFlight;
Die geparsten Gebäudedaten werden an ein benutzerdefiniertes Geometrieelement gesendet, um die Geokoordinaten in 3D-Koordinaten umzuwandeln.
constexpr auto convertGeoCoordToVertexPosition = [](const float lat, const float lon) -> QVector3D { const double scale = 1.212; const double geoToPositionScale = 1000000 * scale; const double XOffsetFromCenter = 537277 * scale; const double YOffsetFromCenter = 327957 * scale; double x = (lon/360.0 + 0.5) * geoToPositionScale; double y = (1.0-log(qTan(qDegreesToRadians(lat)) + 1.0 / qCos(qDegreesToRadians(lat))) / M_PI) * 0.5 * geoToPositionScale; return QVector3D( x - XOffsetFromCenter, YOffsetFromCenter - y, 0.0 ); };
Die erforderlichen Daten für die Index- und Eckpunktpuffer, wie Position, Normalen, Tangenten und UV-Koordinaten, werden generiert.
for ( const QVariant &baseData : geoVariantsList ) { for ( const QVariant &dataValue : baseData.toMap()["data"_L1].toList() ) { const auto featureMap = dataValue.toMap(); const auto properties = featureMap["properties"_L1].toMap(); const auto buildingCoords = featureMap["data"_L1].value<QGeoPolygon>().perimeter(); float height = 0.15 * properties["height"_L1].toLongLong(); float levels = static_cast<float>(properties["levels"_L1].toLongLong()); QColor color = QColor::fromString( properties["color"_L1].toString()); if ( !color.isValid() || color == QColor(Qt::GlobalColor::black)) color = QColor(Qt::GlobalColor::white); QColor roofColor = QColor::fromString( properties["roofColor"_L1].toString()); if ( !roofColor.isValid() || roofColor == QColor(Qt::GlobalColor::black) ) roofColor = color; QVector3D subsetMinBound = QVector3D(maxFloat, maxFloat, maxFloat); QVector3D subsetMaxBound = QVector3D(minFloat, minFloat, minFloat); qsizetype numSubsetVertices = buildingCoords.size() * 2; qsizetype lastVertexDataCount = vertexData.size(); qsizetype lastIndexDataCount = indexData.size(); vertexData.resize( lastVertexDataCount + numSubsetVertices * strideVertex ); indexData.resize( lastIndexDataCount + ( numSubsetVertices - 2 ) * stridePrimitive ); float *vbPtr = &reinterpret_cast<float *>(vertexData.data())[globalVertexCounter * strideVertexLen]; uint32_t *ibPtr = &reinterpret_cast<uint32_t *>(indexData.data())[globalPrimitiveCounter * 3]; qsizetype subsetVertexCounter = 0; QVector3D lastBaseVertexPos; QVector3D lastExtrudedVertexPos; QVector3D currentBaseVertexPos; QVector3D currentExtrudedVertexPos; QVector3D subsetPolygonCenter; using PolygonVertex = std::array<double, 2>; using PolygonVertices = std::vector<PolygonVertex>; PolygonVertices roofPolygonVertices; for ( const QGeoCoordinate &buildingPoint : buildingCoords ) { ... std::vector<PolygonVertices> roofPolygonsVertices; roofPolygonsVertices.push_back( roofPolygonVertices ); std::vector<uint32_t> roofIndices = mapbox::earcut<uint32_t>(roofPolygonsVertices); lastVertexDataCount = vertexData.size(); lastIndexDataCount = indexData.size(); vertexData.resize( lastVertexDataCount + roofPolygonVertices.size() * strideVertex ); indexData.resize( lastIndexDataCount + roofIndices.size() * sizeof(uint32_t) ); vbPtr = &reinterpret_cast<float *>(vertexData.data())[globalVertexCounter * strideVertexLen]; ibPtr = &reinterpret_cast<uint32_t *>(indexData.data())[globalPrimitiveCounter * 3]; for ( const uint32_t &roofIndex : roofIndices ) { *ibPtr++ = roofIndex + globalVertexCounter; } qsizetype roofPrimitiveCount = roofIndices.size() / 3; globalPrimitiveCounter += roofPrimitiveCount; for ( const PolygonVertex &polygonVertex : roofPolygonVertices ) { //position *vbPtr++ = polygonVertex.at(0); *vbPtr++ = polygonVertex.at(1); *vbPtr++ = height; //normal *vbPtr++ = 0.0; *vbPtr++ = 0.0; *vbPtr++ = 1.0; //tangent *vbPtr++ = 1.0; *vbPtr++ = 0.0; *vbPtr++ = 0.0; //binormal *vbPtr++ = 0.0; *vbPtr++ = 1.0; *vbPtr++ = 0.0; //color/ *vbPtr++ = roofColor.redF(); *vbPtr++ = roofColor.greenF(); *vbPtr++ = roofColor.blueF(); *vbPtr++ = 1.0; //texcoord *vbPtr++ = 1.0; *vbPtr++ = 1.0; *vbPtr++ = 0.0; *vbPtr++ = 1.0; ++subsetVertexCounter; ++globalVertexCounter; } } } } } clear();
Die heruntergeladenen PNG-Daten werden an ein benutzerdefiniertes Element QQuick3DTextureData gesendet, um das PNG-Format in eine Textur für Kartenkacheln zu konvertieren.
void CustomTextureData::setImageData(const QByteArray &data) { QImage image = QImage::fromData(data).convertToFormat(QImage::Format_RGBA8888); setTextureData( QByteArray(reinterpret_cast<const char*>(image.constBits()), image.sizeInBytes()) ); setSize( image.size() ); setHasTransparency(false); setFormat(Format::RGBA8); }
Die Anwendung verwendet Kameraposition, Ausrichtung, Zoomstufe und Neigung, um die nächstgelegenen Kacheln in der Ansicht zu finden.
void OSMManager::setCameraProperties(const QVector3D &position, const QVector3D &right, float cameraZoom, float minimumZoom, float maximumZoom, float cameraTilt, float minimumTilt, float maximumTilt) { float tiltFactor = (cameraTilt - minimumTilt) / qMax(maximumTilt - minimumTilt, 1.0); float zoomFactor = (cameraZoom - minimumZoom) / qMax(maximumZoom - minimumZoom, 1.0); // Forward vector align to the XY plane QVector3D forwardVector = QVector3D::crossProduct(right, QVector3D(0.0, 0.0, -1.0)).normalized(); QVector3D projectionOfForwardOnXY = position + forwardVector * tiltFactor * zoomFactor * 50.0; QQueue<OSMTileData> queue; for ( int forwardIndex = -20; forwardIndex <= 20; ++forwardIndex ){ for ( int sidewardIndex = -20; sidewardIndex <= 20; ++sidewardIndex ){ QVector3D transferredPosition = projectionOfForwardOnXY + QVector3D(float(m_tileSizeX * sidewardIndex), float(m_tileSizeY * forwardIndex), 0.0); addBuildingRequestToQueue(queue, m_startBuildingTileX + int(transferredPosition.x() / m_tileSizeX), m_startBuildingTileY - int(transferredPosition.y() / m_tileSizeY)); } } const QPoint projectedTile{m_startBuildingTileX + int(projectionOfForwardOnXY.x() / m_tileSizeX), m_startBuildingTileY - int(projectionOfForwardOnXY.y() / m_tileSizeY)}; auto closer = [projectedTile](const OSMTileData &v1, const OSMTileData &v2) -> bool { return v1.distanceTo(projectedTile) < v2.distanceTo(projectedTile); }; std::sort(queue.begin(), queue.end(), closer); m_request->getBuildingsData( queue ); m_request->getMapsData( queue );
Erzeugt die Warteschlange für Kachelanfragen.
void OSMManager::addBuildingRequestToQueue(QQueue<OSMTileData> &queue, int tileX, int tileY, int zoomLevel) { OSMTileData data{tileX, tileY, zoomLevel};
Steuert
Wenn Sie die Anwendung ausführen, verwenden Sie die folgenden Steuerelemente für die Navigation.
Windows | Android | |
---|---|---|
Schwenken | Linke Maustaste + Ziehen | Ziehen |
Zoomen | Mausrad | Kneifen |
Drehen | Rechte Maustaste + Ziehen | k.A. |
OSMCameraController { id: cameraController origin: originNode camera: cameraNode }
Rendering
Jedes Stück der Kartenkachel besteht aus einem QML-Modell (der 3D-Geometrie) und einem benutzerdefinierten Material, das ein Rechteck als Basis für die Textur der Kachelkarte verwendet.
... id: chunkModelMap Node { property variant mapData: null property int tileX: 0 property int tileY: 0 property int zoomLevel: 0 Model { id: basePlane position: Qt.vector3d( osmManager.tileSizeX * tileX, osmManager.tileSizeY * -tileY, 0.0 ) scale: Qt.vector3d( osmManager.tileSizeX / 100., osmManager.tileSizeY / 100., 0.5) source: "#Rectangle" materials: [ CustomMaterial { property TextureInput tileTexture: TextureInput { enabled: true texture: Texture { textureData: CustomTextureData { Component.onCompleted: setImageData( mapData ) } } } shadingMode: CustomMaterial.Shaded cullMode: Material.BackFaceCulling fragmentShader: "customshadertiles.frag" } ] }
Die Anwendung verwendet benutzerdefinierte Geometrie zum Rendern von Kachelgebäuden.
... id: chunkModelBuilding Node { property variant geoVariantsList: null property int tileX: 0 property int tileY: 0 property int zoomLevel: 0 Model { id: model scale: Qt.vector3d(1, 1, 1) OSMGeometry { id: osmGeometry Component.onCompleted: updateData( geoVariantsList ) onGeometryReady:{ model.geometry = osmGeometry } } materials: [ CustomMaterial { shadingMode: CustomMaterial.Shaded cullMode: Material.BackFaceCulling vertexShader: "customshaderbuildings.vert" fragmentShader: "customshaderbuildings.frag" } ] }
Um Gebäudeteile wie z. B. Dächer mit einem einzigen Zeichenaufruf zu rendern, wird ein benutzerdefinierter Shader verwendet.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause VARYING vec4 color; float rectangle(vec2 samplePosition, vec2 halfSize) { vec2 componentWiseEdgeDistance = abs(samplePosition) - halfSize; float outsideDistance = length(max(componentWiseEdgeDistance, 0.0)); float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0.0); return outsideDistance + insideDistance; } void MAIN() { vec2 tc = UV0; vec2 uv = fract(tc * UV1.x); //UV1.x number of levels uv = uv * 2.0 - 1.0; uv.x = 0.0; uv.y = smoothstep(0.0, 0.2, rectangle( vec2(uv.x, uv.y + 0.5), vec2(0.2)) ); BASE_COLOR = vec4(color.xyz * mix( clamp( ( vec3( 0.4, 0.4, 0.4 ) + tc.y) * ( vec3( 0.6, 0.6, 0.6 ) + uv.y) , 0.0, 1.0), vec3(1.0), UV1.y ), 1.0); // UV1.y as is roofTop ROUGHNESS = 0.3; METALNESS = 0.0; FRESNEL_POWER = 1.0; }
Ausführen des Beispiels
Zum Ausführen des Beispiels von Qt Creatorauszuführen, öffnen Sie den Modus Welcome und wählen Sie das Beispiel unter Examples aus. Weitere Informationen finden Sie unter Erstellen und Ausführen eines Beispiels.
Siehe auch QML-Anwendungen.
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.