TileGrid.java
- /*
- * *************************************************************************************************************************************************************
- *
- * MapView: a JavaFX map renderer for tile-based servers
- * http://tidalwave.it/projects/mapview
- *
- * Copyright (C) 2024 - 2025 by Tidalwave s.a.s. (http://tidalwave.it)
- *
- * *************************************************************************************************************************************************************
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * 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
- * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- *
- * *************************************************************************************************************************************************************
- *
- * git clone https://bitbucket.org/tidalwave/mapview-src
- * git clone https://github.com/tidalwave-it/mapview-src
- *
- * *************************************************************************************************************************************************************
- */
- package it.tidalwave.mapviewer.javafx.impl;
- import jakarta.annotation.Nonnull;
- import java.util.HashMap;
- import java.util.Map;
- import java.util.function.Consumer;
- import java.net.URI;
- import javafx.beans.property.ObjectProperty;
- import javafx.scene.Node;
- import javafx.scene.layout.GridPane;
- import javafx.scene.layout.StackPane;
- import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
- import it.tidalwave.mapviewer.MapCoordinates;
- import it.tidalwave.mapviewer.TileSource;
- import it.tidalwave.mapviewer.impl.MapViewModel;
- import it.tidalwave.mapviewer.javafx.MapView;
- import lombok.experimental.Accessors;
- import lombok.extern.slf4j.Slf4j;
- /***************************************************************************************************************************************************************
- *
- * A grid of tiles, used to completely fill an arbitrary area of a graphic device.
- *
- * @author Fabrizio Giudici
- *
- **************************************************************************************************************************************************************/
- @Slf4j @Accessors(fluent = true)
- public class TileGrid extends StackPane
- {
- enum Dirty
- {
- /** Not dirty */ NONE,
- /** Only grid needs to be rebuilt. */ GRID,
- /** All need to be rebuilt, */ ALL
- }
- /** The owner component. */
- @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP2")
- private final MapView parent;
- /** The tile source. */
- @Nonnull
- private final ObjectProperty<TileSource> tileSource;
- /** The model. */
- @Nonnull
- private final MapViewModel model;
- /** The tile cache. */
- @Nonnull
- private final TileCache tileCache;
- /** Whether this control needs to be redrawn. */
- private Dirty dirty = Dirty.NONE;
- /** The map of overlays indexed by name. */
- private final Map<String, MapOverlay> overlayByName = new HashMap<>();
- /** The container of tiles. */
- private final GridPane tilePane = new GridPane();
- /** The container of overlays. */
- private final StackPane overlayPane = new StackPane();
- /***********************************************************************************************************************************************************
- * Creates a grid of tiles.
- * @param parent the map view control
- * @param model the map model
- * @param tileSource the tile source
- * @param tileCache the tile cache
- **********************************************************************************************************************************************************/
- @SuppressFBWarnings({"EI_EXPOSE_REP2", "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR"})
- public TileGrid (@Nonnull final MapView parent,
- @Nonnull final MapViewModel model,
- @Nonnull final ObjectProperty<TileSource> tileSource,
- @Nonnull final TileCache tileCache)
- {
- this.parent = parent;
- this.tileSource = tileSource;
- this.model = model;
- this.tileCache = tileCache;
- getChildren().addAll(tilePane, overlayPane);
- parent.layoutBoundsProperty().addListener((_1, _2, _3) -> setDirty(Dirty.GRID));
- model.setCenterAndZoom(MapCoordinates.of(0, 0), 1);
- tileSource.addListener((_1, _2, _3) -> onTileSourceChanged());
- }
- /***********************************************************************************************************************************************************
- * Sets the coordinates at the center of the grid and the zoom level. This method will update the tiles in the grid with the proper URLs for the required
- * setting. If the grid is already populated, existing tiles are recycled if possible (this is useful while moving the coordinates in order to avoid the
- * number of tiles to download for the next position).
- * @param coordinates the coordinates at the center of the tile
- * @param zoomLevel the zoom level
- **********************************************************************************************************************************************************/
- public void setCenterAndZoom (@Nonnull final MapCoordinates coordinates, final double zoomLevel)
- {
- log.debug("setCenterAndZoom({}, {})", coordinates, zoomLevel);
- model.setCenterAndZoom(coordinates, zoomLevel);
- createTiles();
- recreateOverlays();
- setDirty(Dirty.ALL);
- }
- /***********************************************************************************************************************************************************
- * {@return the coordinates of the point at the center of the map}.
- **********************************************************************************************************************************************************/
- @Nonnull
- public MapCoordinates getCenter()
- {
- return model.center();
- }
- /***********************************************************************************************************************************************************
- * Translates the tile grid. If the translation is so large that the tile at the center changes, the grid is recomputed and translated back.
- * @param deltaX the drag in screen coordinates
- * @param deltaY the drag in screen coordinates
- **********************************************************************************************************************************************************/
- public void translate (final double deltaX, final double deltaY)
- {
- log.trace("translate({}, {})", deltaX, deltaY);
- final var prevTileCenter = model.tileCenter();
- model.setCenterAndZoom(model.pointCenter().translated(-deltaX, -deltaY), model.zoom());
- final var tileCenter = model.tileCenter();
- if (!prevTileCenter.equals(tileCenter))
- {
- createTiles();
- // no need to recreate overlays, just translate them
- final var dX = overlayPane.getTranslateX() -(tileCenter.column - prevTileCenter.column) * tileSource.get().getTileSize();
- final var dY = overlayPane.getTranslateY() -(tileCenter.row - prevTileCenter.row) * tileSource.get().getTileSize();
- log.debug("translate overlays: {}, {}", dX, dY);
- overlayPane.setTranslateX(dX);
- overlayPane.setTranslateY(dY);
- setDirty(Dirty.GRID);
- }
- else
- {
- applyTranslate();
- }
- }
- /***********************************************************************************************************************************************************
- * Adds an overlay.
- * @param name the name of the overlay
- * @param creator the overlay creator
- **********************************************************************************************************************************************************/
- public void addOverlay (@Nonnull final String name, @Nonnull final Consumer<MapView.OverlayHelper> creator)
- {
- final var overlay = new MapOverlay(model, creator);
- overlayPane.getChildren().add(overlay);
- overlayByName.put(name, overlay);
- overlay.create();
- }
- /***********************************************************************************************************************************************************
- * Removes an overlay.
- * @param name the name of the overlay to remove
- **********************************************************************************************************************************************************/
- public void removeOverlay (@Nonnull final String name)
- {
- if (overlayByName.containsKey(name))
- {
- overlayPane.getChildren().remove(overlayByName.remove(name));
- }
- }
- /***********************************************************************************************************************************************************
- * Removes all overlays.
- **********************************************************************************************************************************************************/
- public void removeAllOverlays()
- {
- overlayByName.clear();
- overlayPane.getChildren().clear();
- }
- /***********************************************************************************************************************************************************
- * {@inheritDoc}
- **********************************************************************************************************************************************************/
- @Override
- protected void layoutChildren()
- {
- log.trace("layoutChildren");
- if (dirty != Dirty.NONE && isVisible())
- {
- final var parentWidth = parent.getWidth();
- final var parentHeight = parent.getHeight();
- final var centerTileChanged = model.updateGridSize(parentWidth, parentHeight);
- model.recompute();
- if (centerTileChanged)
- {
- log.debug("new view size: {} x {}, new grid size: {} x {}", parentWidth, parentHeight, model.columns(), model.rows());
- createTiles();
- if (dirty == Dirty.ALL)
- {
- recreateOverlays();
- }
- }
- else
- {
- applyTranslate();
- }
- }
- dirty = Dirty.NONE;
- super.layoutChildren();
- }
- /***********************************************************************************************************************************************************
- *
- **********************************************************************************************************************************************************/
- private void onTileSourceChanged()
- {
- log.debug("onTileSourceChanged()");
- model.setTileSource(tileSource.get());
- createTiles();
- setDirty(Dirty.GRID);
- }
- /***********************************************************************************************************************************************************
- *
- **********************************************************************************************************************************************************/
- private void createTiles()
- {
- log.debug("createTiles()");
- tilePane.getChildren().clear();
- model.iterateOnGrid((pos, url) -> tilePane.add(createTile(url), pos.column(), pos.row(), 1, 1));
- applyTranslate();
- }
- /***********************************************************************************************************************************************************
- *
- **********************************************************************************************************************************************************/
- private void recreateOverlays()
- {
- log.debug("recreateOverlays()");
- overlayPane.setTranslateX(0);
- overlayPane.setTranslateY(0);
- overlayByName.values().forEach(MapOverlay::create);
- }
- /***********************************************************************************************************************************************************
- *
- **********************************************************************************************************************************************************/
- @Nonnull
- private Node createTile (@Nonnull final URI uri)
- {
- return new Tile(tileCache, tileSource.get(), uri, tileSource.get().getTileSize(), (int)model.zoom());
- }
- /***********************************************************************************************************************************************************
- *
- **********************************************************************************************************************************************************/
- private void applyTranslate()
- {
- setTranslateX(model.gridOffset().x());
- setTranslateY(model.gridOffset().y());
- }
- /***********************************************************************************************************************************************************
- *
- **********************************************************************************************************************************************************/
- private void setDirty (@Nonnull final Dirty dirty)
- {
- this.dirty = dirty;
- setNeedsLayout(true);
- }
- }