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)
{
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;
}例のプログラムの実行
以下の手順でサンプルを実行できます。
- Qt Creator
Welcome モードを開き、Examples からサンプルを選択します。詳細については、Qt Creator の「チュートリアル:ビルドと実行」を参照してください。
- Qt Extension for Visual Studio Code
Command Palette から `Qt: Open Qt examples ` コマンドを実行し、リストからサンプルを選択します。詳細については、Qt Extension for Visual Studio Code の「チュートリアル:ビルドと実行」を参照してください。
「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.