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