本页内容

OSM 建筑物

一个用于查看 OSM(OpenStreetMap)建筑地图数据的 3D 建筑查看器。

地图上渲染的3D OpenStreetMap建筑

概述

本应用程序演示了如何利用 OpenStreetMap(OSM)服务器上的数据,或在服务器不可用时使用本地有限数据集,来创建用于地图显示的 3D 建筑几何模型。

队列处理

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

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)
{
    constQString 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();
            emitbuildingsDataReady( 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();
            emitbuildingsDataReady( importGeoJson(QJsonDocument::fromJson( data )),tile.TileX,tile.TileY,tile.ZoomLevel );
        }else{
            constQByteArray message= reply->readAll();
            staticQByteArray lastMessage;
            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)
{
    constQString 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();
            emitmapsDataReady(  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();
            emitmapsDataReady( data,tile.TileX,tile.TileY,tile.ZoomLevel );
        }else{
            constQByteArray message= reply->readAll();
            staticQByteArray lastMessage;
            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 ) {
                        QVector3D position{float(polygonVertex.at(0)),
                                           float(polygonVertex.at(1)), height};
                        QVector3D normal{0.0F, 0.0F, 1.0F};
                        QVector3D tangent{1.0F, 0.0F, 0.0F};
                        QVector3D binormal{0.0F, 1.0F, 0.0F};
                        writeVertex(vbPtr, position, normal, tangent, binormal, roofColor,
                                    1.0F, 1.0F, 1.0F, 0.0F, 1.0F);
                        ++subsetVertexCounter;
                        ++globalVertexCounter;
                    }

                }

            }
        }
    }

    clear();

绘制球体的代码参考了OpenGL 的“Sphere”示例代码。

下载的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};
控制

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

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

地图贴图的每个区块都由一个 QML 模型(3D 几何体)和一个自定义材质组成,该材质以矩形为基础来渲染贴图纹理。

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

该应用程序使用自定义几何体来渲染地块建筑。

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

为了通过一次渲染调用渲染屋顶等建筑部件,采用了自定义着色器。

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

运行示例

您可以通过以下方式运行示例:

示例项目 @ code.qt.io

另请参阅 QML 应用程序。

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