OSMビル

OSM(OpenStreetMap)の建物地図データの3D建物ビューアです。

概要

このアプリケーションは、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) {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, &QNetworkReply::finished, this, [this,reply,tile](){ reply->deleteLater();if( reply->error()==) QNetworkReply( reply->error() ==::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; } } } { url = "png"_L1。    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, &::finished,this,[this.QNetworkReply::finished, this, [this,reply,tile](){ reply->deleteLater();if( reply->error()==) QNetworkReply( reply->error() ==::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()<<メッセージ; } }--m_mapsNumberOfRequestsInFlight ; } -m_mapsNumberOfRequestsInFlight; } -m_mapsNumberOfRequestsInFlight);

このアプリケーションはオンライン・データを解析し、QGeoPolygon のようなジオ・フォーマットのキーと値のリスト(QVariant )に変換する。

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

解析された建物データは、ジオ座標を3D座標に変換するためのカスタムジオメトリ項目に送られます。

    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モデル(3Dジオメトリ)と、タイルマップテクスチャをレンダリングするためのベースとして矩形を使用するカスタムマテリアルで構成されています。

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

屋根のような建物のパーツを1回の描画呼び出しでレンダリングするには、カスタムシェーダを使用します。

// 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 からサンプルを選択します。詳しくは、Building and Running an Exampleを参照してください。

サンプルプロジェクト @ 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.