From 3000c395b692fbd5b9056ea06eaef08eb345941f Mon Sep 17 00:00:00 2001 From: Hallvard Traetteberg <hal@ntnu.no> Date: Fri, 12 Jan 2018 00:17:52 +0100 Subject: [PATCH] Imports and uses FxMapControl. Issue #10. --- tdt4140-gr1800/FxMapControl/.classpath | 26 + tdt4140-gr1800/FxMapControl/.project | 23 + .../org.eclipse.core.resources.prefs | 3 + .../.settings/org.eclipse.jdt.core.prefs | 5 + .../.settings/org.eclipse.m2e.core.prefs | 4 + tdt4140-gr1800/FxMapControl/pom.xml | 15 + .../AzimuthalEquidistantProjection.java | 50 ++ .../fxmapcontrol/AzimuthalProjection.java | 133 ++++ .../java/fxmapcontrol/BingMapsTileLayer.java | 83 +++ .../java/fxmapcontrol/BingMapsTileSource.java | 33 + .../fxmapcontrol/CenteredBoundingBox.java | 39 ++ .../EquirectangularProjection.java | 59 ++ .../java/fxmapcontrol/GnomonicProjection.java | 54 ++ .../src/main/java/fxmapcontrol/IMapNode.java | 15 + .../main/java/fxmapcontrol/ITileCache.java | 33 + .../java/fxmapcontrol/ITileImageLoader.java | 15 + .../java/fxmapcontrol/ImageFileCache.java | 177 +++++ .../src/main/java/fxmapcontrol/Location.java | 72 ++ .../src/main/java/fxmapcontrol/Map.java | 160 +++++ .../src/main/java/fxmapcontrol/MapBase.java | 626 ++++++++++++++++++ .../java/fxmapcontrol/MapBoundingBox.java | 74 +++ .../main/java/fxmapcontrol/MapGraticule.java | 287 ++++++++ .../src/main/java/fxmapcontrol/MapImage.java | 75 +++ .../main/java/fxmapcontrol/MapImageLayer.java | 348 ++++++++++ .../src/main/java/fxmapcontrol/MapItem.java | 61 ++ .../main/java/fxmapcontrol/MapItemBase.java | 69 ++ .../java/fxmapcontrol/MapItemsControl.java | 238 +++++++ .../src/main/java/fxmapcontrol/MapLayer.java | 46 ++ .../src/main/java/fxmapcontrol/MapLine.java | 103 +++ .../src/main/java/fxmapcontrol/MapNode.java | 97 +++ .../main/java/fxmapcontrol/MapNodeHelper.java | 66 ++ .../main/java/fxmapcontrol/MapPolygon.java | 86 +++ .../main/java/fxmapcontrol/MapPolyline.java | 126 ++++ .../main/java/fxmapcontrol/MapProjection.java | 170 +++++ .../main/java/fxmapcontrol/MapTileLayer.java | 369 +++++++++++ .../fxmapcontrol/StereographicProjection.java | 54 ++ .../src/main/java/fxmapcontrol/Tile.java | 84 +++ .../src/main/java/fxmapcontrol/TileGrid.java | 67 ++ .../java/fxmapcontrol/TileImageLoader.java | 200 ++++++ .../main/java/fxmapcontrol/TileSource.java | 148 +++++ .../fxmapcontrol/ViewportChangedEvent.java | 42 ++ .../fxmapcontrol/WebMercatorProjection.java | 82 +++ .../main/java/fxmapcontrol/WmsImageLayer.java | 209 ++++++ tdt4140-gr1800/app.ui/pom.xml | 10 +- .../java/tdt4140/gr1800/app/ui/MapMarker.java | 26 + .../tdt4140/gr1800/app/ui/MapPathLine.java | 24 + .../tdt4140/gr1800/app/ui/FxApp.fxml | 56 +- .../java/tdt4140/gr1800/app/ui/FxAppTest.java | 5 +- tdt4140-gr1800/pom.xml | 2 +- tdt4140-gr18nn/pom.xml | 1 - 50 files changed, 4814 insertions(+), 36 deletions(-) create mode 100644 tdt4140-gr1800/FxMapControl/.classpath create mode 100644 tdt4140-gr1800/FxMapControl/.project create mode 100644 tdt4140-gr1800/FxMapControl/.settings/org.eclipse.core.resources.prefs create mode 100644 tdt4140-gr1800/FxMapControl/.settings/org.eclipse.jdt.core.prefs create mode 100644 tdt4140-gr1800/FxMapControl/.settings/org.eclipse.m2e.core.prefs create mode 100644 tdt4140-gr1800/FxMapControl/pom.xml create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/AzimuthalEquidistantProjection.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/AzimuthalProjection.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/BingMapsTileLayer.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/BingMapsTileSource.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/CenteredBoundingBox.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/EquirectangularProjection.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/GnomonicProjection.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/IMapNode.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ITileCache.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ITileImageLoader.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ImageFileCache.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Location.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Map.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapBase.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapBoundingBox.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapGraticule.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapImage.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapImageLayer.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItem.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItemBase.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItemsControl.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapLayer.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapLine.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapNode.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapNodeHelper.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapPolygon.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapPolyline.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapProjection.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapTileLayer.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/StereographicProjection.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Tile.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileGrid.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileImageLoader.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileSource.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ViewportChangedEvent.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/WebMercatorProjection.java create mode 100644 tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/WmsImageLayer.java create mode 100644 tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/MapMarker.java create mode 100644 tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/MapPathLine.java diff --git a/tdt4140-gr1800/FxMapControl/.classpath b/tdt4140-gr1800/FxMapControl/.classpath new file mode 100644 index 0000000..af1430b --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/.classpath @@ -0,0 +1,26 @@ +<?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> diff --git a/tdt4140-gr1800/FxMapControl/.project b/tdt4140-gr1800/FxMapControl/.project new file mode 100644 index 0000000..3038f0e --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/.project @@ -0,0 +1,23 @@ +<?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> diff --git a/tdt4140-gr1800/FxMapControl/.settings/org.eclipse.core.resources.prefs b/tdt4140-gr1800/FxMapControl/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..e9441bb --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding/<project>=UTF-8 diff --git a/tdt4140-gr1800/FxMapControl/.settings/org.eclipse.jdt.core.prefs b/tdt4140-gr1800/FxMapControl/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..714351a --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +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 diff --git a/tdt4140-gr1800/FxMapControl/.settings/org.eclipse.m2e.core.prefs b/tdt4140-gr1800/FxMapControl/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/tdt4140-gr1800/FxMapControl/pom.xml b/tdt4140-gr1800/FxMapControl/pom.xml new file mode 100644 index 0000000..b8847d8 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/pom.xml @@ -0,0 +1,15 @@ +<?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> diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/AzimuthalEquidistantProjection.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/AzimuthalEquidistantProjection.java new file mode 100644 index 0000000..9d5db19 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/AzimuthalEquidistantProjection.java @@ -0,0 +1,50 @@ +/* + * 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); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/AzimuthalProjection.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/AzimuthalProjection.java new file mode 100644 index 0000000..2715171 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/AzimuthalProjection.java @@ -0,0 +1,133 @@ +/* + * 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); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/BingMapsTileLayer.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/BingMapsTileLayer.java new file mode 100644 index 0000000..ce57a7e --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/BingMapsTileLayer.java @@ -0,0 +1,83 @@ +/* + * 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; + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/BingMapsTileSource.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/BingMapsTileSource.java new file mode 100644 index 0000000..e4001af --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/BingMapsTileSource.java @@ -0,0 +1,33 @@ +/* + * 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)); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/CenteredBoundingBox.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/CenteredBoundingBox.java new file mode 100644 index 0000000..acfd258 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/CenteredBoundingBox.java @@ -0,0 +1,39 @@ +/* + * 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); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/EquirectangularProjection.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/EquirectangularProjection.java new file mode 100644 index 0000000..eb69442 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/EquirectangularProjection.java @@ -0,0 +1,59 @@ +/* + * 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()); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/GnomonicProjection.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/GnomonicProjection.java new file mode 100644 index 0000000..b79c9dc --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/GnomonicProjection.java @@ -0,0 +1,54 @@ +/* + * 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); + } + +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/IMapNode.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/IMapNode.java new file mode 100644 index 0000000..7c9f107 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/IMapNode.java @@ -0,0 +1,15 @@ +/* + * 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); +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ITileCache.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ITileCache.java new file mode 100644 index 0000000..cfcb522 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ITileCache.java @@ -0,0 +1,33 @@ +/* + * 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); +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ITileImageLoader.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ITileImageLoader.java new file mode 100644 index 0000000..9cf04aa --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ITileImageLoader.java @@ -0,0 +1,15 @@ +/* + * 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); +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ImageFileCache.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ImageFileCache.java new file mode 100644 index 0000000..29c337c --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ImageFileCache.java @@ -0,0 +1,177 @@ +/* + * 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"; + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Location.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Location.java new file mode 100644 index 0000000..8d656a6 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Location.java @@ -0,0 +1,72 @@ +/* + * 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; + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Map.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Map.java new file mode 100644 index 0000000..cf650e5 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Map.java @@ -0,0 +1,160 @@ +/* + * 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(); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapBase.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapBase.java new file mode 100644 index 0000000..000892e --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapBase.java @@ -0,0 +1,626 @@ +/* + * FX Map Control - https://github.com/ClemensFischer/FX-Map-Control + * © 2017 Clemens Fischer + */ +package fxmapcontrol; + +import java.util.List; +import javafx.animation.Animation; +import javafx.animation.Interpolator; +import javafx.animation.Transition; +import javafx.beans.DefaultProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; +import javafx.css.CssMetaData; +import javafx.css.SimpleStyleableObjectProperty; +import javafx.css.Styleable; +import javafx.css.StyleableObjectProperty; +import javafx.css.StyleablePropertyFactory; +import javafx.geometry.Bounds; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.layout.Region; +import javafx.scene.shape.Rectangle; +import javafx.scene.transform.Affine; +import javafx.scene.transform.Rotate; +import javafx.scene.transform.Scale; +import javafx.util.Duration; + +/** + * The map control. Renders map content provided by one or more MapTileLayers. The visible map area + * is defined by the center and zoomLevel properties. The map can be rotated by an angle that is + * given by the heading property. MapBase can contain different child nodes, which typically + * implement the IMapNode interface. + */ +@DefaultProperty(value = "children") +public class MapBase extends Region implements IMapNode { + + private static final StyleablePropertyFactory<MapBase> propertyFactory + = new StyleablePropertyFactory<>(Region.getClassCssMetaData()); + + private static final CssMetaData<MapBase, Duration> tileFadeDurationCssMetaData + = propertyFactory.createDurationCssMetaData("-fx-tile-fade-duration", s -> s.tileFadeDurationProperty); + + private static final CssMetaData<MapBase, Duration> transitionDurationCssMetaData + = propertyFactory.createDurationCssMetaData("-fx-transition-duration", s -> s.transitionDurationProperty); + + private final StyleableObjectProperty<Duration> tileFadeDurationProperty + = new SimpleStyleableObjectProperty<>(tileFadeDurationCssMetaData, this, "tileFadeDuration", Duration.seconds(0.2)); + + private final StyleableObjectProperty<Duration> transitionDurationProperty + = new SimpleStyleableObjectProperty<>(transitionDurationCssMetaData, this, "transitionDuration", Duration.seconds(0.3)); + + private final ObjectProperty<MapProjection> projectionProperty = new SimpleObjectProperty<>(this, "projection", new WebMercatorProjection()); + private final ObjectProperty<Location> projectionCenterProperty = new SimpleObjectProperty<>(this, "projectionCenter"); + private final ObjectProperty<Location> centerProperty = new SimpleObjectProperty<>(this, "center", new Location(0d, 0d)); + private final ObjectProperty<Location> targetCenterProperty = new SimpleObjectProperty<>(this, "targetCenter", new Location(0d, 0d)); + private final DoubleProperty zoomLevelProperty = new SimpleDoubleProperty(this, "zoomLevel"); + private final DoubleProperty targetZoomLevelProperty = new SimpleDoubleProperty(this, "targetZoomLevel"); + private final DoubleProperty headingProperty = new SimpleDoubleProperty(this, "heading"); + private final DoubleProperty targetHeadingProperty = new SimpleDoubleProperty(this, "targetHeading"); + private final Scale scaleTransform = new Scale(); + private final Rotate rotateTransform = new Rotate(); + private final Affine scaleRotateTransform = new Affine(); + private final CenterTransition centerTransition = new CenterTransition(); + private final ZoomLevelTransition zoomLevelTransition = new ZoomLevelTransition(); + private final HeadingTransition headingTransition = new HeadingTransition(); + + private Location transformCenter = new Location(0d, 0d); + private Point2D viewportCenter = new Point2D(0d, 0d); + private double centerLongitude; + private double minZoomLevel = 1d; + private double maxZoomLevel = 19d; + private boolean internalUpdate; + + public MapBase() { + getStyleClass().add("map-base"); + getChildren().addListener(new MapNodeHelper.ChildrenListener(this)); + + Rectangle clip = new Rectangle(getWidth(), getHeight()); + setClip(clip); + + layoutBoundsProperty().addListener((observable, oldValue, newValue) -> { + resetTransformCenter(); + updateTransform(false, false); + clip.setWidth(newValue.getWidth()); + clip.setHeight(newValue.getHeight()); + }); + + Tile.setFadeDuration(getTileFadeDuration()); + + tileFadeDurationProperty.addListener((observable, oldValue, newValue) -> { + Tile.setFadeDuration(newValue); + }); + + transitionDurationProperty.addListener((observable, oldValue, newValue) -> { + centerTransition.setDuration(newValue); + zoomLevelTransition.setDuration(newValue); + headingTransition.setDuration(newValue); + }); + + projectionProperty.addListener(observable -> { + resetTransformCenter(); + updateTransform(false, true); + }); + + projectionCenterProperty.addListener(observable -> { + if (getProjection().isAzimuthal()) { + resetTransformCenter(); + updateTransform(false, false); + } + }); + + centerProperty.addListener(new CenterChangeListener()); + targetCenterProperty.addListener(new TargetCenterChangeListener()); + zoomLevelProperty.addListener(new ZoomLevelChangeListener()); + targetZoomLevelProperty.addListener(new TargetZoomLevelChangeListener()); + headingProperty.addListener(new HeadingChangeListener()); + targetHeadingProperty.addListener(new TargetHeadingChangeListener()); + } + + public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { + return propertyFactory.getCssMetaData(); + } + + @Override + public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { + return getClassCssMetaData(); + } + + @Override + public final ObservableList<Node> getChildren() { + return super.getChildren(); + } + + @Override + public final MapBase getMap() { + return this; + } + + @Override + public final void setMap(MapBase map) { + throw new IllegalStateException(); + } + + public final ObjectProperty<Duration> tileFadeDurationProperty() { + return tileFadeDurationProperty; + } + + public final Duration getTileFadeDuration() { + return tileFadeDurationProperty.get(); + } + + public final void setTileFadeDuration(Duration tileFadeDuration) { + tileFadeDurationProperty.set(tileFadeDuration); + } + + public final ObjectProperty<Duration> transitionDurationProperty() { + return transitionDurationProperty; + } + + public final Duration getTransitionDuration() { + return transitionDurationProperty.get(); + } + + public final void setTransitionDuration(Duration transitionDuration) { + transitionDurationProperty.set(transitionDuration); + } + + public final ObjectProperty<MapProjection> projectionProperty() { + return projectionProperty; + } + + public final MapProjection getProjection() { + return projectionProperty.get(); + } + + public final void setProjection(MapProjection projection) { + projectionProperty.set(projection); + } + + public final ObjectProperty<Location> projectionCenterProperty() { + return projectionCenterProperty; + } + + public final Location getProjectionCenter() { + return projectionCenterProperty.get(); + } + + public final void setProjectionCenter(Location projectionCenter) { + projectionCenterProperty.set(projectionCenter); + } + + public final ObjectProperty<Location> centerProperty() { + return centerProperty; + } + + public final Location getCenter() { + return centerProperty.get(); + } + + public final void setCenter(Location center) { + centerProperty.set(center); + } + + public final ObjectProperty<Location> targetCenterProperty() { + return targetCenterProperty; + } + + public final Location getTargetCenter() { + return targetCenterProperty.get(); + } + + public final void setTargetCenter(Location targetCenter) { + targetCenterProperty.set(targetCenter); + } + + public final double getMinZoomLevel() { + return minZoomLevel; + } + + public final void setMinZoomLevel(double minZoomLevel) { + this.minZoomLevel = minZoomLevel; + } + + public final double getMaxZoomLevel() { + return maxZoomLevel; + } + + public final void setMaxZoomLevel(double maxZoomLevel) { + this.maxZoomLevel = maxZoomLevel; + } + + public final DoubleProperty zoomLevelProperty() { + return zoomLevelProperty; + } + + public final double getZoomLevel() { + return zoomLevelProperty.get(); + } + + public final void setZoomLevel(double zoomLevel) { + zoomLevelProperty.set(zoomLevel); + } + + public final DoubleProperty targetZoomLevelProperty() { + return targetZoomLevelProperty; + } + + public final double getTargetZoomLevel() { + return targetZoomLevelProperty.get(); + } + + public final void setTargetZoomLevel(double targetZoomLevel) { + targetZoomLevelProperty.set(targetZoomLevel); + } + + public final DoubleProperty headingProperty() { + return headingProperty; + } + + public final double getHeading() { + return headingProperty.get(); + } + + public final void setHeading(double heading) { + headingProperty.set(heading); + } + + public final DoubleProperty targetHeadingProperty() { + return targetHeadingProperty; + } + + public final double getTargetHeading() { + return targetHeadingProperty.get(); + } + + public final void setTargetHeading(double targetHeading) { + targetHeadingProperty.set(targetHeading); + } + + public final Scale getScaleTransform() { + return scaleTransform; + } + + public final Rotate getRotateTransform() { + return rotateTransform; + } + + public final Affine getScaleRotateTransform() { + return scaleRotateTransform; + } + + public final void setTransformCenter(Point2D center) { + transformCenter = getProjection().viewportPointToLocation(center); + viewportCenter = center; + } + + public final void resetTransformCenter() { + transformCenter = null; + viewportCenter = new Point2D(getWidth() / 2d, getHeight() / 2d); + } + + public final void translateMap(Point2D translation) { + if (transformCenter != null) { + resetTransformCenter(); + } + + if (translation.getX() != 0d || translation.getY() != 0d) { + setCenter(getProjection().viewportPointToLocation(new Point2D( + getWidth() / 2d - translation.getX(), + getHeight() / 2d - translation.getY()))); + } + } + + public final void zoomMap(Point2D center, double zoomLevel, boolean animated) { + zoomLevel = Math.min(Math.max(zoomLevel, getMinZoomLevel()), getMaxZoomLevel()); + + if (animated && getProjection().isNormalCylindrical()) { + if (getTargetZoomLevel() != zoomLevel) { + setTransformCenter(center); + setTargetZoomLevel(zoomLevel); + } + } else if (getZoomLevel() != zoomLevel) { + setTransformCenter(center); + setZoomLevel(zoomLevel); + } + } + + public final void rotateMap(Point2D center, double heading, boolean animated) { + if (animated) { + if (getTargetHeading() != heading) { + setTransformCenter(center); + setTargetHeading(heading); + } + } else if (getHeading() != heading) { + setTransformCenter(center); + setHeading(heading); + } + } + + public final void zoomToBounds(MapBoundingBox boundingBox) { + if (boundingBox != null && boundingBox.hasValidBounds()) { + Bounds bounds = getProjection().boundingBoxToBounds(boundingBox); + double scale0 = 1d / getProjection().getViewportScale(0d); + double lonScale = scale0 * getWidth() / bounds.getWidth(); + double latScale = scale0 * getHeight() / bounds.getHeight(); + double lonZoom = Math.log(lonScale) / Math.log(2d); + double latZoom = Math.log(latScale) / Math.log(2d); + + setTargetHeading(0d); + setTargetZoomLevel(Math.min(lonZoom, latZoom)); + setTargetCenter(getProjection().pointToLocation(new Point2D( + bounds.getMinX() + bounds.getWidth() / 2d, + bounds.getMinY() + bounds.getHeight() / 2d))); + } + } + + public final boolean isCenterAnimationRunning() { + return centerTransition.getStatus() == Animation.Status.RUNNING; + } + + public final boolean isZoomLevelAnimationRunning() { + return zoomLevelTransition.getStatus() == Animation.Status.RUNNING; + } + + public final boolean isHeadingAnimationRunning() { + return headingTransition.getStatus() == Animation.Status.RUNNING; + } + + private void updateTransform(boolean resetTransformCenter, boolean projectionChanged) { + MapProjection projection = getProjection(); + Location center = transformCenter != null ? transformCenter : getCenter(); + + projection.setViewportTransform( + getProjectionCenter() != null ? getProjectionCenter() : getCenter(), + center, viewportCenter, getZoomLevel(), getHeading()); + + if (transformCenter != null) { + center = projection.viewportPointToLocation(new Point2D(getWidth() / 2d, getHeight() / 2d)); + + double latitude = center.getLatitude(); + + if (latitude < -projection.maxLatitude() || latitude > projection.maxLatitude()) { + latitude = Math.min(Math.max(latitude, -projection.maxLatitude()), projection.maxLatitude()); + resetTransformCenter = true; + } + + center = new Location(latitude, Location.normalizeLongitude(center.getLongitude())); + + internalUpdate = true; + setCenter(center); + internalUpdate = false; + + if (resetTransformCenter) { + resetTransformCenter(); + projection.setViewportTransform( + getProjectionCenter() != null ? getProjectionCenter() : center, + center, viewportCenter, getZoomLevel(), getHeading()); + } + } + + Point2D scale = projection.getMapScale(center); + scaleTransform.setX(scale.getX()); + scaleTransform.setY(scale.getY()); + rotateTransform.setAngle(getHeading()); + scaleRotateTransform.setToTransform(scaleTransform.createConcatenation(rotateTransform)); + + fireEvent(new ViewportChangedEvent(projectionChanged, getCenter().getLongitude() - centerLongitude)); + + centerLongitude = getCenter().getLongitude(); + } + + private Location adjustCenterProperty(ObjectProperty<Location> property, Location value) { + double maxLatitude = getProjection().maxLatitude(); + internalUpdate = true; + if (value == null) { + value = new Location(0d, 0d); + property.setValue(value); + } else if (value.getLongitude() < -180d || value.getLongitude() > 180d + || value.getLatitude() < -maxLatitude || value.getLatitude() > maxLatitude) { + value = new Location( + Math.min(Math.max(value.getLatitude(), -maxLatitude), maxLatitude), + Location.normalizeLongitude(value.getLongitude())); + property.setValue(value); + } + internalUpdate = false; + return value; + } + + private double adjustZoomLevelProperty(DoubleProperty property, double value) { + if (value < minZoomLevel || value > maxZoomLevel) { + internalUpdate = true; + value = Math.min(Math.max(value, minZoomLevel), maxZoomLevel); + property.set(value); + internalUpdate = false; + } + return value; + } + + private double adjustHeadingProperty(DoubleProperty property, double value) { + if (value < 0d || value > 360d) { + internalUpdate = true; + value = ((value % 360d) + 360d) % 360d; + property.set(value); + internalUpdate = false; + } + return value; + } + + private class CenterChangeListener implements ChangeListener<Location> { + + @Override + public void changed(ObservableValue<? extends Location> observable, Location oldValue, Location newValue) { + if (!internalUpdate) { + newValue = adjustCenterProperty(centerProperty, newValue); + + if (!isCenterAnimationRunning()) { + internalUpdate = true; + setTargetCenter(newValue); + internalUpdate = false; + } + + resetTransformCenter(); + updateTransform(false, false); + } + } + } + + private class TargetCenterChangeListener implements ChangeListener<Location> { + + @Override + public void changed(ObservableValue<? extends Location> observable, Location oldValue, Location newValue) { + if (!internalUpdate) { + centerTransition.start(getCenter(), adjustCenterProperty(targetCenterProperty, newValue)); + } + } + } + + private class ZoomLevelChangeListener implements ChangeListener<Number> { + + @Override + public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { + if (!internalUpdate) { + double value = adjustZoomLevelProperty(zoomLevelProperty, newValue.doubleValue()); + + if (isZoomLevelAnimationRunning()) { + updateTransform(false, false); + } else { + internalUpdate = true; + setTargetZoomLevel(value); + internalUpdate = false; + updateTransform(true, false); + } + } + } + } + + private class TargetZoomLevelChangeListener implements ChangeListener<Number> { + + @Override + public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { + if (!internalUpdate) { + zoomLevelTransition.start(getZoomLevel(), adjustZoomLevelProperty(targetZoomLevelProperty, newValue.doubleValue())); + } + } + } + + private class HeadingChangeListener implements ChangeListener<Number> { + + @Override + public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { + if (!internalUpdate) { + double value = adjustHeadingProperty(headingProperty, newValue.doubleValue()); + + if (isHeadingAnimationRunning()) { + updateTransform(false, false); + } else { + internalUpdate = true; + setTargetHeading(value); + internalUpdate = false; + updateTransform(true, false); + } + } + } + } + + private class TargetHeadingChangeListener implements ChangeListener<Number> { + + @Override + public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { + if (!internalUpdate) { + headingTransition.start(getHeading(), adjustHeadingProperty(targetHeadingProperty, newValue.doubleValue())); + } + } + } + + private abstract class TransitionBase<T> extends Transition { + + protected T fromValue; + protected T toValue; + + public TransitionBase() { + setDuration(getTransitionDuration()); + setInterpolator(Interpolator.EASE_OUT); + setOnFinished(e -> updateTransform(true, false)); + } + + public final void setDuration(Duration duration) { + setCycleDuration(duration); + } + + protected final void start() { + if (getCycleDuration().greaterThan(Duration.ZERO)) { + playFromStart(); + } else { + interpolate(1d); + } + } + } + + private class CenterTransition extends TransitionBase<Location> { + + public void start(Location from, Location to) { + if (!from.equals(to)) { + double longitude = Location.normalizeLongitude(to.getLongitude()); + if (longitude > from.getLongitude() + 180d) { + longitude -= 360d; + } else if (longitude < from.getLongitude() - 180d) { + longitude += 360d; + } + fromValue = from; + toValue = new Location(to.getLatitude(), longitude); + start(); + } + } + + @Override + protected void interpolate(double f) { + setCenter(new Location( + (1d - f) * fromValue.getLatitude() + f * toValue.getLatitude(), + (1d - f) * fromValue.getLongitude() + f * toValue.getLongitude())); + } + } + + private class ZoomLevelTransition extends TransitionBase<Double> { + + public void start(double from, double to) { + if (from != to) { + fromValue = from; + toValue = to; + start(); + } + } + + @Override + protected void interpolate(double f) { + setZoomLevel((1d - f) * fromValue + f * toValue); + } + }; + + private class HeadingTransition extends TransitionBase<Double> { + + public void start(double from, double to) { + if (from != to) { + if (to - from > 180d) { + to -= 360d; + } else if (to - from < -180d) { + to += 360d; + } + fromValue = from; + toValue = to; + start(); + } + } + + @Override + protected void interpolate(double f) { + setHeading((1d - f) * fromValue + f * toValue); + } + }; +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapBoundingBox.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapBoundingBox.java new file mode 100644 index 0000000..10a29ae --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapBoundingBox.java @@ -0,0 +1,74 @@ +/* + * 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); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapGraticule.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapGraticule.java new file mode 100644 index 0000000..d164327 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapGraticule.java @@ -0,0 +1,287 @@ +/* + * 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); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapImage.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapImage.java new file mode 100644 index 0000000..6bb9d1d --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapImage.java @@ -0,0 +1,75 @@ +/* + * 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); + } + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapImageLayer.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapImageLayer.java new file mode 100644 index 0000000..b9d2205 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapImageLayer.java @@ -0,0 +1,348 @@ +/* + * 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; + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItem.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItem.java new file mode 100644 index 0000000..04d79c7 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItem.java @@ -0,0 +1,61 @@ +/* + * 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); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItemBase.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItemBase.java new file mode 100644 index 0000000..f3e3cd8 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItemBase.java @@ -0,0 +1,69 @@ +/* + * 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); + } + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItemsControl.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItemsControl.java new file mode 100644 index 0000000..744d1a7 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapItemsControl.java @@ -0,0 +1,238 @@ +/* + * 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; + } + } + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapLayer.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapLayer.java new file mode 100644 index 0000000..e804cf5 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapLayer.java @@ -0,0 +1,46 @@ +/* + * 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() { + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapLine.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapLine.java new file mode 100644 index 0000000..99be3ff --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapLine.java @@ -0,0 +1,103 @@ +/* + * 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); + } + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapNode.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapNode.java new file mode 100644 index 0000000..0a8bdb3 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapNode.java @@ -0,0 +1,97 @@ +/* + * 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); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapNodeHelper.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapNodeHelper.java new file mode 100644 index 0000000..74a1c7f --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapNodeHelper.java @@ -0,0 +1,66 @@ +/* + * 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())); + + } + } + } + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapPolygon.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapPolygon.java new file mode 100644 index 0000000..e9b1230 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapPolygon.java @@ -0,0 +1,86 @@ +/* + * 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 + } + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapPolyline.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapPolyline.java new file mode 100644 index 0000000..1f03010 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapPolyline.java @@ -0,0 +1,126 @@ +/* + * 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; + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapProjection.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapProjection.java new file mode 100644 index 0000000..9f57f24 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapProjection.java @@ -0,0 +1,170 @@ +/* + * 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())); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapTileLayer.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapTileLayer.java new file mode 100644 index 0000000..7c8534c --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/MapTileLayer.java @@ -0,0 +1,369 @@ +/* + * 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())); + } + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/StereographicProjection.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/StereographicProjection.java new file mode 100644 index 0000000..30f05fe --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/StereographicProjection.java @@ -0,0 +1,54 @@ +/* + * 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); + } + +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Tile.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Tile.java new file mode 100644 index 0000000..0a0a193 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/Tile.java @@ -0,0 +1,84 @@ +/* + * 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); + } + } + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileGrid.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileGrid.java new file mode 100644 index 0000000..73b17c0 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileGrid.java @@ -0,0 +1,67 @@ +/* + * 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); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileImageLoader.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileImageLoader.java new file mode 100644 index 0000000..7e2a19e --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileImageLoader.java @@ -0,0 +1,200 @@ +/* + * 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; + } + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileSource.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileSource.java new file mode 100644 index 0000000..c7506bb --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/TileSource.java @@ -0,0 +1,148 @@ +/* + * 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)); + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ViewportChangedEvent.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ViewportChangedEvent.java new file mode 100644 index 0000000..1ffae6c --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/ViewportChangedEvent.java @@ -0,0 +1,42 @@ +/* + * FX Map Control - https://github.com/ClemensFischer/FX-Map-Control + * © 2017 Clemens Fischer + */ +package fxmapcontrol; + +import javafx.event.Event; +import javafx.event.EventType; + +/** + * Fired by MapBase when the viewport changes. + */ +public class ViewportChangedEvent extends Event { + + public static final EventType<ViewportChangedEvent> VIEWPORT_CHANGED + = new EventType(Event.ANY, "VIEWPORT_CHANGED"); + + private final boolean projectionChanged; + private final double longitudeOffset; + + public ViewportChangedEvent(boolean projectionChanged, double longitudeOffset) { + super(VIEWPORT_CHANGED); + this.projectionChanged = projectionChanged; + this.longitudeOffset = longitudeOffset; + } + + /** + * Indicates if the map projection has changed, i.e. if a TileLayer or MapImageLayer should be + * immediately updated, or MapPath Data in cartesian map coordinates should be recalculated. + */ + public final boolean getProjectionChanged() { + return projectionChanged; + } + + /** + * Offset of the map center longitude value from the previous viewport. + * Used to detect if the map center has moved across 180° longitude. + */ + public final double getLongitudeOffset() { + return longitudeOffset; + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/WebMercatorProjection.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/WebMercatorProjection.java new file mode 100644 index 0000000..44285b8 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/WebMercatorProjection.java @@ -0,0 +1,82 @@ +/* + * 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 Web Mercator + * Projection. Longitude values are transformed linearly to X values in meters, by multiplying with + * METERS_PER_DEGREE. Latitude values in the interval [-maxLatitude .. maxLatitude] are transformed + * to Y values in meters in the interval [-R*pi .. R*pi], R=WGS84_EQUATORIAL_RADIUS. + */ +public class WebMercatorProjection extends MapProjection { + + public static final double MAX_LATITUDE = yToLatitude(180.); + + public WebMercatorProjection() { + this("EPSG:3857"); + } + + public WebMercatorProjection(String crsId) { + this.crsId = crsId; + } + + @Override + public boolean isWebMercator() { + return true; + } + + @Override + public boolean isNormalCylindrical() { + return true; + } + + @Override + public boolean isAzimuthal() { + return false; + } + + @Override + public double maxLatitude() { + return MAX_LATITUDE; + } + + @Override + public Point2D getMapScale(Location location) { + double scale = viewportScale / Math.cos(location.getLatitude() * Math.PI / 180d); + + return new Point2D(scale, scale); + } + + @Override + public Point2D locationToPoint(Location location) { + return new Point2D( + METERS_PER_DEGREE * location.getLongitude(), + METERS_PER_DEGREE * latitudeToY(location.getLatitude())); + } + + @Override + public Location pointToLocation(Point2D point) { + return new Location( + yToLatitude(point.getY() / METERS_PER_DEGREE), + point.getX() / METERS_PER_DEGREE); + } + + @Override + public double getViewportScale(double zoomLevel) { + return super.getViewportScale(zoomLevel) / METERS_PER_DEGREE; + } + + public static double latitudeToY(double latitude) { + return latitude <= -90 ? Double.NEGATIVE_INFINITY + : latitude >= 90 ? Double.POSITIVE_INFINITY + : Math.log(Math.tan(latitude * Math.PI / 360 + Math.PI / 4)) / Math.PI * 180; + } + + public static double yToLatitude(double y) { + return Math.atan(Math.sinh(y * Math.PI / 180)) / Math.PI * 180; + } +} diff --git a/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/WmsImageLayer.java b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/WmsImageLayer.java new file mode 100644 index 0000000..035f735 --- /dev/null +++ b/tdt4140-gr1800/FxMapControl/src/main/java/fxmapcontrol/WmsImageLayer.java @@ -0,0 +1,209 @@ +/* + * FX Map Control - https://github.com/ClemensFischer/FX-Map-Control + * © 2016 Clemens Fischer + */ +package fxmapcontrol; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.logging.Level; +import java.util.logging.Logger; +import javafx.beans.InvalidationListener; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javax.xml.parsers.DocumentBuilder; +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.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * Map image overlay. Fills the entire viewport with map images provided by a Web Map Service (WMS). + * The base request URL is specified by the serviceUrl property. + */ +public class WmsImageLayer extends MapImageLayer { + + private final StringProperty serviceUrlProperty = new SimpleStringProperty(this, "serviceUrl"); + private final StringProperty versionProperty = new SimpleStringProperty(this, "version", "1.3.0"); + private final StringProperty layersProperty = new SimpleStringProperty(this, "layers"); + private final StringProperty stylesProperty = new SimpleStringProperty(this, "styles"); + private final StringProperty parametersProperty = new SimpleStringProperty(this, "parameters"); + private final BooleanProperty transparentProperty = new SimpleBooleanProperty(this, "transparent"); + + public WmsImageLayer(String serviceUrl, String layers) { + this(); + setServiceUrl(serviceUrl); + setLayers(layers); + } + + public WmsImageLayer() { + InvalidationListener listener = observable -> updateImage(); + serviceUrlProperty.addListener(listener); + versionProperty.addListener(listener); + layersProperty.addListener(listener); + stylesProperty.addListener(listener); + parametersProperty.addListener(listener); + transparentProperty.addListener(listener); + } + + public final StringProperty serviceUrlProperty() { + return serviceUrlProperty; + } + + public final String getServiceUrl() { + return serviceUrlProperty.get(); + } + + public final void setServiceUrl(String serviceUrl) { + serviceUrlProperty.set(serviceUrl); + } + + public final StringProperty versionProperty() { + return versionProperty; + } + + public final String getVersion() { + return versionProperty.get(); + } + + public final void setVersion(String version) { + versionProperty.set(version); + } + + public final StringProperty layersProperty() { + return layersProperty; + } + + public final String getLayers() { + return layersProperty.get(); + } + + public final void setLayers(String layers) { + layersProperty.set(layers); + } + + public final StringProperty stylesProperty() { + return stylesProperty; + } + + public final String getStyles() { + return stylesProperty.get(); + } + + public final void setStyles(String styles) { + stylesProperty.set(styles); + } + + public final StringProperty parametersProperty() { + return parametersProperty; + } + + public final String getParameters() { + return parametersProperty.get(); + } + + public final void setParameters(String parameters) { + parametersProperty.set(parameters); + } + + public final BooleanProperty transparentProperty() { + return transparentProperty; + } + + public final boolean isTransparent() { + return transparentProperty.get(); + } + + public final void setTransparent(boolean transparent) { + transparentProperty.set(transparent); + } + + public ObservableList<String> getAllLayers() { + ObservableList<String> layers = FXCollections.observableArrayList(); + String url = getServiceUrl(); + + if (url != null && !url.isEmpty()) { + try { + url += "?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities"; + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document; + + try (InputStream inputStream = connection.getInputStream()) { + document = docBuilder.parse(inputStream); + } + + NodeList layerNodes = document.getDocumentElement().getElementsByTagName("Layer"); + + if (layerNodes.getLength() > 0) { + Element rootLayer = (Element) layerNodes.item(0); + layerNodes = rootLayer.getElementsByTagName("Layer"); + + for (int i = 0; i < layerNodes.getLength(); i++) { + Node layerNode = layerNodes.item(i); + + if (layerNode.getNodeType() == Node.ELEMENT_NODE) { + NodeList nameNodes = ((Element) layerNode).getElementsByTagName("Name"); + + if (nameNodes.getLength() > 0) { + layers.add(((Element) nameNodes.item(0)).getTextContent()); + } + } + } + } + } + } catch (IOException | ParserConfigurationException | SAXException | DOMException ex) { + Logger.getLogger(WmsImageLayer.class.getName()).log(Level.SEVERE, null, ex); + } + } + + return layers; + } + + @Override + protected boolean updateImage(MapBoundingBox boundingBox) { + String serviceUrl = getServiceUrl(); + + if (serviceUrl == null || serviceUrl.isEmpty()) { + return false; + } + + String version = getVersion() != null ? getVersion() : "1.3.0"; + String queryParameters = getMap().getProjection().wmsQueryParameters(boundingBox, version); + + if (queryParameters == null || queryParameters.isEmpty()) { + return false; + } + + String imageUrl = serviceUrl + + "?SERVICE=WMS" + + "&VERSION=" + version + + "&REQUEST=GetMap" + + "&LAYERS=" + (getLayers() != null ? getLayers() : "") + + "&STYLES=" + (getStyles() != null ? getStyles() : "") + + "&" + queryParameters + + "&FORMAT=image/png" + + "&TRANSPARENT=" + (isTransparent() ? "TRUE" : "FALSE"); + + if (getParameters() != null) { + imageUrl += "&" + getParameters(); + } + + imageUrl = imageUrl.replace(" ", "%20"); + + updateImage(imageUrl); + return true; + } +} diff --git a/tdt4140-gr1800/app.ui/pom.xml b/tdt4140-gr1800/app.ui/pom.xml index bf8d4bf..0511740 100644 --- a/tdt4140-gr1800/app.ui/pom.xml +++ b/tdt4140-gr1800/app.ui/pom.xml @@ -16,14 +16,18 @@ <version>0.0.1-SNAPSHOT</version> </dependency> - <!-- https://mvnrepository.com/artifact/com.lynden/GMapsFX --> + <!-- https://mvnrepository.com/artifact/com.lynden/GMapsFX <dependency> <groupId>com.lynden</groupId> <artifactId>GMapsFX</artifactId> <version>2.12.0</version> </dependency> - <!-- <dependency> <groupId>fischer.clemens</groupId> <artifactId>fx-map-control</artifactId> - <version>1.0</version> </dependency> --> + --> + <dependency> + <groupId>fischer.clemens</groupId> + <artifactId>fx-map-control</artifactId> + <version>1.0</version> + </dependency> <dependency> <groupId>junit</groupId> diff --git a/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/MapMarker.java b/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/MapMarker.java new file mode 100644 index 0000000..d85568e --- /dev/null +++ b/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/MapMarker.java @@ -0,0 +1,26 @@ +package tdt4140.gr1800.app.ui; + +import fxmapcontrol.Location; +import fxmapcontrol.MapItem; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Circle; +import tdt4140.gr1800.app.core.LatLong; + +public class MapMarker extends MapItem<LatLong> { + + public MapMarker(LatLong latLong) { + setLocation(new Location(latLong.latitude, latLong.longitude)); + final Circle circle = new Circle(); + circle.setRadius(5); + circle.setFill(getMarkerColor(false)); + getChildren().add(circle); + selectedProperty().addListener((booleanProperty, oldValue, newValue) -> { + circle.setFill(getMarkerColor(newValue)); + }); + } + + protected Paint getMarkerColor(boolean selected) { + return selected ? Color.BLUE : Color.GREEN; + } +} diff --git a/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/MapPathLine.java b/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/MapPathLine.java new file mode 100644 index 0000000..005b068 --- /dev/null +++ b/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/MapPathLine.java @@ -0,0 +1,24 @@ +package tdt4140.gr1800.app.ui; + +import fxmapcontrol.MapItem; +import fxmapcontrol.MapNode; +import javafx.scene.paint.Color; +import javafx.scene.shape.Line; +import tdt4140.gr1800.app.core.LatLong; + +public class MapPathLine extends MapItem<LatLong> { + + public MapPathLine(MapNode start, MapNode end) { + setLocation(start.getLocation()); + final Line line = new Line(); + line.setStrokeWidth(3); + line.setStroke(Color.GREEN); + end.translateXProperty().addListener((prop, oldValue, newValue) -> { + line.setEndX(end.getTranslateX() - getTranslateX()); + }); + end.translateYProperty().addListener((prop, oldValue, newValue) -> { + line.setEndY(end.getTranslateY() - getTranslateY()); + }); + getChildren().add(line); + } +} diff --git a/tdt4140-gr1800/app.ui/src/main/resources/tdt4140/gr1800/app/ui/FxApp.fxml b/tdt4140-gr1800/app.ui/src/main/resources/tdt4140/gr1800/app/ui/FxApp.fxml index bbefa50..96ba6f7 100644 --- a/tdt4140-gr1800/app.ui/src/main/resources/tdt4140/gr1800/app/ui/FxApp.fxml +++ b/tdt4140-gr1800/app.ui/src/main/resources/tdt4140/gr1800/app/ui/FxApp.fxml @@ -7,38 +7,38 @@ <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> -<?import com.lynden.gmapsfx.GoogleMapView?> +<?import fxmapcontrol.Map?> <?import javafx.scene.control.ComboBox?> <?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.HBox?> <?import javafx.scene.control.TextField?> <?import javafx.scene.control.Button?> -<BorderPane xmlns:fx="http://javafx.com/fxml" - fx:controller="tdt4140.gr1800.app.ui.FxAppController" - prefHeight="750" prefWidth="1000"> - <top> - <VBox> - <HBox> - <TextField fx:id="geolocationsFileText" promptText="Geo-locations file" onAction="#loadGeolocationsFile"/> - <Button text="Browse..." onAction="#handleBrowseGeolocationsFile"/> - </HBox> - <ComboBox fx:id="geoLocationsSelector"> - <BorderPane.margin> - <Insets left="5" right="5" top="5" bottom="5" /> - </BorderPane.margin> - </ComboBox> - </VBox> - </top> - <center> - <GoogleMapView fx:id="mapView"> - </GoogleMapView> - </center> - <bottom> - <Slider fx:id="zoomSlider" min="1" max="20" value="9"> - <BorderPane.margin> - <Insets left="5" right="5" top="5" bottom="5" /> - </BorderPane.margin> - </Slider> - </bottom> +<BorderPane xmlns:fx="http://javafx.com/fxml" fx:controller="tdt4140.gr1800.app.ui.FxAppController" + prefHeight="750" prefWidth="1000"> + <top> + <VBox> + <MenuBar> + <menus> + <fx:include fx:id="fileMenu" source="FileMenu.fxml"/> + </menus> + </MenuBar> + <ComboBox fx:id="geoLocationsSelector"> + <BorderPane.margin> + <Insets left="5" right="5" top="5" bottom="5" /> + </BorderPane.margin> + </ComboBox> + </VBox> + </top> + <center> + <Map fx:id="mapView" center="63,10" maxZoomLevel="15"> + </Map> + </center> + <bottom> + <Slider fx:id="zoomSlider" min="1" max="20" value="9"> + <BorderPane.margin> + <Insets left="5" right="5" top="5" bottom="5" /> + </BorderPane.margin> + </Slider> + </bottom> </BorderPane> diff --git a/tdt4140-gr1800/app.ui/src/test/java/tdt4140/gr1800/app/ui/FxAppTest.java b/tdt4140-gr1800/app.ui/src/test/java/tdt4140/gr1800/app/ui/FxAppTest.java index 18951d0..254f782 100644 --- a/tdt4140-gr1800/app.ui/src/test/java/tdt4140/gr1800/app/ui/FxAppTest.java +++ b/tdt4140-gr1800/app.ui/src/test/java/tdt4140/gr1800/app/ui/FxAppTest.java @@ -4,8 +4,7 @@ import org.junit.Assert; import org.junit.Test; import org.testfx.framework.junit.ApplicationTest; -import com.lynden.gmapsfx.GoogleMapView; - +import fxmapcontrol.MapBase; import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.Parent; @@ -28,6 +27,6 @@ public class FxAppTest extends ApplicationTest { @Test public void testMapExists() { Node map = lookup("#mapView").query(); - Assert.assertTrue(map instanceof GoogleMapView); + Assert.assertTrue(map instanceof MapBase); } } diff --git a/tdt4140-gr1800/pom.xml b/tdt4140-gr1800/pom.xml index b619bf6..af5f743 100644 --- a/tdt4140-gr1800/pom.xml +++ b/tdt4140-gr1800/pom.xml @@ -13,7 +13,7 @@ <modules> <module>app.core</module> + <module>FxMapControl</module> <module>app.ui</module> - </modules> </project> \ No newline at end of file diff --git a/tdt4140-gr18nn/pom.xml b/tdt4140-gr18nn/pom.xml index ef59106..6163fc4 100644 --- a/tdt4140-gr18nn/pom.xml +++ b/tdt4140-gr18nn/pom.xml @@ -14,6 +14,5 @@ <modules> <module>app.core</module> <module>app.ui</module> - </modules> </project> \ No newline at end of file -- GitLab