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. import static java.lang.Double.doubleToLongBits;

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

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

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

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

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

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

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

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

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

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

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

  112.         if (!center.equals(model.center()) || doubleToLongBits(zoom) != doubleToLongBits(model.zoom())) // defensive
  113.           {
  114.             model.setCenterAndZoom(center, zoom);
  115.             createTiles();
  116.             recreateOverlays();
  117.             setDirty(Dirty.ALL);
  118.           }
  119.       }

  120.     /***********************************************************************************************************************************************************
  121.      * {@return the coordinates of the point at the center of the map}.
  122.      **********************************************************************************************************************************************************/
  123.     @Nonnull
  124.     public MapCoordinates getCenter()
  125.       {
  126.         return model.center();
  127.       }

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

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

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

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

  178.     /***********************************************************************************************************************************************************
  179.      * Removes all overlays.
  180.      **********************************************************************************************************************************************************/
  181.     public void removeAllOverlays()
  182.       {
  183.         overlayByName.clear();
  184.         overlayPane.getChildren().clear();
  185.       }

  186.     /***********************************************************************************************************************************************************
  187.      * {@inheritDoc}
  188.      **********************************************************************************************************************************************************/
  189.     @Override
  190.     protected void layoutChildren()
  191.       {
  192.         log.trace("layoutChildren");

  193.         if (dirty != Dirty.NONE && isVisible())
  194.           {
  195.             final var parentWidth = parent.getWidth();
  196.             final var parentHeight = parent.getHeight();
  197.             final var centerTileChanged = model.updateGridSize(parentWidth, parentHeight);
  198.             model.recompute();

  199.             if (centerTileChanged)
  200.               {
  201.                 log.debug("new view size: {} x {}, new grid size: {} x {}", parentWidth, parentHeight, model.columns(), model.rows());
  202.                 createTiles();

  203.                 if (dirty == Dirty.ALL)
  204.                   {
  205.                     recreateOverlays();
  206.                   }
  207.               }
  208.             else
  209.               {
  210.                 applyTranslate();
  211.               }
  212.           }

  213.         dirty = Dirty.NONE;
  214.         super.layoutChildren();
  215.       }

  216.     /***********************************************************************************************************************************************************
  217.      *
  218.      **********************************************************************************************************************************************************/
  219.     private void onTileSourceChanged()
  220.       {
  221.         log.debug("onTileSourceChanged()");
  222.         model.setTileSource(tileSource.get());
  223.         createTiles();
  224.         setDirty(Dirty.GRID);
  225.       }

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

  236.     /***********************************************************************************************************************************************************
  237.      *
  238.      **********************************************************************************************************************************************************/
  239.     private void recreateOverlays()
  240.       {
  241.         log.debug("recreateOverlays()");
  242.         overlayPane.setTranslateX(0);
  243.         overlayPane.setTranslateY(0);
  244.         overlayByName.values().forEach(MapOverlay::create);
  245.       }

  246.     /***********************************************************************************************************************************************************
  247.      *
  248.      **********************************************************************************************************************************************************/
  249.     @Nonnull
  250.     private Node createTile (@Nonnull final URI uri)
  251.       {
  252.         return new Tile(tileCache, tileSource.get(), uri, tileSource.get().getTileSize(), (int)model.zoom());
  253.       }

  254.     /***********************************************************************************************************************************************************
  255.      *
  256.      **********************************************************************************************************************************************************/
  257.     private void applyTranslate()
  258.       {
  259.         setTranslateX(model.gridOffset().x());
  260.         setTranslateY(model.gridOffset().y());
  261.       }

  262.     /***********************************************************************************************************************************************************
  263.      *
  264.      **********************************************************************************************************************************************************/
  265.     private void setDirty (@Nonnull final Dirty dirty)
  266.       {
  267.         this.dirty = dirty;
  268.         setNeedsLayout(true);
  269.       }
  270.   }