Skip to content
Snippets Groups Projects
Commit 3000c395 authored by Hallvard Trætteberg's avatar Hallvard Trætteberg
Browse files

Imports and uses FxMapControl. Issue #10.

parent 4e8e8e36
No related branches found
No related tags found
No related merge requests found
Showing
with 2768 additions and 0 deletions
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2017 Clemens Fischer
*/
package fxmapcontrol;
/**
* A bounding box with south and north latitude and west and east longitude values in degrees.
*/
public class MapBoundingBox {
private double south;
private double west;
private double north;
private double east;
public MapBoundingBox() {
}
public MapBoundingBox(double south, double west, double north, double east) {
this.south = Math.min(Math.max(south, -90d), 90d);
this.west = west;
this.north = Math.min(Math.max(north, -90d), 90d);
this.east = east;
}
public double getSouth() {
return south;
}
public void setSouth(double south) {
this.south = Math.min(Math.max(south, -90d), 90d);
}
public double getWest() {
return west;
}
public void setWest(double west) {
this.west = west;
}
public double getNorth() {
return north;
}
public void setNorth(double north) {
this.north = Math.min(Math.max(north, -90d), 90d);
}
public double getEast() {
return east;
}
public void setEast(double east) {
this.east = east;
}
public double getWidth() {
return east - west;
}
public double getHeight() {
return north - south;
}
public boolean hasValidBounds() {
return south < north && west < east;
}
public MapBoundingBox clone() {
return new MapBoundingBox(south, west, north, east);
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleablePropertyFactory;
import javafx.geometry.BoundingBox;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
/**
* Draws a graticule overlay.
*/
public class MapGraticule extends Parent implements IMapNode {
private static final StyleablePropertyFactory<MapGraticule> propertyFactory
= new StyleablePropertyFactory<>(Parent.getClassCssMetaData());
private static final CssMetaData<MapGraticule, Font> fontCssMetaData
= propertyFactory.createFontCssMetaData("-fx-font", s -> s.fontProperty);
private static final CssMetaData<MapGraticule, Paint> textFillCssMetaData
= propertyFactory.createPaintCssMetaData("-fx-text-fill", s -> s.textFillProperty);
private static final CssMetaData<MapGraticule, Paint> strokeCssMetaData
= propertyFactory.createPaintCssMetaData("-fx-stroke", s -> s.strokeProperty);
private static final CssMetaData<MapGraticule, Number> strokeWidthCssMetaData
= propertyFactory.createSizeCssMetaData("-fx-stroke-width", s -> s.strokeWidthProperty);
private static final CssMetaData<MapGraticule, Number> minLineDistanceCssMetaData
= propertyFactory.createSizeCssMetaData("-fx-min-line-distance", s -> s.minLineDistanceProperty);
private final StyleableObjectProperty<Font> fontProperty
= new SimpleStyleableObjectProperty<>(fontCssMetaData, this, "font", Font.getDefault());
private final StyleableObjectProperty<Paint> textFillProperty
= new SimpleStyleableObjectProperty<>(textFillCssMetaData, this, "textFill", Color.BLACK);
private final StyleableObjectProperty<Paint> strokeProperty
= new SimpleStyleableObjectProperty<>(strokeCssMetaData, this, "stroke", Color.BLACK);
private final StyleableDoubleProperty strokeWidthProperty
= new SimpleStyleableDoubleProperty(strokeWidthCssMetaData, this, "strokeWidth", 0.5);
private final StyleableDoubleProperty minLineDistanceProperty
= new SimpleStyleableDoubleProperty(minLineDistanceCssMetaData, this, "minLineDistance", 150d);
private final MapNodeHelper mapNode = new MapNodeHelper(e -> viewportChanged());
public MapGraticule() {
getStyleClass().add("map-graticule");
setMouseTransparent(true);
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return propertyFactory.getCssMetaData();
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
@Override
public final MapBase getMap() {
return mapNode.getMap();
}
@Override
public void setMap(MapBase map) {
mapNode.setMap(map);
if (map != null) {
viewportChanged();
}
}
public final ObjectProperty<Font> fontProperty() {
return fontProperty;
}
public final Font getFont() {
return fontProperty.get();
}
public final void setFont(Font font) {
fontProperty.set(font);
}
public final ObjectProperty<Paint> textFillProperty() {
return textFillProperty;
}
public final Paint getTextFill() {
return textFillProperty.get();
}
public final void setTextFill(Paint textFill) {
textFillProperty.set(textFill);
}
public final ObjectProperty<Paint> strokeProperty() {
return strokeProperty;
}
public final Paint getStroke() {
return strokeProperty.get();
}
public final void setStroke(Paint stroke) {
strokeProperty.set(stroke);
}
public final DoubleProperty strokeWidthProperty() {
return strokeWidthProperty;
}
public final double getStrokeWidth() {
return strokeWidthProperty.get();
}
public final void setStrokeWidth(double strokeWidth) {
strokeWidthProperty.set(strokeWidth);
}
public final DoubleProperty minLineDistanceProperty() {
return minLineDistanceProperty;
}
public final double getMinLineDistance() {
return minLineDistanceProperty.get();
}
public final void setMinLineDistance(double minLineDistance) {
minLineDistanceProperty.set(minLineDistance);
}
private void viewportChanged() {
MapBase map = getMap();
MapProjection projection = map.getProjection();
ObservableList<Node> children = getChildren();
if (projection.isNormalCylindrical()) {
MapBoundingBox mapBounds = projection.viewportBoundsToBoundingBox(
new BoundingBox(0, 0, map.getWidth(), map.getHeight()));
double lineDistance = getLineDistance();
double latLabelStart = Math.ceil(mapBounds.getSouth() / lineDistance) * lineDistance;
double lonLabelStart = Math.ceil(mapBounds.getWest() / lineDistance) * lineDistance;
ArrayList<PathElement> pathElements = new ArrayList<>();
for (double lat = latLabelStart; lat <= mapBounds.getNorth(); lat += lineDistance) {
Point2D lineStart = projection.locationToViewportPoint(new Location(lat, mapBounds.getWest()));
Point2D lineEnd = projection.locationToViewportPoint(new Location(lat, mapBounds.getEast()));
pathElements.add(new MoveTo(lineStart.getX(), lineStart.getY()));
pathElements.add(new LineTo(lineEnd.getX(), lineEnd.getY()));
}
for (double lon = lonLabelStart; lon <= mapBounds.getEast(); lon += lineDistance) {
Point2D lineStart = projection.locationToViewportPoint(new Location(mapBounds.getSouth(), lon));
Point2D lineEnd = projection.locationToViewportPoint(new Location(mapBounds.getNorth(), lon));
pathElements.add(new MoveTo(lineStart.getX(), lineStart.getY()));
pathElements.add(new LineTo(lineEnd.getX(), lineEnd.getY()));
}
Path path;
if (children.isEmpty()) {
path = new Path();
path.strokeProperty().bind(strokeProperty);
path.strokeWidthProperty().bind(strokeWidthProperty);
children.add(path);
} else {
path = (Path) children.get(0);
}
path.getElements().setAll(pathElements);
Font font = getFont();
int childIndex = 1;
if (font != null) {
String format = getLabelFormat(lineDistance);
Rotate rotate = new Rotate(map.getHeading());
for (double lat = latLabelStart; lat <= mapBounds.getNorth(); lat += lineDistance) {
for (double lon = lonLabelStart; lon <= mapBounds.getEast(); lon += lineDistance) {
Point2D pos = projection.locationToViewportPoint(new Location(lat, lon));
Translate translate = new Translate(pos.getX(), pos.getY());
Text text;
if (childIndex < children.size()) {
text = (Text) children.get(childIndex);
text.getTransforms().set(0, translate);
text.getTransforms().set(1, rotate);
} else {
text = new Text();
text.fillProperty().bind(textFillProperty);
text.setX(3);
text.setY(-font.getSize() / 4);
text.getTransforms().add(translate);
text.getTransforms().add(rotate);
children.add(text);
}
text.setFont(getFont());
text.setText(getLabelText(lat, format, "NS") + "\n"
+ getLabelText(Location.normalizeLongitude(lon), format, "EW"));
childIndex++;
}
}
}
children.remove(childIndex, children.size());
} else {
children.clear();
}
}
private double getLineDistance() {
double minDistance = getMinLineDistance() * 360d / (Math.pow(2d, getMap().getZoomLevel()) * TileSource.TILE_SIZE);
double scale = 1d;
if (minDistance < 1d) {
scale = minDistance < 1d / 60d ? 3600d : 60d;
minDistance *= scale;
}
double[] lineDistances = new double[]{1d, 2d, 5d, 10d, 15d, 30d, 60d};
int i = 0;
while (i < lineDistances.length - 1 && lineDistances[i] < minDistance) {
i++;
}
return lineDistances[i] / scale;
}
private static String getLabelFormat(double lineDistance) {
if (lineDistance < 1d / 60d) {
return "%c %d°%02d'%02d\"";
}
if (lineDistance < 1d) {
return "%c %d°%02d'";
}
return "%c %d°";
}
private static String getLabelText(double value, String format, String hemispheres) {
char hemisphere = hemispheres.charAt(0);
if (value < -1e-8) // ~1mm
{
value = -value;
hemisphere = hemispheres.charAt(1);
}
int seconds = (int) Math.round(value * 3600d);
return String.format(format, hemisphere, seconds / 3600, (seconds / 60) % 60, seconds % 60);
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2017 Clemens Fischer
*/
package fxmapcontrol;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Bounds;
import javafx.scene.image.ImageView;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
/**
* Fills a rectangular map area defined by the boundingBox property with an Image.
*/
public class MapImage extends ImageView implements IMapNode {
private final ObjectProperty<MapBoundingBox> boundingBoxProperty = new SimpleObjectProperty<>(this, "boundingBox");
private final MapNodeHelper mapNode = new MapNodeHelper(e -> updateLayout());
public MapImage() {
setMouseTransparent(true);
boundingBoxProperty.addListener(observable -> updateLayout());
}
@Override
public final MapBase getMap() {
return mapNode.getMap();
}
@Override
public void setMap(MapBase map) {
mapNode.setMap(map);
updateLayout();
}
public final ObjectProperty<MapBoundingBox> boundingBoxProperty() {
return boundingBoxProperty;
}
public final MapBoundingBox getBoundingBox() {
return boundingBoxProperty.get();
}
public final void setBoundingBox(MapBoundingBox boundingBox) {
boundingBoxProperty.set(boundingBox);
}
private void updateLayout() {
Affine viewportTransform = null;
Bounds bounds = null;
MapBase map = getMap();
MapBoundingBox boundingBox = getBoundingBox();
if (map != null && boundingBox != null) {
bounds = map.getProjection().boundingBoxToBounds(boundingBox);
viewportTransform = map.getProjection().getViewportTransform();
}
if (bounds != null) {
getTransforms().setAll(viewportTransform, Transform.scale(1d, -1d, 0d, bounds.getMaxY()));
setX(bounds.getMinX());
setY(bounds.getMaxY());
setFitWidth(bounds.getWidth());
setFitHeight(bounds.getHeight());
} else {
getTransforms().clear();
setX(0);
setY(0);
setFitWidth(0);
setFitHeight(0);
}
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.FadeTransition;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableBooleanProperty;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleablePropertyFactory;
import javafx.geometry.BoundingBox;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.image.Image;
import javafx.util.Duration;
/**
* Map image overlay. Fills the entire viewport with map images provided by a web service, e.g. a
* Web Map Service (WMS). The image must be provided by the abstract updateImage(MapBoundingBox) method.
*/
public abstract class MapImageLayer extends Parent implements IMapNode {
private static final StyleablePropertyFactory<MapImageLayer> propertyFactory
= new StyleablePropertyFactory<>(Parent.getClassCssMetaData());
private static final CssMetaData<MapImageLayer, Duration> updateDelayCssMetaData
= propertyFactory.createDurationCssMetaData("-fx-update-delay", s -> s.updateDelayProperty);
private static final CssMetaData<MapImageLayer, Boolean> updateWhileViewportChangingCssMetaData
= propertyFactory.createBooleanCssMetaData("-fx-update-while-viewport-changing", s -> s.updateWhileViewportChangingProperty);
private static final CssMetaData<MapImageLayer, Number> relativeImageSizeCssMetaData
= propertyFactory.createSizeCssMetaData("-fx-relative-image-size", s -> s.relativeImageSizeProperty);
private final StyleableObjectProperty<Duration> updateDelayProperty
= new SimpleStyleableObjectProperty<>(updateDelayCssMetaData, this, "updateDelay", Duration.seconds(0.2));
private final StyleableBooleanProperty updateWhileViewportChangingProperty
= new SimpleStyleableBooleanProperty(updateWhileViewportChangingCssMetaData, this, "updateWhileViewportChanging");
private final StyleableDoubleProperty relativeImageSizeProperty
= new SimpleStyleableDoubleProperty(relativeImageSizeCssMetaData, this, "relativeImageSize", 1d);
private final DoubleProperty minLatitudeProperty = new SimpleDoubleProperty(this, "minLatitude", Double.NaN);
private final DoubleProperty maxLatitudeProperty = new SimpleDoubleProperty(this, "maxLatitude", Double.NaN);
private final DoubleProperty minLongitudeProperty = new SimpleDoubleProperty(this, "minLongitude", Double.NaN);
private final DoubleProperty maxLongitudeProperty = new SimpleDoubleProperty(this, "maxLongitude", Double.NaN);
private final DoubleProperty maxBoundingBoxWidthProperty = new SimpleDoubleProperty(this, "maxBoundingBoxWidth", Double.NaN);
private final Timeline updateTimeline = new Timeline();
private final MapNodeHelper mapNode = new MapNodeHelper(e -> viewportChanged(e.getProjectionChanged(), e.getLongitudeOffset()));
private MapBoundingBox boundingBox;
private boolean updateInProgress;
public MapImageLayer() {
getStyleClass().add("map-image-layer");
setMouseTransparent(true);
updateTimeline.getKeyFrames().add(new KeyFrame(getUpdateDelay(), e -> updateImage()));
updateDelayProperty.addListener(observable
-> updateTimeline.getKeyFrames().set(0, new KeyFrame(getUpdateDelay(), e -> updateImage())));
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return propertyFactory.getCssMetaData();
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
@Override
public final MapBase getMap() {
return mapNode.getMap();
}
@Override
public void setMap(MapBase map) {
mapNode.setMap(map);
getChildren().forEach(image -> ((MapImage) image).setMap(map));
updateImage();
}
public final ObjectProperty<Duration> updateDelayProperty() {
return updateDelayProperty;
}
public final Duration getUpdateDelay() {
return updateDelayProperty.get();
}
public final void setUpdateDelay(Duration updateDelay) {
updateDelayProperty.set(updateDelay);
}
public final BooleanProperty updateWhileViewportChangingProperty() {
return updateWhileViewportChangingProperty;
}
public final boolean getUpdateWhileViewportChanging() {
return updateWhileViewportChangingProperty.get();
}
public final void setUpdateWhileViewportChanging(boolean updateWhileViewportChanging) {
updateWhileViewportChangingProperty.set(updateWhileViewportChanging);
}
public final DoubleProperty relativeImageSizeProperty() {
return relativeImageSizeProperty;
}
public final double getRelativeImageSize() {
return relativeImageSizeProperty.get();
}
public final void setRelativeImageSize(double relativeImageSize) {
relativeImageSizeProperty.set(relativeImageSize);
}
public final DoubleProperty minLatitudeProperty() {
return minLatitudeProperty;
}
public final double getMinLatitude() {
return minLatitudeProperty.get();
}
public final void setMinLatitude(double minLatitude) {
minLatitudeProperty.set(minLatitude);
}
public final DoubleProperty maxLatitudeProperty() {
return maxLatitudeProperty;
}
public final double getMaxLatitude() {
return maxLatitudeProperty.get();
}
public final void setMaxLatitude(double maxLatitude) {
maxLatitudeProperty.set(maxLatitude);
}
public final DoubleProperty minLongitudeProperty() {
return minLongitudeProperty;
}
public final double getMinLongitude() {
return minLongitudeProperty.get();
}
public final void setMinLongitude(double minLongitude) {
minLongitudeProperty.set(minLongitude);
}
public final DoubleProperty maxLongitudeProperty() {
return maxLongitudeProperty;
}
public final double getMaxLongitude() {
return maxLongitudeProperty.get();
}
public final void setMaxLongitude(double maxLongitude) {
maxLongitudeProperty.set(maxLongitude);
}
public final DoubleProperty maxBoundingBoxWidthProperty() {
return maxBoundingBoxWidthProperty;
}
public final double getMaxBoundingBoxWidth() {
return maxBoundingBoxWidthProperty.get();
}
public final void setMaxBoundingBoxWidth(double maxBoundingBoxWidth) {
maxBoundingBoxWidthProperty.set(maxBoundingBoxWidth);
}
private void viewportChanged(boolean projectionChanged, double longitudeOffset) {
if (projectionChanged) {
updateImage((Image) null);
updateImage();
} else {
if (Math.abs(longitudeOffset) > 180d && boundingBox != null && boundingBox.hasValidBounds()) {
double offset = 360d * Math.signum(longitudeOffset);
boundingBox.setWest(boundingBox.getWest() + offset);
boundingBox.setEast(boundingBox.getEast() + offset);
getChildren().forEach(image -> {
MapImage mapImage = (MapImage) image;
MapBoundingBox bbox = mapImage.getBoundingBox();
if (bbox != null && bbox.hasValidBounds()) {
mapImage.setBoundingBox(new MapBoundingBox(
bbox.getSouth(), bbox.getWest() + offset,
bbox.getNorth(), bbox.getEast() + offset));
}
});
}
if (getUpdateWhileViewportChanging()) {
updateTimeline.play();
} else {
updateTimeline.playFromStart();
}
}
}
protected void updateImage() {
MapBase map = getMap();
if (updateInProgress) {
updateTimeline.playFromStart(); // update image on next timer tick
} else if (map != null && map.getWidth() > 0 && map.getHeight() > 0) {
updateInProgress = true;
MapProjection projection = map.getProjection();
double width = map.getWidth() * getRelativeImageSize();
double height = map.getHeight() * getRelativeImageSize();
double x = (map.getWidth() - width) / 2d;
double y = (map.getHeight() - height) / 2d;
boundingBox = projection.viewportBoundsToBoundingBox(new BoundingBox(x, y, width, height));
if (boundingBox != null && boundingBox.hasValidBounds()) {
if (!Double.isNaN(getMinLatitude()) && boundingBox.getSouth() < getMinLatitude()) {
boundingBox.setSouth(getMinLatitude());
}
if (!Double.isNaN(getMinLongitude()) && boundingBox.getWest() < getMinLongitude()) {
boundingBox.setWest(getMinLongitude());
}
if (!Double.isNaN(getMaxLatitude()) && boundingBox.getNorth() > getMaxLatitude()) {
boundingBox.setNorth(getMaxLatitude());
}
if (!Double.isNaN(getMaxLongitude()) && boundingBox.getEast() > getMaxLongitude()) {
boundingBox.setEast(getMaxLongitude());
}
if (!Double.isNaN(getMaxBoundingBoxWidth()) && boundingBox.getWidth() > getMaxBoundingBoxWidth()) {
double d = (boundingBox.getWidth() - getMaxBoundingBoxWidth()) / 2;
boundingBox.setWest(boundingBox.getWest() + d);
boundingBox.setEast(boundingBox.getEast() - d);
}
}
boolean imageUpdated = false;
try {
imageUpdated = updateImage(boundingBox);
} catch (Exception ex) {
Logger.getLogger(MapImageLayer.class.getName()).log(Level.SEVERE, null, ex);
}
if (!imageUpdated) {
updateImage((Image) null);
}
}
}
/**
* Creates an image request URL or a javafx.scene.image.Image for the specified bounding box.
* Must either call updateImage(String) or updateImage(Image) or return false on failure.
*
* @param boundingBox the image's bounding box
* @return true on success, false on failure
*/
protected abstract boolean updateImage(MapBoundingBox boundingBox);
protected void updateImage(String url) {
Image image = new Image(url, true);
image.progressProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.doubleValue() >= 1d) {
updateImage(image);
}
});
image.errorProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
updateImage((Image) null);
}
});
}
protected final void updateImage(Image image) {
MapBase map = getMap();
if (map != null) {
ObservableList<Node> children = getChildren();
MapImage mapImage;
if (children.isEmpty()) {
mapImage = new MapImage();
mapImage.setMap(map);
mapImage.setOpacity(0d);
children.add(mapImage);
mapImage = new MapImage();
mapImage.setMap(map);
mapImage.setOpacity(0d);
} else {
mapImage = (MapImage) children.remove(0);
}
children.add(mapImage);
mapImage.setImage(image);
mapImage.setBoundingBox(boundingBox != null ? boundingBox.clone() : null);
if (image != null) {
FadeTransition fadeTransition = new FadeTransition(map.getTileFadeDuration(), mapImage);
fadeTransition.setToValue(1d);
fadeTransition.setOnFinished(e -> children.get(0).setOpacity(0d));
fadeTransition.play();
} else {
children.get(0).setOpacity(0d);
children.get(1).setOpacity(0d);
}
}
updateInProgress = false;
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
/**
* Container node of an item in a MapItemsControl.
*
* @param <T> the item type of the parent MapItemsControl.
*/
public class MapItem<T> extends MapNode {
private final BooleanProperty selectedProperty = new SimpleBooleanProperty(this, "selected");
private final T item;
public MapItem() {
this(null, true);
}
public MapItem(T item) {
this(item, true);
}
public MapItem(T item, boolean defaultClickBehavior) {
this.item = item;
getStyleClass().add("map-item");
if (defaultClickBehavior) {
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == MouseButton.PRIMARY) {
setSelected(!isSelected());
e.consume();
}
});
}
}
public final T getItem() {
return item;
}
public final BooleanProperty selectedProperty() {
return selectedProperty;
}
public final boolean isSelected() {
return selectedProperty.get();
}
public final void setSelected(boolean selected) {
selectedProperty.set(selected);
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Point2D;
/**
* Extendable base class for a map item with a location.
*/
public class MapItemBase extends MapLayer {
private final ObjectProperty<Location> locationProperty = new SimpleObjectProperty<>(this, "location");
public MapItemBase() {
getStyleClass().add("map-item-base");
locationProperty.addListener(observable -> updateViewportPosition());
}
public final ObjectProperty<Location> locationProperty() {
return locationProperty;
}
public final Location getLocation() {
return locationProperty.get();
}
public final void setLocation(Location location) {
locationProperty.set(location);
}
@Override
public void setMap(MapBase map) {
super.setMap(map);
viewportChanged();
}
@Override
protected void viewportChanged() {
updateViewportPosition();
}
protected final void updateViewportPosition() {
Location location = getLocation();
Point2D viewportPosition = null;
MapBase map;
if (location != null && (map = getMap()) != null) {
viewportPosition = map.getProjection().locationToViewportPoint(new Location(
location.getLatitude(),
Location.nearestLongitude(location.getLongitude(), map.getCenter().getLongitude())));
}
viewportPositionChanged(viewportPosition);
}
protected void viewportPositionChanged(Point2D viewportPosition) {
if (viewportPosition != null) {
setTranslateX(viewportPosition.getX());
setTranslateY(viewportPosition.getY());
} else {
setTranslateX(0d);
setTranslateY(0d);
}
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.util.Collection;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
import javafx.scene.control.SelectionMode;
import javafx.util.Callback;
/**
* Manages a collection of selectable items on a map. Uses MapItem as item container node class.
*
* @param <T> the (element) type of the {@code items}, {@code selectedItem} and
* {@code selectedItems} properties. If T does not extend {@link MapItem}, an item generator
* callback must be assigned to the {@code itemGenerator} property.
*/
public class MapItemsControl<T> extends Parent implements IMapNode {
private final ListProperty<T> itemsProperty = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final ObjectProperty<SelectionMode> selectionModeProperty = new SimpleObjectProperty<>(this, "selectionMode", SelectionMode.SINGLE);
private final ObjectProperty<T> selectedItemProperty = new SimpleObjectProperty<>(this, "selectedItem");
private final ObservableList<T> selectedItems = FXCollections.observableArrayList();
private final MapItemSelectedListener itemSelectedListener = new MapItemSelectedListener();
private Callback<T, MapItem<T>> itemGenerator;
private MapBase map;
public MapItemsControl() {
itemsProperty.addListener((ListChangeListener.Change<? extends T> change) -> {
while (change.next()) {
if (change.wasRemoved()) {
removeChildren(change.getRemoved());
}
if (change.wasAdded()) {
addChildren(change.getAddedSubList(), change.getFrom());
}
}
});
selectedItemProperty.addListener((observable, oldValue, newValue) -> {
if (oldValue != null && getSelectionMode() == SelectionMode.SINGLE) {
setItemSelected(oldValue, false);
}
if (newValue != null) {
setItemSelected(newValue, true);
}
});
selectedItems.addListener((ListChangeListener.Change<? extends T> change) -> {
while (change.next()) {
if (change.wasRemoved()) {
change.getRemoved().forEach(item -> setItemSelected(item, false));
}
if (change.wasAdded()) {
change.getAddedSubList().forEach(item -> setItemSelected(item, true));
}
}
});
}
@Override
public final MapBase getMap() {
return map;
}
@Override
public void setMap(MapBase map) {
this.map = map;
getChildren().forEach(item -> ((MapItem) item).setMap(map));
}
/**
* Gets the {@code Callback<T, MapItem>} that generates a MapItem if T does not extend MapItem.
*
* @return the {@code Callback<T, MapItem>}
*/
public final Callback<T, MapItem<T>> getItemGenerator() {
return itemGenerator;
}
/**
* Sets the {@code Callback<T, MapItem>} that generates a MapItem if T does not extend MapItem.
*
* @param itemGenerator the {@code Callback<T, MapItem>}
*/
public final void setItemGenerator(Callback<T, MapItem<T>> itemGenerator) {
this.itemGenerator = itemGenerator;
}
public final ListProperty<T> itemsProperty() {
return itemsProperty;
}
public final ObservableList<T> getItems() {
return itemsProperty.get();
}
public final void setItems(ObservableList<T> items) {
itemsProperty.set(items);
}
public final ObjectProperty<SelectionMode> selectionModeProperty() {
return selectionModeProperty;
}
public final SelectionMode getSelectionMode() {
return selectionModeProperty.get();
}
public final void setSelectionMode(SelectionMode selectionMode) {
selectionModeProperty.set(selectionMode);
}
public final ObjectProperty<T> selectedItemProperty() {
return selectedItemProperty;
}
public final T getSelectedItem() {
return selectedItemProperty.get();
}
public final void setSelectedItem(T selectedItem) {
selectedItemProperty.set(getItems().contains(selectedItem) ? selectedItem : null);
}
public final ObservableList<T> getSelectedItems() {
return selectedItems;
}
public final MapItem getMapItem(T item) {
return item instanceof MapItem
? (MapItem) item
: (MapItem) getChildren().stream()
.filter(node -> ((MapItem) node).getItem() == item)
.findFirst().orElse(null);
}
private MapItem createMapItem(T item) {
return item instanceof MapItem
? (MapItem) item
: (itemGenerator != null ? itemGenerator.call(item) : null);
}
private void setItemSelected(T item, boolean selected) {
if (getItems().contains(item)) {
MapItem mapItem = getMapItem(item);
if (mapItem != null) {
mapItem.setSelected(selected);
}
}
}
private void addChildren(Collection<? extends T> items, int position) {
for (T item : items) {
MapItem mapItem = createMapItem(item);
if (mapItem != null) {
mapItem.setMap(map);
if (mapItem.isSelected()) {
addSelectedItem(item);
}
mapItem.selectedProperty().addListener(itemSelectedListener);
getChildren().add(position++, mapItem);
}
}
}
private void removeChildren(Collection<? extends T> items) {
for (T item : items) {
MapItem mapItem = getMapItem(item);
if (mapItem != null) {
mapItem.setMap(null);
mapItem.selectedProperty().removeListener(itemSelectedListener);
getChildren().remove(mapItem);
}
removeSelectedItem(item);
}
}
private void addSelectedItem(T item) {
if (getItems().contains(item) && !selectedItems.contains(item)) {
if (getSelectionMode() == SelectionMode.SINGLE) {
selectedItems.clear();
}
selectedItems.add(item);
if (getSelectedItem() == null || getSelectionMode() == SelectionMode.SINGLE) {
setSelectedItem(item);
}
}
}
private void removeSelectedItem(T item) {
if (selectedItems.remove(item) && getSelectedItem() == item) {
setSelectedItem(selectedItems.isEmpty() ? null : selectedItems.get(0));
}
}
private class MapItemSelectedListener implements ChangeListener<Boolean> {
private boolean selectionChanging;
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (!selectionChanging) {
selectionChanging = true;
ReadOnlyProperty<? extends Boolean> property = (ReadOnlyProperty<? extends Boolean>) observable;
MapItem mapItem = (MapItem) property.getBean();
T item = (T) mapItem.getItem();
if (item == null) {
item = (T) mapItem;
}
if (newValue) {
addSelectedItem(item);
} else {
removeSelectedItem(item);
}
selectionChanging = false;
}
}
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import javafx.beans.DefaultProperty;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.Parent;
/**
* Extendable base class for map overlays.
*/
@DefaultProperty(value="children")
public class MapLayer extends Parent implements IMapNode {
private final MapNodeHelper mapNode = new MapNodeHelper(e -> viewportChanged());
public MapLayer() {
getStyleClass().add("map-layer");
getChildren().addListener(new MapNodeHelper.ChildrenListener(this));
}
@Override
public final ObservableList<Node> getChildren() {
return super.getChildren();
}
@Override
public final MapBase getMap() {
return mapNode.getMap();
}
@Override
public void setMap(MapBase map) {
mapNode.setMap(map);
getChildren().stream()
.filter(node -> node instanceof IMapNode)
.forEach(node -> ((IMapNode) node).setMap(map));
}
protected void viewportChanged() {
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Point2D;
import javafx.scene.shape.Line;
/**
* A Line with start and end points given as geographic positions.
*/
public class MapLine extends Line implements IMapNode {
private final ObjectProperty<Location> startLocationProperty = new SimpleObjectProperty<>(this, "startLocation");
private final ObjectProperty<Location> endLocationProperty = new SimpleObjectProperty<>(this, "endLocation");
private final MapNodeHelper mapNode = new MapNodeHelper(e -> updatePoints());
public MapLine() {
getStyleClass().add("map-line");
startLocationProperty.addListener(observable -> updateStartPoint());
endLocationProperty.addListener(observable -> updateEndPoint());
}
public MapLine(Location start, Location end) {
this();
setStartLocation(start);
setEndLocation(end);
}
@Override
public MapBase getMap() {
return mapNode.getMap();
}
@Override
public void setMap(MapBase map) {
mapNode.setMap(map);
updatePoints();
}
public final ObjectProperty<Location> startLocationProperty() {
return startLocationProperty;
}
public final Location getStartLocation() {
return startLocationProperty.get();
}
public final void setStartLocation(Location location) {
startLocationProperty.set(location);
}
public final ObjectProperty<Location> endLocationProperty() {
return endLocationProperty;
}
public final Location getEndLocation() {
return endLocationProperty.get();
}
public final void setEndLocation(Location location) {
endLocationProperty.set(location);
}
private void updatePoints() {
updateStartPoint();
updateEndPoint();
}
private void updateStartPoint() {
MapBase map = getMap();
Location start = getStartLocation();
if (map != null && start != null) {
Point2D p = map.getProjection().locationToViewportPoint(start);
setStartX(p.getX());
setStartY(p.getY());
setVisible(getEndLocation() != null);
} else {
setVisible(false);
setStartX(0);
setStartY(0);
}
}
private void updateEndPoint() {
MapBase map = getMap();
Location end = getEndLocation();
if (map != null && end != null) {
Point2D p = map.getProjection().locationToViewportPoint(end);
setEndX(p.getX());
setEndY(p.getY());
setVisible(getStartLocation() != null);
} else {
setVisible(false);
setEndX(0);
setEndY(0);
}
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Parent;
/**
* Base class for map items and overlays. If the location property is set to a non-null value, the
* MapNode is placed at the appropriate map viewport position, otherwise at the top left corner.
*/
@DefaultProperty(value = "children")
public class MapNode extends Parent implements IMapNode {
private final MapNodeHelper mapNode = new MapNodeHelper(e -> viewportChanged());
private final ObjectProperty<Location> locationProperty = new SimpleObjectProperty<>(this, "location");
public MapNode() {
getStyleClass().add("map-node");
getChildren().addListener(new MapNodeHelper.ChildrenListener(this));
locationProperty.addListener(observable -> updateViewportPosition());
}
public final ObjectProperty<Location> locationProperty() {
return locationProperty;
}
public final Location getLocation() {
return locationProperty.get();
}
public final void setLocation(Location location) {
locationProperty.set(location);
}
@Override
public final ObservableList<Node> getChildren() {
return super.getChildren();
}
@Override
public final MapBase getMap() {
return mapNode.getMap();
}
@Override
public void setMap(MapBase map) {
mapNode.setMap(map);
getChildren().stream()
.filter(node -> node instanceof IMapNode)
.forEach(node -> ((IMapNode) node).setMap(map));
viewportChanged();
}
protected void viewportChanged() {
updateViewportPosition();
}
protected void viewportPositionChanged(Point2D viewportPosition) {
if (viewportPosition != null) {
setTranslateX(viewportPosition.getX());
setTranslateY(viewportPosition.getY());
} else {
setTranslateX(0d);
setTranslateY(0d);
}
}
private void updateViewportPosition() {
Location location = getLocation();
Point2D viewportPosition = null;
MapBase map;
if (location != null && (map = getMap()) != null) {
viewportPosition = map.getProjection().locationToViewportPoint(location);
if (viewportPosition.getX() < 0d || viewportPosition.getX() > map.getWidth()
|| viewportPosition.getY() < 0d || viewportPosition.getY() > map.getHeight()) {
viewportPosition = map.getProjection().locationToViewportPoint(new Location(
location.getLatitude(),
Location.nearestLongitude(location.getLongitude(), map.getCenter().getLongitude())));
}
}
viewportPositionChanged(viewportPosition);
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2017 Clemens Fischer
*/
package fxmapcontrol;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.scene.Node;
/**
* Helper class for implementing the IMapNode interface.
*/
public final class MapNodeHelper implements IMapNode {
private final EventHandler<ViewportChangedEvent> viewportChangedHandler;
private MapBase map;
public MapNodeHelper(EventHandler<ViewportChangedEvent> viewportChangedHandler) {
this.viewportChangedHandler = viewportChangedHandler;
}
@Override
public final MapBase getMap() {
return map;
}
@Override
public final void setMap(MapBase map) {
if (this.map != null) {
this.map.removeEventHandler(ViewportChangedEvent.VIEWPORT_CHANGED, viewportChangedHandler);
}
this.map = map;
if (this.map != null) {
this.map.addEventHandler(ViewportChangedEvent.VIEWPORT_CHANGED, viewportChangedHandler);
}
}
public static class ChildrenListener implements ListChangeListener<Node> {
private final IMapNode mapNode;
public ChildrenListener(IMapNode parentNode) {
mapNode = parentNode;
}
@Override
public void onChanged(ListChangeListener.Change<? extends Node> change) {
while (change.next()) {
if (change.wasRemoved()) {
change.getRemoved().stream()
.filter(node -> node instanceof IMapNode)
.forEach(node -> ((IMapNode) node).setMap(null));
}
if (change.wasAdded()) {
change.getAddedSubList().stream()
.filter(node -> node instanceof IMapNode)
.forEach(node -> ((IMapNode) node).setMap(mapNode.getMap()));
}
}
}
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.util.Collection;
import java.util.List;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.StrokeLineJoin;
/**
* A Polygon with points given as geographic positions by the locations property. The optional location property helps
* to calculate a viewport position that is nearest to the map center, in the same way as it is done for MapItems.
*/
public class MapPolygon extends Polygon implements IMapNode {
private final ListProperty<Location> locationsProperty = new SimpleListProperty<>(this, "locations", FXCollections.observableArrayList());
private final ObjectProperty<Location> locationProperty = new SimpleObjectProperty<>(this, "location");
private final MapNodeHelper mapNode = new MapNodeHelper(e -> updatePoints());
public MapPolygon() {
getStyleClass().add("map-polygon");
setFill(null);
setStrokeLineJoin(StrokeLineJoin.ROUND);
locationsProperty.addListener((ListChangeListener.Change<? extends Location> c) -> updatePoints());
}
public MapPolygon(Collection<Location> locations) {
this();
getLocations().addAll(locations);
}
@Override
public final MapBase getMap() {
return mapNode.getMap();
}
@Override
public void setMap(MapBase map) {
mapNode.setMap(map);
updatePoints();
}
public final ListProperty<Location> locationsProperty() {
return locationsProperty;
}
public final ObservableList<Location> getLocations() {
return locationsProperty.get();
}
public final void setLocations(ObservableList<Location> locations) {
locationsProperty.set(locations);
}
public final ObjectProperty<Location> locationProperty() {
return locationProperty;
}
public final Location getLocation() {
return locationProperty.get();
}
public final void setLocation(Location location) {
locationProperty.set(location);
}
private void updatePoints() {
List<Double> points = MapPolyline.updatePoints(getMap(), getLocation(), getLocations());
if (points != null) {
getPoints().setAll(points);
} else {
getPoints().setAll(new Double[] { -1000d, -1000d }); // clear() or empty collection is ignored
}
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Point2D;
import javafx.scene.shape.Polyline;
import javafx.scene.shape.StrokeLineJoin;
/**
* A Polyline with points given as geographic positions by the locations property. The optional
* location property helps to calculate a viewport position that is nearest to the map center, in
* the same way as it is done for MapItems.
*/
public class MapPolyline extends Polyline implements IMapNode {
private final ListProperty<Location> locationsProperty = new SimpleListProperty<>(this, "locations", FXCollections.observableArrayList());
private final ObjectProperty<Location> locationProperty = new SimpleObjectProperty<>(this, "location");
private final MapNodeHelper mapNode = new MapNodeHelper(e -> updatePoints());
public MapPolyline() {
getStyleClass().add("map-polyline");
setFill(null);
setStrokeLineJoin(StrokeLineJoin.ROUND);
locationsProperty.addListener((ListChangeListener.Change<? extends Location> c) -> updatePoints());
}
public MapPolyline(Collection<Location> locations) {
this();
getLocations().addAll(locations);
}
@Override
public final MapBase getMap() {
return mapNode.getMap();
}
@Override
public void setMap(MapBase map) {
mapNode.setMap(map);
updatePoints();
}
public final ListProperty<Location> locationsProperty() {
return locationsProperty;
}
public final ObservableList<Location> getLocations() {
return locationsProperty.get();
}
public final void setLocations(ObservableList<Location> locations) {
locationsProperty.set(locations);
}
public final ObjectProperty<Location> locationProperty() {
return locationProperty;
}
public final Location getLocation() {
return locationProperty.get();
}
public final void setLocation(Location location) {
locationProperty.set(location);
}
private void updatePoints() {
List<Double> points = updatePoints(getMap(), getLocation(), getLocations());
if (points != null) {
getPoints().setAll(points);
} else {
getPoints().setAll(new Double[] { -1000d, -1000d }); // clear() or empty collection is ignored
}
}
static List<Double> updatePoints(MapBase map, Location location, ObservableList<Location> locations) {
ArrayList<Double> points = null;
if (map != null && locations != null && !locations.isEmpty()) {
MapProjection projection = map.getProjection();
double longitudeOffset = 0d;
if (location != null && projection.isNormalCylindrical()) {
Point2D viewportPosition = projection.locationToViewportPoint(location);
if (viewportPosition.getX() < 0d || viewportPosition.getX() > map.getWidth()
|| viewportPosition.getY() < 0d || viewportPosition.getY() > map.getHeight()) {
double nearestLongitude = Location.nearestLongitude(location.getLongitude(), map.getCenter().getLongitude());
longitudeOffset = nearestLongitude - location.getLongitude();
}
}
points = new ArrayList<>(locations.size() * 2);
for (Location loc : locations) {
Point2D p = projection.locationToViewportPoint(
new Location(loc.getLatitude(), loc.getLongitude() + longitudeOffset));
if (Double.isInfinite(p.getX()) || Double.isInfinite(p.getY())) {
points = null;
break;
}
points.add(p.getX());
points.add(p.getY());
}
}
return points;
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2017 Clemens Fischer
*/
package fxmapcontrol;
import java.util.Locale;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.transform.Affine;
import javafx.scene.transform.NonInvertibleTransformException;
/**
* Defines a map projection between geographic coordinates and cartesian map coordinates and
* viewport coordinates, i.e. pixels.
*/
public abstract class MapProjection {
public static final double WGS84_EQUATORIAL_RADIUS = 6378137d;
public static final double WGS84_FLATTENING = 1d / 298.257223563;
public static final double METERS_PER_DEGREE = WGS84_EQUATORIAL_RADIUS * Math.PI / 180d;
protected final Affine viewportTransform = new Affine();
protected final Affine inverseViewportTransform = new Affine();
protected double viewportScale;
protected String crsId;
/**
* Gets the WMS 1.3.0 CRS Identifier.
*/
public String getCrsId() {
return crsId;
}
/**
* Sets the WMS 1.3.0 CRS Identifier.
*/
public void setCrsId(String crsId) {
this.crsId = crsId;
}
/**
* Gets the transformation from cartesian map coordinates to viewport coordinates.
*/
public Affine getViewportTransform() {
return viewportTransform;
}
/**
* Gets the transformation from viewport coordinates to cartesian map coordinates.
*/
public Affine getInverseViewportTransform() {
return inverseViewportTransform;
}
/**
* Indicates if this is a web mercator projection, i.e. compatible with TileLayer.
*/
public abstract boolean isWebMercator();
/**
* Indicates if this is a normal cylindrical projection, i.e. compatible with MapGraticule.
*/
public abstract boolean isNormalCylindrical();
/**
* Indicates if this is an azimuthal projection.
*/
public abstract boolean isAzimuthal();
/**
* Gets the absolute value of the minimum and maximum latitude that can be transformed.
*/
public abstract double maxLatitude();
/**
* Gets the map scale at the specified Location as viewport coordinate units per meter (px/m).
*/
public abstract Point2D getMapScale(Location location);
/**
* Transforms a Location in geographic coordinates to a Point2D in cartesian map coordinates.
*/
public abstract Point2D locationToPoint(Location location);
/**
* Transforms a Point2D in cartesian map coordinates to a Location in geographic coordinates.
*/
public abstract Location pointToLocation(Point2D point);
/**
* Transforms a MapBoundingBox in geographic coordinates to Bounds in cartesian map coordinates.
*/
public Bounds boundingBoxToBounds(MapBoundingBox boundingBox) {
Point2D sw = locationToPoint(new Location(boundingBox.getSouth(), boundingBox.getWest()));
Point2D ne = locationToPoint(new Location(boundingBox.getNorth(), boundingBox.getEast()));
return new BoundingBox(sw.getX(), sw.getY(), ne.getX() - sw.getX(), ne.getY() - sw.getY());
}
/**
* Transforms Bounds in cartesian map coordinates to a BoundingBox in geographic coordinates.
*/
public MapBoundingBox boundsToBoundingBox(Bounds bounds) {
Location sw = pointToLocation(new Point2D(bounds.getMinX(), bounds.getMinY()));
Location ne = pointToLocation(new Point2D(bounds.getMaxX(), bounds.getMaxY()));
return new MapBoundingBox(sw.getLatitude(), sw.getLongitude(), ne.getLatitude(), ne.getLongitude());
}
public Point2D locationToViewportPoint(Location location) {
return viewportTransform.transform(locationToPoint(location));
}
public Location viewportPointToLocation(Point2D point) {
return pointToLocation(inverseViewportTransform.transform(point));
}
public MapBoundingBox viewportBoundsToBoundingBox(Bounds bounds) {
return boundsToBoundingBox(inverseViewportTransform.transform(bounds));
}
public double getViewportScale(double zoomLevel) {
return Math.pow(2d, zoomLevel) * TileSource.TILE_SIZE / 360d;
}
public void setViewportTransform(Location projectionCenter, Location mapCenter, Point2D viewportCenter, double zoomLevel, double heading) {
viewportScale = getViewportScale(zoomLevel);
Point2D center = locationToPoint(mapCenter);
Affine transform = new Affine();
transform.prependTranslation(-center.getX(), -center.getY());
transform.prependScale(viewportScale, -viewportScale);
transform.prependRotation(heading);
transform.prependTranslation(viewportCenter.getX(), viewportCenter.getY());
viewportTransform.setToTransform(transform);
try {
transform.invert();
} catch (NonInvertibleTransformException ex) {
throw new RuntimeException(ex); // this will never happen
}
inverseViewportTransform.setToTransform(transform);
}
public String wmsQueryParameters(MapBoundingBox boundingBox, String version) {
if (crsId == null || crsId.isEmpty()) {
return null;
}
String format = "CRS=%s&BBOX=%f,%f,%f,%f&WIDTH=%d&HEIGHT=%d";
if (version.startsWith("1.1.")) {
format = "SRS=%s&BBOX=%f,%f,%f,%f&WIDTH=%d&HEIGHT=%d";
} else if (crsId.equals("EPSG:4326")) {
format = "CRS=%1$s&BBOX=%3$f,%2$f,%5$f,%4$f&WIDTH=%6$d&HEIGHT=%7$d";
}
Bounds bounds = boundingBoxToBounds(boundingBox);
return String.format(Locale.ROOT, format, crsId,
bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY(),
(int) Math.round(viewportScale * bounds.getWidth()),
(int) Math.round(viewportScale * bounds.getHeight()));
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableBooleanProperty;
import javafx.css.SimpleStyleableIntegerProperty;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableIntegerProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleablePropertyFactory;
import javafx.geometry.Point2D;
import javafx.scene.Parent;
import javafx.scene.image.ImageView;
import javafx.scene.transform.Affine;
import javafx.util.Duration;
/**
* Fills the map viewport with map tiles from a TileSource.
*/
public class MapTileLayer extends Parent implements IMapNode {
private static final StyleablePropertyFactory<MapTileLayer> propertyFactory
= new StyleablePropertyFactory<>(Parent.getClassCssMetaData());
private static final CssMetaData<MapTileLayer, Number> maxDownloadThreadsCssMetaData
= propertyFactory.createSizeCssMetaData("-fx-max-download-threads", s -> s.maxDownloadThreadsProperty);
private static final CssMetaData<MapTileLayer, Duration> updateDelayCssMetaData
= propertyFactory.createDurationCssMetaData("-fx-update-delay", s -> s.updateDelayProperty);
private static final CssMetaData<MapTileLayer, Boolean> updateWhileViewportChangingCssMetaData
= propertyFactory.createBooleanCssMetaData("-fx-update-while-viewport-changing", s -> s.updateWhileViewportChangingProperty);
private final StyleableIntegerProperty maxDownloadThreadsProperty
= new SimpleStyleableIntegerProperty(maxDownloadThreadsCssMetaData, this, "maxDownloadThreads", 4);
private final StyleableObjectProperty<Duration> updateDelayProperty
= new SimpleStyleableObjectProperty<>(updateDelayCssMetaData, this, "updateDelay", Duration.seconds(0.2));
private final StyleableBooleanProperty updateWhileViewportChangingProperty
= new SimpleStyleableBooleanProperty(updateWhileViewportChangingCssMetaData, this, "updateWhileViewportChanging", true);
private final ObjectProperty<TileSource> tileSourceProperty = new SimpleObjectProperty<>(this, "tileSource");
private final ITileImageLoader tileImageLoader;
private final Timeline updateTimeline = new Timeline();
private final MapNodeHelper mapNode = new MapNodeHelper(e -> viewportChanged(e.getProjectionChanged(), e.getLongitudeOffset()));
private ArrayList<Tile> tiles = new ArrayList<>();
private TileGrid tileGrid;
private double zoomLevelOffset;
private int minZoomLevel;
private int maxZoomLevel = 18;
private String name;
public static MapTileLayer getOpenStreetMapLayer() {
return new MapTileLayer("OpenStreetMap", "http://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png", 0, 19);
}
public MapTileLayer() {
this(new TileImageLoader());
}
public MapTileLayer(String name, String tileUrlFormat, int minZoomLevel, int maxZoomLevel) {
this(new TileImageLoader(), name, tileUrlFormat, minZoomLevel, maxZoomLevel);
}
public MapTileLayer(ITileImageLoader tileImageLoader, String name, String tileUrlFormat, int minZoomLevel, int maxZoomLevel) {
this(tileImageLoader);
this.name = name;
tileSourceProperty.set(new TileSource(tileUrlFormat));
this.minZoomLevel = minZoomLevel;
this.maxZoomLevel = maxZoomLevel;
}
public MapTileLayer(ITileImageLoader tileImageLoader) {
getStyleClass().add("map-tile-layer");
getTransforms().add(new Affine());
setMouseTransparent(true);
this.tileImageLoader = tileImageLoader;
tileSourceProperty.addListener(observable -> updateTiles(true));
updateTimeline.getKeyFrames().add(new KeyFrame(getUpdateDelay(), e -> updateTileGrid()));
updateDelayProperty.addListener(observable
-> updateTimeline.getKeyFrames().set(0, new KeyFrame(getUpdateDelay(), e -> updateTileGrid())));
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return propertyFactory.getCssMetaData();
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
@Override
public final MapBase getMap() {
return mapNode.getMap();
}
@Override
public void setMap(MapBase map) {
mapNode.setMap(map);
updateTileGrid();
}
public final IntegerProperty maxDownloadThreadsProperty() {
return maxDownloadThreadsProperty;
}
public final int getMaxDownloadThreads() {
return maxDownloadThreadsProperty.get();
}
public final void setMaxDownloadThreads(int maxDownloadThreads) {
maxDownloadThreadsProperty.set(maxDownloadThreads);
}
public final ObjectProperty<Duration> updateDelayProperty() {
return updateDelayProperty;
}
public final Duration getUpdateDelay() {
return updateDelayProperty.get();
}
public final void setUpdateDelay(Duration updateDelay) {
updateDelayProperty.set(updateDelay);
}
public final BooleanProperty updateWhileViewportChangingProperty() {
return updateWhileViewportChangingProperty;
}
public final boolean getUpdateWhileViewportChanging() {
return updateWhileViewportChangingProperty.get();
}
public final void setUpdateWhileViewportChanging(boolean updateWhileViewportChanging) {
updateWhileViewportChangingProperty.set(updateWhileViewportChanging);
}
public final ObjectProperty<TileSource> tileSourceProperty() {
return tileSourceProperty;
}
public final TileSource getTileSource() {
return tileSourceProperty.get();
}
public final void setTileSource(TileSource tileSource) {
tileSourceProperty.set(tileSource);
}
public final double getZoomLevelOffset() {
return zoomLevelOffset;
}
public final void setZoomLevelOffset(double zoomLevelOffset) {
this.zoomLevelOffset = zoomLevelOffset;
}
public final int getMinZoomLevel() {
return minZoomLevel;
}
public final void setMinZoomLevel(int minZoomLevel) {
this.minZoomLevel = minZoomLevel;
}
public final int getMaxZoomLevel() {
return maxZoomLevel;
}
public final void setMaxZoomLevel(int maxZoomLevel) {
this.maxZoomLevel = maxZoomLevel;
}
public final String getName() {
return name;
}
public final void setName(String name) {
this.name = name;
}
protected void updateTileGrid() {
updateTimeline.stop();
if (getMap() != null && getMap().getProjection().isWebMercator()) {
TileGrid grid = getTileGrid();
if (!grid.equals(tileGrid)) {
tileGrid = grid;
setTransform();
updateTiles(false);
}
} else {
tileGrid = null;
updateTiles(true);
}
}
private void viewportChanged(boolean projectionChanged, double longitudeOffset) {
if (tileGrid == null || projectionChanged || Math.abs(longitudeOffset) > 180d) {
// update immediately when map projection has changed or map center has moved across 180° longitude
updateTileGrid();
} else {
setTransform();
if (getUpdateWhileViewportChanging()) {
updateTimeline.play();
} else {
updateTimeline.playFromStart();
}
}
}
private Point2D getTileCenter(double tileScale) {
Location center = getMap().getCenter();
return new Point2D(
tileScale * (0.5 + center.getLongitude() / 360d),
tileScale * (0.5 - WebMercatorProjection.latitudeToY(center.getLatitude()) / 360d));
}
private TileGrid getTileGrid() {
MapBase map = getMap();
int tileZoomLevel = Math.max(0, (int) Math.floor(map.getZoomLevel() + zoomLevelOffset));
double tileScale = (1 << tileZoomLevel);
double scale = tileScale / (Math.pow(2d, map.getZoomLevel()) * TileSource.TILE_SIZE);
Point2D tileCenter = getTileCenter(tileScale);
Affine transform = new Affine();
transform.prependTranslation(-map.getWidth() / 2d, -map.getHeight() / 2d);
transform.prependScale(scale, scale);
transform.prependRotation(-map.getHeading());
transform.prependTranslation(tileCenter.getX(), tileCenter.getY());
// get tile index values of viewport rectangle
Point2D p1 = transform.transform(new Point2D(0d, 0d));
Point2D p2 = transform.transform(new Point2D(map.getWidth(), 0d));
Point2D p3 = transform.transform(new Point2D(0d, map.getHeight()));
Point2D p4 = transform.transform(new Point2D(map.getWidth(), map.getHeight()));
return new TileGrid(tileZoomLevel,
(int) Math.floor(Math.min(Math.min(p1.getX(), p2.getX()), Math.min(p3.getX(), p4.getX()))),
(int) Math.floor(Math.min(Math.min(p1.getY(), p2.getY()), Math.min(p3.getY(), p4.getY()))),
(int) Math.floor(Math.max(Math.max(p1.getX(), p2.getX()), Math.max(p3.getX(), p4.getX()))),
(int) Math.floor(Math.max(Math.max(p1.getY(), p2.getY()), Math.max(p3.getY(), p4.getY()))));
}
private void setTransform() {
MapBase map = getMap();
double tileScale = (1 << tileGrid.getZoomLevel());
double scale = Math.pow(2d, map.getZoomLevel()) / tileScale;
Point2D tileCenter = getTileCenter(tileScale);
double tileOriginX = TileSource.TILE_SIZE * (tileCenter.getX() - tileGrid.getXMin());
double tileOriginY = TileSource.TILE_SIZE * (tileCenter.getY() - tileGrid.getYMin());
Affine transform = new Affine();
transform.prependTranslation(-tileOriginX, -tileOriginY);
transform.prependScale(scale, scale);
transform.prependRotation(map.getHeading());
transform.prependTranslation(map.getWidth() / 2d, map.getHeight() / 2d);
getTransforms().set(0, transform);
}
private void updateTiles(boolean clearTiles) {
if (!tiles.isEmpty()) {
tileImageLoader.cancelLoadTiles(this);
}
if (clearTiles) {
tiles.clear();
}
MapBase map = getMap();
ArrayList<Tile> newTiles = new ArrayList<>();
if (map != null && tileGrid != null && getTileSource() != null) {
int maxZoom = Math.min(tileGrid.getZoomLevel(), maxZoomLevel);
int minZoom = minZoomLevel;
if (minZoom < maxZoom && this != map.getChildrenUnmodifiable().stream().findFirst().orElse(null)) {
// do not load background tiles if this is not the base layer
minZoom = maxZoom;
}
for (int tz = minZoom; tz <= maxZoom; tz++) {
int tileSize = 1 << (tileGrid.getZoomLevel() - tz);
int x1 = (int) Math.floor((double) tileGrid.getXMin() / tileSize); // may be negative
int x2 = tileGrid.getXMax() / tileSize;
int y1 = Math.max(tileGrid.getYMin() / tileSize, 0);
int y2 = Math.min(tileGrid.getYMax() / tileSize, (1 << tz) - 1);
for (int ty = y1; ty <= y2; ty++) {
for (int tx = x1; tx <= x2; tx++) {
int z = tz;
int x = tx;
int y = ty;
Tile tile = tiles.stream()
.filter(t -> t.getZoomLevel() == z && t.getX() == x && t.getY() == y)
.findAny().orElse(null);
if (tile == null) {
tile = new Tile(z, x, y);
int xIndex = tile.getXIndex();
Tile equivalentTile = tiles.stream()
.filter(t -> t.getZoomLevel() == z && t.getXIndex() == xIndex && t.getY() == y && t.getImage() != null)
.findAny().orElse(null);
if (equivalentTile != null) {
tile.setImage(equivalentTile.getImage(), false);
}
}
newTiles.add(tile);
}
}
}
}
tiles = newTiles;
if (tiles.isEmpty()) {
getChildren().clear();
} else {
getChildren().setAll(tiles.stream()
.map(tile -> {
ImageView imageView = tile.getImageView();
int size = TileSource.TILE_SIZE << (tileGrid.getZoomLevel() - tile.getZoomLevel());
imageView.setX(size * tile.getX() - TileSource.TILE_SIZE * tileGrid.getXMin());
imageView.setY(size * tile.getY() - TileSource.TILE_SIZE * tileGrid.getYMin());
imageView.setFitWidth(size);
imageView.setFitHeight(size);
return imageView;
})
.collect(Collectors.toList()));
tileImageLoader.beginLoadTiles(this, tiles.stream()
.filter(tile -> tile.isPending())
.collect(Collectors.toList()));
}
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2017 Clemens Fischer
*/
package fxmapcontrol;
import static fxmapcontrol.AzimuthalProjection.getAzimuthDistance;
import javafx.geometry.Point2D;
import static fxmapcontrol.AzimuthalProjection.getLocation;
/**
* Transforms geographic coordinates to cartesian coordinates according to the Stereographic Projection.
*/
public class StereographicProjection extends AzimuthalProjection {
public StereographicProjection() {
this("AUTO2:97002"); // GeoServer non-standard CRS ID
}
public StereographicProjection(String crsId) {
this.crsId = crsId;
}
@Override
public Point2D locationToPoint(Location location) {
if (location.equals(projectionCenter)) {
return new Point2D(0d, 0d);
}
double[] azimuthDistance = getAzimuthDistance(projectionCenter, location);
double azimuth = azimuthDistance[0];
double distance = azimuthDistance[1];
double mapDistance = 2d * WGS84_EQUATORIAL_RADIUS * Math.tan(distance / 2d);
return new Point2D(mapDistance * Math.sin(azimuth), mapDistance * Math.cos(azimuth));
}
@Override
public Location pointToLocation(Point2D point) {
double x = point.getX();
double y = point.getY();
if (x == 0d && y == 0d) {
return projectionCenter;
}
double azimuth = Math.atan2(x, y);
double mapDistance = Math.sqrt(x * x + y * y);
double distance = 2d * Math.atan(mapDistance / (2d * WGS84_EQUATORIAL_RADIUS));
return getLocation(projectionCenter, azimuth, distance);
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import javafx.animation.FadeTransition;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Duration;
/**
* Provides the ImageView that display a map tile image.
*/
public class Tile {
private static Duration fadeDuration;
public static Duration getFadeDuration() {
return fadeDuration;
}
public static void setFadeDuration(Duration duration) {
fadeDuration = duration;
}
private final int zoomLevel;
private final int x;
private final int y;
private final ImageView imageView;
private boolean pending;
public Tile(int zoomLevel, int x, int y) {
this.zoomLevel = zoomLevel;
this.x = x;
this.y = y;
imageView = new ImageView();
imageView.setOpacity(0d);
pending = true;
}
public final int getZoomLevel() {
return zoomLevel;
}
public final int getX() {
return x;
}
public final int getY() {
return y;
}
public final int getXIndex() {
int numTiles = 1 << zoomLevel;
return ((x % numTiles) + numTiles) % numTiles;
}
public final boolean isPending() {
return pending;
}
public final ImageView getImageView() {
return imageView;
}
public final Image getImage() {
return imageView.getImage();
}
public final void setImage(Image image, boolean fade) {
pending = false;
if (image != null) {
imageView.setImage(image);
if (fade && fadeDuration != null && fadeDuration.greaterThan(Duration.ZERO)) {
FadeTransition fadeTransition = new FadeTransition(fadeDuration, imageView);
fadeTransition.setToValue(1d);
fadeTransition.play();
} else {
imageView.setOpacity(1d);
}
}
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
/**
* Defines zoom level and tile index ranges of a MapTileLayer.
*/
public class TileGrid {
private final int zoomLevel;
private final int xMin;
private final int yMin;
private final int xMax;
private final int yMax;
public TileGrid(int zoomLevel, int xMin, int yMin, int xMax, int yMax) {
this.zoomLevel = zoomLevel;
this.xMin = xMin;
this.yMin = yMin;
this.xMax = xMax;
this.yMax = yMax;
}
public final int getZoomLevel() {
return zoomLevel;
}
public final int getXMin() {
return xMin;
}
public final int getYMin() {
return yMin;
}
public final int getXMax() {
return xMax;
}
public final int getYMax() {
return yMax;
}
public final boolean equals(TileGrid tileGrid) {
return tileGrid != null
&& zoomLevel == tileGrid.zoomLevel
&& xMin == tileGrid.xMin
&& yMin == tileGrid.yMin
&& xMax == tileGrid.xMax
&& yMax == tileGrid.yMax;
}
@Override
public final boolean equals(Object obj) {
return (obj instanceof TileGrid) && equals((TileGrid)obj);
}
@Override
public int hashCode() {
return Integer.hashCode(zoomLevel)
^ Integer.hashCode(xMin)
^ Integer.hashCode(yMin)
^ Integer.hashCode(xMax)
^ Integer.hashCode(yMax);
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.scene.image.Image;
/**
* Default ITileImageLoader implementation.
* Optionally caches tile images in a static ITileCache instance.
*/
public class TileImageLoader implements ITileImageLoader {
private static final int MINIMUM_EXPIRATION = 3600; // one hour
private static final int DEFAULT_EXPIRATION = 3600 * 24; // one day
private static final ThreadFactory threadFactory = runnable -> {
Thread thread = new Thread(runnable, TileImageLoader.class.getSimpleName() + " Thread");
thread.setDaemon(true);
return thread;
};
private static ITileCache cache;
private final Set<LoadImageService> pendingServices = Collections.synchronizedSet(new HashSet<LoadImageService>());
private int threadPoolSize;
private ExecutorService serviceExecutor;
public static void setCache(ITileCache cache) {
TileImageLoader.cache = cache;
}
@Override
public void beginLoadTiles(MapTileLayer tileLayer, Iterable<Tile> tiles) {
if (threadPoolSize != tileLayer.getMaxDownloadThreads()) {
threadPoolSize = tileLayer.getMaxDownloadThreads();
if (serviceExecutor != null) {
serviceExecutor.shutdown();
}
serviceExecutor = Executors.newFixedThreadPool(threadPoolSize, threadFactory);
}
for (Tile tile : tiles) {
LoadImageService service = new LoadImageService(tileLayer, tile);
pendingServices.add(service);
service.start();
}
}
@Override
public void cancelLoadTiles(MapTileLayer tileLayer) {
pendingServices.forEach(s -> ((LoadImageService) s).cancel());
pendingServices.clear();
}
private class LoadImageService extends Service<Image> {
private final MapTileLayer tileLayer;
private final Tile tile;
public LoadImageService(MapTileLayer tileLayer, Tile tile) {
this.tileLayer = tileLayer;
this.tile = tile;
setExecutor(serviceExecutor);
}
@Override
protected void running() {
super.running();
pendingServices.remove(this);
}
@Override
protected void succeeded() {
super.succeeded();
tile.setImage(getValue(), true);
}
@Override
protected Task<Image> createTask() {
return new Task<Image>() {
@Override
protected Image call() {
String tileLayerName = tileLayer.getName();
String tileUrl = tileLayer.getTileSource().getUrl(tile.getXIndex(), tile.getY(), tile.getZoomLevel());
if (cache == null
|| tileLayerName == null
|| tileLayerName.isEmpty()
|| tileUrl.startsWith("file:")) { // no caching, create Image directly from URL
return new Image(tileUrl);
}
long now = new Date().getTime();
ITileCache.CacheItem cacheItem
= cache.get(tileLayerName, tile.getXIndex(), tile.getY(), tile.getZoomLevel());
Image image = null;
if (cacheItem != null) {
try {
try (ByteArrayInputStream memoryStream = new ByteArrayInputStream(cacheItem.getBuffer())) {
image = new Image(memoryStream);
}
if (cacheItem.getExpiration() > now) { // cached image not expired
return image;
}
} catch (IOException ex) {
Logger.getLogger(TileImageLoader.class.getName()).log(Level.WARNING, ex.toString());
}
}
ImageStream imageStream = null;
int expiration = 0;
try {
HttpURLConnection connection = (HttpURLConnection) new URL(tileUrl).openConnection();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
try (InputStream inputStream = connection.getInputStream()) {
imageStream = new ImageStream(inputStream);
image = imageStream.getImage();
}
String cacheControl = connection.getHeaderField("cache-control");
if (cacheControl != null) {
String maxAge = Arrays.stream(cacheControl.split(","))
.filter(s -> s.contains("max-age="))
.findFirst().orElse(null);
if (maxAge != null) {
expiration = Integer.parseInt(maxAge.trim().substring(8));
}
}
} else {
Logger.getLogger(TileImageLoader.class.getName()).log(Level.INFO,
String.format("%s: %d %s", tileUrl, connection.getResponseCode(), connection.getResponseMessage()));
}
} catch (IOException | NumberFormatException ex) {
Logger.getLogger(TileImageLoader.class.getName()).log(Level.WARNING, ex.toString());
}
if (image != null && imageStream != null) { // download succeeded, write image to cache
if (expiration <= 0) {
expiration = DEFAULT_EXPIRATION;
} else if (expiration < MINIMUM_EXPIRATION) {
expiration = MINIMUM_EXPIRATION;
}
cache.set(tileLayerName, tile.getXIndex(), tile.getY(), tile.getZoomLevel(),
imageStream.getBuffer(), now + 1000L * expiration);
}
return image;
}
};
}
}
private static class ImageStream extends BufferedInputStream {
public ImageStream(InputStream inputStream) {
super(inputStream);
}
public int getLength() {
return count;
}
public byte[] getBuffer() {
return buf;
}
public Image getImage() throws IOException {
mark(Integer.MAX_VALUE);
Image image = new Image(this);
reset();
return image;
}
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.util.Locale;
/**
* Provides the URL of a map tile.
*/
public class TileSource {
private interface UrlFormatter {
String getUrl(int x, int y, int z);
}
public static final int TILE_SIZE = 256;
private UrlFormatter urlFormatter;
private String urlFormat;
public TileSource() {
}
public TileSource(String urlFormat) {
setUrlFormat(urlFormat);
}
public static TileSource valueOf(String urlFormat) {
return new TileSource(urlFormat);
}
public final String getUrlFormat() {
return urlFormat;
}
public final void setUrlFormat(String urlFormat) {
if (urlFormat == null || urlFormat.isEmpty()) {
throw new IllegalArgumentException("urlFormat must not be null or empty");
}
if (urlFormat.contains("{x}") && urlFormat.contains("{y}") && urlFormat.contains("{z}")) {
if (urlFormat.contains("{c}")) {
urlFormatter = (x, y, z) -> getOpenStreetMapUrl(x, y, z);
} else if (urlFormat.contains("{n}")) {
urlFormatter = (x, y, z) -> getMapQuestUrl(x, y, z);
} else {
urlFormatter = (x, y, z) -> getDefaultUrl(x, y, z);
}
} else if (urlFormat.contains("{q}")) {
urlFormatter = (x, y, z) -> getQuadKeyUrl(x, y, z);
} else if (urlFormat.contains("{W}") && urlFormat.contains("{S}")
&& urlFormat.contains("{E}") && urlFormat.contains("{N}")) {
urlFormatter = (x, y, z) -> getBoundingBoxUrl(x, y, z);
} else if (urlFormat.contains("{w}") && urlFormat.contains("{s}")
&& urlFormat.contains("{e}") && urlFormat.contains("{n}")) {
urlFormatter = (x, y, z) -> getLatLonBoundingBoxUrl(x, y, z);
} else {
throw new IllegalArgumentException("Invalid urlFormat: " + urlFormat);
}
this.urlFormat = urlFormat;
}
public String getUrl(int x, int y, int zoomLevel) {
return urlFormatter != null
? urlFormatter.getUrl(x, y, zoomLevel)
: null;
}
private String getDefaultUrl(int x, int y, int zoomLevel) {
return urlFormat
.replace("{x}", Integer.toString(x))
.replace("{y}", Integer.toString(y))
.replace("{z}", Integer.toString(zoomLevel));
}
private String getOpenStreetMapUrl(int x, int y, int zoomLevel) {
int hostIndex = (x + y) % 3;
return urlFormat
.replace("{c}", "abc".substring(hostIndex, hostIndex + 1))
.replace("{x}", Integer.toString(x))
.replace("{y}", Integer.toString(y))
.replace("{z}", Integer.toString(zoomLevel));
}
private String getMapQuestUrl(int x, int y, int zoomLevel) {
int hostIndex = (x + y) % 4 + 1;
return urlFormat
.replace("{n}", Integer.toString(hostIndex))
.replace("{x}", Integer.toString(x))
.replace("{y}", Integer.toString(y))
.replace("{z}", Integer.toString(zoomLevel));
}
private String getQuadKeyUrl(int x, int y, int zoomLevel) {
if (zoomLevel < 1) {
return null;
}
char[] quadkey = new char[zoomLevel];
for (int z = zoomLevel - 1; z >= 0; z--, x /= 2, y /= 2) {
quadkey[z] = (char) ('0' + 2 * (y % 2) + (x % 2));
}
return urlFormat
.replace("{i}", new String(quadkey, zoomLevel - 1, 1))
.replace("{q}", new String(quadkey));
}
private String getBoundingBoxUrl(int x, int y, int zoomLevel) {
double tileSize = 360d / (1 << zoomLevel); // tile width in degrees
double west = MapProjection.METERS_PER_DEGREE * (x * tileSize - 180d);
double east = MapProjection.METERS_PER_DEGREE * ((x + 1) * tileSize - 180d);
double south = MapProjection.METERS_PER_DEGREE * (180d - (y + 1) * tileSize);
double north = MapProjection.METERS_PER_DEGREE * (180d - y * tileSize);
return urlFormat
.replace("{W}", String.format(Locale.ROOT, "%.1f", west))
.replace("{S}", String.format(Locale.ROOT, "%.1f", south))
.replace("{E}", String.format(Locale.ROOT, "%.1f", east))
.replace("{N}", String.format(Locale.ROOT, "%.1f", north))
.replace("{X}", Integer.toString(TILE_SIZE))
.replace("{Y}", Integer.toString(TILE_SIZE));
}
private String getLatLonBoundingBoxUrl(int x, int y, int zoomLevel) {
double tileSize = 360d / (1 << zoomLevel); // tile width in degrees
double west = x * tileSize - 180d;
double east = (x + 1) * tileSize - 180d;
double south = WebMercatorProjection.yToLatitude(180d - (y + 1) * tileSize);
double north = WebMercatorProjection.yToLatitude(180d - y * tileSize);
return urlFormat
.replace("{w}", String.format(Locale.ROOT, "%.6f", west))
.replace("{s}", String.format(Locale.ROOT, "%.6f", south))
.replace("{e}", String.format(Locale.ROOT, "%.6f", east))
.replace("{n}", String.format(Locale.ROOT, "%.6f", north))
.replace("{X}", Integer.toString(TILE_SIZE))
.replace("{Y}", Integer.toString(TILE_SIZE));
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment