Skip to content

Package: MapView$Options

MapView$Options

nameinstructionbranchcomplexitylinemethod
MapView.Options(Path, boolean, int, int, Supplier, Function)
M: 0 C: 21
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
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.mapview.javafx;
27:
28: import jakarta.annotation.Nonnull;
29: import java.util.ArrayList;
30: import java.util.Collection;
31: import java.util.List;
32: import java.util.concurrent.ExecutorService;
33: import java.util.concurrent.Executors;
34: import java.util.function.BiConsumer;
35: import java.util.function.Consumer;
36: import java.util.function.Function;
37: import java.util.function.Supplier;
38: import java.nio.file.Path;
39: import javafx.animation.Interpolatable;
40: import javafx.animation.Interpolator;
41: import javafx.animation.KeyFrame;
42: import javafx.animation.KeyValue;
43: import javafx.animation.Timeline;
44: import javafx.beans.property.DoubleProperty;
45: import javafx.beans.property.ObjectProperty;
46: import javafx.beans.property.ReadOnlyDoubleProperty;
47: import javafx.beans.property.ReadOnlyListProperty;
48: import javafx.beans.property.SimpleDoubleProperty;
49: import javafx.beans.property.SimpleListProperty;
50: import javafx.beans.property.SimpleObjectProperty;
51: import javafx.collections.ObservableList;
52: import javafx.scene.Node;
53: import javafx.scene.image.Image;
54: import javafx.scene.input.InputEvent;
55: import javafx.scene.input.MouseEvent;
56: import javafx.scene.input.ScrollEvent;
57: import javafx.scene.input.ZoomEvent;
58: import javafx.scene.layout.AnchorPane;
59: import javafx.scene.layout.Region;
60: import javafx.util.Duration;
61: import javafx.application.Platform;
62: import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
63: import it.tidalwave.mapview.MapArea;
64: import it.tidalwave.mapview.MapCoordinates;
65: import it.tidalwave.mapview.MapViewPoint;
66: import it.tidalwave.mapview.OpenStreetMapTileSource;
67: import it.tidalwave.mapview.TileSource;
68: import it.tidalwave.mapview.impl.MapViewModel;
69: import it.tidalwave.mapview.impl.RangeLimitedDoubleProperty;
70: import it.tidalwave.mapview.impl.TileCache;
71: import it.tidalwave.mapview.javafx.impl.TileGrid;
72: import it.tidalwave.mapview.javafx.impl.Translation;
73: import org.apiguardian.api.API;
74: import lombok.Getter;
75: import lombok.RequiredArgsConstructor;
76: import lombok.Setter;
77: import lombok.With;
78: import lombok.experimental.Accessors;
79: import lombok.extern.slf4j.Slf4j;
80: import static org.apiguardian.api.API.Status.STABLE;
81: import static java.lang.Double.doubleToLongBits;
82: import static javafx.collections.FXCollections.observableList;
83: import static javafx.util.Duration.ZERO;
84:
85: /***************************************************************************************************************************************************************
86: *
87: * A JavaFX control capable to render a map based on tiles. It must be associated to a {@link TileSource} that provides the tile bitmaps; two instances are
88: * provided, {@link OpenStreetMapTileSource} and {@link it.tidalwave.mapview.OpenTopoMapTileSource}. Further sources can be easily implemented by overriding
89: * {@link it.tidalwave.mapview.spi.TileSourceSupport}.
90: * The basic properties of a {@code MapView} are:
91: *
92: * <ul>
93: * <li>{@link #tileSourceProperty()}: the tile source (that can be changed during the life of {@code MapView});</li>
94: * <li>{@link #centerProperty()}: the coordinates that are rendered at the center of the screen;</li>
95: * <li>{@link #zoomProperty()}: the detail level for the map, going from 1 (the lowest) to a value depending on the tile source.</li>
96: * </ul>
97: *
98: * Other properties are:
99: *
100: * <ul>
101: * <li>{@link #minZoomProperty()} (read only): the minimum zoom level allowed;</li>
102: * <li>{@link #maxZoomProperty()} (read only): the maximum zoom level allowed;</li>
103: * <li>{@link #mouseCoordinatesProperty()} (read only): the coordinates corresponding to the point where the mouse is;</li>
104: * <li>{@link #areaProperty()} (read only): the rectangular area delimited by north, east, south, west coordinates that is currently rendered.</li>
105: * </ul>
106: *
107: * The method {@link #fitArea(MapArea)} can be used to adapt rendering parameters so that the given area is rendered; this is useful e.g. when one wants to
108: * render a GPS track.
109: *
110: * Maps can be scrolled by dragging and re-centered by double-clicking on a point. In general, it is possible to associate various behaviours with common
111: * gestures thanks to the methods:
112: *
113: * <ul>
114: * <li>{@link #setSingleClickBehaviour(BiConsumer)}</li>
115: * <li>{@link #setDoubleClickBehaviour(BiConsumer)}</li>
116: * <li>{@link #setDragBehaviour(BiConsumer)}</li>
117: * <li>{@link #setScrollBehaviour(BiConsumer)}</li>
118: * </ul>
119: *
120: * and the pre-defined behaviours:
121: *
122: * <u>
123: * <li>{@link #DO_NOTHING}</li>
124: * <li>{@link #RECENTER}</li>
125: * <li>{@link #TRANSLATE}</li>
126: * <li>{@link #ZOOM}</li>
127: * </u>
128: *
129: * It is possible to add and remove overlays that move in solid with the map:
130: *
131: * <ul>
132: * <li>{@link #addOverlay(String, Consumer)}</li>
133: * <li>{@link #removeOverlay(String)}</li>
134: * <li>{@link #removeAllOverlays()}</li>
135: * </ul>
136: *
137: * @see OpenStreetMapTileSource
138: * @see it.tidalwave.mapview.OpenTopoMapTileSource
139: *
140: * @author Fabrizio Giudici
141: *
142: **************************************************************************************************************************************************************/
143: @API(status = STABLE)
144: @Slf4j
145: public class MapView extends Region
146: {
147: /** Behaviour that does nothing. @since 1.0-ALPHA-4 */
148: public static final BiConsumer<MapView, InputEvent> DO_NOTHING = (_1, _2) -> {};
149:
150: /** Behaviour that re-centers the map on the point where the mouse is. @since 1.0-ALPHA-4 */
151: public static final BiConsumer<MapView, InputEvent> RECENTER = MapView::recenter;
152:
153: /** Behaviour that zooms the map on the point where the mouse is. @since 1.0-ALPHA-4 */
154: public static final BiConsumer<MapView, InputEvent> ZOOM = MapView::zoom;
155:
156: /** Behaviour that translates the map of the same amount f mouse drag. @since 1.0-ALPHA-4 */
157: public static final BiConsumer<MapView, InputEvent> TRANSLATE = MapView::translate;
158:
159: private static final int DEFAULT_TILE_POOL_SIZE = 10;
160: private static final int DEFAULT_TILE_QUEUE_CAPACITY = 1000;
161: private static final OpenStreetMapTileSource DEFAULT_TILE_SOURCE = new OpenStreetMapTileSource();
162:
163: /** The placeholder used while the tile image has not been loaded yet. */
164: private static final Supplier<Image> WAITING_IMAGE = () -> new Image(MapView.class.getResource("/hold-on.gif").toExternalForm());
165:
166: /***********************************************************************************************************************************************************
167: * This helper class provides methods useful for creating map overlays.
168: **********************************************************************************************************************************************************/
169: @API(status = STABLE)
170: @RequiredArgsConstructor(staticName = "of") @Accessors(fluent = true)
171: public static class OverlayHelper
172: {
173: @Nonnull
174: private final MapViewModel model;
175:
176: @Nonnull
177: private final ObservableList<Node> children;
178:
179: /*******************************************************************************************************************************************************
180: * Adds a {@link Node} to the overlay.
181: * @param node the {@code Node}
182: ******************************************************************************************************************************************************/
183: public void add (@Nonnull final Node node)
184: {
185: children.add(node);
186: }
187:
188: /*******************************************************************************************************************************************************
189: * Adds multiple {@link Node}s to the overlay.
190: * @param nodes the {@code Node}s
191: ******************************************************************************************************************************************************/
192: public void addAll (@Nonnull final Collection<? extends Node> nodes)
193: {
194: children.addAll(nodes);
195: }
196:
197: /*******************************************************************************************************************************************************
198: * {@return a map view point corresponding to the given coordinates}. This view point must be used to draw to the overlay.
199: * @param coordinates the coordinates
200: ******************************************************************************************************************************************************/
201: @Nonnull
202: public MapViewPoint toMapViewPoint (@Nonnull final MapCoordinates coordinates)
203: {
204: final var gridOffset = model.gridOffset();
205: final var point = model.coordinatesToMapViewPoint(coordinates);
206: return MapViewPoint.of(point.x() - gridOffset.x(), point.y() - gridOffset.y());
207: }
208:
209: /*******************************************************************************************************************************************************
210: * {@return the coordinates corresponding at the center of the map view}.
211: ******************************************************************************************************************************************************/
212: @Nonnull
213: public MapCoordinates getCenter()
214: {
215: return model.center();
216: }
217:
218: /*******************************************************************************************************************************************************
219: * {@return the zoom level}.
220: ******************************************************************************************************************************************************/
221: public double getZoom()
222: {
223: return model.zoom();
224: }
225:
226: /*******************************************************************************************************************************************************
227: * {@return the area rendered in the map view}.
228: ******************************************************************************************************************************************************/
229: @Nonnull
230: public MapArea getArea()
231: {
232: return model.getArea();
233: }
234: }
235:
236: /***********************************************************************************************************************************************************
237: * Options for creating a {@code MapView}. Don't directly create an instance of this class, but use {@link MapView#options()} and then set the desired
238: * attributes with a {@code with*()} method.
239: * @param cacheFolder the {@link Path} of the folder where cached tiles are stored
240: * @param downloadAllowed whether downloading tiles is allowed
241: * @param poolSize the number of parallel thread of the tile downloader
242: * @param tileQueueCapacity the capacity of the tile queue
243: * @param waitingImage a {@link Supplier} of the image to be rendered while the tile bitmap has not been downloaded yet
244: * @param executorService the {@link ExecutorService} to load tiles in backgrounds
245: **********************************************************************************************************************************************************/
246: @API(status = STABLE)
247: @With
248: public record Options(@Nonnull Path cacheFolder,
249: boolean downloadAllowed,
250: int poolSize,
251: int tileQueueCapacity,
252: @Nonnull Supplier<Image> waitingImage,
253: @Nonnull Function<Integer, ExecutorService> executorService) {}
254:
255: /** The tile source. */
256: @Nonnull
257: private final SimpleObjectProperty<TileSource> tileSource;
258:
259: /** The coordinates at the center of the map view. */
260: @Nonnull
261: private final SimpleObjectProperty<MapCoordinates> center;
262:
263: /** The zoom level. */
264: @Nonnull
265: private final RangeLimitedDoubleProperty zoom;
266:
267: /** The minimum zoom level. */
268: @Nonnull
269: private final SimpleDoubleProperty minZoom;
270:
271: /** The maximum zoom level. */
272: @Nonnull
273: private final SimpleDoubleProperty maxZoom;
274:
275: /** The coordinates corresponding to the mouse position on the map. */
276: @Nonnull
277: private final SimpleObjectProperty<MapCoordinates> mouseCoordinates;
278:
279: /** The rectangular area in the view. */
280: @Nonnull
281: private final SimpleObjectProperty<MapArea> area;
282:
283: /** The list of names of overlays. */
284: private final SimpleListProperty<String> overlayNamesProperty = new SimpleListProperty<>(observableList(new ArrayList<>()));
285:
286: /** The model for this control. */
287: @Nonnull
288: private final MapViewModel model;
289:
290: /** The tile grid that the rendering relies upon. */
291: @Nonnull
292: private final TileGrid tileGrid;
293:
294: /** A cache for tiles. */
295: @Nonnull
296: private final TileCache tileCache;
297:
298: /** What to do in case of single click. */
299: @Getter @Setter
300: private BiConsumer<MapView, InputEvent> singleClickBehaviour = DO_NOTHING;
301:
302: /** What to do in case of double click. */
303: @Getter @Setter
304: private BiConsumer<MapView, InputEvent> doubleClickBehaviour = DO_NOTHING;
305:
306: /** What to do in case of drag. */
307: @Getter @Setter
308: private BiConsumer<MapView, InputEvent> dragBehaviour = TRANSLATE;
309:
310: /** What to do in case of scroll. */
311: @Getter @Setter
312: private BiConsumer<MapView, InputEvent> scrollBehaviour = DO_NOTHING;
313:
314: /** The duration of the re-centering animation. */
315: @Getter @Setter
316: private Duration recenterDuration = Duration.millis(200);
317:
318: /** True if a zoom operation is in progress. */
319: private boolean zooming;
320:
321: /** True if a drag operation is in progress. */
322: private boolean dragging;
323:
324: /** The latest x coordinate in drag. */
325: private double dragX;
326:
327: /** The latest y coordinate in drag. */
328: private double dragY;
329:
330: /** The latest value in scroll. */
331: private double scroll;
332:
333: /** A guard to manage reentrant calls to {@link #setCenterAndZoom(MapCoordinates, double)}. */
334: private boolean reentrantGuard;
335:
336: /***********************************************************************************************************************************************************
337: * Creates a new instance.
338: * @param options options for the control
339: **********************************************************************************************************************************************************/
340: @SuppressWarnings("this-escape") @SuppressFBWarnings({"MALICIOUS_CODE", "CT_CONSTRUCTOR_THROW"})
341: public MapView (@Nonnull final Options options)
342: {
343: if (!Platform.isFxApplicationThread())
344: {
345: throw new IllegalStateException("Must be instantiated on JavaFX thread");
346: }
347:
348: tileSource = new SimpleObjectProperty<>(this, "tileSource", DEFAULT_TILE_SOURCE);
349: model = new MapViewModel(tileSource.get());
350: tileCache = new TileCache(options);
351: tileGrid = new TileGrid(this, model, tileSource, tileCache);
352: center = new SimpleObjectProperty<>(this, "center", tileGrid.getCenter());
353: zoom = new RangeLimitedDoubleProperty(this, "zoom", model.zoom(), tileSource.get().getMinZoomLevel(), tileSource.get().getMaxZoomLevel());
354: minZoom = new SimpleDoubleProperty(this, "minZoom", tileSource.get().getMinZoomLevel());
355: maxZoom = new SimpleDoubleProperty(this, "maxZoom", tileSource.get().getMaxZoomLevel());
356: mouseCoordinates = new SimpleObjectProperty<>(this, "mouseCoordinates", MapCoordinates.of(0, 0));
357: area = new SimpleObjectProperty<>(this, "area", MapArea.of(0, 0, 0, 0));
358: tileSource.addListener((_1, _2, _3) -> onTileSourceChanged());
359: center.addListener((_1, _2, newValue) -> setCenterAndZoom(newValue, zoom.get()));
360: zoom.addListener((_1, _2, newValue) -> setCenterAndZoom(center.get(), newValue.intValue()));
361: getChildren().add(tileGrid);
362: AnchorPane.setLeftAnchor(tileGrid, 0d);
363: AnchorPane.setRightAnchor(tileGrid, 0d);
364: AnchorPane.setTopAnchor(tileGrid, 0d);
365: AnchorPane.setBottomAnchor(tileGrid, 0d);
366: setOnMouseClicked(this::onMouseClicked);
367: setOnZoom(this::onZoom);
368: setOnZoomStarted(this::onZoomStarted);
369: setOnZoomFinished(this::onZoomFinished);
370: setOnMouseMoved(this::onMouseMoved);
371: setOnScroll(this::onScroll);
372: tileGrid.setOnMousePressed(this::onMousePressed);
373: tileGrid.setOnMouseReleased(this::onMouseReleased);
374: tileGrid.setOnMouseDragged(this::onMouseDragged);
375: }
376:
377: /***********************************************************************************************************************************************************
378: * {@return a new set of default options}.
379: * @see Options
380: **********************************************************************************************************************************************************/
381: @Nonnull
382: public static Options options()
383: {
384: return new Options(Path.of(System.getProperty("java.io.tmpdir")),
385: true,
386: DEFAULT_TILE_POOL_SIZE,
387: DEFAULT_TILE_QUEUE_CAPACITY,
388: WAITING_IMAGE,
389: Executors::newFixedThreadPool);
390: }
391:
392: /***********************************************************************************************************************************************************
393: * {@return the tile source}.
394: * @see #setTileSource(TileSource)
395: * @see #tileSourceProperty()
396: **********************************************************************************************************************************************************/
397: @Nonnull
398: public final TileSource getTileSource()
399: {
400: return tileSource.get();
401: }
402:
403: /***********************************************************************************************************************************************************
404: * Sets the tile source. Changing the tile source might change the zoom level to make sure it is within the limits of the new source.
405: * @param tileSource the tile source
406: * @see #getTileSource()
407: * @see #tileSourceProperty()
408: **********************************************************************************************************************************************************/
409: public final void setTileSource (@Nonnull final TileSource tileSource)
410: {
411: this.tileSource.set(tileSource);
412: }
413:
414: /***********************************************************************************************************************************************************
415: * {@return the tile source property}.
416: * @see #setTileSource(TileSource)
417: * @see #getTileSource()
418: **********************************************************************************************************************************************************/
419: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
420: public final ObjectProperty<TileSource> tileSourceProperty()
421: {
422: return tileSource;
423: }
424:
425: /***********************************************************************************************************************************************************
426: * {@return the center coordinates}.
427: * @see #setCenter(MapCoordinates)
428: * @see #centerProperty()
429: **********************************************************************************************************************************************************/
430: @Nonnull
431: public final MapCoordinates getCenter()
432: {
433: return center.get();
434: }
435:
436: /***********************************************************************************************************************************************************
437: * Sets the coordinates to show at the center of the map.
438: * @param center the center coordinates
439: * @see #getCenter()
440: * @see #centerProperty()
441: **********************************************************************************************************************************************************/
442: public final void setCenter (@Nonnull final MapCoordinates center)
443: {
444: this.center.set(center);
445: }
446:
447: /***********************************************************************************************************************************************************
448: * {@return the center property}.
449: * @see #getCenter()
450: * @see #setCenter(MapCoordinates)
451: **********************************************************************************************************************************************************/
452: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
453: public final ObjectProperty<MapCoordinates> centerProperty()
454: {
455: return center;
456: }
457:
458: /***********************************************************************************************************************************************************
459: * {@return the zoom level}.
460: * @see #setZoom(double)
461: * @see #zoomProperty()
462: **********************************************************************************************************************************************************/
463: public final double getZoom()
464: {
465: return zoom.get();
466: }
467:
468: /***********************************************************************************************************************************************************
469: * Sets the zoom level.
470: * @param zoom the zoom level
471: * @see #getZoom()
472: * @see #zoomProperty()
473: **********************************************************************************************************************************************************/
474: public final void setZoom (final double zoom)
475: {
476: this.zoom.set(zoom);
477: }
478:
479: /***********************************************************************************************************************************************************
480: * {@return the zoom level property}.
481: * @see #getZoom()
482: * @see #setZoom(double)
483: **********************************************************************************************************************************************************/
484: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
485: public final DoubleProperty zoomProperty()
486: {
487: return zoom;
488: }
489:
490: /***********************************************************************************************************************************************************
491: * {@return the min zoom level}.
492: * @see #minZoomProperty()
493: **********************************************************************************************************************************************************/
494: public final double getMinZoom()
495: {
496: return minZoom.get();
497: }
498:
499: /***********************************************************************************************************************************************************
500: * {@return the min zoom level property}.
501: * @see #getMinZoom()
502: **********************************************************************************************************************************************************/
503: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
504: public final ReadOnlyDoubleProperty minZoomProperty()
505: {
506: return minZoom;
507: }
508:
509: /***********************************************************************************************************************************************************
510: * {@return the max zoom level}.
511: * @see #maxZoomProperty()
512: **********************************************************************************************************************************************************/
513: public final double getMaxZoom()
514: {
515: return maxZoom.get();
516: }
517:
518: /***********************************************************************************************************************************************************
519: * {@return the max zoom level property}.
520: * @see #getMaxZoom()
521: **********************************************************************************************************************************************************/
522: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
523: public final ReadOnlyDoubleProperty maxZoomProperty()
524: {
525: return maxZoom;
526: }
527:
528: /***********************************************************************************************************************************************************
529: * {@return the coordinates corresponding to the point where the mouse is}.
530: **********************************************************************************************************************************************************/
531: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
532: public final ObjectProperty<MapCoordinates> mouseCoordinatesProperty ()
533: {
534: return mouseCoordinates;
535: }
536:
537: /***********************************************************************************************************************************************************
538: * {@return the area rendered on the map}.
539: * @see #areaProperty()
540: * @see #fitArea(MapArea)
541: **********************************************************************************************************************************************************/
542: @Nonnull
543: public final MapArea getArea()
544: {
545: return area.get();
546: }
547:
548: /***********************************************************************************************************************************************************
549: * {@return the area rendered on the map}.
550: * @see #getArea()
551: * @see #fitArea(MapArea)
552: **********************************************************************************************************************************************************/
553: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
554: public final ObjectProperty<MapArea> areaProperty()
555: {
556: return area;
557: }
558:
559: /***********************************************************************************************************************************************************
560: * Fits the zoom level and centers the map so that the two corners are visible.
561: * @param area the area to fit
562: * @see #getArea()
563: * @see #areaProperty()
564: **********************************************************************************************************************************************************/
565: public void fitArea (@Nonnull final MapArea area)
566: {
567: log.debug("fitArea({})", area);
568: setCenterAndZoom(area.getCenter(), model.computeFittingZoom(area));
569: }
570:
571: /***********************************************************************************************************************************************************
572: * {@return the scale of the map in meters per pixel}.
573: **********************************************************************************************************************************************************/
574: // @Nonnegative
575: public double getMetersPerPixel()
576: {
577: return tileSource.get().metersPerPixel(tileGrid.getCenter(), zoom.get());
578: }
579:
580: /***********************************************************************************************************************************************************
581: * {@return a point on the map corresponding to the given coordinates}.
582: * @param coordinates the coordinates
583: **********************************************************************************************************************************************************/
584: @Nonnull
585: public MapViewPoint coordinatesToPoint (@Nonnull final MapCoordinates coordinates)
586: {
587: return model.coordinatesToMapViewPoint(coordinates);
588: }
589:
590: /***********************************************************************************************************************************************************
591: * {@return the coordinates corresponding to a given point on the map}.
592: * @param point the point on the map
593: **********************************************************************************************************************************************************/
594: @Nonnull
595: public MapCoordinates pointToCoordinates (@Nonnull final MapViewPoint point)
596: {
597: return model.mapViewPointToCoordinates(point);
598: }
599:
600: /***********************************************************************************************************************************************************
601: * Adds an overlay, passing a callback that will be responsible for rendering the overlay, when needed.
602: * @param name the name of the overlay
603: * @param creator the overlay creator
604: * @see OverlayHelper
605: **********************************************************************************************************************************************************/
606: public void addOverlay (@Nonnull final String name, @Nonnull final Consumer<OverlayHelper> creator)
607: {
608: tileGrid.addOverlay(name, creator);
609: overlayNamesProperty.add(name);
610: }
611:
612: /***********************************************************************************************************************************************************
613: * Removes an overlay.
614: * @param name the name of the overlay to remove
615: **********************************************************************************************************************************************************/
616: public void removeOverlay (@Nonnull final String name)
617: {
618: tileGrid.removeOverlay(name);
619: overlayNamesProperty.remove(name);
620: }
621:
622: /***********************************************************************************************************************************************************
623: * Removes all overlays.
624: **********************************************************************************************************************************************************/
625: public void removeAllOverlays()
626: {
627: tileGrid.removeAllOverlays();
628: overlayNamesProperty.clear();
629: }
630:
631: /***********************************************************************************************************************************************************
632: * {@return a list of overlay names}.
633: * @since 1.0-ALPHA-3
634: **********************************************************************************************************************************************************/
635: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
636: public final ReadOnlyListProperty<String> getOverlayNamesProperty()
637: {
638: return overlayNamesProperty;
639: }
640:
641: /***********************************************************************************************************************************************************
642: * {@return a list of overlay names}.
643: * @since 1.0-ALPHA-3
644: **********************************************************************************************************************************************************/
645: @Nonnull
646: public final List<String> getOverlayNames()
647: {
648: return overlayNamesProperty.get();
649: }
650:
651: /***********************************************************************************************************************************************************
652: * Sets both the center and the zoom level. This method has got a reentrant protection since it touches the {@link #center} and {@link #zoom} properties,
653: * that in turn will fire events that call back this method, the first time with the previous zoom level. There's no way to change them atomically.
654: * @param center the center
655: * @param zoom the zoom level
656: **********************************************************************************************************************************************************/
657: private void setCenterAndZoom (@Nonnull final MapCoordinates center, final double zoom)
658: {
659: if (!reentrantGuard)
660: {
661: try
662: {
663: reentrantGuard = true;
664: log.trace("setCenterAndZoom({}, {})", center, zoom);
665:
666: if (!center.equals(tileGrid.getCenter()) || doubleToLongBits(zoom) != doubleToLongBits(model.zoom()))
667: {
668: tileCache.retainPendingTiles((int)zoom);
669: tileGrid.setCenterAndZoom(center, zoom);
670: this.center.set(center);
671: this.zoom.set(zoom);
672: area.set(model.getArea());
673: }
674: }
675: finally // defensive
676: {
677: reentrantGuard = false;
678: }
679: }
680: }
681:
682: /***********************************************************************************************************************************************************
683: * Translate the map center by the specified amount.
684: * @param dx the horizontal amount
685: * @param dy the vertical amount
686: **********************************************************************************************************************************************************/
687: private void translateCenter (final double dx, final double dy)
688: {
689: tileGrid.translate(dx, dy);
690: center.set(model.center());
691: area.set(model.getArea());
692: }
693:
694: /***********************************************************************************************************************************************************
695: * This method is called when the tile source has been changed.
696: **********************************************************************************************************************************************************/
697: private void onTileSourceChanged()
698: {
699: final var minZoom = tileSourceProperty().get().getMinZoomLevel();
700: final var maxZoom = tileSourceProperty().get().getMaxZoomLevel();
701: zoom.setLimits(minZoom, maxZoom);
702: this.minZoom.set(minZoom);
703: this.maxZoom.set(maxZoom);
704: setNeedsLayout(true);
705: }
706:
707: /***********************************************************************************************************************************************************
708: * Mouse callback.
709: **********************************************************************************************************************************************************/
710: private void onMousePressed (@Nonnull final MouseEvent event)
711: {
712: if (!zooming)
713: {
714: dragging = true;
715: dragX = event.getSceneX();
716: dragY = event.getSceneY();
717: log.trace("onMousePressed: {} {}", dragX, dragY);
718: }
719: }
720:
721: /***********************************************************************************************************************************************************
722: * Mouse callback.
723: **********************************************************************************************************************************************************/
724: private void onMouseReleased (@Nonnull final MouseEvent ignored)
725: {
726: log.trace("onMouseReleased");
727: dragging = false;
728: }
729:
730: /***********************************************************************************************************************************************************
731: * Mouse callback.
732: **********************************************************************************************************************************************************/
733: private void onMouseDragged (@Nonnull final MouseEvent event)
734: {
735: if (!zooming && dragging)
736: {
737: translate(event);
738: dragX = event.getSceneX();
739: dragY = event.getSceneY();
740: }
741: }
742:
743: /***********************************************************************************************************************************************************
744: * Mouse callback.
745: **********************************************************************************************************************************************************/
746: private void onMouseClicked (@Nonnull final MouseEvent event)
747: {
748: log.trace("onMouseClicked({}, {}, {})", event.getY(), event.getY(), event.getClickCount());
749:
750: switch (event.getClickCount())
751: {
752: case 1:
753: singleClickBehaviour.accept(this, event);
754: break;
755:
756: case 2:
757: doubleClickBehaviour.accept(this, event);
758: break;
759: }
760: }
761:
762: /***********************************************************************************************************************************************************
763: * Mouse callback.
764: **********************************************************************************************************************************************************/
765: private void onMouseMoved (@Nonnull final MouseEvent event)
766: {
767: mouseCoordinates.set(pointToCoordinates(MapViewPoint.of(event)));
768: }
769:
770: /***********************************************************************************************************************************************************
771: * Gesture callback.
772: **********************************************************************************************************************************************************/
773: private void onZoomStarted (@Nonnull final ZoomEvent event)
774: {
775: log.trace("onZoomStarted({})", event);
776: zooming = true;
777: dragging = false;
778: }
779:
780: /***********************************************************************************************************************************************************
781: * Gesture callback.
782: **********************************************************************************************************************************************************/
783: private void onZoomFinished (@Nonnull final ZoomEvent event)
784: {
785: log.trace("onZoomFinished({})", event);
786: zooming = false;
787: }
788:
789: /***********************************************************************************************************************************************************
790: * Gesture callback.
791: **********************************************************************************************************************************************************/
792: private void onZoom (@Nonnull final ZoomEvent event)
793: {
794: log.trace("onZoom({})", event);
795: }
796:
797: /***********************************************************************************************************************************************************
798: * Mouse callback.
799: **********************************************************************************************************************************************************/
800: private void onScroll (@Nonnull final ScrollEvent event)
801: {
802: log.trace("onScroll({})", event);
803: scrollBehaviour.accept(this, event);
804: }
805:
806: /***********************************************************************************************************************************************************
807: * Re-centers on the point where the mouse has been clicked.
808: **********************************************************************************************************************************************************/
809: private void recenter (@Nonnull final InputEvent event)
810: {
811: if (event instanceof final MouseEvent gestureEvent)
812: {
813: final var delta = Translation.of(getWidth() / 2 - gestureEvent.getX(), getHeight() / 2 - gestureEvent.getY());
814: final var target = new SimpleObjectProperty<>(Translation.of(0, 0));
815: target.addListener((__, oldValue, newValue) ->
816: translateCenter(newValue.x() - oldValue.x(), newValue.y() - oldValue.y()));
817: animate(target, Translation.of(0, 0), delta, recenterDuration);
818: }
819: }
820:
821: /***********************************************************************************************************************************************************
822: * Re-centers on the point where the mouse has been clicked.
823: **********************************************************************************************************************************************************/
824: private void zoom (@Nonnull final InputEvent event)
825: {
826: if (event instanceof final ScrollEvent scrollEvent)
827: {
828: final var amount = -Math.signum(Math.floor(scrollEvent.getDeltaY() - scroll));
829: scroll = scrollEvent.getDeltaY();
830: log.debug("zoom change for scroll: {}", amount);
831: zoom.set(Math.round(zoom.get() + amount));
832: }
833: }
834:
835: /***********************************************************************************************************************************************************
836: * Translates the map.
837: **********************************************************************************************************************************************************/
838: private void translate (@Nonnull final InputEvent event)
839: {
840: if (event instanceof final MouseEvent mouseEvent)
841: {
842: translateCenter(mouseEvent.getSceneX() - dragX, mouseEvent.getSceneY() - dragY);
843: }
844: }
845:
846: /***********************************************************************************************************************************************************
847: * Animates a property. If the duration is zero, the property is immediately set.
848: * @param <T> the static type of the property to animate
849: * @param target the property to animate
850: * @param startValue the start value of the property
851: * @param endValue the end value of the property
852: * @param duration the duration of the animation
853: **********************************************************************************************************************************************************/
854: private static <T extends Interpolatable<T>> void animate (@Nonnull final ObjectProperty<T> target,
855: @Nonnull final T startValue,
856: @Nonnull final T endValue,
857: @Nonnull final Duration duration)
858: {
859: if (duration.equals(ZERO))
860: {
861: target.set(endValue);
862: }
863: else
864: {
865: final var start = new KeyFrame(ZERO, new KeyValue(target, startValue));
866: final var end = new KeyFrame(duration, new KeyValue(target, endValue, Interpolator.EASE_OUT));
867: new Timeline(start, end).play();
868: }
869: }
870:
871: // FIXME: on close shut down the tile cache executor service.
872: }