MapViewExampleController.java

/*
 * *************************************************************************************************************************************************************
 *
 * MapView: a JavaFX map renderer for tile-based servers
 * http://tidalwave.it/projects/mapview
 *
 * Copyright (C) 2024 - 2026 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.mapview.javafx.example;

import jakarta.annotation.Nonnull;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.jenetics.jpx.GPX;
import io.jenetics.jpx.Track;
import io.jenetics.jpx.TrackSegment;
import it.tidalwave.mapview.MapArea;
import it.tidalwave.mapview.MapCoordinates;
import it.tidalwave.mapview.OpenStreetMapTileSource;
import it.tidalwave.mapview.OpenTopoMapTileSource;
import it.tidalwave.mapview.TileSource;
import it.tidalwave.mapview.javafx.MapView;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import static lombok.AccessLevel.PROTECTED;

/***************************************************************************************************************************************************************
 * @author      Fabrizio Giudici
 **************************************************************************************************************************************************************/
@RequiredArgsConstructor @Getter(PROTECTED) @Slf4j
public final class MapViewExampleController
  {
    private static final Path CACHE_FOLDER = Path.of("target/tile-cache");

    /** The {@code User-Agent} identifying this application to tile servers. */
    private static final String USER_AGENT = "MapView-Example (+http://tidalwave.it/projects/mapview)";

    /* package */ static final MapView.Options DEFAULT_OPTIONS = MapView.options().withCacheFolder(CACHE_FOLDER).withUserAgent(USER_AGENT);

    private static final String TRACK_OVERLAY_NAME = "track";
    private static final MapCoordinates START = MapCoordinates.of(44.4072, 8.9340);
    private static final double START_ZOOM = 8;
    private static final MapArea ITALY = MapArea.of(47.115, 18.480, 36.6199, 6.749);
    private static final MapArea FRANCE = MapArea.of(51.148, 9.560, 2.053, -54.524);
    private static final MapArea SWITZERLAND = MapArea.of(47.830, 10.442, 45.776, 6.022);
    private static final MapArea ALEUTIAN = MapArea.of(62.67, -147.38, 46.32, 161.64);
    private static final TileSource osm = new OpenStreetMapTileSource();
    private static final TileSource otm = new OpenTopoMapTileSource();

    /** {@return the pane that contains the map}. */
    @FXML
    private AnchorPane apAnchorPane;

    /** {@return the slider that controls the zoom level}. */
    @FXML
    private Slider slZoom;

    /** {@return the button that zooms in}. */
    @FXML
    private Button btZoomIn;

    /** {@return the button that zooms out}. */
    @FXML
    private Button btZoomOut;

    /** {@return the button that resets center and zoom}. */
    @FXML
    private Button btReset;

    /** {@return the button that shows Italy}. */
    @FXML
    private Button btShowItaly;

    /** {@return the button that shows France}. */
    @FXML
    private Button btShowFrance;

    /** {@return the button that shows Switzerland}. */
    @FXML
    private Button btShowSwitzerland;

    /** {@return the button that shows the Aleutian Islands}. */
    @FXML
    private Button btShowAleutian;

    /** {@return the button that centers the map on (0, 0)}. */
    @FXML
    private Button btZeroZero;

    /** {@return the button that shows a track}. */
    @FXML
    private Button btTrack;

    /** {@return the button that selects OpenStreetMap}. */
    @FXML
    private Button btOSM;

    /** {@return the button that selects OpenTopoMap}. */
    @FXML
    private Button btOTM;

    /** {@return the label that shows the coordinates under the mouse}. */
    @FXML
    private Label lbCoordinates;

    /** {@return the label that shows the rendered area}. */
    @FXML
    private Label lbArea;

    /** {@return the label that shows the coordinates of the center}. */
    @FXML
    private Label lbCenterCoordinates;

    /** {@return the label that shows the zoom level}. */
    @FXML
    private Label lbZoom;

    /** {@return the map view}. */
    private MapView mapView;

    /** {@return the executor service for background tasks}. */
    @Nonnull
    private final ExecutorService executorService = Executors.newFixedThreadPool(1);

    /**
     * The options for the map view.
     * -- GETTER --
     * {@return the options for the map view}.
     */
    @Nonnull
    private final MapView.Options options;

    /***********************************************************************************************************************************************************
     *
     **********************************************************************************************************************************************************/
    public MapViewExampleController()
      {
        this(DEFAULT_OPTIONS);
      }

    /***********************************************************************************************************************************************************
     *
     **********************************************************************************************************************************************************/
    public void initialize()
      {
        mapView = new MapView(options);
        mapView.setZoom(START_ZOOM);
        mapView.setCenter(START);
        mapView.setDoubleClickBehaviour(MapView.RECENTER);
        AnchorPane.setLeftAnchor(mapView, 0.0);
        AnchorPane.setRightAnchor(mapView, 0.0);
        AnchorPane.setTopAnchor(mapView, 0.0);
        AnchorPane.setBottomAnchor(mapView, 0.0);
        apAnchorPane.getChildren().add(mapView);
        slZoom.minProperty().bind(mapView.minZoomProperty());
        slZoom.maxProperty().bind(mapView.maxZoomProperty());
        slZoom.valueProperty().bindBidirectional(mapView.zoomProperty());
        mapView.centerProperty().addListener((_1, _2, coordinates) -> lbCenterCoordinates.setText(coordinates.toFormattedString()));
        mapView.zoomProperty().addListener((_1, _2, zoom) -> lbZoom.setText(Integer.toString(zoom.intValue())));
        mapView.areaProperty().addListener((_1, _2, area) -> lbArea.setText(area.toFormattedString()));
        mapView.mouseCoordinatesProperty().addListener((_1, _2, coordinates) -> lbCoordinates.setText(coordinates.toFormattedString()));
        btZoomIn.setOnAction(event -> mapView.setZoom(mapView.getZoom() + 1));
        btZoomOut.setOnAction(event -> mapView.setZoom(mapView.getZoom() - 1));
        btReset.setOnAction(event -> { mapView.setCenter(START); mapView.setZoom(START_ZOOM); });
        btShowItaly.setOnAction(event -> mapView.fitArea(ITALY));
        btShowFrance.setOnAction(event -> mapView.fitArea(FRANCE));
        btShowSwitzerland.setOnAction(event -> mapView.fitArea(SWITZERLAND));
        btShowAleutian.setOnAction(event -> mapView.fitArea(ALEUTIAN));
        btZeroZero.setOnAction(event -> mapView.setCenter(MapCoordinates.of(0, 0)));
        btOSM.setOnAction(event -> mapView.setTileSource(osm));
        btOTM.setOnAction(event -> mapView.setTileSource(otm));
        btTrack.setOnAction(event -> renderTrack());
      }

    /***********************************************************************************************************************************************************
     * Releases the resources: the {@link MapView} and the executor used to load the track.
     **********************************************************************************************************************************************************/
    public void dispose()
      {
        mapView.dispose();
        executorService.shutdownNow();
      }

    /***********************************************************************************************************************************************************
     *
     **********************************************************************************************************************************************************/
    @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
    public void renderTrack()
      {
        record PointsAndArea(List<MapCoordinates> points, MapArea area) {}

        final var task = new Task<PointsAndArea>()
          {
            @Override @Nonnull
            protected PointsAndArea call()
              {
                final var coordinates = loadTrack().segments()
                                                   .flatMap(TrackSegment::points)
                                                   .map(p -> MapCoordinates.of(p.getLatitude().doubleValue(), p.getLongitude().doubleValue()))
                                                   .toList();
                final var area =  MapArea.enclosing(coordinates);
                log.info("track with {} trackPoints, enclosing area: {}", coordinates.size(), area);
                return new PointsAndArea(coordinates, area.orElse(MapArea.of(0, 0, 0, 0)));
              }
          };

        task.setOnSucceeded(event ->
          {
            try
              {
                final var pointsAndArea = task.get();
                mapView.removeOverlay(TRACK_OVERLAY_NAME);
                mapView.addOverlay(TRACK_OVERLAY_NAME, helper -> helper.addAll(pointsAndArea.points.stream().map(wp -> createPoint(helper, wp)).toList()));
                mapView.fitArea(pointsAndArea.area);
              }
            catch (InterruptedException e)
              {
                log.error("", e);
                Thread.currentThread().interrupt();
              }
            catch (ExecutionException e)
              {
                log.error("", e);
              }
          });

        executorService.submit(task);
      }

    /***********************************************************************************************************************************************************
     *
     **********************************************************************************************************************************************************/
    @Nonnull
    private static Node createPoint (@Nonnull final MapView.OverlayHelper helper, @Nonnull final MapCoordinates coordinates)
      {
        final var point = helper.toOverlayPoint(coordinates);
        return new Circle(point.getX(), point.getY(), 2.5, Color.RED);
      }

    /***********************************************************************************************************************************************************
     *
     **********************************************************************************************************************************************************/
    @Nonnull
    private Track loadTrack()
      {
        try (final var is = MapViewExampleController.class.getResourceAsStream("/2014-04-10 1800__20140410_1800.gpx"))
          {
            return GPX.Reader.of(GPX.Reader.Mode.LENIENT).read(is).getTracks().getFirst();
          }
        catch (IOException e)
          {
            log.error(e.toString());
            throw new UncheckedIOException(e);
          }
      }
  }