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ビルディングとマップサーバーからデータをフェッチするためにカスタムリクエストハンドラクラスが実装されています。
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; } );
アプリケーションはオンラインデータを解析し、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 を参照してください。
QML アプリケーションも参照してください 。
©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。