OSM 建筑

此应用程序显示从OpenStreetMap (OSM) 服务器获取的地图,或当服务器不可用时使用本地有限数据集,通过Qt Quick 3D

这是等效C++演示的一个子集,它额外展示了建筑物。不过,此功能需要特殊的许可证密钥。

队列处理

应用程序使用队列来处理并发请求,以加快地图和建筑数据的加载过程。

获取和解析数据

为了实现从OSM地图服务器获取数据,实现了一个自定义请求处理类。

下载的PNG数据被发送到一个自定义的QQuick3DTextureData项目,以将PNG格式转换为地图瓦片的纹理。

应用程序使用相机位置、方向、缩放级别和倾斜角度来查找视图中最近的图块。

控制

当你运行应用程序时,请使用以下控件进行导航。

Windows

Android

平移

鼠标左键 + 拖动

拖动

缩放

鼠标滚轮

捏合

旋转

鼠标右键 + 拖动

不适用

渲染

地图瓦片的每一块都由一个QML模型(3D几何体)和一个自定义材质组成,该材质使用矩形作为基础来渲染瓦片地图纹理。

OSM Buildings Demo

下载 这个 示例

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