Skip to content

Package: TileGrid$Dirty

TileGrid$Dirty

nameinstructionbranchcomplexitylinemethod
static {...}
M: 21 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 4 C: 0
0%
M: 1 C: 0
0%

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