Skip to contentMethod: getArea()
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 area rendered in the map view}.
163: ******************************************************************************************************************************************************/
164: @Nonnull
165: public MapArea getArea()
166: {
167: return model.getArea();
168: }
169: }
170:
171: /***********************************************************************************************************************************************************
172: * Options for creating a {@code MapView}.
173: * @param cacheFolder the {@link Path} of the folder where cached tiles are stored
174: * @param downloadAllowed whether downloading tiles is allowed
175: * @param poolSize the number of parallel thread of the tile downloader
176: * @param tileQueueCapacity the capacity of the tile queue
177: **********************************************************************************************************************************************************/
178: @With
179: public record Options(@Nonnull Path cacheFolder, boolean downloadAllowed, int poolSize, int tileQueueCapacity) {}
180:
181: /** The tile source. */
182: @Nonnull
183: private final SimpleObjectProperty<TileSource> tileSource;
184:
185: /** The coordinates at the center of the map view. */
186: @Nonnull
187: private final SimpleObjectProperty<MapCoordinates> center;
188:
189: /** The zoom level. */
190: @Nonnull
191: private final RangeLimitedDoubleProperty zoom;
192:
193: /** The minimum zoom level. */
194: @Nonnull
195: private final SimpleDoubleProperty minZoom;
196:
197: /** The maximum zoom level. */
198: @Nonnull
199: private final SimpleDoubleProperty maxZoom;
200:
201: /** The coordinates corresponding to the mouse position on the map. */
202: @Nonnull
203: private final SimpleObjectProperty<MapCoordinates> coordinatesUnderMouse;
204:
205: /** The rectangular area in the view. */
206: @Nonnull
207: private final SimpleObjectProperty<MapArea> area;
208:
209: /** The model for this control. */
210: @Nonnull
211: private final MapViewModel model;
212:
213: /** The tile grid that the rendering relies upon. */
214: @Nonnull
215: private final TileGrid tileGrid;
216:
217: /** A cache for tiles. */
218: @Nonnull
219: private final TileCache tileCache;
220:
221: /** Whether double click re-centers the map to the clicked point. */
222: @Getter @Setter
223: private boolean recenterOnDoubleClick = true;
224:
225: /** Whether the vertical scroll gesture should zoom. */
226: @Getter @Setter
227: private boolean scrollToZoom = false;
228:
229: /** The duration of the re-centering animation. */
230: @Getter @Setter
231: private Duration recenterDuration = Duration.millis(200);
232:
233: /** True if a zoom operation is in progress. */
234: private boolean zooming;
235:
236: /** True if a drag operation is in progress. */
237: private boolean dragging;
238:
239: /** The latest x coordinate in drag. */
240: private double dragX;
241:
242: /** The latest y coordinate in drag. */
243: private double dragY;
244:
245: /** The latest value in scroll. */
246: private double scroll;
247:
248: /***********************************************************************************************************************************************************
249: * Creates a new instance.
250: * @param options options for the control
251: **********************************************************************************************************************************************************/
252: @SuppressWarnings("this-escape") @SuppressFBWarnings({"MALICIOUS_CODE", "CT_CONSTRUCTOR_THROW"})
253: public MapView (@Nonnull final Options options)
254: {
255: if (!Platform.isFxApplicationThread())
256: {
257: throw new IllegalStateException("Must be instantiated on JavaFX thread");
258: }
259:
260: tileSource = new SimpleObjectProperty<>(this, "tileSource", DEFAULT_TILE_SOURCE);
261: model = new MapViewModel(tileSource.get());
262: tileCache = new TileCache(options);
263: tileGrid = new TileGrid(this, model, tileSource, tileCache);
264: center = new SimpleObjectProperty<>(this, "center", tileGrid.getCenter());
265: zoom = new RangeLimitedDoubleProperty(this, "zoom", model.zoom(), tileSource.get().getMinZoomLevel(), tileSource.get().getMaxZoomLevel());
266: minZoom = new SimpleDoubleProperty(this, "minZoom", tileSource.get().getMinZoomLevel());
267: maxZoom = new SimpleDoubleProperty(this, "maxZoom", tileSource.get().getMaxZoomLevel());
268: coordinatesUnderMouse = new SimpleObjectProperty<>(this, "coordinatesUnderMouse", MapCoordinates.of(0, 0));
269: area = new SimpleObjectProperty<>(this, "area", MapArea.of(0, 0, 0, 0));
270: tileSource.addListener((_1, _2, _3) -> onTileSourceChanged());
271: center.addListener((_1, _2, newValue) -> setCenterAndZoom(newValue, zoom.get()));
272: zoom.addListener((_1, _2, newValue) -> setCenterAndZoom(center.get(), newValue.intValue()));
273: getChildren().add(tileGrid);
274: AnchorPane.setLeftAnchor(tileGrid, 0d);
275: AnchorPane.setRightAnchor(tileGrid, 0d);
276: AnchorPane.setTopAnchor(tileGrid, 0d);
277: AnchorPane.setBottomAnchor(tileGrid, 0d);
278: // needRefresh();
279: setOnMouseClicked(this::onMouseClicked);
280: setOnZoom(this::onZoom);
281: setOnZoomStarted(this::onZoomStarted);
282: setOnZoomFinished(this::onZoomFinished);
283: setOnMouseMoved(this::onMouseMoved);
284: setOnScroll(this::onScroll);
285: tileGrid.setOnMousePressed(this::onMousePressed);
286: tileGrid.setOnMouseReleased(this::onMouseReleased);
287: tileGrid.setOnMouseDragged(this::onMouseDragged);
288: }
289:
290: /***********************************************************************************************************************************************************
291: * {@return a new set of default options}.
292: **********************************************************************************************************************************************************/
293: @Nonnull
294: public static Options options()
295: {
296: return new Options(Path.of(System.getProperty("java.io.tmpdir")), true, DEFAULT_TILE_POOL_SIZE, DEFAULT_TILE_QUEUE_CAPACITY);
297: }
298:
299: /***********************************************************************************************************************************************************
300: * {@return the tile source}.
301: * @see #setTileSource(TileSource)
302: * @see #tileSourceProperty()
303: **********************************************************************************************************************************************************/
304: @Nonnull
305: public final TileSource getTileSource()
306: {
307: return tileSource.get();
308: }
309:
310: /***********************************************************************************************************************************************************
311: * 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.
312: * @param tileSource the tile source
313: * @see #getTileSource()
314: * @see #tileSourceProperty()
315: **********************************************************************************************************************************************************/
316: public final void setTileSource (@Nonnull final TileSource tileSource)
317: {
318: this.tileSource.set(tileSource);
319: }
320:
321: /***********************************************************************************************************************************************************
322: * {@return the tile source property}.
323: * @see #setTileSource(TileSource)
324: * @see #getTileSource()
325: **********************************************************************************************************************************************************/
326: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
327: public final ObjectProperty<TileSource> tileSourceProperty()
328: {
329: return tileSource;
330: }
331:
332: /***********************************************************************************************************************************************************
333: * {@return the center coordinates}.
334: * @see #setCenter(MapCoordinates)
335: * @see #centerProperty()
336: **********************************************************************************************************************************************************/
337: @Nonnull
338: public final MapCoordinates getCenter()
339: {
340: return center.get();
341: }
342:
343: /***********************************************************************************************************************************************************
344: * Sets the coordinates to show at the center of the map.
345: * @param center the center coordinates
346: * @see #getCenter()
347: * @see #centerProperty()
348: **********************************************************************************************************************************************************/
349: public final void setCenter (@Nonnull final MapCoordinates center)
350: {
351: this.center.set(center);
352: }
353:
354: /***********************************************************************************************************************************************************
355: * {@return the center property}.
356: * @see #getCenter()
357: * @see #setCenter(MapCoordinates)
358: **********************************************************************************************************************************************************/
359: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
360: public final ObjectProperty<MapCoordinates> centerProperty()
361: {
362: return center;
363: }
364:
365: /***********************************************************************************************************************************************************
366: * {@return the zoom level}.
367: * @see #setZoom(double)
368: * @see #zoomProperty()
369: **********************************************************************************************************************************************************/
370: public final double getZoom()
371: {
372: return zoom.get();
373: }
374:
375: /***********************************************************************************************************************************************************
376: * Sets the zoom level.
377: * @param zoom the zoom level
378: * @see #getZoom()
379: * @see #zoomProperty()
380: **********************************************************************************************************************************************************/
381: public final void setZoom (final double zoom)
382: {
383: this.zoom.set(zoom);
384: }
385:
386: /***********************************************************************************************************************************************************
387: * {@return the zoom level property}.
388: * @see #getZoom()
389: * @see #setZoom(double)
390: **********************************************************************************************************************************************************/
391: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
392: public final DoubleProperty zoomProperty()
393: {
394: return zoom;
395: }
396:
397: /***********************************************************************************************************************************************************
398: * {@return the min zoom level}.
399: * @see #minZoomProperty()
400: **********************************************************************************************************************************************************/
401: public final double getMinZoom()
402: {
403: return minZoom.get();
404: }
405:
406: /***********************************************************************************************************************************************************
407: * {@return the min zoom level property}.
408: * @see #getMinZoom()
409: **********************************************************************************************************************************************************/
410: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
411: public final ReadOnlyDoubleProperty minZoomProperty()
412: {
413: return minZoom;
414: }
415:
416: /***********************************************************************************************************************************************************
417: * {@return the max zoom level}.
418: * @see #maxZoomProperty()
419: **********************************************************************************************************************************************************/
420: public final double getMaxZoom()
421: {
422: return maxZoom.get();
423: }
424:
425: /***********************************************************************************************************************************************************
426: * {@return the max zoom level property}.
427: * @see #getMaxZoom()
428: **********************************************************************************************************************************************************/
429: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
430: public final ReadOnlyDoubleProperty maxZoomProperty()
431: {
432: return maxZoom;
433: }
434:
435: /***********************************************************************************************************************************************************
436: * {@return the coordinates corresponding to the point where the mouse is}.
437: **********************************************************************************************************************************************************/
438: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
439: public final ObjectProperty<MapCoordinates> coordinatesUnderMouseProperty()
440: {
441: return coordinatesUnderMouse;
442: }
443:
444: /***********************************************************************************************************************************************************
445: * {@return the area rendered on the map}.
446: * @see #areaProperty()
447: * @see #fitArea(MapArea)
448: **********************************************************************************************************************************************************/
449: @Nonnull
450: public final MapArea getArea()
451: {
452: return area.get();
453: }
454:
455: /***********************************************************************************************************************************************************
456: * {@return the area rendered on the map}.
457: * @see #getArea()
458: * @see #fitArea(MapArea)
459: **********************************************************************************************************************************************************/
460: @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP")
461: public final ObjectProperty<MapArea> areaProperty()
462: {
463: return area;
464: }
465:
466: /***********************************************************************************************************************************************************
467: * Fits the zoom level and centers the map so that the two corners are visible.
468: * @param area the area to fit
469: * @see #getArea()
470: * @see #areaProperty()
471: **********************************************************************************************************************************************************/
472: public void fitArea (@Nonnull final MapArea area)
473: {
474: log.debug("fitArea({})", area);
475: setCenterAndZoom(area.getCenter(), model.computeFittingZoom(area));
476: }
477:
478: /***********************************************************************************************************************************************************
479: * {@return the scale of the map in meters per pixel}.
480: **********************************************************************************************************************************************************/
481: // @Nonnegative
482: public double getMetersPerPixel()
483: {
484: return tileSource.get().metersPerPixel(tileGrid.getCenter(), zoom.get());
485: }
486:
487: /***********************************************************************************************************************************************************
488: * {@return a point on the map corresponding to the given coordinates}.
489: * @param coordinates the coordinates
490: **********************************************************************************************************************************************************/
491: @Nonnull
492: public MapViewPoint coordinatesToPoint (@Nonnull final MapCoordinates coordinates)
493: {
494: return model.coordinatesToMapViewPoint(coordinates);
495: }
496:
497: /***********************************************************************************************************************************************************
498: * {@return the coordinates corresponding to a given point on the map}.
499: * @param point the point on the map
500: **********************************************************************************************************************************************************/
501: @Nonnull
502: public MapCoordinates pointToCoordinates (@Nonnull final MapViewPoint point)
503: {
504: return model.mapViewPointToCoordinates(point);
505: }
506:
507: /***********************************************************************************************************************************************************
508: * Adds an overlay, passing a callback that will be responsible for rendering the overlay, when needed.
509: * @param name the name of the overlay
510: * @param creator the overlay creator
511: * @see OverlayHelper
512: **********************************************************************************************************************************************************/
513: public void addOverlay (@Nonnull final String name, @Nonnull final Consumer<OverlayHelper> creator)
514: {
515: tileGrid.addOverlay(name, creator);
516: }
517:
518: /***********************************************************************************************************************************************************
519: * Removes an overlay.
520: * @param name the name of the overlay to remove
521: **********************************************************************************************************************************************************/
522: public void removeOverlay (@Nonnull final String name)
523: {
524: tileGrid.removeOverlay(name);
525: }
526:
527: /***********************************************************************************************************************************************************
528: * Removes all overlays.
529: **********************************************************************************************************************************************************/
530: public void removeAllOverlays()
531: {
532: tileGrid.removeAllOverlays();
533: }
534:
535: /***********************************************************************************************************************************************************
536: * Sets both the center and the zoom level.
537: * @param center the center
538: * @param zoom the zoom level
539: **********************************************************************************************************************************************************/
540: private void setCenterAndZoom (@Nonnull final MapCoordinates center, final double zoom)
541: {
542: log.trace("setCenterAndZoom({}, {})", center, zoom);
543:
544: if (!center.equals(tileGrid.getCenter()) || doubleToLongBits(zoom) != doubleToLongBits(model.zoom()))
545: {
546: tileCache.retainPendingTiles((int)zoom);
547: tileGrid.setCenterAndZoom(center, zoom);
548: this.center.set(center);
549: this.zoom.set(zoom);
550: area.set(model.getArea());
551: }
552: }
553:
554: /***********************************************************************************************************************************************************
555: * Translate the map center by the specified amount.
556: * @param dx the horizontal amount
557: * @param dy the vertical amount
558: **********************************************************************************************************************************************************/
559: private void translateCenter (final double dx, final double dy)
560: {
561: tileGrid.translate(dx, dy);
562: center.set(model.center());
563: area.set(model.getArea());
564: }
565:
566: /***********************************************************************************************************************************************************
567: * This method is called when the tile source has been changed.
568: **********************************************************************************************************************************************************/
569: private void onTileSourceChanged()
570: {
571: final var minZoom = tileSourceProperty().get().getMinZoomLevel();
572: final var maxZoom = tileSourceProperty().get().getMaxZoomLevel();
573: zoom.setLimits(minZoom, maxZoom);
574: this.minZoom.set(minZoom);
575: this.maxZoom.set(maxZoom);
576: setNeedsLayout(true);
577: }
578:
579: /***********************************************************************************************************************************************************
580: * Mouse callback.
581: **********************************************************************************************************************************************************/
582: private void onMousePressed (@Nonnull final MouseEvent event)
583: {
584: if (!zooming)
585: {
586: dragging = true;
587: dragX = event.getSceneX();
588: dragY = event.getSceneY();
589: log.trace("onMousePressed: {} {}", dragX, dragY);
590: }
591: }
592:
593: /***********************************************************************************************************************************************************
594: * Mouse callback.
595: **********************************************************************************************************************************************************/
596: private void onMouseReleased (@Nonnull final MouseEvent ignored)
597: {
598: log.trace("onMouseReleased");
599: dragging = false;
600: }
601:
602: /***********************************************************************************************************************************************************
603: * Mouse callback.
604: **********************************************************************************************************************************************************/
605: private void onMouseDragged (@Nonnull final MouseEvent event)
606: {
607: if (!zooming && dragging)
608: {
609: translateCenter(event.getSceneX() - dragX, event.getSceneY() - dragY);
610: dragX = event.getSceneX();
611: dragY = event.getSceneY();
612: }
613: }
614:
615: /***********************************************************************************************************************************************************
616: * Mouse callback.
617: **********************************************************************************************************************************************************/
618: private void onMouseClicked (@Nonnull final MouseEvent event)
619: {
620: if (recenterOnDoubleClick && (event.getClickCount() == 2))
621: {
622: final var delta = Translation.of(getWidth() / 2 - event.getX(), getHeight() / 2 - event.getY());
623: final var target = new SimpleObjectProperty<>(Translation.of(0, 0));
624: target.addListener((__, oldValue, newValue) ->
625: translateCenter(newValue.x - oldValue.x, newValue.y - oldValue.y));
626: animate(target, Translation.of(0, 0), delta, recenterDuration);
627: }
628: }
629:
630: /***********************************************************************************************************************************************************
631: * Mouse callback.
632: **********************************************************************************************************************************************************/
633: private void onMouseMoved (@Nonnull final MouseEvent event)
634: {
635: coordinatesUnderMouse.set(pointToCoordinates(MapViewPoint.of(event)));
636: }
637:
638: /***********************************************************************************************************************************************************
639: * Gesture callback.
640: **********************************************************************************************************************************************************/
641: private void onZoomStarted (@Nonnull final ZoomEvent event)
642: {
643: log.trace("onZoomStarted({})", event);
644: zooming = true;
645: dragging = false;
646: }
647:
648: /***********************************************************************************************************************************************************
649: * Gesture callback.
650: **********************************************************************************************************************************************************/
651: private void onZoomFinished (@Nonnull final ZoomEvent event)
652: {
653: log.trace("onZoomFinished({})", event);
654: zooming = false;
655: }
656:
657: /***********************************************************************************************************************************************************
658: * Gesture callback.
659: **********************************************************************************************************************************************************/
660: private void onZoom (@Nonnull final ZoomEvent event)
661: {
662: log.trace("onZoom({})", event);
663: }
664:
665: /***********************************************************************************************************************************************************
666: * Mouse callback.
667: **********************************************************************************************************************************************************/
668: private void onScroll (@Nonnull final ScrollEvent event)
669: {
670: if (scrollToZoom)
671: {
672: log.info("onScroll({})", event);
673: final var amount = -Math.signum(Math.floor(event.getDeltaY() - scroll));
674: scroll = event.getDeltaY();
675: log.debug("zoom change for scroll: {}", amount);
676: System.err.println("AMOUNT " + amount);
677: zoom.set(Math.round(zoom.get() + amount));
678: }
679: }
680:
681: /***********************************************************************************************************************************************************
682: * Animates a property. If the duration is zero, the property is immediately set.
683: * @param <T> the static type of the property to animate
684: * @param target the property to animate
685: * @param startValue the start value of the property
686: * @param endValue the end value of the property
687: * @param duration the duration of the animation
688: **********************************************************************************************************************************************************/
689: private static <T extends Interpolatable<T>> void animate (@Nonnull final ObjectProperty<T> target,
690: @Nonnull final T startValue,
691: @Nonnull final T endValue,
692: @Nonnull final Duration duration)
693: {
694: if (duration.equals(ZERO))
695: {
696: target.set(endValue);
697: }
698: else
699: {
700: final var start = new KeyFrame(ZERO, new KeyValue(target, startValue));
701: final var end = new KeyFrame(duration, new KeyValue(target, endValue, Interpolator.EASE_OUT));
702: new Timeline(start, end).play();
703: }
704: }
705:
706: // FIXME: on close shut down the tile cache executor service.
707: }