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