TileGrid.java

  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.impl;

  27. import jakarta.annotation.Nonnull;
  28. import java.util.HashMap;
  29. import java.util.Map;
  30. import java.util.function.Consumer;
  31. import java.net.URI;
  32. import javafx.beans.property.ObjectProperty;
  33. import javafx.scene.Node;
  34. import javafx.scene.layout.GridPane;
  35. import javafx.scene.layout.StackPane;
  36. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  37. import it.tidalwave.mapviewer.MapCoordinates;
  38. import it.tidalwave.mapviewer.TileSource;
  39. import it.tidalwave.mapviewer.impl.MapViewModel;
  40. import it.tidalwave.mapviewer.javafx.MapView;
  41. import lombok.experimental.Accessors;
  42. import lombok.extern.slf4j.Slf4j;

  43. /***************************************************************************************************************************************************************
  44.  *
  45.  * A grid of tiles, used to completely fill an arbitrary area of a graphic device.
  46.  *
  47.  * @author  Fabrizio Giudici
  48.  *
  49.  **************************************************************************************************************************************************************/
  50. @Slf4j @Accessors(fluent = true)
  51. public class TileGrid extends StackPane
  52.   {
  53.     enum Dirty
  54.       {
  55.         /** Not dirty */ NONE,
  56.         /** Only grid needs to be rebuilt. */ GRID,
  57.         /** All need to be rebuilt, */ ALL
  58.       }

  59.     /** The owner component. */
  60.     @Nonnull @SuppressFBWarnings("EI_EXPOSE_REP2")
  61.     private final MapView parent;

  62.     /** The tile source. */
  63.     @Nonnull
  64.     private final ObjectProperty<TileSource> tileSource;

  65.     /** The model. */
  66.     @Nonnull
  67.     private final MapViewModel model;

  68.     /** The tile cache. */
  69.     @Nonnull
  70.     private final TileCache tileCache;

  71.     /** Whether this control needs to be redrawn. */
  72.     private Dirty dirty = Dirty.NONE;

  73.     /** The map of overlays indexed by name. */
  74.     private final Map<String, MapOverlay> overlayByName = new HashMap<>();

  75.     /** The container of tiles. */
  76.     private final GridPane tilePane = new GridPane();

  77.     /** The container of overlays. */
  78.     private final StackPane overlayPane = new StackPane();

  79.     /***********************************************************************************************************************************************************
  80.      * Creates a grid of tiles.
  81.      * @param   parent      the map view control
  82.      * @param   model       the map model
  83.      * @param   tileSource  the tile source
  84.      * @param   tileCache   the tile cache
  85.      **********************************************************************************************************************************************************/
  86.     @SuppressFBWarnings({"EI_EXPOSE_REP2", "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR"})
  87.     public TileGrid (@Nonnull final MapView parent,
  88.                      @Nonnull final MapViewModel model,
  89.                      @Nonnull final ObjectProperty<TileSource> tileSource,
  90.                      @Nonnull final TileCache tileCache)
  91.       {
  92.         this.parent = parent;
  93.         this.tileSource = tileSource;
  94.         this.model = model;
  95.         this.tileCache = tileCache;
  96.         getChildren().addAll(tilePane, overlayPane);
  97.         parent.layoutBoundsProperty().addListener((_1, _2, _3) -> setDirty(Dirty.GRID));
  98.         model.setCenterAndZoom(MapCoordinates.of(0, 0), 1);
  99.         tileSource.addListener((_1, _2, _3) -> onTileSourceChanged());
  100.       }

  101.     /***********************************************************************************************************************************************************
  102.      * 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
  103.      * 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
  104.      * number of tiles to download for the next position).
  105.      * @param  coordinates   the coordinates at the center of the tile
  106.      * @param  zoomLevel     the zoom level
  107.      **********************************************************************************************************************************************************/
  108.     public void setCenterAndZoom (@Nonnull final MapCoordinates coordinates, final double zoomLevel)
  109.       {
  110.         log.debug("setCenterAndZoom({}, {})", coordinates, zoomLevel);
  111.         model.setCenterAndZoom(coordinates, zoomLevel);
  112.         createTiles();
  113.         recreateOverlays();
  114.         setDirty(Dirty.ALL);
  115.       }

  116.     /***********************************************************************************************************************************************************
  117.      * {@return the coordinates of the point at the center of the map}.
  118.      **********************************************************************************************************************************************************/
  119.     @Nonnull
  120.     public MapCoordinates getCenter()
  121.       {
  122.         return model.center();
  123.       }

  124.     /***********************************************************************************************************************************************************
  125.      * Translates the tile grid. If the translation is so large that the tile at the center changes, the grid is recomputed and translated back.
  126.      * @param   deltaX    the drag in screen coordinates
  127.      * @param   deltaY    the drag in screen coordinates
  128.      **********************************************************************************************************************************************************/
  129.     public void translate (final double deltaX, final double deltaY)
  130.       {
  131.         log.trace("translate({}, {})", deltaX, deltaY);
  132.         final var prevTileCenter = model.tileCenter();
  133.         model.setCenterAndZoom(model.pointCenter().translated(-deltaX, -deltaY), model.zoom());
  134.         final var tileCenter = model.tileCenter();

  135.         if (!prevTileCenter.equals(tileCenter))
  136.           {
  137.             createTiles();
  138.             // no need to recreate overlays, just translate them
  139.             final var dX = overlayPane.getTranslateX() -(tileCenter.column - prevTileCenter.column) * tileSource.get().getTileSize();
  140.             final var dY = overlayPane.getTranslateY() -(tileCenter.row - prevTileCenter.row) * tileSource.get().getTileSize();
  141.             log.debug("translate overlays: {}, {}", dX, dY);
  142.             overlayPane.setTranslateX(dX);
  143.             overlayPane.setTranslateY(dY);
  144.             setDirty(Dirty.GRID);
  145.           }
  146.         else
  147.           {
  148.             applyTranslate();
  149.           }
  150.       }

  151.     /***********************************************************************************************************************************************************
  152.      * Adds an overlay.
  153.      * @param   name      the name of the overlay
  154.      * @param   creator   the overlay creator
  155.      **********************************************************************************************************************************************************/
  156.     public void addOverlay (@Nonnull final String name, @Nonnull final Consumer<MapView.OverlayHelper> creator)
  157.       {
  158.         final var overlay = new MapOverlay(model, creator);
  159.         overlayPane.getChildren().add(overlay);
  160.         overlayByName.put(name, overlay);
  161.         overlay.create();
  162.       }

  163.     /***********************************************************************************************************************************************************
  164.      * Removes an overlay.
  165.      * @param   name      the name of the overlay to remove
  166.      **********************************************************************************************************************************************************/
  167.     public void removeOverlay (@Nonnull final String name)
  168.       {
  169.         if (overlayByName.containsKey(name))
  170.           {
  171.             overlayPane.getChildren().remove(overlayByName.remove(name));
  172.           }
  173.       }

  174.     /***********************************************************************************************************************************************************
  175.      * Removes all overlays.
  176.      **********************************************************************************************************************************************************/
  177.     public void removeAllOverlays()
  178.       {
  179.         overlayByName.clear();
  180.         overlayPane.getChildren().clear();
  181.       }

  182.     /***********************************************************************************************************************************************************
  183.      * {@inheritDoc}
  184.      **********************************************************************************************************************************************************/
  185.     @Override
  186.     protected void layoutChildren()
  187.       {
  188.         log.trace("layoutChildren");

  189.         if (dirty != Dirty.NONE && isVisible())
  190.           {
  191.             final var parentWidth = parent.getWidth();
  192.             final var parentHeight = parent.getHeight();
  193.             final var centerTileChanged = model.updateGridSize(parentWidth, parentHeight);
  194.             model.recompute();

  195.             if (centerTileChanged)
  196.               {
  197.                 log.debug("new view size: {} x {}, new grid size: {} x {}", parentWidth, parentHeight, model.columns(), model.rows());
  198.                 createTiles();

  199.                 if (dirty == Dirty.ALL)
  200.                   {
  201.                     recreateOverlays();
  202.                   }
  203.               }
  204.             else
  205.               {
  206.                 applyTranslate();
  207.               }
  208.           }

  209.         dirty = Dirty.NONE;
  210.         super.layoutChildren();
  211.       }

  212.     /***********************************************************************************************************************************************************
  213.      *
  214.      **********************************************************************************************************************************************************/
  215.     private void onTileSourceChanged()
  216.       {
  217.         log.debug("onTileSourceChanged()");
  218.         model.setTileSource(tileSource.get());
  219.         createTiles();
  220.         setDirty(Dirty.GRID);
  221.       }

  222.     /***********************************************************************************************************************************************************
  223.      *
  224.      **********************************************************************************************************************************************************/
  225.     private void createTiles()
  226.       {
  227.         log.debug("createTiles()");
  228.         tilePane.getChildren().clear();
  229.         model.iterateOnGrid((pos, url) -> tilePane.add(createTile(url), pos.column(), pos.row(), 1, 1));
  230.         applyTranslate();
  231.       }

  232.     /***********************************************************************************************************************************************************
  233.      *
  234.      **********************************************************************************************************************************************************/
  235.     private void recreateOverlays()
  236.       {
  237.         log.debug("recreateOverlays()");
  238.         overlayPane.setTranslateX(0);
  239.         overlayPane.setTranslateY(0);
  240.         overlayByName.values().forEach(MapOverlay::create);
  241.       }

  242.     /***********************************************************************************************************************************************************
  243.      *
  244.      **********************************************************************************************************************************************************/
  245.     @Nonnull
  246.     private Node createTile (@Nonnull final URI uri)
  247.       {
  248.         return new Tile(tileCache, tileSource.get(), uri, tileSource.get().getTileSize(), (int)model.zoom());
  249.       }

  250.     /***********************************************************************************************************************************************************
  251.      *
  252.      **********************************************************************************************************************************************************/
  253.     private void applyTranslate()
  254.       {
  255.         setTranslateX(model.gridOffset().x());
  256.         setTranslateY(model.gridOffset().y());
  257.       }

  258.     /***********************************************************************************************************************************************************
  259.      *
  260.      **********************************************************************************************************************************************************/
  261.     private void setDirty (@Nonnull final Dirty dirty)
  262.       {
  263.         this.dirty = dirty;
  264.         setNeedsLayout(true);
  265.       }
  266.   }