このページでは

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

アプリケーションはオンラインデータを解析し、それを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 ) {
                        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"
                    }
                ]
            }

屋根などの建物の各部分を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;
}

例のプログラムの実行

以下の手順でサンプルを実行できます。

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.