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