KOSMIndoorMap

view.cpp
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
13using namespace KOSMIndoorMap;
14
15static constexpr const double SceneWorldSize = 256.0; // size of the scene when containing the full world
16static constexpr const double LatitudeLimit = 85.05112879806592; // invtan(sinh(pi)) + radToDeg
17static constexpr const auto MaxZoomFactor = 21; // 2^MaxZoomFactor subdivisions of the scene space
18
19View::View(QObject *parent)
20 : QObject(parent)
21{
22 setBeginTime(QDateTime::currentDateTime());
23}
24
25View::~View() = default;
26
27QPointF View::mapGeoToScene(OSM::Coordinate coord)
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
36QRectF View::mapGeoToScene(OSM::BoundingBox box)
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
43OSM::Coordinate View::mapSceneToGeo(QPointF p)
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
51OSM::BoundingBox View::mapSceneToGeo(const QRectF &box)
52{
53 const auto c1 = mapSceneToGeo(box.bottomLeft());
54 const auto c2 = mapSceneToGeo(box.topRight());
55 return OSM::BoundingBox(c1, c2);
56}
57
58int View::screenHeight() const
59{
60 return m_screenSize.height();
61}
62
63int View::screenWidth() const
64{
65 return m_screenSize.width();
66}
67
68void 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 updateViewport();
82}
83
84int View::level() const
85{
86 return m_level;
87}
88
89void View::setLevel(int level)
90{
91 if (m_level == level) {
92 return;
93 }
94
95 m_level = level;
96 Q_EMIT floorLevelChanged();
97}
98
99double View::zoomLevel() const
100{
101 const auto dx = m_viewport.width() / (screenWidth() / SceneWorldSize) / 360.0;
102 return - std::log2(dx);
103}
104
105void View::setZoomLevel(double zoom, QPointF screenCenter)
106{
107 m_viewport = viewportForZoom(zoom, screenCenter);
108 updateViewport();
109}
110
111QRectF View::viewport() const
112{
113 return m_viewport;
114}
115
116void View::setViewport(const QRectF &viewport)
117{
118 m_viewport = viewport;
119 constrainViewToScene();
120 updateViewport();
121}
122
123QRectF View::viewportForZoom(double zoom, QPointF screenCenter) const
124{
125 auto z = std::pow(2.0, - std::min(zoom, (double)MaxZoomFactor));
126 const auto dx = ((screenWidth() / SceneWorldSize) * 360.0 * z) - m_viewport.width();
127 const auto dy = ((screenHeight() / SceneWorldSize) * 360.0 * z) - m_viewport.height();
128
129 const auto centerScene = mapScreenToScene(screenCenter);
130 if (!m_viewport.contains(centerScene)) {
131 return m_viewport; // invalid input
132 }
133
134 const auto xr = (centerScene.x() - m_viewport.x()) / m_viewport.width();
135 const auto yr = (centerScene.y() - m_viewport.y()) / m_viewport.height();
136
137 QRectF viewport(m_viewport);
138 viewport.adjust(-xr * dx, -yr * dy, (1-xr) * dx, (1-yr) * dy);
139 return constrainedViewport(viewport);
140}
141
142QRectF View::sceneBoundingBox() const
143{
144 return m_bbox;
145}
146
147void View::setSceneBoundingBox(OSM::BoundingBox bbox)
148{
149 setSceneBoundingBox(mapGeoToScene(bbox));
150}
151
152void View::setSceneBoundingBox(const QRectF &bbox)
153{
154 if (m_bbox == bbox) {
155 return;
156 }
157 m_bbox = bbox;
158
159 // scale to fit horizontally
160 m_viewport = bbox;
161 const auto screenAspectRatio = (double)screenWidth() / (double)screenHeight();
162 m_viewport.setHeight(m_viewport.width() / screenAspectRatio);
163
164 // if necessary, scale to fit vertically
165 if (m_viewport.height() > m_bbox.height()) {
166 const auto dy = (double)m_bbox.height() / (double)m_viewport.height();
167 m_viewport.setHeight(m_viewport.height() * dy);
168 m_viewport.setWidth(m_viewport.width() * dy);
169 }
170
171 updateViewport();
172}
173
174
175QPointF View::mapSceneToScreen(QPointF scenePos) const
176{
177 return m_sceneToScreenTransform.map(scenePos);
178}
179
180QRectF View::mapSceneToScreen(const QRectF &sceneRect) const
181{
182 return QRectF(mapSceneToScreen(sceneRect.topLeft()), mapSceneToScreen(sceneRect.bottomRight()));
183}
184
185QPointF View::mapScreenToScene(QPointF screenPos) const
186{
187 // TODO this can be implemented more efficiently
188 return m_screenToSceneTransform.map(screenPos);
189}
190
191double View::mapScreenDistanceToSceneDistance(double distance) const
192{
193 const auto p1 = mapScreenToScene(m_viewport.center());
194 const auto p2 = mapScreenToScene(m_viewport.center() + QPointF(1.0, 0));
195 // ### does not consider rotations, needs to take the actual distance between p1 and p2 for that
196 return std::abs(p2.x() - p1.x()) * distance;
197}
198
199void View::panScreenSpace(QPoint offset)
200{
201 auto dx = offset.x() * (m_viewport.width() / screenWidth());
202 auto dy = offset.y() * (m_viewport.height() / screenHeight());
203 m_viewport.adjust(dx, dy, dx, dy);
204 constrainViewToScene();
205 updateViewport();
206}
207
208QTransform View::sceneToScreenTransform() const
209{
210 return m_sceneToScreenTransform;
211}
212
213void View::zoomIn(QPointF screenCenter)
214{
215 setZoomLevel(zoomLevel() + 1, screenCenter);
216}
217
218void View::zoomOut(QPointF screenCenter)
219{
220 setZoomLevel(zoomLevel() - 1, screenCenter);
221}
222
223void View::constrainViewToScene()
224{
225 m_viewport = constrainedViewport(m_viewport);
226}
227
228QRectF View::constrainedViewport(QRectF viewport) const
229{
230 // ensure we don't scale smaller than the bounding box
231 const auto s = std::min(viewport.width() / m_bbox.width(), viewport.height() / m_bbox.height());
232 if (s > 1.0) {
233 viewport.setWidth(viewport.width() / s);
234 viewport.setHeight(viewport.height() / s);
235 }
236
237 // ensure we don't pan outside of the bounding box
238 if (m_bbox.left() < viewport.left() && m_bbox.right() < viewport.right()) {
239 const auto dx = std::min(viewport.left() - m_bbox.left(), viewport.right() - m_bbox.right());
240 viewport.adjust(-dx, 0, -dx, 0);
241 } else if (m_bbox.right() > viewport.right() && m_bbox.left() > viewport.left()) {
242 const auto dx = std::min(m_bbox.right() - viewport.right(), m_bbox.left() - viewport.left());
243 viewport.adjust(dx, 0, dx, 0);
244 }
245
246 if (m_bbox.top() < viewport.top() && m_bbox.bottom() < viewport.bottom()) {
247 const auto dy = std::min(viewport.top() - m_bbox.top(), viewport.bottom() - m_bbox.bottom());
248 viewport.adjust(0, -dy, 0, -dy);
249 } else if (m_bbox.bottom() > viewport.bottom() && m_bbox.top() > viewport.top()) {
250 const auto dy = std::min(m_bbox.bottom() - viewport.bottom(), m_bbox.top() - viewport.top());
251 viewport.adjust(0, dy, 0, dy);
252 }
253
254 return viewport;
255}
256
257double View::mapMetersToScene(double meters) const
258{
259 const auto scale = m_viewport.width() / m_screenWidthInMeters;
260 return meters * scale;
261}
262
263double View::mapMetersToScreen(double meters) const
264{
265 const auto r = meters / m_screenWidthInMeters;
266 return r * m_screenSize.width();
267}
268
269double View::mapScreenToMeters(int pixels) const
270{
271 const auto r = (double)pixels / (double)m_screenSize.width();
272 return r * m_screenWidthInMeters;
273}
274
275double View::panX() const
276{
277 const auto r = (m_viewport.left() - m_bbox.left()) / m_bbox.width();
278 return panWidth() * r;
279}
280
281double View::panY() const
282{
283 const auto r = (m_viewport.top() - m_bbox.top()) / m_bbox.height();
284 return panHeight() * r;
285}
286
287double View::panWidth() const
288{
289 const auto r = m_bbox.width() / m_viewport.width();
290 return screenWidth() * r;
291}
292
293double View::panHeight() const
294{
295 const auto r = m_bbox.height() / m_viewport.height();
296 return screenHeight() * r;
297}
298
299void View::panTopLeft(double x, double y)
300{
301 m_viewport.moveLeft(m_bbox.x() + m_bbox.width() * (x / panWidth()));
302 m_viewport.moveTop(m_bbox.y() + m_bbox.height() * (y / panHeight()));
303 constrainViewToScene();
304 updateViewport();
305}
306
307QTransform View::deviceTransform() const
308{
309 return m_deviceTransform;
310}
311
312void View::setDeviceTransform(const QTransform &t)
313{
314 m_deviceTransform = t;
315}
316
317void View::centerOnGeoCoordinate(QPointF geoCoord)
318{
319 const auto sceneCenter = mapGeoToScene(OSM::Coordinate(geoCoord.y(), geoCoord.x()));
320 m_viewport.moveCenter(sceneCenter);
321 constrainViewToScene();
322 updateViewport();
323}
324
325void View::updateViewport()
326{
327 // ### this fails for distances above 180° due to OSM::distance wrapping around
328 // doesn't matter for our use-case though, we are looking at much much smaller areas
329 m_screenWidthInMeters = OSM::distance(mapSceneToGeo(QPointF(m_viewport.left(), m_viewport.center().y())), mapSceneToGeo(QPointF(m_viewport.right(), m_viewport.center().y())));
330 if (m_screenWidthInMeters == 0.0) {
331 m_screenWidthInMeters = 1.0; // ensure we don't end up with a division by zero down the line
332 }
333
334 m_sceneToScreenTransform = {};
335 m_sceneToScreenTransform.scale(screenWidth() / (m_viewport.width()), screenHeight() / (m_viewport.height()));
336 m_sceneToScreenTransform.translate(-m_viewport.x(), -m_viewport.y());
337
338 m_screenToSceneTransform = m_sceneToScreenTransform.inverted();
339
340 Q_EMIT transformationChanged();
341}
342
343QDateTime View::beginTime() const
344{
345 return m_beginTime;
346}
347
348void View::setBeginTime(const QDateTime &beginTime)
349{
350 const auto alignedTime = QDateTime(beginTime.date(), {beginTime.time().hour(), beginTime.time().minute()});
351 if (m_beginTime == alignedTime) {
352 return;
353 }
354 m_beginTime = alignedTime;
355 Q_EMIT timeChanged();
356}
357
358QDateTime View::endTime() const
359{
360 return m_endTime;
361}
362
363void View::setEndTime(const QDateTime& endTime)
364{
365 const auto alignedTime = QDateTime(endTime.date(), {endTime.time().hour(), endTime.time().minute()});
366 if (m_endTime == alignedTime) {
367 return;
368 }
369 m_endTime = alignedTime;
370 Q_EMIT timeChanged();
371}
372
373#include "moc_view.cpp"
Bounding box, ie.
Definition datatypes.h:95
Coordinate, stored as 1e7 * degree to avoid floating point precision issues, and offset to unsigned v...
Definition datatypes.h:37
OSM-based multi-floor indoor maps for buildings.
KOSM_EXPORT double distance(double lat1, double lon1, double lat2, double lon2)
Distance between two coordinates.
Definition geomath.cpp:16
constexpr double radToDeg(double rad)
Radian to degree conversion.
Definition geomath.h:24
constexpr double degToRad(double deg)
Degree to radian conversion.
Definition geomath.h:19
QDateTime currentDateTime()
QDate date() const const
Q_EMITQ_EMIT
int x() const const
int y() const const
qreal x() const const
qreal y() const const
void adjust(qreal dx1, qreal dy1, qreal dx2, qreal dy2)
qreal bottom() const const
QPointF bottomLeft() const const
QPointF bottomRight() const const
qreal height() const const
qreal left() const const
qreal right() const const
void setHeight(qreal height)
void setWidth(qreal width)
qreal top() const const
QPointF topLeft() const const
QPointF topRight() const const
qreal width() const const
int height() const const
void setWidth(int width)
int width() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:03 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.