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