OSM 빌딩
OSM(오픈스트리트맵) 건물 지도 데이터의 3D 건물 뷰어입니다.
개요
이 애플리케이션은 오픈스트리트맵(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 { 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 { 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};
컨트롤
애플리케이션을 실행할 때 다음 컨트롤을 사용하여 탐색합니다.
Windows | Android | |
---|---|---|
팬 | 마우스 왼쪽 버튼 + 드래그 | 드래그 |
줌 | 마우스 휠 | 핀치 |
회전 | 마우스 오른쪽 버튼 + 드래그 | n/a |
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" } ] }
한 번의 드로우 호출로 지붕과 같은 건물 부분을 렌더링하려면 커스텀 셰이더가 사용됩니다.
// 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 에서 예제를 선택합니다. 자세한 내용은 예제 빌드 및 실행을 참조하세요.
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.