1 /*
2     SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "view.h"
8 
9 #include <osm/geomath.h>
10 
11 #include <cmath>
12 
13 using namespace KOSMIndoorMap;
14 
15 static constexpr const double SceneWorldSize = 256.0; // size of the scene when containing the full world
16 static constexpr const double LatitudeLimit = 85.05112879806592; // invtan(sinh(pi)) + radToDeg
17 static constexpr const auto MaxZoomFactor = 21; // 2^MaxZoomFactor subdivisions of the scene space
18 
View(QObject * parent)19 View::View(QObject *parent)
20     : QObject(parent)
21 {
22     setBeginTime(QDateTime::currentDateTime());
23 }
24 
25 View::~View() = default;
26 
mapGeoToScene(OSM::Coordinate coord) const27 QPointF View::mapGeoToScene(OSM::Coordinate coord) const
28 {
29     const auto lat = qBound(-LatitudeLimit, coord.latF(), LatitudeLimit);
30     return QPointF(
31         (coord.lonF() + 180.0) / 360.0 * SceneWorldSize,
32         SceneWorldSize / (2.0 * M_PI) * (M_PI - std::log(std::tan((M_PI / 4.0) + ((OSM::degToRad(lat) / 2.0)))))
33     );
34 }
35 
mapGeoToScene(OSM::BoundingBox box) const36 QRectF View::mapGeoToScene(OSM::BoundingBox box) const
37 {
38     const auto p1 = mapGeoToScene(box.min);
39     const auto p2 = mapGeoToScene(box.max);
40     return QRectF(QPointF(p1.x(), p2.y()), QPointF(p2.x(), p1.y()));
41 }
42 
mapSceneToGeo(QPointF p) const43 OSM::Coordinate View::mapSceneToGeo(QPointF p) const
44 {
45     return OSM::Coordinate(
46         OSM::radToDeg(std::atan(std::sinh(M_PI * (1 - 2 * (p.y() / SceneWorldSize))))),
47         (p.x() / SceneWorldSize) * 360.0 - 180.0
48     );
49 }
50 
mapSceneToGeo(const QRectF & box) const51 OSM::BoundingBox View::mapSceneToGeo(const QRectF &box) const
52 {
53     const auto c1 = mapSceneToGeo(box.bottomLeft());
54     const auto c2 = mapSceneToGeo(box.topRight());
55     return OSM::BoundingBox(c1, c2);
56 }
57 
screenHeight() const58 int View::screenHeight() const
59 {
60     return m_screenSize.height();
61 }
62 
screenWidth() const63 int View::screenWidth() const
64 {
65     return m_screenSize.width();
66 }
67 
setScreenSize(QSize size)68 void View::setScreenSize(QSize size)
69 {
70     if (size.width() <= 0.0 || size.height() <= 0.0 || size == m_screenSize) {
71         return;
72     }
73 
74     const auto dx = (double)size.width() / (double)screenWidth();
75     const auto dy = (double)size.height() / (double)screenHeight();
76     m_screenSize = size;
77 
78     m_viewport.setWidth(m_viewport.width() * dx);
79     m_viewport.setHeight(m_viewport.height() * dy);
80     constrainViewToScene();
81     Q_EMIT transformationChanged();
82 }
83 
level() const84 int View::level() const
85 {
86     return m_level;
87 }
88 
setLevel(int level)89 void View::setLevel(int level)
90 {
91     if (m_level == level) {
92         return;
93     }
94 
95     m_level = level;
96     Q_EMIT floorLevelChanged();
97 }
98 
zoomLevel() const99 double View::zoomLevel() const
100 {
101     const auto dx = m_viewport.width() / (screenWidth() / SceneWorldSize) / 360.0;
102     return - std::log2(dx);
103 }
104 
setZoomLevel(double zoom,QPointF screenCenter)105 void View::setZoomLevel(double zoom, QPointF screenCenter)
106 {
107     auto z = std::pow(2.0, - std::min(zoom, (double)MaxZoomFactor));
108     const auto dx = ((screenWidth() / SceneWorldSize) * 360.0 * z) - m_viewport.width();
109     const auto dy = ((screenHeight() / SceneWorldSize) * 360.0 * z) - m_viewport.height();
110 
111     const auto centerScene = mapScreenToScene(screenCenter);
112     if (!m_viewport.contains(centerScene)) {
113         return; // invalid input
114     }
115 
116     const auto xr = (centerScene.x() - m_viewport.x()) / m_viewport.width();
117     const auto yr = (centerScene.y() - m_viewport.y()) / m_viewport.height();
118 
119     m_viewport.adjust(-xr * dx, -yr * dy, (1-xr) * dx, (1-yr) * dy);
120     constrainViewToScene();
121     Q_EMIT transformationChanged();
122 }
123 
viewport() const124 QRectF View::viewport() const
125 {
126     return m_viewport;
127 }
128 
setViewport(const QRectF & viewport)129 void View::setViewport(const QRectF &viewport)
130 {
131     m_viewport = viewport;
132     constrainViewToScene();
133 }
134 
sceneBoundingBox() const135 QRectF View::sceneBoundingBox() const
136 {
137     return m_bbox;
138 }
139 
setSceneBoundingBox(OSM::BoundingBox bbox)140 void View::setSceneBoundingBox(OSM::BoundingBox bbox)
141 {
142     setSceneBoundingBox(mapGeoToScene(bbox));
143 }
144 
setSceneBoundingBox(const QRectF & bbox)145 void View::setSceneBoundingBox(const QRectF &bbox)
146 {
147     if (m_bbox == bbox) {
148         return;
149     }
150     m_bbox = bbox;
151 
152     // scale to fit horizontally
153     m_viewport = bbox;
154     const auto screenAspectRatio = (double)screenWidth() / (double)screenHeight();
155     m_viewport.setHeight(m_viewport.width() / screenAspectRatio);
156 
157     // if necessary, scale to fit vertically
158     if (m_viewport.height() > m_bbox.height()) {
159         const auto dy = (double)m_bbox.height() / (double)m_viewport.height();
160         m_viewport.setHeight(m_viewport.height() * dy);
161         m_viewport.setWidth(m_viewport.width() * dy);
162     }
163 
164     Q_EMIT transformationChanged();
165 }
166 
167 
mapSceneToScreen(QPointF scenePos) const168 QPointF View::mapSceneToScreen(QPointF scenePos) const
169 {
170     return sceneToScreenTransform().map(scenePos);
171 }
172 
mapSceneToScreen(const QRectF & sceneRect) const173 QRectF View::mapSceneToScreen(const QRectF &sceneRect) const
174 {
175     return QRectF(mapSceneToScreen(sceneRect.topLeft()), mapSceneToScreen(sceneRect.bottomRight()));
176 }
177 
mapScreenToScene(QPointF screenPos) const178 QPointF View::mapScreenToScene(QPointF screenPos) const
179 {
180     // TODO this can be implemented more efficiently
181     return sceneToScreenTransform().inverted().map(screenPos);
182 }
183 
mapScreenDistanceToSceneDistance(double distance) const184 double View::mapScreenDistanceToSceneDistance(double distance) const
185 {
186     const auto p1 = mapScreenToScene(m_viewport.center());
187     const auto p2 = mapScreenToScene(m_viewport.center() + QPointF(1.0, 0));
188     // ### does not consider rotations, needs to take the actual distance between p1 and p2 for that
189     return std::abs(p2.x() - p1.x()) * distance;
190 }
191 
panScreenSpace(QPoint offset)192 void View::panScreenSpace(QPoint offset)
193 {
194     auto dx = offset.x() * (m_viewport.width() / screenWidth());
195     auto dy = offset.y() * (m_viewport.height() / screenHeight());
196     m_viewport.adjust(dx, dy, dx, dy);
197     constrainViewToScene();
198 }
199 
sceneToScreenTransform() const200 QTransform View::sceneToScreenTransform() const
201 {
202     QTransform t;
203     t.scale(screenWidth() / (m_viewport.width()), screenHeight() / (m_viewport.height()));
204     t.translate(-m_viewport.x(), -m_viewport.y());
205     return t;
206 }
207 
zoomIn(QPointF screenCenter)208 void View::zoomIn(QPointF screenCenter)
209 {
210     setZoomLevel(zoomLevel() + 1, screenCenter);
211 }
212 
zoomOut(QPointF screenCenter)213 void View::zoomOut(QPointF screenCenter)
214 {
215     setZoomLevel(zoomLevel() - 1, screenCenter);
216 }
217 
constrainViewToScene()218 void View::constrainViewToScene()
219 {
220     // ensure we don't scale smaller than the bounding box
221     const auto s = std::min(m_viewport.width() / m_bbox.width(), m_viewport.height() / m_bbox.height());
222     if (s > 1.0) {
223         m_viewport.setWidth(m_viewport.width() / s);
224         m_viewport.setHeight(m_viewport.height() / s);
225     }
226 
227     // ensure we don't pan outside of the bounding box
228     if (m_bbox.left() < m_viewport.left() && m_bbox.right() < m_viewport.right()) {
229         const auto dx = std::min(m_viewport.left() - m_bbox.left(), m_viewport.right() - m_bbox.right());
230         m_viewport.adjust(-dx, 0, -dx, 0);
231     } else if (m_bbox.right() > m_viewport.right() && m_bbox.left() > m_viewport.left()) {
232         const auto dx = std::min(m_bbox.right() - m_viewport.right(), m_bbox.left() - m_viewport.left());
233         m_viewport.adjust(dx, 0, dx, 0);
234     }
235 
236     if (m_bbox.top() < m_viewport.top() && m_bbox.bottom() < m_viewport.bottom()) {
237         const auto dy = std::min(m_viewport.top() - m_bbox.top(), m_viewport.bottom() - m_bbox.bottom());
238         m_viewport.adjust(0, -dy, 0, -dy);
239     } else if (m_bbox.bottom() > m_viewport.bottom() && m_bbox.top() > m_viewport.top()) {
240         const auto dy = std::min(m_bbox.bottom() - m_viewport.bottom(), m_bbox.top() - m_viewport.top());
241         m_viewport.adjust(0, dy, 0, dy);
242     }
243 }
244 
mapMetersToScene(double meters) const245 double View::mapMetersToScene(double meters) const
246 {
247     // ### this fails for distances above 180° due to OSM::distance wrapping around
248     // doesn't matter for our use-case though, we are looking at much much smaller areas
249     const auto d = OSM::distance(mapSceneToGeo(QPointF(m_viewport.left(), m_viewport.center().y())), mapSceneToGeo(QPointF(m_viewport.right(), m_viewport.center().y())));
250     const auto scale = m_viewport.width() / d;
251     return meters * scale;
252 }
253 
mapMetersToScreen(double meters) const254 double View::mapMetersToScreen(double meters) const
255 {
256     const auto d = OSM::distance(mapSceneToGeo(QPointF(m_viewport.left(), m_viewport.center().y())), mapSceneToGeo(QPointF(m_viewport.right(), m_viewport.center().y())));
257     const auto r = meters / d;
258     return r * m_screenSize.width();
259 }
260 
mapScreenToMeters(int pixels) const261 double View::mapScreenToMeters(int pixels) const
262 {
263     const auto d = OSM::distance(mapSceneToGeo(QPointF(m_viewport.left(), m_viewport.center().y())), mapSceneToGeo(QPointF(m_viewport.right(), m_viewport.center().y())));
264     const auto r = (double)pixels / (double)m_screenSize.width();
265     return d * r;
266 }
267 
panX() const268 double View::panX() const
269 {
270     const auto r = (m_viewport.left() - m_bbox.left()) / m_bbox.width();
271     return panWidth() * r;
272 }
273 
panY() const274 double View::panY() const
275 {
276     const auto r = (m_viewport.top() - m_bbox.top()) / m_bbox.height();
277     return panHeight() * r;
278 }
279 
panWidth() const280 double View::panWidth() const
281 {
282     const auto r = m_bbox.width() / m_viewport.width();
283     return screenWidth() * r;
284 }
285 
panHeight() const286 double View::panHeight() const
287 {
288     const auto r = m_bbox.height() / m_viewport.height();
289     return screenHeight() * r;
290 }
291 
panTopLeft(double x,double y)292 void View::panTopLeft(double x, double y)
293 {
294     m_viewport.moveLeft(m_bbox.x() + m_bbox.width() * (x / panWidth()));
295     m_viewport.moveTop(m_bbox.y() + m_bbox.height() * (y / panHeight()));
296     constrainViewToScene();
297 }
298 
deviceTransform() const299 QTransform View::deviceTransform() const
300 {
301     return m_deviceTransform;
302 }
303 
setDeviceTransform(const QTransform & t)304 void View::setDeviceTransform(const QTransform &t)
305 {
306     m_deviceTransform = t;
307 }
308 
centerOnGeoCoordinate(QPointF geoCoord)309 void View::centerOnGeoCoordinate(QPointF geoCoord)
310 {
311     const auto sceneCenter = mapGeoToScene(OSM::Coordinate(geoCoord.y(), geoCoord.x()));
312     m_viewport.moveCenter(sceneCenter);
313     constrainViewToScene();
314     Q_EMIT transformationChanged();
315 }
316 
beginTime() const317 QDateTime View::beginTime() const
318 {
319     return m_beginTime;
320 }
321 
setBeginTime(const QDateTime & beginTime)322 void View::setBeginTime(const QDateTime &beginTime)
323 {
324     const auto alignedTime = QDateTime(beginTime.date(), {beginTime.time().hour(), beginTime.time().minute()});
325     if (m_beginTime == alignedTime) {
326         return;
327     }
328     m_beginTime = alignedTime;
329     Q_EMIT timeChanged();
330 }
331 
endTime() const332 QDateTime View::endTime() const
333 {
334     return m_endTime;
335 }
336 
setEndTime(const QDateTime & endTime)337 void View::setEndTime(const QDateTime& endTime)
338 {
339     const auto alignedTime = QDateTime(endTime.date(), {endTime.time().hour(), endTime.time().minute()});
340     if (m_endTime == alignedTime) {
341         return;
342     }
343     m_endTime = alignedTime;
344     Q_EMIT timeChanged();
345 }
346