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