OSM 建筑

OSM(OpenStreetMap)建筑物地图数据的三维建筑物查看器。

概述

本应用程序演示了如何使用 OpenStreetMap (OSM) 服务器的数据或服务器不可用时本地有限的数据集创建三维建筑几何图形,以便在地图上显示。

队列处理

该应用程序使用队列来处理并发请求,以加快地图和建筑数据的加载过程。

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);
获取和解析数据

为从 OSM 建筑和地图服务器获取数据,程序实现了一个自定义请求处理程序类。

voidOSMRequest::getBuildingsData(constQQueue<OSMTileData> &buildingsQueue) {if( buildingsQueue.isEmpty() )return; m_buildingsQueue=buildingsQueue;if(!m_queuesTimer.isActive() ) m_queuesTimer.start(); }voidOSMRequest::getBuildingsDataRequest(constOSMTileData&tile) {constQStringfileName= "data/"_L1+tileKey(tile)+ ".json"_L1;    QFileInfofile(fileName);if( file.size()> 0){ QFilefile(fileName);if( file.open(QFile::ReadOnly)){            QByteArraydata=file.readAll(); file.close();emitbuildingsDataReady( importGeoJson(QJsonDocument::fromJson( data )),tile.TileX,tile.TileY,tile.ZoomLevel );--m_buildingsNumberOfRequestsInFlight;return; } } QUrlurl=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, &QNetworkReplyif( reply->error() ==QNetworkReply::NoError ) { QByteArraydata=  reply->readAll();emitbuildingsDataReady( importGeoJson(QJsonDocument::fromJson( data )),tile.TileX,tile.TileY,tile.ZoomLevel ); }else{constQByteArraymessage=  reply->readAll();staticQByteArraylastMessage;if(message!=lastMessage) { lastMessage=message;                qWarning().noquote() << "OSMRequest::getBuildingsData " << reply->error()
                                    <<  reply->url()<<message; } }--m_buildingsNumberOfRequestsInFlight; } });voidOSMRequest::getMapsData(constQQueue<OSMTileData> &mapsQueue) {if( mapsQueue.isEmpty() )return; m_mapsQueue=mapsQueue;if(!m_queuesTimer.isActive() ) m_queuesTimer.start(); }voidOSMRequest::getMapsDataRequest(constOSMTileData&tile) {constQStringfileName= "data/"_L1+tileKey(tile)+ ".png"_L1;    QFileInfofile(fileName);if( file.size()> 0) { QFilefile(fileName);if( file.open(QFile::ReadOnly)){            QByteArraydata=file.readAll(); file.close();emitmapsDataReady( data,tile.TileX,tile.TileY,tile.ZoomLevel );--m_mapsNumberOfRequestsInFlight;return; } } QUrlurl=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 ) { QByteArraydata=  reply->readAll();emitmapsDataReady( data,tile.TileX,tile.TileY,tile.ZoomLevel ); }else{constQByteArraymessage=reply->readAll();staticQByteArraylastMessage;if(message!=lastMessage) { lastMessage=message;                qWarning().noquote() << "OSMRequest::getMapsDataRequest" << reply->error()
                                    <<  reply->url()<<message; } }--m_mapsNumberOfRequestsInFlight; } });

应用程序解析在线数据,将其转换为QVariant 地理格式的键和值列表,如QGeoPolygon

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

解析后的建筑数据将发送到自定义几何项,以便将地理坐标转换为三维坐标。

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

索引和顶点缓冲区所需的数据(如位置、法线、切线和 UV 坐标)也会生成。

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

下载的 PNG 数据会被发送到一个自定义的QQuick3DTextureData 项目,将 PNG 格式转换为贴图的纹理。

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

应用程序使用相机位置、方向、缩放级别和倾斜度来查找视图中最近的地块。

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

生成磁贴请求队列。

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

运行应用程序时,可使用以下控件进行导航。

视窗安卓
平移鼠标左键 + 拖动拖动
缩放鼠标滚轮
旋转鼠标右键 + 拖动不适用
        OSMCameraController {
            id: cameraController
            origin: originNode
            camera: cameraNode
        }
渲染

每块瓦片贴图都由一个 QML 模型(三维几何体)和一个自定义材质组成,自定义材质使用矩形作为渲染瓦片贴图纹理的基底。

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

应用程序使用自定义几何体来渲染瓦片建筑。

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

要通过一次绘制调用渲染屋顶等建筑部件,需要使用自定义着色器。

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

运行示例

要运行来自 Qt Creator,打开Welcome 模式,然后从Examples 中选择示例。更多信息,请参见Qt Creator: 教程:构建并运行

示例项目 @ code.qt.io

另请参阅 QML 应用程序

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