OSM Buildings

A 3D building viewer of OSM (OpenStreetMap) buildings map data.

Overview

This application demonstrates how to create 3D building geometry for display on a map using data from OpenStreetMap (OSM) servers or a locally limited data set when the server is unavailable.

Queue handling

The application uses a queue to handle concurrent requests to boost up the loading process of maps and building data.

 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);
Fetching and parsing data

A custom request handler class is implemented for fetching the data from the OSM building and map servers.

 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(fileName);
         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::number(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(fileName);
     if ( file.size() > 0) {
         QFile file(fileName);
         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::number(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;
     } );

The application parses the online data to convert it to a QVariant list of keys and values in geo formats such as QGeoPolygon.

             emit buildingsDataReady( importGeoJson(QJsonDocument::fromJson( data )), tile.TileX, tile.TileY, tile.ZoomLevel );
             --m_buildingsNumberOfRequestsInFlight;

The parsed building data is sent to a custom geometry item to convert the geo coordinates to 3D coordinates.

     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 );
     };

The required data for the index and vertex buffers, such as position, normals, tangents, and UV coordinates, is generated.

     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();

The downloaded PNG data is sent to a custom QQuick3DTextureData item to convert the PNG format to a texture for map tiles.

 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);
 }

The application uses camera position, orientation, zoom level, and tilt to find the nearest tiles in the view.

 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 );

Generates the tiles request queue.

 void OSMManager::addBuildingRequestToQueue(QQueue<OSMTileData> &queue, int tileX, int tileY, int zoomLevel)
 {
     OSMTileData data{tileX, tileY, zoomLevel};
Controls

When you run the application, use the following controls for navigation.

WindowsAndroid
PanLeft mouse button + dragDrag
ZoomMouse wheelPinch
RotateRight mouse button + dragn/a
         OSMCameraController {
             id: cameraController
             origin: originNode
             camera: cameraNode
         }
Rendering

Every chunk of the map tile consists of a QML model (the 3D geometry) and a custom material which uses a rectangle as a base to render the tilemap texture.

         ...
         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"
                     }
                 ]
             }

The application uses custom geometry to render tile buildings.

         ...
         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"
                     }
                 ]
             }

To render building parts such as rooftops with one draw call, a custom shader is used.

 // 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;
 }

Running the Example

To run the example from Qt Creator, open the Welcome mode and select the example from Examples. For more information, see Qt Creator: Tutorial: Build and run.

Example project @ code.qt.io

See also QML Applications.