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);
}
}
}