OSM 建筑¶
此应用程序显示从OpenStreetMap (OSM) 服务器获取的地图,或当服务器不可用时使用本地有限数据集,通过Qt Quick 3D
。
这是等效C++演示的一个子集,它额外展示了建筑物。不过,此功能需要特殊的许可证密钥。
队列处理¶
应用程序使用队列来处理并发请求,以加快地图和建筑数据的加载过程。
获取和解析数据¶
为了实现从OSM地图服务器获取数据,实现了一个自定义请求处理类。
下载的PNG
数据被发送到一个自定义的QQuick3DTextureData
项目,以将PNG
格式转换为地图瓦片的纹理。
应用程序使用相机位置、方向、缩放级别和倾斜角度来查找视图中最近的图块。
控制¶
当你运行应用程序时,请使用以下控件进行导航。
Windows |
Android |
|
平移 |
鼠标左键 + 拖动 |
拖动 |
缩放 |
鼠标滚轮 |
捏合 |
旋转 |
鼠标右键 + 拖动 |
不适用 |
渲染¶
地图瓦片的每一块都由一个QML模型(3D几何体)和一个自定义材质组成,该材质使用矩形作为基础来渲染瓦片地图纹理。

# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import sys
from pathlib import Path
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtGui import QGuiApplication
from PySide6.QtCore import QCoreApplication
from manager import OSMManager, CustomTextureData # noqa: F401
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.addImportPath(Path(__file__).parent)
engine.loadFromModule("OSMBuildings", "Main")
if not engine.rootObjects():
sys.exit(-1)
exit_code = QCoreApplication.exec()
del engine
sys.exit(exit_code)
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtQuick3D import QQuick3DTextureData
from PySide6.QtQml import QmlElement
from PySide6.QtGui import QImage, QVector3D
from PySide6.QtCore import QByteArray, QObject, Property, Slot, Signal
from request import OSMTileData, OSMRequest
# To be used on the @QmlElement decorator
# (QML_IMPORT_MINOR_VERSION is optional)
QML_IMPORT_NAME = "OSMBuildings"
QML_IMPORT_MAJOR_VERSION = 1
@QmlElement
class OSMManager(QObject):
mapsDataReady = Signal(QByteArray, int, int, int)
def __init__(self, parent=None):
super().__init__(parent)
self.m_request = OSMRequest(self)
self.m_startBuildingTileX = 17605
self.m_startBuildingTileY = 10746
self.m_tileSizeX = 37
self.m_tileSizeY = 37
self.m_request.mapsDataReady.connect(self._slotMapsDataReady)
def tileSizeX(self):
return self.m_tileSizeX
def tileSizeY(self):
return self.m_tileSizeY
@Slot(QByteArray, int, int, int)
def _slotMapsDataReady(self, mapData, tileX, tileY, zoomLevel):
self.mapsDataReady.emit(mapData, tileX - self.m_startBuildingTileX,
tileY - self.m_startBuildingTileY, zoomLevel)
@Slot(QVector3D, QVector3D, float, float, float, float, float, float)
def setCameraProperties(self, position, right,
cameraZoom, minimumZoom, maximumZoom,
cameraTilt, minimumTilt, maximumTilt):
tiltFactor = (cameraTilt - minimumTilt) / max(maximumTilt - minimumTilt, 1.0)
zoomFactor = (cameraZoom - minimumZoom) / max(maximumZoom - minimumZoom, 1.0)
# Forward vector align to the XY plane
forwardVector = QVector3D.crossProduct(right, QVector3D(0.0, 0.0, -1.0)).normalized()
projectionOfForwardOnXY = position + forwardVector * tiltFactor * zoomFactor * 50.0
queue = []
for forwardIndex in range(-20, 21):
for sidewardIndex in range(-20, 21):
vx = float(self.m_tileSizeX * sidewardIndex)
vy = float(self.m_tileSizeY * forwardIndex)
transferredPosition = projectionOfForwardOnXY + QVector3D(vx, vy, 0)
tile_x = self.m_startBuildingTileX + int(transferredPosition.x() / self.m_tileSizeX)
tile_y = self.m_startBuildingTileY - int(transferredPosition.y() / self.m_tileSizeY)
self.addBuildingRequestToQueue(queue, tile_x, tile_y)
projectedTileX = (self.m_startBuildingTileX + int(projectionOfForwardOnXY.x()
/ self.m_tileSizeX))
projectedTileY = (self.m_startBuildingTileY - int(projectionOfForwardOnXY.y()
/ self.m_tileSizeY))
def tile_sort_key(tile_data):
return tile_data.distanceTo(projectedTileX, projectedTileY)
queue.sort(key=tile_sort_key)
self.m_request.getMapsData(queue.copy())
def addBuildingRequestToQueue(self, queue, tileX, tileY, zoomLevel=15):
queue.append(OSMTileData(tileX, tileY, zoomLevel))
@Slot(result=bool)
def isDemoToken(self):
return self.m_request.isDemoToken()
@Slot(str)
def setToken(self, token):
self.m_request.setToken(token)
@Slot(result=str)
def token(self):
return self.m_request.token()
tileSizeX = Property(int, tileSizeX, constant=True)
tileSizeY = Property(int, tileSizeY, constant=True)
@QmlElement
class CustomTextureData(QQuick3DTextureData):
@Slot(QByteArray)
def setImageData(self, data):
image = QImage.fromData(data).convertToFormat(QImage.Format.Format_RGBA8888)
self.setTextureData(QByteArray(bytearray(image.constBits())))
self.setSize(image.size())
self.setHasTransparency(False)
self.setFormat(QQuick3DTextureData.Format.RGBA8)
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import math
import sys
from dataclasses import dataclass
from functools import partial
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PySide6.QtCore import (QByteArray, QTimer, QFile, QFileInfo,
QObject, QUrl, Signal, Slot)
# %1 = zoom level(is dynamic), %2 = x tile number, %3 = y tile number
URL_OSMB_MAP = "https://tile-a.openstreetmap.fr/hot/{}/{}/{}.png"
@dataclass
class OSMTileData:
TileX: int = 0
TileY: int = 0
ZoomLevel: int = 1
def distanceTo(self, x, y):
deltaX = float(self.TileX) - float(x)
deltaY = float(self.TileY) - float(y)
return math.sqrt(deltaX * deltaX + deltaY * deltaY)
def __eq__(self, rhs):
return self._equals(rhs)
def __ne__(self, rhs):
return not self._equals(rhs)
def __hash__(self):
return hash((self.TileX, self.TileY, self.ZoomLevel))
def _equals(self, rhs):
return (self.TileX == rhs.TileX and self.TileY == rhs.TileY
and self.ZoomLevel == rhs.ZoomLevel)
def tileKey(tile):
return f"{tile.ZoomLevel},{tile.TileX},{tile.TileY}"
class OSMRequest(QObject):
mapsDataReady = Signal(QByteArray, int, int, int)
def __init__(self, parent):
super().__init__(parent)
self.m_mapsNumberOfRequestsInFlight = 0
self.m_queuesTimer = QTimer()
self.m_queuesTimer.setInterval(0)
self.m_buildingsQueue = []
self.m_mapsQueue = []
self.m_networkAccessManager = QNetworkAccessManager()
self.m_token = ""
self.m_queuesTimer.timeout.connect(self._slotTimeOut)
self.m_queuesTimer.setInterval(0)
self.m_lastBuildingsDataError = ""
self.m_lastMapsDataError = ""
@Slot()
def _slotTimeOut(self):
if not self.m_buildingsQueue and not self.m_mapsQueue:
self.m_queuesTimer.stop()
else:
numConcurrentRequests = 6
if self.m_mapsQueue and self.m_mapsNumberOfRequestsInFlight < numConcurrentRequests:
self.getMapsDataRequest(self.m_mapsQueue[0])
del self.m_mapsQueue[0]
self.m_mapsNumberOfRequestsInFlight += 1
def isDemoToken(self):
return not self.m_token
def token(self):
return self.m_token
def setToken(self, token):
self.m_token = token
def getBuildingsData(self, buildingsQueue):
if not buildingsQueue:
return
self.m_buildingsQueue = buildingsQueue
if not self.m_queuesTimer.isActive():
self.m_queuesTimer.start()
def getMapsData(self, mapsQueue):
if not mapsQueue:
return
self.m_mapsQueue = mapsQueue
if not self.m_queuesTimer.isActive():
self.m_queuesTimer.start()
def getMapsDataRequest(self, tile):
fileName = "data/" + tileKey(tile) + ".png"
if QFileInfo.exists(fileName):
file = QFile(fileName)
if file.open(QFile.ReadOnly):
data = file.readAll()
file.close()
self.mapsDataReady.emit(data, tile.TileX, tile.TileY, tile.ZoomLevel)
self.m_mapsNumberOfRequestsInFlight -= 1
return
url = QUrl(URL_OSMB_MAP.format(tile.ZoomLevel, tile.TileX, tile.TileY))
reply = self.m_networkAccessManager.get(QNetworkRequest(url))
reply.finished.connect(partial(self._mapsDataReceived, reply, tile))
@Slot(OSMTileData)
def _mapsDataReceived(self, reply, tile):
reply.deleteLater()
if reply.error() == QNetworkReply.NoError:
data = reply.readAll()
self.mapsDataReady.emit(data, tile.TileX, tile.TileY, tile.ZoomLevel)
else:
message = reply.readAll().data().decode('utf-8')
if message != self.m_lastMapsDataError:
self.m_lastMapsDataError = message
print("OSMRequest.getMapsDataRequest", reply.error(),
reply.url(), message, file=sys.stderr)
self.m_mapsNumberOfRequestsInFlight -= 1
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Window
import QtQuick3D
import QtQuick3D.Helpers
import OSMBuildings
Window {
width: 1024
height: 768
visible: true
title: qsTr("OSM Buildings")
OSMManager {
id: osmManager
onMapsDataReady: function( mapData, tileX, tileY, zoomLevel ){
mapModels.addModel(mapData, tileX, tileY, zoomLevel)
}
}
Component {
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"
}
]
}
}
}
View3D {
id: v3d
anchors.fill: parent
environment: ExtendedSceneEnvironment {
id: env
backgroundMode: SceneEnvironment.Color
clearColor: "#8099b3"
fxaaEnabled: true
fog: Fog {
id: theFog
color:"#8099b3"
enabled: true
depthEnabled: true
depthFar: 600
}
}
Node {
id: originNode
eulerRotation: Qt.vector3d(50.0, 0.0, 0.0)
PerspectiveCamera {
id: cameraNode
frustumCullingEnabled: true
clipFar: 600
clipNear: 100
fieldOfView: 90
z: 100
onZChanged: originNode.updateManagerCamera()
}
Component.onCompleted: updateManagerCamera()
onPositionChanged: updateManagerCamera()
onRotationChanged: updateManagerCamera()
function updateManagerCamera(){
osmManager.setCameraProperties( originNode.position,
originNode.right, cameraNode.z,
cameraController.minimumZoom,
cameraController.maximumZoom,
originNode.eulerRotation.x,
cameraController.minimumTilt,
cameraController.maximumTilt )
}
}
DirectionalLight {
color: Qt.rgba(1.0, 1.0, 0.95, 1.0)
ambientColor: Qt.rgba(0.5, 0.45, 0.45, 1.0)
rotation: Quaternion.fromEulerAngles(-10, -45, 0)
}
Node {
id: mapModels
function addModel(mapData, tileX, tileY, zoomLevel)
{
chunkModelMap.createObject( mapModels, { "mapData": mapData,
"tileX": tileX,
"tileY": tileY,
"zoomLevel": zoomLevel
} )
}
}
OSMCameraController {
id: cameraController
origin: originNode
camera: cameraNode
}
}
Item {
id: tokenArea
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.margins: 10
Text {
id: tokenInputArea
visible: false
anchors.left: parent.left
anchors.bottom: parent.bottom
color: "white"
styleColor: "black"
style: Text.Outline
text: "Open street map tile token: "
Rectangle {
border.width: 1
border.color: "black"
anchors.fill: tokenTxtInput
anchors.rightMargin: -30
Text {
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 2
anchors.rightMargin: 8
color: "blue"
styleColor: "white"
style: Text.Outline
text: "OK"
Behavior on scale {
NumberAnimation {
easing.type: Easing.OutBack
}
}
MouseArea {
anchors.fill: parent
anchors.margins: -10
onPressedChanged: {
if (pressed)
parent.scale = 0.9
else
parent.scale = 1.0
}
onClicked: {
tokenInputArea.visible = false
osmManager.setToken(tokenTxtInput.text)
tokenWarning.demoToken = osmManager.isDemoToken()
tokenWarning.visible = true
}
}
}
}
TextInput {
id: tokenTxtInput
clip: true
anchors.left: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: -3
height: tokenTxtInput.contentHeight + 5
width: 110
leftPadding: 5
rightPadding: 5
}
}
Text {
id: tokenWarning
property bool demoToken: true
anchors.left: parent.left
anchors.bottom: parent.bottom
color: "white"
styleColor: "black"
style: Text.Outline
text: demoToken ? "You are using the OSM limited demo token " :
"You are using a token "
Text {
anchors.left: parent.right
color: "blue"
styleColor: "white"
style: Text.Outline
text: "click here to change"
Behavior on scale {
NumberAnimation {
easing.type: Easing.OutBack
}
}
MouseArea {
anchors.fill: parent
onPressedChanged: {
if (pressed)
parent.scale = 0.9
else
parent.scale = 1.0
}
onClicked: {
tokenWarning.visible = false
tokenTxtInput.text = osmManager.token()
tokenInputArea.visible = true
}
}
}
}
}
}
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick3D
Item {
id: root
required property Node origin
required property Camera camera
property real xSpeed: 0.05
property real ySpeed: 0.05
property bool xInvert: false
property bool yInvert: false
property bool mouseEnabled: true
property bool panEnabled: true
readonly property bool inputsNeedProcessing: status.useMouse || status.isPanning
readonly property real minimumZoom: 30
readonly property real maximumZoom: 200
readonly property real minimumTilt: 0
readonly property real maximumTilt: 80
implicitWidth: parent.width
implicitHeight: parent.height
Connections {
target: camera
Component.onCompleted: {
onZChanged()
}
function onZChanged() {
// Adjust near/far values based on distance
let distance = camera.z
if (distance < 1) {
camera.clipNear = 0.01
camera.clipFar = 100
} else if (distance < 100) {
camera.clipNear = 0.1
camera.clipFar = 1000
} else {
camera.clipNear = 1
camera.clipFar = 10000
}
}
}
DragHandler {
id: dragHandler
target: null
enabled: mouseEnabled
acceptedModifiers: Qt.NoModifier
acceptedButtons: Qt.RightButton
onCentroidChanged: {
mouseMoved(Qt.vector2d(centroid.position.x, centroid.position.y), false);
}
onActiveChanged: {
if (active)
mousePressed(Qt.vector2d(centroid.position.x, centroid.position.y));
else
mouseReleased(Qt.vector2d(centroid.position.x, centroid.position.y));
}
}
DragHandler {
id: ctrlDragHandler
target: null
enabled: mouseEnabled && panEnabled
//acceptedModifiers: Qt.ControlModifier
onCentroidChanged: {
panEvent(Qt.vector2d(centroid.position.x, centroid.position.y));
}
onActiveChanged: {
if (active)
startPan(Qt.vector2d(centroid.position.x, centroid.position.y));
else
endPan();
}
}
PinchHandler {
id: pinchHandler
target: null
enabled: mouseEnabled
property real distance: 0.0
onCentroidChanged: {
panEvent(Qt.vector2d(centroid.position.x, centroid.position.y))
}
onActiveChanged: {
if (active) {
startPan(Qt.vector2d(centroid.position.x, centroid.position.y))
distance = root.camera.z
} else {
endPan()
distance = 0.0
}
}
onScaleChanged: {
camera.z = distance * (1 / scale)
camera.z = Math.min(Math.max(camera.z, minimumZoom), maximumZoom)
}
}
TapHandler {
onTapped: root.forceActiveFocus()
}
WheelHandler {
id: wheelHandler
orientation: Qt.Vertical
target: null
enabled: mouseEnabled
onWheel: event => {
let delta = -event.angleDelta.y * 0.01;
camera.z += camera.z * 0.1 * delta
camera.z = Math.min(Math.max(camera.z, minimumZoom), maximumZoom)
}
}
function mousePressed(newPos) {
root.forceActiveFocus()
status.currentPos = newPos
status.lastPos = newPos
status.useMouse = true;
}
function mouseReleased(newPos) {
status.useMouse = false;
}
function mouseMoved(newPos: vector2d) {
status.currentPos = newPos;
}
function startPan(pos: vector2d) {
status.isPanning = true;
status.currentPanPos = pos;
status.lastPanPos = pos;
}
function endPan() {
status.isPanning = false;
}
function panEvent(newPos: vector2d) {
status.currentPanPos = newPos;
}
FrameAnimation {
id: updateTimer
running: root.inputsNeedProcessing
onTriggered: status.processInput(frameTime * 100)
}
QtObject {
id: status
property bool useMouse: false
property bool isPanning: false
property vector2d lastPos: Qt.vector2d(0, 0)
property vector2d lastPanPos: Qt.vector2d(0, 0)
property vector2d currentPos: Qt.vector2d(0, 0)
property vector2d currentPanPos: Qt.vector2d(0, 0)
property real rotateAlongZ: 0
property real rotateAlongXY: 50.0
function processInput(frameDelta) {
if (useMouse) {
// Get the delta
var delta = Qt.vector2d(lastPos.x - currentPos.x,
lastPos.y - currentPos.y);
var rotateX = delta.x * xSpeed * frameDelta
if ( xInvert )
rotateX = -rotateX
rotateAlongZ += rotateX;
let rotateAlongZRad = rotateAlongZ * (Math.PI / 180.)
origin.rotate(rotateX, Qt.vector3d(0.0, 0.0, -1.0), Node.SceneSpace)
var rotateY = delta.y * -ySpeed * frameDelta
if ( yInvert )
rotateY = -rotateY;
let preRotateAlongXY = rotateAlongXY + rotateY
if ( preRotateAlongXY <= maximumTilt && preRotateAlongXY >= minimumTilt )
{
rotateAlongXY = preRotateAlongXY
origin.rotate(rotateY, Qt.vector3d(Math.cos(rotateAlongZRad), Math.sin(-rotateAlongZRad), 0.0), Node.SceneSpace)
}
lastPos = currentPos;
}
if (isPanning) {
let delta = currentPanPos.minus(lastPanPos);
delta.x = -delta.x
delta.x = (delta.x / root.width) * camera.z * frameDelta
delta.y = (delta.y / root.height) * camera.z * frameDelta
let velocity = Qt.vector3d(0, 0, 0)
// X Movement
let xDirection = origin.right
velocity = velocity.plus(Qt.vector3d(xDirection.x * delta.x,
xDirection.y * delta.x,
xDirection.z * delta.x));
// Z Movement
let zDirection = origin.right.crossProduct(Qt.vector3d(0.0, 0.0, -1.0))
velocity = velocity.plus(Qt.vector3d(zDirection.x * delta.y,
zDirection.y * delta.y,
zDirection.z * delta.y));
origin.position = origin.position.plus(velocity)
lastPanPos = currentPanPos
}
}
}
}
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
void MAIN() {
vec2 tc = UV0;
BASE_COLOR = vec4( texture(tileTexture, vec2(tc.x, 1.0 - tc.y )).xyz, 1.0 );
ROUGHNESS = 0.3;
METALNESS = 0.0;
FRESNEL_POWER = 1.0;
}