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