Package: TileGrid
TileGrid
name | instruction | branch | complexity | line | method | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
TileGrid(MapView, MapViewModel, ObjectProperty, TileCache) |
|
|
|
|
|
||||||||||||||||||||
addOverlay(String, Consumer) |
|
|
|
|
|
||||||||||||||||||||
applyTranslate() |
|
|
|
|
|
||||||||||||||||||||
createTile(URI) |
|
|
|
|
|
||||||||||||||||||||
createTiles() |
|
|
|
|
|
||||||||||||||||||||
getCenter() |
|
|
|
|
|
||||||||||||||||||||
lambda$createTiles$2(TilePos, URI) |
|
|
|
|
|
||||||||||||||||||||
lambda$new$0(ObservableValue, Bounds, Bounds) |
|
|
|
|
|
||||||||||||||||||||
lambda$new$1(ObservableValue, TileSource, TileSource) |
|
|
|
|
|
||||||||||||||||||||
layoutChildren() |
|
|
|
|
|
||||||||||||||||||||
onTileSourceChanged() |
|
|
|
|
|
||||||||||||||||||||
recreateOverlays() |
|
|
|
|
|
||||||||||||||||||||
removeAllOverlays() |
|
|
|
|
|
||||||||||||||||||||
removeOverlay(String) |
|
|
|
|
|
||||||||||||||||||||
setCenterAndZoom(MapCoordinates, double) |
|
|
|
|
|
||||||||||||||||||||
setDirty(TileGrid.Dirty) |
|
|
|
|
|
||||||||||||||||||||
static {...} |
|
|
|
|
|
||||||||||||||||||||
translate(double, double) |
|
|
|
|
|
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.javafx.impl;
27:
28: import jakarta.annotation.Nonnull;
29: import java.util.HashMap;
30: import java.util.Map;
31: import java.util.function.Consumer;
32: import java.net.URI;
33: import javafx.beans.property.ObjectProperty;
34: import javafx.scene.Node;
35: import javafx.scene.layout.GridPane;
36: import javafx.scene.layout.StackPane;
37: import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
38: import it.tidalwave.mapview.MapCoordinates;
39: import it.tidalwave.mapview.TileSource;
40: import it.tidalwave.mapview.impl.MapViewModel;
41: import it.tidalwave.mapview.impl.TileCache;
42: import it.tidalwave.mapview.javafx.MapView;
43: import lombok.experimental.Accessors;
44: import lombok.extern.slf4j.Slf4j;
45: import static java.lang.Double.doubleToLongBits;
46:
47: /***************************************************************************************************************************************************************
48: *
49: * A grid of tiles, used to completely fill an arbitrary area of a graphic device.
50: *
51: * @author Fabrizio Giudici
52: *
53: **************************************************************************************************************************************************************/
54: @Slf4j @Accessors(fluent = true)
55: public class TileGrid extends StackPane
56: {
57: enum Dirty
58: {
59: /** Not dirty */ NONE,
60: /** Only grid needs to be rebuilt. */ GRID,
61: /** All need to be rebuilt, */ ALL
62: }
63:
64: /** The owner component. */
65: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP2")
66: private final MapView parent;
67:
68: /** The tile source. */
69: @Nonnull
70: private final ObjectProperty<TileSource> tileSource;
71:
72: /** The model. */
73: @Nonnull
74: private final MapViewModel model;
75:
76: /** The tile cache. */
77: @Nonnull
78: private final TileCache tileCache;
79:
80: /** Whether this control needs to be redrawn. */
81: private Dirty dirty = Dirty.NONE;
82:
83: /** The map of overlays indexed by name. */
84: private final Map<String, MapOverlay> overlayByName = new HashMap<>();
85:
86: /** The container of tiles. */
87: private final GridPane tilePane = new GridPane();
88:
89: /** The container of overlays. */
90: private final StackPane overlayPane = new StackPane();
91:
92: /***********************************************************************************************************************************************************
93: * Creates a grid of tiles.
94: * @param parent the map view control
95: * @param model the map model
96: * @param tileSource the tile source
97: * @param tileCache the tile cache
98: **********************************************************************************************************************************************************/
99: @SuppressFBWarnings({"EI_EXPOSE_REP2", "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR"})
100: public TileGrid (@Nonnull final MapView parent,
101: @Nonnull final MapViewModel model,
102: @Nonnull final ObjectProperty<TileSource> tileSource,
103: @Nonnull final TileCache tileCache)
104: {
105: this.parent = parent;
106: this.tileSource = tileSource;
107: this.model = model;
108: this.tileCache = tileCache;
109: getChildren().addAll(tilePane, overlayPane);
110: parent.layoutBoundsProperty().addListener((_1, _2, _3) -> setDirty(Dirty.GRID));
111: model.setCenterAndZoom(MapCoordinates.of(0, 0), 1);
112: tileSource.addListener((_1, _2, _3) -> onTileSourceChanged());
113: }
114:
115: /***********************************************************************************************************************************************************
116: * Sets the coordinates at the center of the grid and the zoom level. This method will update the tiles in the grid with the proper URLs for the required
117: * setting. If the grid is already populated, existing tiles are recycled if possible (this is useful while moving the coordinates in order to avoid the
118: * number of tiles to download for the next position).
119: * @param center the center at the center of the tile
120: * @param zoom the zoom level
121: **********************************************************************************************************************************************************/
122: public void setCenterAndZoom (@Nonnull final MapCoordinates center, final double zoom)
123: {
124: log.debug("setCenterAndZoom({}, {})", center, zoom);
125:
126:• if (!center.equals(model.center()) || doubleToLongBits(zoom) != doubleToLongBits(model.zoom())) // defensive
127: {
128: model.setCenterAndZoom(center, zoom);
129: createTiles();
130: recreateOverlays();
131: setDirty(Dirty.ALL);
132: }
133: }
134:
135: /***********************************************************************************************************************************************************
136: * {@return the coordinates of the point at the center of the map}.
137: **********************************************************************************************************************************************************/
138: @Nonnull
139: public MapCoordinates getCenter()
140: {
141: return model.center();
142: }
143:
144: /***********************************************************************************************************************************************************
145: * Translates the tile grid. If the translation is so large that the tile at the center changes, the grid is recomputed and translated back.
146: * @param deltaX the drag in screen coordinates
147: * @param deltaY the drag in screen coordinates
148: **********************************************************************************************************************************************************/
149: public void translate (final double deltaX, final double deltaY)
150: {
151: log.trace("translate({}, {})", deltaX, deltaY);
152: final var prevTileCenter = model.tileCenter();
153: model.setCenterAndZoom(model.pointCenter().translated(-deltaX, -deltaY), model.zoom());
154: final var tileCenter = model.tileCenter();
155:
156:• if (!prevTileCenter.equals(tileCenter))
157: {
158: createTiles();
159: // no need to recreate overlays, just translate them
160: final var dX = overlayPane.getTranslateX() -(tileCenter.column - prevTileCenter.column) * tileSource.get().getTileSize();
161: final var dY = overlayPane.getTranslateY() -(tileCenter.row - prevTileCenter.row) * tileSource.get().getTileSize();
162: log.debug("translate overlays: {}, {}", dX, dY);
163: overlayPane.setTranslateX(dX);
164: overlayPane.setTranslateY(dY);
165: setDirty(Dirty.GRID);
166: }
167: else
168: {
169: applyTranslate();
170: }
171: }
172:
173: /***********************************************************************************************************************************************************
174: * Adds an overlay.
175: * @param name the name of the overlay
176: * @param creator the overlay creator
177: **********************************************************************************************************************************************************/
178: public void addOverlay (@Nonnull final String name, @Nonnull final Consumer<MapView.OverlayHelper> creator)
179: {
180: final var overlay = new MapOverlay(model, creator);
181: overlayPane.getChildren().add(overlay);
182: overlayByName.put(name, overlay);
183: overlay.create();
184: }
185:
186: /***********************************************************************************************************************************************************
187: * Removes an overlay.
188: * @param name the name of the overlay to remove
189: **********************************************************************************************************************************************************/
190: public void removeOverlay (@Nonnull final String name)
191: {
192:• if (overlayByName.containsKey(name))
193: {
194: overlayPane.getChildren().remove(overlayByName.remove(name));
195: }
196: }
197:
198: /***********************************************************************************************************************************************************
199: * Removes all overlays.
200: **********************************************************************************************************************************************************/
201: public void removeAllOverlays()
202: {
203: overlayByName.clear();
204: overlayPane.getChildren().clear();
205: }
206:
207: /***********************************************************************************************************************************************************
208: * {@inheritDoc}
209: **********************************************************************************************************************************************************/
210: @Override
211: protected void layoutChildren()
212: {
213: log.trace("layoutChildren");
214:
215:• if (dirty != Dirty.NONE && isVisible())
216: {
217: final var parentWidth = parent.getWidth();
218: final var parentHeight = parent.getHeight();
219: final var centerTileChanged = model.updateGridSize(parentWidth, parentHeight);
220: model.recompute();
221:
222:• if (centerTileChanged)
223: {
224: log.debug("new view size: {} x {}, new grid size: {} x {}", parentWidth, parentHeight, model.columns(), model.rows());
225: createTiles();
226:
227:• if (dirty == Dirty.ALL)
228: {
229: recreateOverlays();
230: }
231: }
232: else
233: {
234: applyTranslate();
235: }
236: }
237:
238: dirty = Dirty.NONE;
239: super.layoutChildren();
240: }
241:
242: /***********************************************************************************************************************************************************
243: *
244: **********************************************************************************************************************************************************/
245: private void onTileSourceChanged()
246: {
247: log.debug("onTileSourceChanged()");
248: model.setTileSource(tileSource.get());
249: createTiles();
250: setDirty(Dirty.GRID);
251: }
252:
253: /***********************************************************************************************************************************************************
254: *
255: **********************************************************************************************************************************************************/
256: private void createTiles()
257: {
258: log.debug("createTiles()");
259: tilePane.getChildren().clear();
260: model.iterateOnGrid((pos, url) -> tilePane.add(createTile(url), pos.column(), pos.row(), 1, 1));
261: applyTranslate();
262: }
263:
264: /***********************************************************************************************************************************************************
265: *
266: **********************************************************************************************************************************************************/
267: private void recreateOverlays()
268: {
269: log.debug("recreateOverlays()");
270: overlayPane.setTranslateX(0);
271: overlayPane.setTranslateY(0);
272: overlayByName.values().forEach(MapOverlay::create);
273: }
274:
275: /***********************************************************************************************************************************************************
276: *
277: **********************************************************************************************************************************************************/
278: @Nonnull
279: private Node createTile (@Nonnull final URI uri)
280: {
281: return new Tile(tileCache, tileSource.get(), uri, tileSource.get().getTileSize(), (int)model.zoom());
282: }
283:
284: /***********************************************************************************************************************************************************
285: *
286: **********************************************************************************************************************************************************/
287: private void applyTranslate()
288: {
289: setTranslateX(model.gridOffset().x());
290: setTranslateY(model.gridOffset().y());
291: }
292:
293: /***********************************************************************************************************************************************************
294: *
295: **********************************************************************************************************************************************************/
296: private void setDirty (@Nonnull final Dirty dirty)
297: {
298: this.dirty = dirty;
299: setNeedsLayout(true);
300: }
301: }