OSM Buildings | Qt 6.11
Description: A 3D building viewer of OSM (OpenStreetMap) buildings map data.

- Qt 6.11
- OSM Buildings
- Qt 6.11.1 Reference Documentation

# 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.

| Windows | Android
--- | --- | ---
Pan | Left mouse button + drag | Drag
Zoom | Mouse wheel | Pinch
Rotate | Right mouse button + drag | n/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 {
                 id: 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 * node.tileX, osmManager.tileSizeY * -node.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( node.mapData )
                                     } }
                             }
                             shadingMode: CustomMaterial.Shaded
                             cullMode: Material.BackFaceCulling
                             fragmentShader: "customshadertiles.frag"
                         }
                     ]
                 }

The application uses custom geometry to render tile buildings.

             ...
             id: chunkModelBuilding
             Node {
                 id: 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( node.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.