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 1625 additions and 0 deletions
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>fx-map-control</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.source=1.8
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<name>FxMapControl</name>
<groupId>fischer.clemens</groupId>
<artifactId>fx-map-control</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
</project>
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2017 Clemens Fischer
*/
package fxmapcontrol;
import javafx.geometry.Point2D;
/**
* Transforms geographic coordinates to cartesian coordinates according to the Azimuthal Equidistant
* Projection.
*/
public class AzimuthalEquidistantProjection extends AzimuthalProjection {
public AzimuthalEquidistantProjection() {
// No known standard or de-facto standard CRS ID
}
public AzimuthalEquidistantProjection(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 = WGS84_EQUATORIAL_RADIUS * azimuthDistance[1];
return new Point2D(distance * Math.sin(azimuth), distance * 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 distance = Math.sqrt(x * x + y * y) / WGS84_EQUATORIAL_RADIUS;
return getLocation(projectionCenter, azimuth, distance);
}
}
/*
* 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;
/**
* Base class for azimuthal map projections.
*/
public abstract class AzimuthalProjection extends MapProjection {
protected Location projectionCenter = new Location(0d, 0d);
@Override
public boolean isWebMercator() {
return false;
}
@Override
public boolean isNormalCylindrical() {
return false;
}
@Override
public boolean isAzimuthal() {
return true;
}
@Override
public double maxLatitude() {
return 90d;
}
@Override
public Point2D getMapScale(Location location) {
return new Point2D(viewportScale, viewportScale);
}
@Override
public Bounds boundingBoxToBounds(MapBoundingBox boundingBox) {
if (boundingBox instanceof CenteredBoundingBox) {
CenteredBoundingBox cbbox = (CenteredBoundingBox) boundingBox;
Point2D center = locationToPoint(cbbox.getCenter());
double width = cbbox.getWidth();
double height = cbbox.getHeight();
return new BoundingBox(center.getX() - width / 2d, center.getY() - height / 2d, width, height);
}
return super.boundingBoxToBounds(boundingBox);
}
@Override
public MapBoundingBox boundsToBoundingBox(Bounds bounds) {
Location center = pointToLocation(new Point2D(
bounds.getMinX() + bounds.getWidth() / 2d,
bounds.getMinY() + bounds.getHeight() / 2d));
return new CenteredBoundingBox(center, bounds.getWidth(), bounds.getHeight()); // width and height in meters
}
@Override
public double getViewportScale(double zoomLevel) {
return super.getViewportScale(zoomLevel) / METERS_PER_DEGREE;
}
@Override
public void setViewportTransform(Location projectionCenter, Location mapCenter, Point2D viewportCenter, double zoomLevel, double heading) {
this.projectionCenter = projectionCenter;
super.setViewportTransform(projectionCenter, mapCenter, viewportCenter, zoomLevel, heading);
}
@Override
public String wmsQueryParameters(MapBoundingBox boundingBox, String version) {
if (crsId == null || crsId.isEmpty()) {
return null;
}
Bounds bounds = boundingBoxToBounds(boundingBox);
String crs = version.startsWith("1.1.") ? "SRS" : "CRS";
return String.format(Locale.ROOT,
"%s=%s,1,%f,%f&BBOX=%f,%f,%f,%f&WIDTH=%d&HEIGHT=%d",
crs, crsId, projectionCenter.getLongitude(), projectionCenter.getLatitude(),
bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY(),
(int) Math.round(viewportScale * bounds.getWidth()),
(int) Math.round(viewportScale * bounds.getHeight()));
}
/**
* Calculates azimuth and distance in radians from location1 to location2.
*/
public static double[] getAzimuthDistance(Location location1, Location location2) {
double lat1 = location1.getLatitude() * Math.PI / 180d;
double lon1 = location1.getLongitude() * Math.PI / 180d;
double lat2 = location2.getLatitude() * Math.PI / 180d;
double lon2 = location2.getLongitude() * Math.PI / 180d;
double cosLat1 = Math.cos(lat1);
double sinLat1 = Math.sin(lat1);
double cosLat2 = Math.cos(lat2);
double sinLat2 = Math.sin(lat2);
double cosLon12 = Math.cos(lon2 - lon1);
double sinLon12 = Math.sin(lon2 - lon1);
double cosDistance = sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosLon12;
double azimuth = Math.atan2(sinLon12, cosLat1 * sinLat2 / cosLat2 - sinLat1 * cosLon12);
double distance = Math.acos(Math.max(Math.min(cosDistance, 1d), -1d));
return new double[]{azimuth, distance};
}
/**
* Calculates the Location of the point given by azimuth and distance in radians from location.
*/
public static Location getLocation(Location location, double azimuth, double distance) {
double lat = location.getLatitude() * Math.PI / 180d;
double sinDistance = Math.sin(distance);
double cosDistance = Math.cos(distance);
double cosAzimuth = Math.cos(azimuth);
double sinAzimuth = Math.sin(azimuth);
double cosLat1 = Math.cos(lat);
double sinLat1 = Math.sin(lat);
double sinLat2 = sinLat1 * cosDistance + cosLat1 * sinDistance * cosAzimuth;
double lat2 = Math.asin(Math.max(Math.min(sinLat2, 1d), -1d));
double dLon = Math.atan2(sinDistance * sinAzimuth, cosLat1 * cosDistance - sinLat1 * sinDistance * cosAzimuth);
return new Location(180d / Math.PI * lat2, location.getLongitude() + 180d / Math.PI * dLon);
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.io.IOException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Displays Bing Maps tiles. The static apiKey property must be set to a Bing Maps API Key.
*/
public class BingMapsTileLayer extends MapTileLayer {
public enum MapMode {
Road, Aerial, AerialWithLabels
}
private static String apiKey;
private final MapMode mapMode;
public BingMapsTileLayer(MapMode mode) {
this(new TileImageLoader(), mode);
}
public BingMapsTileLayer(ITileImageLoader tileImageLoader, MapMode mode) {
super(tileImageLoader);
mapMode = mode;
if (apiKey == null || apiKey.isEmpty()) {
Logger.getLogger(BingMapsTileLayer.class.getName()).log(Level.SEVERE, "BingMapsTileLayer requires a Bing Maps API Key.");
} else {
try {
String url = String.format("http://dev.virtualearth.net/REST/V1/Imagery/Metadata/%s?output=xml&key=%s", mapMode.toString(), apiKey);
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new URL(url).openStream());
Element metadataElement = (Element) document.getElementsByTagName("ImageryMetadata").item(0);
Element imageUrlElement = (Element) metadataElement.getElementsByTagName("ImageUrl").item(0);
Element subdomainsElement = (Element) metadataElement.getElementsByTagName("ImageUrlSubdomains").item(0);
Element zoomMinElement = (Element) metadataElement.getElementsByTagName("ZoomMin").item(0);
Element zoomMaxElement = (Element) metadataElement.getElementsByTagName("ZoomMax").item(0);
NodeList subdomainStrings = subdomainsElement.getElementsByTagName("string");
String[] subdomains = new String[subdomainStrings.getLength()];
for (int i = 0; i < subdomains.length; i++) {
subdomains[i] = subdomainStrings.item(i).getTextContent();
}
setName("Bing Maps " + mapMode);
setTileSource(new BingMapsTileSource(imageUrlElement.getTextContent(), subdomains));
setMinZoomLevel(Integer.parseInt(zoomMinElement.getTextContent()));
setMaxZoomLevel(Integer.parseInt(zoomMaxElement.getTextContent()));
} catch (IOException | ParserConfigurationException | SAXException | DOMException ex) {
Logger.getLogger(BingMapsTileLayer.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
public static String getApiKey() {
return apiKey;
}
public static void setApiKey(String key) {
apiKey = key;
}
public final MapMode getMapMode() {
return mapMode;
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
class BingMapsTileSource extends TileSource {
private final String[] subdomains;
public BingMapsTileSource(String urlFormat, String[] subdomains) {
super(urlFormat);
this.subdomains = subdomains;
}
@Override
public String getUrl(int x, int y, int zoomLevel) {
if (zoomLevel < 1) {
return null;
}
String subdomain = subdomains[(x + y) % subdomains.length];
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 getUrlFormat()
.replace("{subdomain}", subdomain)
.replace("{quadkey}", new String(quadkey));
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2017 Clemens Fischer
*/
package fxmapcontrol;
/**
*/
public class CenteredBoundingBox extends MapBoundingBox {
private final Location center;
private final double width;
private final double height;
public CenteredBoundingBox(Location center, double width, double height) {
this.center = center;
this.width = width;
this.height = height;
}
public Location getCenter() {
return center;
}
@Override
public double getWidth() {
return width;
}
@Override
public double getHeight() {
return height;
}
@Override
public MapBoundingBox clone() {
return new CenteredBoundingBox(center, width, height);
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2017 Clemens Fischer
*/
package fxmapcontrol;
import javafx.geometry.Point2D;
/**
* Transforms geographic coordinates to cartesian coordinates according to the Equirectangular
* Projection. Longitude and Latitude values are transformed identically to X and Y.
*/
public class EquirectangularProjection extends MapProjection {
public EquirectangularProjection() {
this("EPSG:4326");
}
public EquirectangularProjection(String crsId) {
this.crsId = crsId;
}
@Override
public boolean isWebMercator() {
return false;
}
@Override
public boolean isNormalCylindrical() {
return true;
}
@Override
public boolean isAzimuthal() {
return false;
}
@Override
public double maxLatitude() {
return 90d;
}
@Override
public Point2D getMapScale(Location location) {
return new Point2D(
viewportScale / (METERS_PER_DEGREE * Math.cos(location.getLatitude() * Math.PI / 180d)),
viewportScale / METERS_PER_DEGREE);
}
@Override
public Point2D locationToPoint(Location location) {
return new Point2D(location.getLongitude(), location.getLatitude());
}
@Override
public Location pointToLocation(Point2D point) {
return new Location(point.getY(), point.getX());
}
}
/*
* 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 Gnomonic Projection.
*/
public class GnomonicProjection extends AzimuthalProjection {
public GnomonicProjection() {
this("AUTO2:97001"); // GeoServer non-standard CRS ID
}
public GnomonicProjection(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 = distance < Math.PI / 2d ? WGS84_EQUATORIAL_RADIUS * Math.tan(distance) : Double.POSITIVE_INFINITY;
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 = Math.atan(mapDistance / WGS84_EQUATORIAL_RADIUS);
return getLocation(projectionCenter, azimuth, distance);
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
/**
* Represents a Node in the visual tree of a MapBase instance.
*/
public interface IMapNode {
MapBase getMap();
void setMap(MapBase map);
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
/**
* Provides methods for caching tile image buffers.
*/
public interface ITileCache {
public static class CacheItem {
private final byte[] buffer;
private final long expiration; // milliseconds since 1970/01/01 00:00:00 UTC
public CacheItem(byte[] buffer, long expiration) {
this.buffer = buffer;
this.expiration = expiration;
}
public final byte[] getBuffer() {
return buffer;
}
public final long getExpiration() {
return expiration;
}
}
CacheItem get(String tileLayerName, int x, int y, int zoomLevel);
void set(String tileLayerName, int x, int y, int zoomLevel, byte[] buffer, long expiration);
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
/**
* Provides methods to begin and cancel loading of map tile images for a specific MapTileLayer.
*/
public interface ITileImageLoader {
void beginLoadTiles(MapTileLayer tileLayer, Iterable<Tile> tiles);
void cancelLoadTiles(MapTileLayer tileLayer);
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Default ITileCache implementation. Caches tile image files in a directory given by the rootDirectory property.
*/
public class ImageFileCache implements ITileCache {
// For compatibility with XAML Map Control ImageFileCache, expiration dates are stored as .NET DateTime ticks,
// i.e. 100-nanosecond intervals since 0001/01/01 00:00:00 UTC. The DATETIME_OFFSET and DATETIME_FACTOR constants
// are used to convert to and from java.util.Date milliseconds since 1970/01/01 00:00:00 UTC.
private static final long DATETIME_OFFSET = 62135596800000L;
private static final long DATETIME_FACTOR = 10000L;
private static final ByteBuffer EXPIRATION_MARKER = ByteBuffer.wrap("EXPIRES:".getBytes(StandardCharsets.US_ASCII));
private final Path rootDirectory;
public ImageFileCache(Path rootDirectory) {
this.rootDirectory = rootDirectory;
//System.out.println(rootDirectory.toAbsolutePath());
}
public ImageFileCache() {
this(getDefaultRootDirectory());
}
public static final Path getDefaultRootDirectory() {
String osName = System.getProperty("os.name").toLowerCase();
if (osName.contains("windows")) {
String programData = System.getenv("ProgramData");
if (programData != null) {
// use default XAML Map Control cache directory
return Paths.get(programData, "MapControl", "TileCache");
}
} else {//if (osName.contains("linux")) {
return Paths.get("/var", "tmp", "FxMapControl-Cache");
}
return null;
}
public final Path getRootDirectory() {
return rootDirectory;
}
@Override
public CacheItem get(String tileLayerName, int x, int y, int zoomLevel) {
File cacheDir = rootDirectory
.resolve(tileLayerName)
.resolve(Integer.toString(zoomLevel))
.resolve(Integer.toString(x)).toFile();
String fileNameFilter = Integer.toString(y) + ".";
if (cacheDir.isDirectory()) {
File[] cacheFiles = cacheDir.listFiles((dir, name) -> name.startsWith(fileNameFilter));
if (cacheFiles.length > 0) {
//System.out.println("Reading " + cacheFiles[0].getPath());
try {
byte[] buffer = new byte[(int) cacheFiles[0].length()];
long expiration = 0;
try (FileInputStream fileStream = new FileInputStream(cacheFiles[0])) {
fileStream.read(buffer);
}
if (buffer.length >= 16 && ByteBuffer.wrap(buffer, buffer.length - 16, 8).equals(EXPIRATION_MARKER)) {
expiration = ByteBuffer.wrap(buffer, buffer.length - 8, 8).order(ByteOrder.LITTLE_ENDIAN)
.getLong() / DATETIME_FACTOR - DATETIME_OFFSET;
}
return new CacheItem(buffer, expiration);
} catch (IOException ex) {
Logger.getLogger(ImageFileCache.class.getName()).log(Level.WARNING, ex.toString());
}
}
}
return null;
}
@Override
public void set(String tileLayerName, int x, int y, int zoomLevel, byte[] buffer, long expiration) {
File cacheFile = rootDirectory
.resolve(tileLayerName)
.resolve(Integer.toString(zoomLevel))
.resolve(Integer.toString(x))
.resolve(String.format("%d%s", y, getFileExtension(buffer))).toFile();
//System.out.println("Writing " + cacheFile.getPath() + ", Expires " + new Date(expiration));
try {
cacheFile.getParentFile().mkdirs();
try (FileOutputStream fileStream = new FileOutputStream(cacheFile)) {
fileStream.write(buffer, 0, buffer.length);
fileStream.write(EXPIRATION_MARKER.array());
fileStream.write(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN)
.putLong((expiration + DATETIME_OFFSET) * DATETIME_FACTOR).array());
}
cacheFile.setReadable(true, false);
cacheFile.setWritable(true, false);
} catch (IOException ex) {
Logger.getLogger(ImageFileCache.class.getName()).log(Level.WARNING, ex.toString());
}
}
private static String getFileExtension(byte[] buffer) {
if (buffer.length >= 8
&& buffer[0] == (byte) 0x89
&& buffer[1] == (byte) 0x50
&& buffer[2] == (byte) 0x4E
&& buffer[3] == (byte) 0x47
&& buffer[4] == (byte) 0x0D
&& buffer[5] == (byte) 0x0A
&& buffer[6] == (byte) 0x1A
&& buffer[7] == (byte) 0x0A) {
return ".png";
}
if (buffer.length >= 3
&& buffer[0] == (byte) 0xFF
&& buffer[1] == (byte) 0xD8
&& buffer[2] == (byte) 0xFF) {
return ".jpg";
}
if (buffer.length >= 3
&& buffer[0] == (byte) 0x47
&& buffer[1] == (byte) 0x49
&& buffer[2] == (byte) 0x46) {
return ".gif";
}
if (buffer.length >= 2
&& buffer[0] == (byte) 0x42
&& buffer[1] == (byte) 0x4D) {
return ".bmp";
}
if (buffer.length >= 4
&& buffer[0] == (byte) 0x49
&& buffer[1] == (byte) 0x49
&& buffer[2] == (byte) 0x2A
&& buffer[3] == (byte) 0x00) {
return ".tif";
}
if (buffer.length >= 4
&& buffer[0] == (byte) 0x4D
&& buffer[1] == (byte) 0x4D
&& buffer[2] == (byte) 0x00
&& buffer[3] == (byte) 0x2A) {
return ".tif";
}
return ".bin";
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
/**
* A geographic location with latitude and longitude values in degrees.
*/
public class Location {
private final double latitude;
private final double longitude;
public Location(double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
public final double getLatitude() {
return latitude;
}
public final double getLongitude() {
return longitude;
}
public final boolean equals(Location location) {
return this == location
|| (latitude == location.latitude && longitude == location.longitude);
}
@Override
public final boolean equals(Object obj) {
return (obj instanceof Location) && equals((Location) obj);
}
@Override
public int hashCode() {
return Double.hashCode(latitude) ^ Double.hashCode(longitude);
}
public static Location valueOf(String locationString) {
String[] pair = locationString.split(",");
if (pair.length != 2) {
throw new IllegalArgumentException(
"Location string must be a comma-separated pair of double values");
}
return new Location(
Double.parseDouble(pair[0]),
Double.parseDouble(pair[1]));
}
public static double normalizeLongitude(double longitude) {
if (longitude < -180.) {
longitude = ((longitude + 180.) % 360.) + 180.;
} else if (longitude > 180.) {
longitude = ((longitude - 180.) % 360.) - 180.;
}
return longitude;
}
static double nearestLongitude(double longitude, double referenceLongitude) {
longitude = normalizeLongitude(longitude);
if (longitude > referenceLongitude + 180d) {
longitude -= 360d;
} else if (longitude < referenceLongitude - 180d) {
longitude += 360d;
}
return longitude;
}
}
/*
* FX Map Control - https://github.com/ClemensFischer/FX-Map-Control
* © 2016 Clemens Fischer
*/
package fxmapcontrol;
import java.util.EnumSet;
import java.util.List;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleablePropertyFactory;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.RotateEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.input.ZoomEvent;
/**
* MapBase with default input event handling.
*/
public class Map extends MapBase {
public enum ManipulationModes {
ROTATE, TRANSLATE, ZOOM;
public static final EnumSet<ManipulationModes> ALL = EnumSet.allOf(ManipulationModes.class);
public static final EnumSet<ManipulationModes> DEFAULT = EnumSet.of(ManipulationModes.TRANSLATE, ManipulationModes.ZOOM);
}
private static final StyleablePropertyFactory<Map> propertyFactory
= new StyleablePropertyFactory<>(MapBase.getClassCssMetaData());
private static final CssMetaData<Map, Number> mouseWheelZoomDeltaCssMetaData
= propertyFactory.createSizeCssMetaData("-fx-mouse-wheel-zoom-delta", s -> s.mouseWheelZoomDeltaProperty);
private final StyleableDoubleProperty mouseWheelZoomDeltaProperty
= new SimpleStyleableDoubleProperty(mouseWheelZoomDeltaCssMetaData, this, "mouseWheelZoomDelta", 1d);
private final ObjectProperty<EnumSet<ManipulationModes>> manipulationModesProperty
= new SimpleObjectProperty<>(this, "manipulationModes", ManipulationModes.DEFAULT);
private final ReadOnlyBooleanWrapper mouseDraggingProperty = new ReadOnlyBooleanWrapper(this, "mouseDragging");
private Point2D mousePosition;
public Map() {
getStyleClass().add("map");
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
if (getManipulationModes().contains(ManipulationModes.TRANSLATE)
&& e.getTarget() == this
&& e.getButton() == MouseButton.PRIMARY
&& e.getClickCount() == 1) {
mousePosition = new Point2D(e.getX(), e.getY());
mouseDraggingProperty.set(true);
setCursor(Cursor.CLOSED_HAND);
}
});
addEventHandler(MouseEvent.MOUSE_RELEASED, e -> {
if (e.getTarget() == this && e.getButton() == MouseButton.PRIMARY) {
mousePosition = null;
mouseDraggingProperty.set(false);
setCursor(null);
}
});
addEventHandler(MouseEvent.MOUSE_DRAGGED, e -> {
if (mousePosition != null) {
Point2D position = new Point2D(e.getX(), e.getY());
translateMap(position.subtract(mousePosition));
mousePosition = position;
}
});
addEventHandler(ScrollEvent.SCROLL, e -> {
if (getManipulationModes().contains(ManipulationModes.ZOOM)
&& e.getTouchCount() == 0
&& !e.isInertia()) { // handle only mouse wheel events
Point2D center = getManipulationModes().contains(ManipulationModes.TRANSLATE)
? new Point2D(e.getX(), e.getY())
: new Point2D(getWidth() / 2d, getHeight() / 2d);
zoomMap(center, getTargetZoomLevel() + Math.signum(e.getDeltaY()) * getMouseWheelZoomDelta(), true);
}
});
addEventHandler(ZoomEvent.ZOOM, e -> {
if (getManipulationModes().contains(ManipulationModes.ZOOM)) {
Point2D center = getManipulationModes().contains(ManipulationModes.TRANSLATE)
? new Point2D(e.getX(), e.getY())
: new Point2D(getWidth() / 2d, getHeight() / 2d);
zoomMap(center, getZoomLevel() + Math.log(e.getZoomFactor()) / Math.log(2d), false);
}
});
addEventHandler(RotateEvent.ROTATE, e -> {
if (getManipulationModes().contains(ManipulationModes.ROTATE)) {
Point2D center = getManipulationModes().contains(ManipulationModes.TRANSLATE)
? new Point2D(e.getX(), e.getY())
: new Point2D(getWidth() / 2d, getHeight() / 2d);
rotateMap(center, getHeading() + e.getAngle(), false);
}
});
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return propertyFactory.getCssMetaData();
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
public final DoubleProperty mouseWheelZoomDeltaProperty() {
return mouseWheelZoomDeltaProperty;
}
public final double getMouseWheelZoomDelta() {
return mouseWheelZoomDeltaProperty.get();
}
public final void setMouseWheelZoomDelta(double mouseWheelZoomDelta) {
mouseWheelZoomDeltaProperty.set(mouseWheelZoomDelta);
}
public final ObjectProperty<EnumSet<ManipulationModes>> manipulationModesProperty() {
return manipulationModesProperty;
}
public final EnumSet<ManipulationModes> getManipulationModes() {
return manipulationModesProperty.get();
}
public final void setManipulationModes(EnumSet<ManipulationModes> manipulationModes) {
manipulationModesProperty.set(manipulationModes);
}
public final ReadOnlyBooleanProperty mouseDraggingProperty() {
return mouseDraggingProperty.getReadOnlyProperty();
}
public final boolean isMouseDragging() {
return mouseDraggingProperty.get();
}
}
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment