Skip to content

Package: MapViewModel

MapViewModel

nameinstructionbranchcomplexitylinemethod
MapViewModel(TileSource)
M: 0 C: 29
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 8
100%
M: 0 C: 1
100%
computeFittingZoom(MapArea)
M: 2 C: 45
96%
M: 1 C: 3
75%
M: 1 C: 2
67%
M: 1 C: 9
90%
M: 0 C: 1
100%
coordinatesToMapViewPoint(MapCoordinates)
M: 9 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
getArea()
M: 0 C: 24
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 3
100%
M: 0 C: 1
100%
getGrid()
M: 0 C: 79
100%
M: 0 C: 4
100%
M: 0 C: 3
100%
M: 0 C: 11
100%
M: 0 C: 1
100%
greaterOdd(int)
M: 0 C: 10
100%
M: 0 C: 2
100%
M: 0 C: 2
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
iterateOnGrid(BiConsumer)
M: 0 C: 33
100%
M: 0 C: 4
100%
M: 0 C: 3
100%
M: 0 C: 5
100%
M: 0 C: 1
100%
mapViewPointToCoordinates(MapViewPoint)
M: 0 C: 9
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
recompute()
M: 0 C: 114
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 7
100%
M: 0 C: 1
100%
setCenterAndZoom(MapCoordinates, double)
M: 0 C: 17
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 5
100%
M: 0 C: 1
100%
setCenterAndZoom(MapPoint, double)
M: 0 C: 18
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 5
100%
M: 0 C: 1
100%
setTileSource(TileSource)
M: 6 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 3 C: 0
0%
M: 1 C: 0
0%
static {...}
M: 0 C: 4
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
toMapPoint(MapViewPoint)
M: 0 C: 19
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
toMapViewPoint(MapPoint)
M: 20 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
updateGridSize(double, double)
M: 0 C: 58
100%
M: 1 C: 3
75%
M: 1 C: 2
67%
M: 0 C: 8
100%
M: 0 C: 1
100%

Coverage

1: /*
2: * *************************************************************************************************************************************************************
3: *
4: * MapView: a JavaFX map renderer for tile-based servers
5: * http://tidalwave.it/projects/mapview
6: *
7: * Copyright (C) 2024 - 2025 by Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *************************************************************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
12: * You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
17: * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
18: *
19: * *************************************************************************************************************************************************************
20: *
21: * git clone https://bitbucket.org/tidalwave/mapview-src
22: * git clone https://github.com/tidalwave-it/mapview-src
23: *
24: * *************************************************************************************************************************************************************
25: */
26: package it.tidalwave.mapviewer.impl;
27:
28: import jakarta.annotation.Nonnull;
29: import java.util.function.BiConsumer;
30: import java.net.URI;
31: import it.tidalwave.mapviewer.MapArea;
32: import it.tidalwave.mapviewer.MapCoordinates;
33: import it.tidalwave.mapviewer.MapPoint;
34: import it.tidalwave.mapviewer.MapViewPoint;
35: import it.tidalwave.mapviewer.TileSource;
36: import it.tidalwave.mapviewer.javafx.MapView;
37: import it.tidalwave.mapviewer.javafx.impl.TilePos;
38: import lombok.Getter;
39: import lombok.RequiredArgsConstructor;
40: import lombok.experimental.Accessors;
41: import lombok.experimental.Delegate;
42: import lombok.extern.slf4j.Slf4j;
43:
44: /***************************************************************************************************************************************************************
45: *
46: * A model (independent of UI technology) that provides parameters for implementing a tile grid based map renderer.
47: *
48: * To understand how this class works, three coordinate systems are to be understood:
49: *
50: * <ul>
51: * <li>classic <b>coordinates</b> composed of latitude and longitude, modelled by the class {@link MapCoordinates}</li>
52: * <li><b>map coordinates</b> that are pixel coordinates in the huge, untiled bitmap that represents a map at a given zoom level; they are
53: * modelled by the class {@link MapPoint}.
54: * <li><b>map view coordinates</b> that are pixel coordinates in the map view, modelled by the class {@link MapViewPoint}</li>
55: * </ul>
56: *
57: * The tile grid is always created large enough to cover the whole clip area, plus a "margin" frame of tiles of {@code MARGIN} tiles. This allows to drag
58: * the map at least by the tile size amount before being forced to reload tiles.
59: *
60: * The rendering component must:
61: *
62: * <ul>
63: * <li>call the {@link #updateGridSize(double, double)} method specifying the size of the rendering region; this method must be called again every time
64: * the rendering region changes size. It returns {@code true} when the grid has been recomputed (because it has been moved so far that a new row or
65: * column of tiles must be downloaded).</li>
66: * <li>call the {@link #setCenterAndZoom(MapCoordinates, double)} or {@link #setCenterAndZoom(MapPoint, double)} methods to set point that the center of
67: * the rendered area, using either coordinates or map points, and the zoom level.
68: * </ul>
69: *
70: * After doing that, this class computes:
71: *
72: * <ul>
73: * <li>the center point in the coordinate system ({@link #center()};</li>
74: * <li>the center point in the map coordinate system ({@link #pointCenter()})</li>
75: * <li>the coordinates (colum and row) of the tile that is rendered at the center ({@link #tileCenter())</li>
76: * <li>the offset in pixels that the center tile must be applied to ({#{@link #tileOffset()}}</li>
77: * <li>the offset in pixels that the grid must be applied to ({#{@link #gridOffset()}}</li>
78: * <li>the number of columns in the grid ({@link #columns()})</li>
79: * <li>the number of rows in the grid ({@link #rows()} ()})</li>
80: * </ul>
81: *
82: * At this point the implementor must invoke {@link #iterateOnGrid(BiConsumer)} that will call back passing the URL and the grid position for each tile.
83: *
84: * Two further methods are available:
85: *
86: * <ul>
87: * <li>{@link #getArea()} returns the coordinates of the rendered area;</li>
88: * <li>{@link #computeFittingZoom(MapArea)} returns the maximum zoom level that allows to fully render the given area.</li>
89: * </ul>
90: *
91: * @author Fabrizio Giudici
92: *
93: **************************************************************************************************************************************************************/
94: @Accessors(fluent = true) @Getter @Slf4j
95: public class MapViewModel
96: {
97: @RequiredArgsConstructor(staticName = "of")
98: static class TileInfo
99: {
100: @Delegate @Nonnull
101: private final TilePos pos;
102:
103: @Getter @Nonnull
104: private final URI uri;
105:
106: @Override @Nonnull
107: public String toString()
108: {
109: return String.format("(%d, %d) - %s", pos.column, pos.row, uri);
110: }
111: }
112:
113: private static final int MARGIN = 1;
114:
115: /** The source of tiles. */
116: @Nonnull
117: private TileSource tileSource;
118:
119: /** The zoom level.*/
120: private double zoom = 1;
121:
122: /** The coordinates rendered at the center of the map — note: this is _not_ the center of the TileGrid, since there is an offset. */
123: private MapCoordinates center = MapCoordinates.of(0, 0);
124:
125: /** The same of above, but expressed in terms of pixel coordinates relative to the map as a huge, untiled image. */
126: private MapPoint pointCenter = MapPoint.of(0, 0);
127:
128: /** The position of the tile that corresponds to the coordinates. */
129: private TilePos tileCenter;
130:
131: /** The offset inside the tile that corresponds to the coordinates. */
132: private TileOffset tileOffset = TileOffset.of(0, 0);
133:
134: private TileOffset gridOffset = TileOffset.of(0, 0);
135:
136: /** How many columns in the TileGrid. */
137: private int columns;
138:
139: /** How many rows in the TileGrid. */
140: private int rows;
141:
142: /** The width of the MapView. */
143: private double mapViewWidth;
144:
145: /** The height of the MapView. */
146: private double mapViewHeight;
147:
148: /***********************************************************************************************************************************************************
149: * @param tileSource the tile source
150: **********************************************************************************************************************************************************/
151: public MapViewModel (@Nonnull final TileSource tileSource)
152: {
153: this.tileSource = tileSource;
154: }
155:
156: /***********************************************************************************************************************************************************
157: * Changes the tile source.
158: * @param tileSource the new tile source
159: **********************************************************************************************************************************************************/
160: public void setTileSource (@Nonnull final TileSource tileSource)
161: {
162: this.tileSource = tileSource;
163: recompute();
164: }
165:
166: /***********************************************************************************************************************************************************
167: * Set the center coordinates and the zoom level.
168: * @param coordinates the coordinates
169: * @param zoom the zoom level
170: **********************************************************************************************************************************************************/
171: public void setCenterAndZoom (@Nonnull final MapCoordinates coordinates, final double zoom)
172: {
173: this.center = coordinates;
174: this.zoom = Math.floor(zoom);
175: pointCenter = tileSource.coordinatesToMapPoint(coordinates, zoom);
176: recompute();
177: }
178:
179: /***********************************************************************************************************************************************************
180: * Set the center point and the zoom level.
181: * @param mapPoint the mapPoint
182: * @param zoom the zoom level
183: **********************************************************************************************************************************************************/
184: public void setCenterAndZoom (@Nonnull final MapPoint mapPoint, final double zoom)
185: {
186: this.pointCenter = mapPoint;
187: this.zoom = Math.floor(zoom);
188: center = tileSource.mapPointToCoordinates(pointCenter, zoom);
189: recompute();
190: }
191:
192: /***********************************************************************************************************************************************************
193: * Updates the size of the grid given the size of the {@link MapView}.
194: * @param mapViewWidth the mapViewWidth of the {@code MapView}
195: * @param mapViewHeight the mapViewHeight of the {@code MapView}
196: * @return {@code true} if the grid size has changed
197: **********************************************************************************************************************************************************/
198: public boolean updateGridSize (final double mapViewWidth, final double mapViewHeight)
199: {
200: this.mapViewWidth = mapViewWidth;
201: this.mapViewHeight = mapViewHeight;
202: final var prevColumns = columns;
203: final var prevRows = rows;
204: final var tileSize = tileSource.getTileSize();
205: columns = greaterOdd((int)((mapViewWidth + tileSize - 1) / tileSize)) + MARGIN * 2;
206: rows = greaterOdd((int)((mapViewHeight + tileSize - 1) / tileSize)) + MARGIN * 2;
207:• return (prevColumns != columns) || (prevRows != rows);
208: }
209:
210: /***********************************************************************************************************************************************************
211: * Iterates over all the tiles providing the URL of the image for each tile.
212: * @param consumer the call back
213: **********************************************************************************************************************************************************/
214: public void iterateOnGrid (@Nonnull final BiConsumer<? super TilePos, ? super URI> consumer)
215: {
216: final var grid = getGrid();
217:
218:• for (int r = 0; r < grid.length; r++)
219: {
220:• for (int c = 0; c < grid[r].length; c++)
221: {
222: consumer.accept(TilePos.of(c, r), grid[r][c].uri);
223: }
224: }
225: }
226:
227: /***********************************************************************************************************************************************************
228: * {@return the zoom level to apply in order to accomodate the given area to the rendered region}.
229: * @param area the area to fit
230: **********************************************************************************************************************************************************/
231: public int computeFittingZoom (@Nonnull final MapArea area)
232: {
233: log.info("computeFittingZoom({})", area);
234: final var center = area.getCenter();
235: final var otherModel = new MapViewModel(tileSource); // a temporary model to compute various attempts
236: otherModel.updateGridSize(mapViewWidth, mapViewHeight);
237:
238:• for (int zoomAttempt = tileSource.getMaxZoomLevel(); zoomAttempt >= tileSource.getMinZoomLevel(); zoomAttempt--)
239: {
240: otherModel.setCenterAndZoom(center, zoomAttempt);
241: final var mapArea = otherModel.getArea();
242:
243:• if (mapArea.contains(area))
244: {
245: return zoomAttempt;
246: }
247: }
248:
249: return 1;
250: }
251:
252: /***********************************************************************************************************************************************************
253: * {@return the smallest rectangular area which encloses the area rendered in the map view}.
254: **********************************************************************************************************************************************************/
255: @Nonnull
256: public MapArea getArea()
257: {
258: final var nw = mapViewPointToCoordinates(MapViewPoint.of(0, 0));
259: final var se = mapViewPointToCoordinates(MapViewPoint.of(mapViewWidth, mapViewHeight));
260: return MapArea.of(nw.latitude(), se.longitude(), se.latitude(), nw.longitude());
261: }
262:
263: /***********************************************************************************************************************************************************
264: * {@return the current grid of tile info}.
265: **********************************************************************************************************************************************************/
266: @Nonnull
267: private TileInfo[][] getGrid()
268: {
269: final int max = (int)Math.pow(2, zoom);
270: // (left, top) tile must be adjusted for half the tile array size
271: final int left = tileCenter.column() - columns / 2;
272: final int top = tileCenter.row() - rows / 2; // rows go top to bottom
273: final var grid = new TileInfo[rows][columns];
274:
275:• for (int r = 0; r < rows; r++)
276: {
277:• for (int c = 0; c < columns; c++)
278: {
279: final var column = Math.floorMod(left + c, max);
280: final var row = Math.floorMod(top + r, max);
281: final var uri = tileSource.getTileUri(column, row, (int)zoom);
282: grid[r][c] = TileInfo.of(TilePos.of(column, row), uri);
283: }
284: }
285:
286: return grid;
287: }
288:
289: /***********************************************************************************************************************************************************
290: * Recomputes the tile center, offset and the grid offset.
291: **********************************************************************************************************************************************************/
292: public void recompute()
293: {
294: // both pixel and tile h-axis goes left -> right, v-axis top -> bottom
295: final var tileSize = tileSource.getTileSize();
296: tileCenter = TilePos.of((int)(pointCenter.x() / tileSize), (int)(pointCenter.y() / tileSize));
297: tileOffset = TileOffset.of(pointCenter.x() % tileSize, pointCenter.y() % tileSize);
298: gridOffset = TileOffset.of(-tileOffset.x() - tileSize * columns / 2.0 + mapViewWidth / 2 + tileSize / 2.0,
299: -tileOffset.y() - tileSize * rows / 2.0 + mapViewHeight / 2 + tileSize / 2.0);
300: log.trace("center: {}, {} - tile center: {} - tile offset: {} - grid offset: {}", center, pointCenter, tileCenter, tileOffset, gridOffset);
301: }
302:
303: /***********************************************************************************************************************************************************
304: * {@return the point relative to the map view corresponding to the given coordinates}.
305: * @param coordinates the coordinates
306: **********************************************************************************************************************************************************/
307: @Nonnull
308: public MapViewPoint coordinatesToMapViewPoint (@Nonnull final MapCoordinates coordinates)
309: {
310: return toMapViewPoint(tileSource.coordinatesToMapPoint(coordinates, zoom));
311: }
312:
313: /***********************************************************************************************************************************************************
314: * {@return the coordinates corresponding to the given mapViewPoint on the map viewer}.
315: * @param mapViewPoint the mapViewPoint relative to the map view: (0,0) is the top left and (w,h) is the bottom right
316: **********************************************************************************************************************************************************/
317: @Nonnull
318: public MapCoordinates mapViewPointToCoordinates (@Nonnull final MapViewPoint mapViewPoint)
319: {
320: return tileSource.mapPointToCoordinates(toMapPoint(mapViewPoint), zoom);
321: }
322:
323: /***********************************************************************************************************************************************************
324: * {@return a point in map view coordinates corresponding to a point in map coordinates}.
325: * @param mapPoint the point
326: **********************************************************************************************************************************************************/
327: @Nonnull
328: private MapViewPoint toMapViewPoint (@Nonnull final MapPoint mapPoint)
329: {
330: return MapViewPoint.of(mapPoint.translated(mapViewWidth / 2 - pointCenter.x(), mapViewHeight / 2 - pointCenter().y()));
331: }
332:
333: /***********************************************************************************************************************************************************
334: * {@return a point in map coordinates corresponding to a point in map view coordinates}.
335: * @param mapViewPoint the point
336: **********************************************************************************************************************************************************/
337: @Nonnull
338: private MapPoint toMapPoint (@Nonnull final MapViewPoint mapViewPoint)
339: {
340: return mapViewPoint.translated(pointCenter.x() - mapViewWidth / 2, pointCenter.y() - mapViewHeight / 2);
341: }
342:
343: /***********************************************************************************************************************************************************
344: * {@return the first greater odd integer of the given number}.
345: * @param n the number
346: **********************************************************************************************************************************************************/
347: /* visible for testing */ static int greaterOdd (final int n)
348: {
349:• return n + ((n % 2 == 0) ? 1 : 0);
350: }
351: }
352: