diff --git a/src/map-quick/IndoorMap.qml b/src/map-quick/IndoorMap.qml index f8c781b..c74d1ca 100644 --- a/src/map-quick/IndoorMap.qml +++ b/src/map-quick/IndoorMap.qml @@ -1,130 +1,127 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import QtQuick 2.12 import QtQuick.Layouts 1.12 import org.kde.kosmindoormap 1.0 import QtQuick.Controls 2.12 as QQC2 /** QML item for displaying a train station or airport map. */ Item { id: mapRoot /** Access to map loading status and progress. */ property alias mapLoader: map.loader /** Path to a MapCSS style sheet used for rendering the map. */ property alias styleSheet: map.styleSheet /** Floor level model. */ property alias floorLevels: map.floorLevels /** Access to the view transformation and floor level selection. */ property alias view: map.view /** Emitted when a map element has been picked by clicking/tapping on it. */ signal elementPicked(var element); MapItemImpl { id: map anchors.fill: parent } Flickable { id: flickable boundsBehavior: Flickable.StopAtBounds contentX: map.view.panX contentY: map.view.panY contentWidth: map.view.panWidth contentHeight: map.view.panHeight anchors.fill: parent Rectangle { color: "red"; width: 100; height: 100 } Rectangle { color: "green"; width: 100; height: 100; x: flickable.contentWidth - width; y: flickable.contentHeight - height; } onContentXChanged: { if (moving) { map.view.panTopLeft(flickable.contentX, flickable.contentY); map.update(); } } onContentYChanged: { if (moving) { map.view.panTopLeft(flickable.contentX, flickable.contentY); map.update(); } } QQC2.ScrollBar.vertical: QQC2.ScrollBar {} QQC2.ScrollBar.horizontal: QQC2.ScrollBar {} TapHandler { acceptedButtons: Qt.LeftButton onTapped: { var root = parent; while (root.parent) { root = root.parent; } var localPos = map.mapFromItem(root, eventPoint.scenePosition.x, eventPoint.scenePosition.y); var element = map.elementAt(localPos.x, localPos.y); if (!element.isNull) { elementPicked(element); } } } PinchHandler { id: pinchHandler target: null - onScaleChanged: { - // TODO - console.log(pinchHandler.scale, pinchHandler.activeScale, pinchHandler.active, pinchHandler.centroid.pressPosition) - if (pinchHandler.activeScale > 2) { - map.view.zoomIn(pinchHandler.centroid.pressPosition); - } else if (pinchHandler.activeScale < 0.5) { - map.view.zoomOut(pinchHandler.centroid.pressPosition); - } + property double initialZoom + onActiveChanged: { + initialZoom = map.view.zoomLevel + } + onActiveScaleChanged: { + map.view.setZoomLevel(pinchHandler.initialZoom + Math.log2(pinchHandler.activeScale), pinchHandler.centroid.pressPosition); } xAxis.enabled: false yAxis.enabled: false minimumRotation: 0.0 maximumRotation: 0.0 } } Connections { target: map.view onTransformationChanged: { - console.log(map.view.panY, flickable.contentY); flickable.contentX = map.view.panX; flickable.contentY = map.view.panY; } } MouseArea { acceptedButtons: Qt.NoButton anchors.fill: parent onWheel: { if (wheel.angleDelta.y > 0) { map.view.zoomIn(Qt.point(wheel.x, wheel.y)); } else { map.view.zoomOut(Qt.point(wheel.x, wheel.y)); } wheel.accepted = true; } } QQC2.BusyIndicator { anchors.centerIn: parent running: map.loader.isLoading } } diff --git a/src/map/renderer/view.cpp b/src/map/renderer/view.cpp index dedfe58..c054e61 100644 --- a/src/map/renderer/view.cpp +++ b/src/map/renderer/view.cpp @@ -1,323 +1,302 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "view.h" #include #include #include using namespace KOSMIndoorMap; static constexpr const double SceneWorldSize = 256.0; // size of the scene when containing the full world static constexpr const double LatitudeLimit = 85.05112879806592; // invtan(sinh(pi)) + radToDeg static constexpr const auto MaxZoomFactor = 21; // 2^MaxZoomFactor subdivisions of the scene space View::View(QObject *parent) : QObject(parent) { } View::~View() = default; QPointF View::mapGeoToScene(OSM::Coordinate coord) const { const auto lat = qBound(-LatitudeLimit, coord.latF(), LatitudeLimit); return QPointF( (coord.lonF() + 180.0) / 360.0 * SceneWorldSize, SceneWorldSize / (2.0 * M_PI) * (M_PI - std::log(std::tan((M_PI / 4.0) + ((OSM::degToRad(lat) / 2.0))))) ); } QRectF View::mapGeoToScene(OSM::BoundingBox box) const { const auto p1 = mapGeoToScene(box.min); const auto p2 = mapGeoToScene(box.max); return QRectF(QPointF(p1.x(), p2.y()), QPointF(p2.x(), p1.y())); } OSM::Coordinate View::mapSceneToGeo(QPointF p) const { return OSM::Coordinate( OSM::radToDeg(std::atan(std::sinh(M_PI * (1 - 2 * (p.y() / SceneWorldSize))))), (p.x() / SceneWorldSize) * 360.0 - 180.0 ); } int View::screenHeight() const { return m_screenSize.height(); } int View::screenWidth() const { return m_screenSize.width(); } void View::setScreenSize(QSize size) { if (size.width() <= 0.0 || size.height() <= 0.0 || size == m_screenSize) { return; } const auto dx = (double)size.width() / (double)screenWidth(); const auto dy = (double)size.height() / (double)screenHeight(); m_screenSize = size; m_viewport.setWidth(m_viewport.width() * dx); m_viewport.setHeight(m_viewport.height() * dy); constrainViewToScene(); Q_EMIT transformationChanged(); } int View::level() const { return m_level; } void View::setLevel(int level) { if (m_level == level) { return; } m_level = level; emit floorLevelChanged(); } double View::zoomLevel() const { const auto dx = m_viewport.width() / (screenWidth() / SceneWorldSize) / 360.0; return - std::log2(dx); } void View::setZoomLevel(double zoom, QPointF screenCenter) { auto z = std::pow(2.0, - std::min(zoom, (double)MaxZoomFactor)); - // TODO use center point const auto dx = ((screenWidth() / SceneWorldSize) * 360.0 * z) - m_viewport.width(); const auto dy = ((screenHeight() / SceneWorldSize) * 360.0 * z) - m_viewport.height(); - m_viewport.adjust(-dx/2.0, -dy/2.0, dx/2.0, dy/2.0); + + const auto centerScene = mapScreenToScene(screenCenter); + if (!m_viewport.contains(centerScene)) { + return; // invalid input + } + + const auto xr = (centerScene.x() - m_viewport.x()) / m_viewport.width(); + const auto yr = (centerScene.y() - m_viewport.y()) / m_viewport.height(); + + m_viewport.adjust(-xr * dx, -yr * dy, (1-xr) * dx, (1-yr) * dy); constrainViewToScene(); emit transformationChanged(); } QRectF View::viewport() const { return m_viewport; } void View::setViewport(const QRectF &viewport) { m_viewport = viewport; constrainViewToScene(); } QRectF View::sceneBoundingBox() const { return m_bbox; } void View::setSceneBoundingBox(OSM::BoundingBox bbox) { setSceneBoundingBox(mapGeoToScene(bbox)); } void View::setSceneBoundingBox(const QRectF &bbox) { if (m_bbox == bbox) { return; } m_bbox = bbox; // scale to fit horizontally m_viewport = bbox; const auto screenAspectRatio = (double)screenWidth() / (double)screenHeight(); m_viewport.setHeight(m_viewport.width() / screenAspectRatio); // if necessary, scale to fit vertically if (m_viewport.height() > m_bbox.height()) { const auto dy = (double)m_bbox.height() / (double)m_viewport.height(); m_viewport.setHeight(m_viewport.height() * dy); m_viewport.setWidth(m_viewport.width() * dy); } Q_EMIT transformationChanged(); } QPointF View::mapSceneToScreen(QPointF scenePos) const { return sceneToScreenTransform().map(scenePos); } QRectF View::mapSceneToScreen(const QRectF &sceneRect) const { return QRectF(mapSceneToScreen(sceneRect.topLeft()), mapSceneToScreen(sceneRect.bottomRight())); } QPointF View::mapScreenToScene(QPointF screenPos) const { // TODO this can be implemented more efficiently return sceneToScreenTransform().inverted().map(screenPos); } double View::mapScreenDistanceToSceneDistance(double distance) const { const auto p1 = mapScreenToScene(m_viewport.center()); const auto p2 = mapScreenToScene(m_viewport.center() + QPointF(1.0, 0)); // ### does not consider rotations, needs to take the actual distance between p1 and p2 for that return std::abs(p2.x() - p1.x()) * distance; } void View::panScreenSpace(QPoint offset) { auto dx = offset.x() * (m_viewport.width() / screenWidth()); auto dy = offset.y() * (m_viewport.height() / screenHeight()); m_viewport.adjust(dx, dy, dx, dy); constrainViewToScene(); } QTransform View::sceneToScreenTransform() const { QTransform t; t.scale(screenWidth() / (m_viewport.width()), screenHeight() / (m_viewport.height())); t.translate(-m_viewport.x(), -m_viewport.y()); return t; } -void View::zoomIn(QPointF center) +void View::zoomIn(QPointF screenCenter) { - const auto factor = std::min(2.0, ((m_viewport.width() / 2.0) / (screenWidth() / SceneWorldSize) / 360.0) * (2 << MaxZoomFactor)); - if (factor <= 1) { - return; - } - - const auto dx = 0.5 * m_viewport.width() * (factor/2); - const auto dy = 0.5 * m_viewport.height() * (factor/2); - - const auto centerScene = mapScreenToScene(center); - const auto xr = (centerScene.x() - m_viewport.x()) / m_viewport.width(); - const auto yr = (centerScene.y() - m_viewport.y()) / m_viewport.height(); - - m_viewport.adjust(xr * dx, yr * dy, - (1-xr) * dx, - (1-yr) * dy); - constrainViewToScene(); - qDebug() << zoomLevel(); - Q_EMIT transformationChanged(); + setZoomLevel(zoomLevel() + 1, screenCenter); } -void View::zoomOut(QPointF center) +void View::zoomOut(QPointF screenCenter) { - if (m_bbox.width() <= m_viewport.width() && m_bbox.height() <= m_viewport.height()) { - return; - } - - const auto dx = m_viewport.width(); - const auto dy = m_viewport.height(); - - const auto centerScene = mapScreenToScene(center); - const auto xr = (centerScene.x() - m_viewport.x()) / m_viewport.width(); - const auto yr = (centerScene.y() - m_viewport.y()) / m_viewport.height(); - - m_viewport.adjust(-xr * dx, -yr * dy, (1-xr) * dx, (1-yr) * dy); - constrainViewToScene(); - qDebug() << zoomLevel(); - Q_EMIT transformationChanged(); + setZoomLevel(zoomLevel() - 1, screenCenter); } void View::constrainViewToScene() { // ensure we don't scale smaller than the bounding box const auto s = std::min(m_viewport.width() / m_bbox.width(), m_viewport.height() / m_bbox.height()); if (s > 1.0) { m_viewport.setWidth(m_viewport.width() / s); m_viewport.setHeight(m_viewport.height() / s); } // ensure we don't pan outside of the bounding box if (m_bbox.left() < m_viewport.left() && m_bbox.right() < m_viewport.right()) { const auto dx = std::min(m_viewport.left() - m_bbox.left(), m_viewport.right() - m_bbox.right()); m_viewport.adjust(-dx, 0, -dx, 0); } else if (m_bbox.right() > m_viewport.right() && m_bbox.left() > m_viewport.left()) { const auto dx = std::min(m_bbox.right() - m_viewport.right(), m_bbox.left() - m_viewport.left()); m_viewport.adjust(dx, 0, dx, 0); } if (m_bbox.top() < m_viewport.top() && m_bbox.bottom() < m_viewport.bottom()) { const auto dy = std::min(m_viewport.top() - m_bbox.top(), m_viewport.bottom() - m_bbox.bottom()); m_viewport.adjust(0, -dy, 0, -dy); } else if (m_bbox.bottom() > m_viewport.bottom() && m_bbox.top() > m_viewport.top()) { const auto dy = std::min(m_bbox.bottom() - m_viewport.bottom(), m_bbox.top() - m_viewport.top()); m_viewport.adjust(0, dy, 0, dy); } } double View::mapMetersToScene(double meters) const { // ### this fails for distances above 180° due to OSM::distance wrapping around // doesn't matter for our use-case though, we are looking at much much smaller areas const auto d = OSM::distance(mapSceneToGeo(QPointF(m_viewport.left(), m_viewport.center().y())), mapSceneToGeo(QPointF(m_viewport.right(), m_viewport.center().y()))); const auto scale = m_viewport.width() / d; return meters * scale; } double View::mapMetersToScreen(double meters) const { const auto d = OSM::distance(mapSceneToGeo(QPointF(m_viewport.left(), m_viewport.center().y())), mapSceneToGeo(QPointF(m_viewport.right(), m_viewport.center().y()))); const auto r = meters / d; return r * m_screenSize.width(); } double View::mapScreenToMeters(int pixels) const { const auto d = OSM::distance(mapSceneToGeo(QPointF(m_viewport.left(), m_viewport.center().y())), mapSceneToGeo(QPointF(m_viewport.right(), m_viewport.center().y()))); const auto r = (double)pixels / (double)m_screenSize.width(); return d * r; } double View::panX() const { const auto r = (m_viewport.left() - m_bbox.left()) / m_bbox.width(); return panWidth() * r; } double View::panY() const { const auto r = (m_viewport.top() - m_bbox.top()) / m_bbox.height(); return panHeight() * r; } double View::panWidth() const { const auto r = m_bbox.width() / m_viewport.width(); return screenWidth() * r; } double View::panHeight() const { const auto r = m_bbox.height() / m_viewport.height(); return screenHeight() * r; } void View::panTopLeft(double x, double y) { m_viewport.moveLeft(m_bbox.x() + m_bbox.width() * (x / panWidth())); m_viewport.moveTop(m_bbox.y() + m_bbox.height() * (y / panHeight())); constrainViewToScene(); } diff --git a/src/map/renderer/view.h b/src/map/renderer/view.h index 7b9e1e3..2ad3401 100644 --- a/src/map/renderer/view.h +++ b/src/map/renderer/view.h @@ -1,145 +1,147 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KOSMINDOORMAP_VIEW_H #define KOSMINDOORMAP_VIEW_H #include #include #include #include class QTransform; namespace KOSMIndoorMap { /** View transformations and transformation manipulation. * There are three different coordinate systems involved here: * - The geographic world coordinates of the OSM input data. * This uses OSM::Coordinate. * - The scene coordinates which have a the Web Mercator projection applied (see https://en.wikipedia.org/wiki/Mercator_projection). * This uses QPointF ranging from 0x0 to 256x256 * - The screen coordinates (ie. visible pixels on screen). * This uses QPoint. * Further, there's also three slight variations of those in use here: * - "HUD" coordinates: elements that follow the scene coordinates for their positioning, * but the screen coordinates regarding scaling and rotation. This is used for map labels. * - Geographic distances. This is needed to display things in a fixed width in meters in the scene, * or to compute the map scale. Note that this only works due to the relatively high zoom levels, * so that earth curvature or map projection effects are negligible. * - "pan space": same transform as screen space, but with the origin at the origin of the scene bounding box * This is useful for implementing scene-wide panning and showing scroll bars. */ class View : public QObject { Q_OBJECT Q_PROPERTY(double panX READ panX NOTIFY transformationChanged) Q_PROPERTY(double panY READ panY NOTIFY transformationChanged) Q_PROPERTY(double panWidth READ panWidth NOTIFY transformationChanged) Q_PROPERTY(double panHeight READ panHeight NOTIFY transformationChanged) Q_PROPERTY(int floorLevel READ level WRITE setLevel NOTIFY floorLevelChanged) Q_PROPERTY(double zoomLevel READ zoomLevel NOTIFY transformationChanged) public: explicit View(QObject *parent = nullptr); ~View(); /** Map a geographic coordinate to a scene coordinate, ie. apply the mercator projection. */ QPointF mapGeoToScene(OSM::Coordinate coord) const; QRectF mapGeoToScene(OSM::BoundingBox box) const; /** Map a scene coordinate to a geographic one, ie. apply the inverse mercator projection. */ OSM::Coordinate mapSceneToGeo(QPointF p) const; /** Screen-space sizes, ie the size of the on-screen area used for displaying. */ int screenWidth() const; int screenHeight() const; void setScreenSize(QSize size); /** The transformation to apply to scene coordinate to get to the view on screen. */ QTransform sceneToScreenTransform() const; /** The (floor) level to display. * @see MapLevel. */ int level() const; void setLevel(int level); /** OSM-compatible zoom level, ie. the 2^level-th subdivision of the scene space. */ double zoomLevel() const; /** Set the zoom level to @p zoom, and adjusting it around center position @p center. */ Q_INVOKABLE void setZoomLevel(double zoom, QPointF screenCenter); /** The sub-rect of the scene bounding box currently displayed. * Specified in scene coordinates. */ QRectF viewport() const; void setViewport(const QRectF &viewport); /** The bounding box of the scene. * The viewport cannot exceed this area. */ QRectF sceneBoundingBox() const; void setSceneBoundingBox(OSM::BoundingBox bbox); void setSceneBoundingBox(const QRectF &bbox); /** Converts a point in scene coordinates to screen coordinates. */ QPointF mapSceneToScreen(QPointF scenePos) const; /** Converts a rectanble in scene coordinates to screen coordinates. */ QRectF mapSceneToScreen(const QRectF &sceneRect) const; /** Converts a point in screen coordinates to scene coordinates. */ QPointF mapScreenToScene(QPointF screenPos) const; /** Converts a distance in screen coordinates to a distance in scene coordinates. */ double mapScreenDistanceToSceneDistance(double distance) const; /** Returns how many units in scene coordinate represent the distance of @p meters in the current view transformation. */ double mapMetersToScene(double meters) const; /** Returns how many pixels on screen represent the distance of @p meters with the current view transformation. */ Q_INVOKABLE double mapMetersToScreen(double meters) const; /** Returns how many meters are represented by @p pixels with the current view transformation. */ Q_INVOKABLE double mapScreenToMeters(int pixels) const; void panScreenSpace(QPoint offset); - Q_INVOKABLE void zoomIn(QPointF center); - Q_INVOKABLE void zoomOut(QPointF center); + /** Increase zoom level by one/scale up by 2x around the screen position @p center. */ + Q_INVOKABLE void zoomIn(QPointF screenCenter); + /** Decrease zoom level by one/scale down by 2x around the screen position @p center. */ + Q_INVOKABLE void zoomOut(QPointF screenCenter); /** Position of the viewport in pan coordinates. */ double panX() const; double panY() const; /** Size of the pan-able area in screen coordinates. */ double panWidth() const; double panHeight() const; /** Move the viewport to the pan coordinates @p x and @p y. */ Q_INVOKABLE void panTopLeft(double x, double y); Q_SIGNALS: void transformationChanged(); void floorLevelChanged(); private: /** Ensure we stay within the bounding box with the viewport, call after viewport modification. */ void constrainViewToScene(); QRectF m_bbox; QRectF m_viewport; QSize m_screenSize; int m_level = 0; }; } #endif // KOSMINDOORMAP_VIEW_H