From 8fa7829a3c13cb6581d0974dcdf1f4db09d3bfbe Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Wed, 10 May 2017 20:52:46 +1200 Subject: [PATCH 01/14] Created a canvas map class to fetch map image from google - also added Bound class to encapsulate map boundary. - created TestMapView and its controller just for testing. #story[928] --- src/main/java/seng302/App.java | 109 +++++++++--------- src/main/java/seng302/models/map/Bound.java | 44 +++++++ .../java/seng302/models/map/CanvasMap.java | 49 ++++++++ .../seng302/models/map/TestMapController.java | 23 ++++ src/main/resources/views/TestMapView.fxml | 13 +++ 5 files changed, 184 insertions(+), 54 deletions(-) create mode 100644 src/main/java/seng302/models/map/Bound.java create mode 100644 src/main/java/seng302/models/map/CanvasMap.java create mode 100644 src/main/java/seng302/models/map/TestMapController.java create mode 100644 src/main/resources/views/TestMapView.fxml diff --git a/src/main/java/seng302/App.java b/src/main/java/seng302/App.java index 1a400afd..4c38a47a 100644 --- a/src/main/java/seng302/App.java +++ b/src/main/java/seng302/App.java @@ -13,67 +13,68 @@ public class App extends Application { @Override public void start(Stage primaryStage) throws Exception { - Parent root = FXMLLoader.load(getClass().getResource("/views/MainView.fxml")); +// Parent root = FXMLLoader.load(getClass().getResource("/views/MainView.fxml")); + Parent root = FXMLLoader.load(getClass().getResource("/views/TestMapView.fxml")); primaryStage.setTitle("RaceVision"); primaryStage.setScene(new Scene(root)); - primaryStage.setMaximized(true); +// primaryStage.setMaximized(true); primaryStage.show(); - primaryStage.setOnCloseRequest(e -> { - StreamParser.appClose(); - StreamReceiver.noMoreBytes(); - System.out.println("[CLIENT] Exiting program"); - System.exit(0); - }); +// primaryStage.setOnCloseRequest(e -> { +// StreamParser.appClose(); +// StreamReceiver.noMoreBytes(); +// System.out.println("[CLIENT] Exiting program"); +// System.exit(0); +// }); } - public static void main(String[] args) { - StreamReceiver sr = null; - - new ServerThread("Racevision Test Server"); - - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - if (args.length == 1 && args[0].equals("-standalone")){ - return; - } - - if (args.length == 3 && args[0].equals("-server")){ - - sr = new StreamReceiver(args[1], Integer.valueOf(args[2]), "RaceStream"); - - } else if(args.length == 2 && args[0].equals("-server")){ - switch (args[1]) { - case "internal": - sr = new StreamReceiver("localhost", 4949, "RaceStream"); - break; - case "staffserver": - sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941, "RaceStream"); - break; - case "official": - sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream"); - break; - } - } - //Change the StreamReceiver in this else block to change the default data source. - else{ - sr = new StreamReceiver("localhost", 4949, "RaceStream"); - } - - sr.start(); - StreamParser streamParser = new StreamParser("StreamParser"); - streamParser.start(); - - launch(args); - - - - } +// public static void main(String[] args) { +// StreamReceiver sr = null; +// +// new ServerThread("Racevision Test Server"); +// +// try { +// Thread.sleep(2000); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// +// if (args.length == 1 && args[0].equals("-standalone")){ +// return; +// } +// +// if (args.length == 3 && args[0].equals("-server")){ +// +// sr = new StreamReceiver(args[1], Integer.valueOf(args[2]), "RaceStream"); +// +// } else if(args.length == 2 && args[0].equals("-server")){ +// switch (args[1]) { +// case "internal": +// sr = new StreamReceiver("localhost", 4949, "RaceStream"); +// break; +// case "staffserver": +// sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941, "RaceStream"); +// break; +// case "official": +// sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream"); +// break; +// } +// } +// //Change the StreamReceiver in this else block to change the default data source. +// else{ +// sr = new StreamReceiver("localhost", 4949, "RaceStream"); +// } +// +// sr.start(); +// StreamParser streamParser = new StreamParser("StreamParser"); +// streamParser.start(); +// +// launch(args); +// +// +// +// } } diff --git a/src/main/java/seng302/models/map/Bound.java b/src/main/java/seng302/models/map/Bound.java new file mode 100644 index 00000000..aa700423 --- /dev/null +++ b/src/main/java/seng302/models/map/Bound.java @@ -0,0 +1,44 @@ +package seng302.models.map; + +/** + * The Bound class is to represent square territorial bounds on a map. It contains + * four extremity double values(N, E, S, W). N and S are represented as latitudes + * in radians. E and W are represented as longitudes in radians. + * + * Created by Haoming on 10/5/17 + */ +public class Bound { + + private double north, east, south, west; + + public Bound(double north, double east, double south, double west) { + this.north = north; + this.east = east; + this.south = south; + this.west = west; + } + + public double getCentreLat() { + return (north + south) / 2; + } + + public double getCentreLng() { + return (east + west) / 2; + } + + public double getNorth() { + return north; + } + + public double getEast() { + return east; + } + + public double getSouth() { + return south; + } + + public double getWest() { + return west; + } +} diff --git a/src/main/java/seng302/models/map/CanvasMap.java b/src/main/java/seng302/models/map/CanvasMap.java new file mode 100644 index 00000000..4a524e49 --- /dev/null +++ b/src/main/java/seng302/models/map/CanvasMap.java @@ -0,0 +1,49 @@ +package seng302.models.map; + +import javafx.scene.image.Image; + +import javax.imageio.ImageIO; +import javax.net.ssl.HttpsURLConnection; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URL; + +public class CanvasMap { + + private Bound bound; + private double width, height; // desired image size + private int zoom; + private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE"; + + public CanvasMap(Bound bound, double width, double height) { + this.bound = bound; + this.width = width; + this.height = height; + } + + public Image getMapImage() { + + try { + System.out.println(getRequest()); + URL url = new URL(getRequest()); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + + return new Image(connection.getInputStream()); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private String getRequest() { + zoom = 14; + StringBuilder sb = new StringBuilder(); + sb.append("https://maps.googleapis.com/maps/api/staticmap?"); + sb.append(String.format("center=%f,%f", bound.getCentreLat(), bound.getCentreLng())); + sb.append(String.format("&zoom=%d", zoom)); + sb.append(String.format("&size=%.0fx%.0f&scale=2", width / 2, height / 2)); + sb.append(String.format("&key=%s", KEY)); + return sb.toString(); + } +} diff --git a/src/main/java/seng302/models/map/TestMapController.java b/src/main/java/seng302/models/map/TestMapController.java new file mode 100644 index 00000000..720dd408 --- /dev/null +++ b/src/main/java/seng302/models/map/TestMapController.java @@ -0,0 +1,23 @@ +package seng302.models.map; + +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; + +import java.net.URL; +import java.util.ResourceBundle; + +public class TestMapController implements Initializable{ + + @FXML + private Canvas mapCanvas; + + @Override + public void initialize(URL location, ResourceBundle resources) { + GraphicsContext gc = mapCanvas.getGraphicsContext2D(); + Bound bound = new Bound(57.662943, 11.848501, 57.673945, 11.824966); + CanvasMap canvasMap = new CanvasMap(bound, 1280, 960); + gc.drawImage(canvasMap.getMapImage(), 0, 0, 1280, 960); + } +} diff --git a/src/main/resources/views/TestMapView.fxml b/src/main/resources/views/TestMapView.fxml new file mode 100644 index 00000000..84df98f3 --- /dev/null +++ b/src/main/resources/views/TestMapView.fxml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + From 3fd8b1b8555d163399217737995a40afdcb8f6ef Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Mon, 15 May 2017 12:24:36 +1200 Subject: [PATCH 02/14] Created Mercator projection to convert between Geo location and planar projection point. - MapGeo and MapPoint encapsulate geo location and planar projection point into classes. #story[928] --- .../models/map/{Bound.java => Boundary.java} | 6 +-- .../java/seng302/models/map/CanvasMap.java | 18 +++---- src/main/java/seng302/models/map/MapGeo.java | 27 ++++++++++ .../java/seng302/models/map/MapPoint.java | 27 ++++++++++ .../models/map/MercatorProjection.java | 52 +++++++++++++++++++ .../seng302/models/map/TestMapController.java | 2 +- 6 files changed, 119 insertions(+), 13 deletions(-) rename src/main/java/seng302/models/map/{Bound.java => Boundary.java} (78%) create mode 100644 src/main/java/seng302/models/map/MapGeo.java create mode 100644 src/main/java/seng302/models/map/MapPoint.java create mode 100644 src/main/java/seng302/models/map/MercatorProjection.java diff --git a/src/main/java/seng302/models/map/Bound.java b/src/main/java/seng302/models/map/Boundary.java similarity index 78% rename from src/main/java/seng302/models/map/Bound.java rename to src/main/java/seng302/models/map/Boundary.java index aa700423..f2d1302c 100644 --- a/src/main/java/seng302/models/map/Bound.java +++ b/src/main/java/seng302/models/map/Boundary.java @@ -1,17 +1,17 @@ package seng302.models.map; /** - * The Bound class is to represent square territorial bounds on a map. It contains + * The Boundary class represents a square territorial bound on a map. It contains * four extremity double values(N, E, S, W). N and S are represented as latitudes * in radians. E and W are represented as longitudes in radians. * * Created by Haoming on 10/5/17 */ -public class Bound { +public class Boundary { private double north, east, south, west; - public Bound(double north, double east, double south, double west) { + public Boundary(double north, double east, double south, double west) { this.north = north; this.east = east; this.south = south; diff --git a/src/main/java/seng302/models/map/CanvasMap.java b/src/main/java/seng302/models/map/CanvasMap.java index 4a524e49..a86be90b 100644 --- a/src/main/java/seng302/models/map/CanvasMap.java +++ b/src/main/java/seng302/models/map/CanvasMap.java @@ -2,28 +2,27 @@ package seng302.models.map; import javafx.scene.image.Image; -import javax.imageio.ImageIO; import javax.net.ssl.HttpsURLConnection; -import java.awt.image.BufferedImage; -import java.io.BufferedReader; -import java.io.InputStreamReader; import java.net.URL; +import java.lang.Math; + public class CanvasMap { - private Bound bound; + private Boundary bound; private double width, height; // desired image size private int zoom; + + private int MERCATOR_RANGE = 256; private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE"; - public CanvasMap(Bound bound, double width, double height) { + public CanvasMap(Boundary bound, double width, double height) { this.bound = bound; this.width = width; this.height = height; } public Image getMapImage() { - try { System.out.println(getRequest()); URL url = new URL(getRequest()); @@ -37,13 +36,14 @@ public class CanvasMap { } private String getRequest() { - zoom = 14; + zoom = 15; StringBuilder sb = new StringBuilder(); sb.append("https://maps.googleapis.com/maps/api/staticmap?"); sb.append(String.format("center=%f,%f", bound.getCentreLat(), bound.getCentreLng())); sb.append(String.format("&zoom=%d", zoom)); sb.append(String.format("&size=%.0fx%.0f&scale=2", width / 2, height / 2)); - sb.append(String.format("&key=%s", KEY)); + sb.append("&style=feature:all|element:labels|visibility:off"); // hide all labels on map +// sb.append(String.format("&key=%s", KEY)); return sb.toString(); } } diff --git a/src/main/java/seng302/models/map/MapGeo.java b/src/main/java/seng302/models/map/MapGeo.java new file mode 100644 index 00000000..96af1dd5 --- /dev/null +++ b/src/main/java/seng302/models/map/MapGeo.java @@ -0,0 +1,27 @@ +package seng302.models.map; + +class MapGeo { + + private double lat, lng; + + MapGeo(double lat, double lng) { + this.lat = lat; + this.lng = lng; + } + + double getLat() { + return lat; + } + + void setLat(double lat) { + this.lat = lat; + } + + double getLng() { + return lng; + } + + void setLng(double lng) { + this.lng = lng; + } +} diff --git a/src/main/java/seng302/models/map/MapPoint.java b/src/main/java/seng302/models/map/MapPoint.java new file mode 100644 index 00000000..aa0e55d0 --- /dev/null +++ b/src/main/java/seng302/models/map/MapPoint.java @@ -0,0 +1,27 @@ +package seng302.models.map; + +class MapPoint { + + private double x, y; + + MapPoint(double x, double y) { + this.x = x; + this.y = y; + } + + double getX() { + return x; + } + + void setX(double x) { + this.x = x; + } + + double getY() { + return y; + } + + void setY(double y) { + this.y = y; + } +} diff --git a/src/main/java/seng302/models/map/MercatorProjection.java b/src/main/java/seng302/models/map/MercatorProjection.java new file mode 100644 index 00000000..4a442123 --- /dev/null +++ b/src/main/java/seng302/models/map/MercatorProjection.java @@ -0,0 +1,52 @@ +package seng302.models.map; + +public class MercatorProjection { + + private double MERCATOR_RANGE = 256; + private double pixelsPerLngDegree, pixelsPerLngRadian; + + + public MercatorProjection() { + pixelsPerLngDegree = MERCATOR_RANGE / 360.0; + pixelsPerLngRadian = MERCATOR_RANGE / (2 * Math.PI); + } + + /** + * A help function keeps the value in bound between -0.9999 and 0.9999. + * @param value in bound value + * @return the value in bound + */ + private double bound(double value) { + return Math.min(Math.max(value, -0.9999), 0.9999); + } + + /** + * Projects a Geo Location (lat, lng) on a planar + * @param geo MapGeo (lat, lng) location to be projected + * @return the projection GeoPoint (x, y) on planar + */ + public MapPoint toMapPoint(MapGeo geo) { + MapPoint point = new MapPoint(0, 0); + MapPoint origin = new MapPoint(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0); + point.setX(origin.getX() + geo.getLng() * pixelsPerLngDegree); + +// NOTE(appleton): Truncating to 0.9999 effectively limits latitude to +// 89.189. This is about a third of a tile past the edge of the world tile. + double sinY = bound(Math.sin(Math.toRadians(geo.getLat()))); + point.setY(origin.getY() + 0.5 * Math.log((1 + sinY) / (1 - sinY)) * (-pixelsPerLngRadian)); + return point; + } + + /** + * Converts the planar projection (x, y) back to Geo Location (lat, lng) + * @param point MapPoint (x, y) to be converted back + * @return the original Geo location converted from the given projection point + */ + public MapGeo toMapGeo(MapPoint point) { + MapPoint origin = new MapPoint(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0); + double lng = (point.getX() - origin.getX()) / pixelsPerLngDegree; + double latRadians = (point.getY() - origin.getY()) / (-pixelsPerLngRadian); + double lat = Math.toDegrees(2 * Math.atan(Math.exp(latRadians)) - Math.PI / 2.0); + return new MapGeo(lat, lng); + } +} diff --git a/src/main/java/seng302/models/map/TestMapController.java b/src/main/java/seng302/models/map/TestMapController.java index 720dd408..6d656231 100644 --- a/src/main/java/seng302/models/map/TestMapController.java +++ b/src/main/java/seng302/models/map/TestMapController.java @@ -16,7 +16,7 @@ public class TestMapController implements Initializable{ @Override public void initialize(URL location, ResourceBundle resources) { GraphicsContext gc = mapCanvas.getGraphicsContext2D(); - Bound bound = new Bound(57.662943, 11.848501, 57.673945, 11.824966); + Boundary bound = new Boundary(57.662943, 11.848501, 57.673945, 11.824966); CanvasMap canvasMap = new CanvasMap(bound, 1280, 960); gc.drawImage(canvasMap.getMapImage(), 0, 0, 1280, 960); } From 4b1a4aae871a5bd44c19ef2853b7952d4e2e4f61 Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Mon, 15 May 2017 13:23:04 +1200 Subject: [PATCH 03/14] Added unit tests for Mercator projection class. - changed its methods to static - add some documentation for its methods #story[928] --- .../java/seng302/models/map/Boundary.java | 32 +++++++------- .../models/map/MercatorProjection.java | 17 +++----- .../models/map/MercatorProjectionTest.java | 42 +++++++++++++++++++ 3 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 src/test/java/seng302/models/map/MercatorProjectionTest.java diff --git a/src/main/java/seng302/models/map/Boundary.java b/src/main/java/seng302/models/map/Boundary.java index f2d1302c..d39a60f0 100644 --- a/src/main/java/seng302/models/map/Boundary.java +++ b/src/main/java/seng302/models/map/Boundary.java @@ -9,36 +9,36 @@ package seng302.models.map; */ public class Boundary { - private double north, east, south, west; + private double northLat, eastLng, southLat, westLng; - public Boundary(double north, double east, double south, double west) { - this.north = north; - this.east = east; - this.south = south; - this.west = west; + public Boundary(double northLat, double eastLng, double southLat, double westLng) { + this.northLat = northLat; + this.eastLng = eastLng; + this.southLat = southLat; + this.westLng = westLng; } public double getCentreLat() { - return (north + south) / 2; + return (northLat + southLat) / 2; } public double getCentreLng() { - return (east + west) / 2; + return (eastLng + westLng) / 2; } - public double getNorth() { - return north; + public double getNorthLat() { + return northLat; } - public double getEast() { - return east; + public double getEastLng() { + return eastLng; } - public double getSouth() { - return south; + public double getSouthLat() { + return southLat; } - public double getWest() { - return west; + public double getWestLng() { + return westLng; } } diff --git a/src/main/java/seng302/models/map/MercatorProjection.java b/src/main/java/seng302/models/map/MercatorProjection.java index 4a442123..915712ba 100644 --- a/src/main/java/seng302/models/map/MercatorProjection.java +++ b/src/main/java/seng302/models/map/MercatorProjection.java @@ -2,21 +2,16 @@ package seng302.models.map; public class MercatorProjection { - private double MERCATOR_RANGE = 256; - private double pixelsPerLngDegree, pixelsPerLngRadian; - - - public MercatorProjection() { - pixelsPerLngDegree = MERCATOR_RANGE / 360.0; - pixelsPerLngRadian = MERCATOR_RANGE / (2 * Math.PI); - } + private static final double MERCATOR_RANGE = 256; + private static final double pixelsPerLngDegree = MERCATOR_RANGE / 360.0; + private static final double pixelsPerLngRadian = MERCATOR_RANGE / (2 * Math.PI); /** * A help function keeps the value in bound between -0.9999 and 0.9999. * @param value in bound value * @return the value in bound */ - private double bound(double value) { + private static double bound(double value) { return Math.min(Math.max(value, -0.9999), 0.9999); } @@ -25,7 +20,7 @@ public class MercatorProjection { * @param geo MapGeo (lat, lng) location to be projected * @return the projection GeoPoint (x, y) on planar */ - public MapPoint toMapPoint(MapGeo geo) { + public static MapPoint toMapPoint(MapGeo geo) { MapPoint point = new MapPoint(0, 0); MapPoint origin = new MapPoint(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0); point.setX(origin.getX() + geo.getLng() * pixelsPerLngDegree); @@ -42,7 +37,7 @@ public class MercatorProjection { * @param point MapPoint (x, y) to be converted back * @return the original Geo location converted from the given projection point */ - public MapGeo toMapGeo(MapPoint point) { + public static MapGeo toMapGeo(MapPoint point) { MapPoint origin = new MapPoint(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0); double lng = (point.getX() - origin.getX()) / pixelsPerLngDegree; double latRadians = (point.getY() - origin.getY()) / (-pixelsPerLngRadian); diff --git a/src/test/java/seng302/models/map/MercatorProjectionTest.java b/src/test/java/seng302/models/map/MercatorProjectionTest.java new file mode 100644 index 00000000..a4350c18 --- /dev/null +++ b/src/test/java/seng302/models/map/MercatorProjectionTest.java @@ -0,0 +1,42 @@ +package seng302.models.map; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Unit test for Mercator Project class. + * Created by hyi25 on 15/05/17. + */ +public class MercatorProjectionTest { + @Test + public void toMapPoint() throws Exception { + MapGeo geo1 = new MapGeo(12.485394, 19.38947); + MapPoint actualPoint1 = MercatorProjection.toMapPoint(geo1); + MapPoint expectedPoint1 = new MapPoint(141.78806755555556, 119.0503853635612); + assertEquals(expectedPoint1.getX(), actualPoint1.getX(), 0.0001); + assertEquals(expectedPoint1.getY(), actualPoint1.getY(), 0.0001); + + MapGeo geo2 = new MapGeo(77.456432, -23.456462); + MapPoint actualPoint2 = MercatorProjection.toMapPoint(geo2); + MapPoint expectedPoint2 = new MapPoint(111.31984924444444, 38.03143323746788); + assertEquals(expectedPoint2.getX(), actualPoint2.getX(), 0.0001); + assertEquals(expectedPoint2.getY(), actualPoint2.getY(), 0.0001); + } + + @Test + public void toMapGeo() throws Exception { + MapPoint point1 = new MapPoint(123.1234, 25.4565); + MapGeo actualGeo1 = MercatorProjection.toMapGeo(point1); + MapGeo expectedGeo1 = new MapGeo(80.77043127275441, -6.857718749999995); + assertEquals(expectedGeo1.getLat(), actualGeo1.getLat(), 0.0001); + assertEquals(expectedGeo1.getLng(), actualGeo1.getLng(), 0.0001); + + MapPoint point2 = new MapPoint(1.235, 255.4565); + MapGeo actualGeo2 = MercatorProjection.toMapGeo(point2); + MapGeo expectedGeo2 = new MapGeo(-84.98475532898011, -178.26328125); + assertEquals(expectedGeo2.getLat(), actualGeo2.getLat(), 0.0001); + assertEquals(expectedGeo2.getLng(), actualGeo2.getLng(), 0.0001); + } + +} \ No newline at end of file From eda3d760774806de3b8bddb94e34337ee1674929 Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Mon, 15 May 2017 17:06:28 +1200 Subject: [PATCH 04/14] Added get map size (width and height) method in canvasMap with given boundary #story[928] --- .../java/seng302/models/map/CanvasMap.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/seng302/models/map/CanvasMap.java b/src/main/java/seng302/models/map/CanvasMap.java index a86be90b..104bb219 100644 --- a/src/main/java/seng302/models/map/CanvasMap.java +++ b/src/main/java/seng302/models/map/CanvasMap.java @@ -13,7 +13,6 @@ public class CanvasMap { private double width, height; // desired image size private int zoom; - private int MERCATOR_RANGE = 256; private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE"; public CanvasMap(Boundary bound, double width, double height) { @@ -46,4 +45,23 @@ public class CanvasMap { // sb.append(String.format("&key=%s", KEY)); return sb.toString(); } + + private MapSize getMapSize(int zoom, Boundary boundary) { + double scale = Math.pow(2, zoom); + MapGeo geoSW = new MapGeo(boundary.getSouthLat(), boundary.getWestLng()); + MapGeo geoNE = new MapGeo(boundary.getNorthLat(), boundary.getEastLng()); + MapPoint pointSW = MercatorProjection.toMapPoint(geoSW); + MapPoint pointNE = MercatorProjection.toMapPoint(geoNE); + return new MapSize(Math.abs(pointNE.getX() - pointSW.getX()), + Math.abs(pointNE.getY() - pointSW.getY())); + } + + class MapSize { + long width, height; + + MapSize(double width, double height) { + this.width = (long) width; + this.height = (long) height; + } + } } From 8dec458ba9f33cff2e0a4947c9285a30480d9c36 Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Mon, 15 May 2017 19:57:23 +1200 Subject: [PATCH 05/14] Added methods to calculate optimal map size given a geo boundary. - From zoom level 20 to 1, once find a size that contains the whole boundary, then the size will be used to retrieve map image from google #story[928] --- .../java/seng302/models/map/Boundary.java | 18 +++--- .../java/seng302/models/map/CanvasMap.java | 64 +++++++++++++++---- src/main/java/seng302/models/map/MapGeo.java | 4 ++ .../java/seng302/models/map/MapPoint.java | 4 ++ .../models/map/MercatorProjection.java | 5 ++ .../seng302/models/map/TestMapController.java | 4 +- 6 files changed, 75 insertions(+), 24 deletions(-) diff --git a/src/main/java/seng302/models/map/Boundary.java b/src/main/java/seng302/models/map/Boundary.java index d39a60f0..4396d95d 100644 --- a/src/main/java/seng302/models/map/Boundary.java +++ b/src/main/java/seng302/models/map/Boundary.java @@ -1,9 +1,9 @@ package seng302.models.map; /** - * The Boundary class represents a square territorial bound on a map. It contains - * four extremity double values(N, E, S, W). N and S are represented as latitudes - * in radians. E and W are represented as longitudes in radians. + * The Boundary class represents a rectangle territorial boundary on a map. It + * contains four extremity double values(N, E, S, W). N and S are represented as + * latitudes in radians. E and W are represented as longitudes in radians. * * Created by Haoming on 10/5/17 */ @@ -18,27 +18,27 @@ public class Boundary { this.westLng = westLng; } - public double getCentreLat() { + double getCentreLat() { return (northLat + southLat) / 2; } - public double getCentreLng() { + double getCentreLng() { return (eastLng + westLng) / 2; } - public double getNorthLat() { + double getNorthLat() { return northLat; } - public double getEastLng() { + double getEastLng() { return eastLng; } - public double getSouthLat() { + double getSouthLat() { return southLat; } - public double getWestLng() { + double getWestLng() { return westLng; } } diff --git a/src/main/java/seng302/models/map/CanvasMap.java b/src/main/java/seng302/models/map/CanvasMap.java index 104bb219..3302112c 100644 --- a/src/main/java/seng302/models/map/CanvasMap.java +++ b/src/main/java/seng302/models/map/CanvasMap.java @@ -7,18 +7,25 @@ import java.net.URL; import java.lang.Math; +/** + * CanvasMap retrieves a map image with given geo boundary from Google Map server. + * By passing a rectangle like geo boundary, it returns a map image with the + * highest resolution. However, due to free quote account usage limit, the maximum + * resolution is only 1280 * 1280. + * + * Created by Haoming on 15/5/2017 + */ public class CanvasMap { - private Boundary bound; - private double width, height; // desired image size + private Boundary boundary; + private long width, height; // desired image size private int zoom; private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE"; - public CanvasMap(Boundary bound, double width, double height) { - this.bound = bound; - this.width = width; - this.height = height; + public CanvasMap(Boundary boundary) { + this.boundary = boundary; + calculateOptimalMapSize(); } public Image getMapImage() { @@ -35,33 +42,64 @@ public class CanvasMap { } private String getRequest() { - zoom = 15; StringBuilder sb = new StringBuilder(); sb.append("https://maps.googleapis.com/maps/api/staticmap?"); - sb.append(String.format("center=%f,%f", bound.getCentreLat(), bound.getCentreLng())); + sb.append(String.format("center=%f,%f", boundary.getCentreLat(), boundary.getCentreLng())); sb.append(String.format("&zoom=%d", zoom)); - sb.append(String.format("&size=%.0fx%.0f&scale=2", width / 2, height / 2)); + sb.append(String.format("&size=%dx%d&scale=2", width, height)); sb.append("&style=feature:all|element:labels|visibility:off"); // hide all labels on map +// sb.append(String.format("&markers=%f,%f", boundary.getSouthLat(), boundary.getWestLng())); // sb.append(String.format("&key=%s", KEY)); return sb.toString(); } + private void calculateOptimalMapSize() { + for (int z = 20; z > 0; z--) { + MapSize mapSize = getMapSize(z, boundary); + zoom = z; + width = mapSize.width; + height = mapSize.height; + // if map size is valid, exit the loop as we have the highest resolution + if (mapSize.isValid()) break; + } + } + private MapSize getMapSize(int zoom, Boundary boundary) { double scale = Math.pow(2, zoom); MapGeo geoSW = new MapGeo(boundary.getSouthLat(), boundary.getWestLng()); MapGeo geoNE = new MapGeo(boundary.getNorthLat(), boundary.getEastLng()); MapPoint pointSW = MercatorProjection.toMapPoint(geoSW); MapPoint pointNE = MercatorProjection.toMapPoint(geoNE); - return new MapSize(Math.abs(pointNE.getX() - pointSW.getX()), - Math.abs(pointNE.getY() - pointSW.getY())); + return new MapSize(Math.abs(pointNE.getX() - pointSW.getX()) * scale, + Math.abs(pointNE.getY() - pointSW.getY()) * scale); } class MapSize { long width, height; MapSize(double width, double height) { - this.width = (long) width; - this.height = (long) height; + this.width = Math.round(width); + this.height = Math.round(height); + } + + /** + * Map size is valid when width and height are both less than 640 pixels + * @return true if both dimensions are less than 640px + */ + boolean isValid() { + return Math.max(width, height) <= 640; } } + + public long getWidth() { + return width; + } + + public long getHeight() { + return height; + } + + public int getZoom() { + return zoom; + } } diff --git a/src/main/java/seng302/models/map/MapGeo.java b/src/main/java/seng302/models/map/MapGeo.java index 96af1dd5..43d02565 100644 --- a/src/main/java/seng302/models/map/MapGeo.java +++ b/src/main/java/seng302/models/map/MapGeo.java @@ -1,5 +1,9 @@ package seng302.models.map; +/** + * A class represent Geo location (latitude, longitude). + * Created by Haoming on 15/5/2017 + */ class MapGeo { private double lat, lng; diff --git a/src/main/java/seng302/models/map/MapPoint.java b/src/main/java/seng302/models/map/MapPoint.java index aa0e55d0..41be919a 100644 --- a/src/main/java/seng302/models/map/MapPoint.java +++ b/src/main/java/seng302/models/map/MapPoint.java @@ -1,5 +1,9 @@ package seng302.models.map; +/** + * A class represent euclidean planar point (x, y) + * Created by Haoming on 15/5/2017 + */ class MapPoint { private double x, y; diff --git a/src/main/java/seng302/models/map/MercatorProjection.java b/src/main/java/seng302/models/map/MercatorProjection.java index 915712ba..b4bf647d 100644 --- a/src/main/java/seng302/models/map/MercatorProjection.java +++ b/src/main/java/seng302/models/map/MercatorProjection.java @@ -1,5 +1,10 @@ package seng302.models.map; +/** + * An utility class useful to convert between Geo locations and Mercator projection + * planar coordinates. + * Created by Haoming on 15/5/2017 + */ public class MercatorProjection { private static final double MERCATOR_RANGE = 256; diff --git a/src/main/java/seng302/models/map/TestMapController.java b/src/main/java/seng302/models/map/TestMapController.java index 6d656231..fc319bcc 100644 --- a/src/main/java/seng302/models/map/TestMapController.java +++ b/src/main/java/seng302/models/map/TestMapController.java @@ -17,7 +17,7 @@ public class TestMapController implements Initializable{ public void initialize(URL location, ResourceBundle resources) { GraphicsContext gc = mapCanvas.getGraphicsContext2D(); Boundary bound = new Boundary(57.662943, 11.848501, 57.673945, 11.824966); - CanvasMap canvasMap = new CanvasMap(bound, 1280, 960); - gc.drawImage(canvasMap.getMapImage(), 0, 0, 1280, 960); + CanvasMap canvasMap = new CanvasMap(bound); + gc.drawImage(canvasMap.getMapImage(), 0, 0, canvasMap.getWidth(), canvasMap.getHeight()); } } From 189ba93e642a28b5f5e3cbe70cc2ed4fa66474ed Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Wed, 10 May 2017 20:52:46 +1200 Subject: [PATCH 06/14] Created a canvas map class to fetch map image from google - also added Bound class to encapsulate map boundary. - created TestMapView and its controller just for testing. #story[928] --- src/main/java/seng302/models/map/Bound.java | 44 +++++++++++++++++ .../java/seng302/models/map/CanvasMap.java | 49 +++++++++++++++++++ .../seng302/models/map/TestMapController.java | 23 +++++++++ src/main/resources/views/TestMapView.fxml | 13 +++++ 4 files changed, 129 insertions(+) create mode 100644 src/main/java/seng302/models/map/Bound.java create mode 100644 src/main/java/seng302/models/map/CanvasMap.java create mode 100644 src/main/java/seng302/models/map/TestMapController.java create mode 100644 src/main/resources/views/TestMapView.fxml diff --git a/src/main/java/seng302/models/map/Bound.java b/src/main/java/seng302/models/map/Bound.java new file mode 100644 index 00000000..aa700423 --- /dev/null +++ b/src/main/java/seng302/models/map/Bound.java @@ -0,0 +1,44 @@ +package seng302.models.map; + +/** + * The Bound class is to represent square territorial bounds on a map. It contains + * four extremity double values(N, E, S, W). N and S are represented as latitudes + * in radians. E and W are represented as longitudes in radians. + * + * Created by Haoming on 10/5/17 + */ +public class Bound { + + private double north, east, south, west; + + public Bound(double north, double east, double south, double west) { + this.north = north; + this.east = east; + this.south = south; + this.west = west; + } + + public double getCentreLat() { + return (north + south) / 2; + } + + public double getCentreLng() { + return (east + west) / 2; + } + + public double getNorth() { + return north; + } + + public double getEast() { + return east; + } + + public double getSouth() { + return south; + } + + public double getWest() { + return west; + } +} diff --git a/src/main/java/seng302/models/map/CanvasMap.java b/src/main/java/seng302/models/map/CanvasMap.java new file mode 100644 index 00000000..4a524e49 --- /dev/null +++ b/src/main/java/seng302/models/map/CanvasMap.java @@ -0,0 +1,49 @@ +package seng302.models.map; + +import javafx.scene.image.Image; + +import javax.imageio.ImageIO; +import javax.net.ssl.HttpsURLConnection; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URL; + +public class CanvasMap { + + private Bound bound; + private double width, height; // desired image size + private int zoom; + private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE"; + + public CanvasMap(Bound bound, double width, double height) { + this.bound = bound; + this.width = width; + this.height = height; + } + + public Image getMapImage() { + + try { + System.out.println(getRequest()); + URL url = new URL(getRequest()); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + + return new Image(connection.getInputStream()); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private String getRequest() { + zoom = 14; + StringBuilder sb = new StringBuilder(); + sb.append("https://maps.googleapis.com/maps/api/staticmap?"); + sb.append(String.format("center=%f,%f", bound.getCentreLat(), bound.getCentreLng())); + sb.append(String.format("&zoom=%d", zoom)); + sb.append(String.format("&size=%.0fx%.0f&scale=2", width / 2, height / 2)); + sb.append(String.format("&key=%s", KEY)); + return sb.toString(); + } +} diff --git a/src/main/java/seng302/models/map/TestMapController.java b/src/main/java/seng302/models/map/TestMapController.java new file mode 100644 index 00000000..720dd408 --- /dev/null +++ b/src/main/java/seng302/models/map/TestMapController.java @@ -0,0 +1,23 @@ +package seng302.models.map; + +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; + +import java.net.URL; +import java.util.ResourceBundle; + +public class TestMapController implements Initializable{ + + @FXML + private Canvas mapCanvas; + + @Override + public void initialize(URL location, ResourceBundle resources) { + GraphicsContext gc = mapCanvas.getGraphicsContext2D(); + Bound bound = new Bound(57.662943, 11.848501, 57.673945, 11.824966); + CanvasMap canvasMap = new CanvasMap(bound, 1280, 960); + gc.drawImage(canvasMap.getMapImage(), 0, 0, 1280, 960); + } +} diff --git a/src/main/resources/views/TestMapView.fxml b/src/main/resources/views/TestMapView.fxml new file mode 100644 index 00000000..84df98f3 --- /dev/null +++ b/src/main/resources/views/TestMapView.fxml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + From 5cc865f0af00e9ddafdeaf2572f397bfc7fb0b91 Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Mon, 15 May 2017 12:24:36 +1200 Subject: [PATCH 07/14] Created Mercator projection to convert between Geo location and planar projection point. - MapGeo and MapPoint encapsulate geo location and planar projection point into classes. #story[928] --- .../models/map/{Bound.java => Boundary.java} | 6 +-- .../java/seng302/models/map/CanvasMap.java | 18 +++---- src/main/java/seng302/models/map/MapGeo.java | 27 ++++++++++ .../java/seng302/models/map/MapPoint.java | 27 ++++++++++ .../models/map/MercatorProjection.java | 52 +++++++++++++++++++ .../seng302/models/map/TestMapController.java | 2 +- 6 files changed, 119 insertions(+), 13 deletions(-) rename src/main/java/seng302/models/map/{Bound.java => Boundary.java} (78%) create mode 100644 src/main/java/seng302/models/map/MapGeo.java create mode 100644 src/main/java/seng302/models/map/MapPoint.java create mode 100644 src/main/java/seng302/models/map/MercatorProjection.java diff --git a/src/main/java/seng302/models/map/Bound.java b/src/main/java/seng302/models/map/Boundary.java similarity index 78% rename from src/main/java/seng302/models/map/Bound.java rename to src/main/java/seng302/models/map/Boundary.java index aa700423..f2d1302c 100644 --- a/src/main/java/seng302/models/map/Bound.java +++ b/src/main/java/seng302/models/map/Boundary.java @@ -1,17 +1,17 @@ package seng302.models.map; /** - * The Bound class is to represent square territorial bounds on a map. It contains + * The Boundary class represents a square territorial bound on a map. It contains * four extremity double values(N, E, S, W). N and S are represented as latitudes * in radians. E and W are represented as longitudes in radians. * * Created by Haoming on 10/5/17 */ -public class Bound { +public class Boundary { private double north, east, south, west; - public Bound(double north, double east, double south, double west) { + public Boundary(double north, double east, double south, double west) { this.north = north; this.east = east; this.south = south; diff --git a/src/main/java/seng302/models/map/CanvasMap.java b/src/main/java/seng302/models/map/CanvasMap.java index 4a524e49..a86be90b 100644 --- a/src/main/java/seng302/models/map/CanvasMap.java +++ b/src/main/java/seng302/models/map/CanvasMap.java @@ -2,28 +2,27 @@ package seng302.models.map; import javafx.scene.image.Image; -import javax.imageio.ImageIO; import javax.net.ssl.HttpsURLConnection; -import java.awt.image.BufferedImage; -import java.io.BufferedReader; -import java.io.InputStreamReader; import java.net.URL; +import java.lang.Math; + public class CanvasMap { - private Bound bound; + private Boundary bound; private double width, height; // desired image size private int zoom; + + private int MERCATOR_RANGE = 256; private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE"; - public CanvasMap(Bound bound, double width, double height) { + public CanvasMap(Boundary bound, double width, double height) { this.bound = bound; this.width = width; this.height = height; } public Image getMapImage() { - try { System.out.println(getRequest()); URL url = new URL(getRequest()); @@ -37,13 +36,14 @@ public class CanvasMap { } private String getRequest() { - zoom = 14; + zoom = 15; StringBuilder sb = new StringBuilder(); sb.append("https://maps.googleapis.com/maps/api/staticmap?"); sb.append(String.format("center=%f,%f", bound.getCentreLat(), bound.getCentreLng())); sb.append(String.format("&zoom=%d", zoom)); sb.append(String.format("&size=%.0fx%.0f&scale=2", width / 2, height / 2)); - sb.append(String.format("&key=%s", KEY)); + sb.append("&style=feature:all|element:labels|visibility:off"); // hide all labels on map +// sb.append(String.format("&key=%s", KEY)); return sb.toString(); } } diff --git a/src/main/java/seng302/models/map/MapGeo.java b/src/main/java/seng302/models/map/MapGeo.java new file mode 100644 index 00000000..96af1dd5 --- /dev/null +++ b/src/main/java/seng302/models/map/MapGeo.java @@ -0,0 +1,27 @@ +package seng302.models.map; + +class MapGeo { + + private double lat, lng; + + MapGeo(double lat, double lng) { + this.lat = lat; + this.lng = lng; + } + + double getLat() { + return lat; + } + + void setLat(double lat) { + this.lat = lat; + } + + double getLng() { + return lng; + } + + void setLng(double lng) { + this.lng = lng; + } +} diff --git a/src/main/java/seng302/models/map/MapPoint.java b/src/main/java/seng302/models/map/MapPoint.java new file mode 100644 index 00000000..aa0e55d0 --- /dev/null +++ b/src/main/java/seng302/models/map/MapPoint.java @@ -0,0 +1,27 @@ +package seng302.models.map; + +class MapPoint { + + private double x, y; + + MapPoint(double x, double y) { + this.x = x; + this.y = y; + } + + double getX() { + return x; + } + + void setX(double x) { + this.x = x; + } + + double getY() { + return y; + } + + void setY(double y) { + this.y = y; + } +} diff --git a/src/main/java/seng302/models/map/MercatorProjection.java b/src/main/java/seng302/models/map/MercatorProjection.java new file mode 100644 index 00000000..4a442123 --- /dev/null +++ b/src/main/java/seng302/models/map/MercatorProjection.java @@ -0,0 +1,52 @@ +package seng302.models.map; + +public class MercatorProjection { + + private double MERCATOR_RANGE = 256; + private double pixelsPerLngDegree, pixelsPerLngRadian; + + + public MercatorProjection() { + pixelsPerLngDegree = MERCATOR_RANGE / 360.0; + pixelsPerLngRadian = MERCATOR_RANGE / (2 * Math.PI); + } + + /** + * A help function keeps the value in bound between -0.9999 and 0.9999. + * @param value in bound value + * @return the value in bound + */ + private double bound(double value) { + return Math.min(Math.max(value, -0.9999), 0.9999); + } + + /** + * Projects a Geo Location (lat, lng) on a planar + * @param geo MapGeo (lat, lng) location to be projected + * @return the projection GeoPoint (x, y) on planar + */ + public MapPoint toMapPoint(MapGeo geo) { + MapPoint point = new MapPoint(0, 0); + MapPoint origin = new MapPoint(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0); + point.setX(origin.getX() + geo.getLng() * pixelsPerLngDegree); + +// NOTE(appleton): Truncating to 0.9999 effectively limits latitude to +// 89.189. This is about a third of a tile past the edge of the world tile. + double sinY = bound(Math.sin(Math.toRadians(geo.getLat()))); + point.setY(origin.getY() + 0.5 * Math.log((1 + sinY) / (1 - sinY)) * (-pixelsPerLngRadian)); + return point; + } + + /** + * Converts the planar projection (x, y) back to Geo Location (lat, lng) + * @param point MapPoint (x, y) to be converted back + * @return the original Geo location converted from the given projection point + */ + public MapGeo toMapGeo(MapPoint point) { + MapPoint origin = new MapPoint(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0); + double lng = (point.getX() - origin.getX()) / pixelsPerLngDegree; + double latRadians = (point.getY() - origin.getY()) / (-pixelsPerLngRadian); + double lat = Math.toDegrees(2 * Math.atan(Math.exp(latRadians)) - Math.PI / 2.0); + return new MapGeo(lat, lng); + } +} diff --git a/src/main/java/seng302/models/map/TestMapController.java b/src/main/java/seng302/models/map/TestMapController.java index 720dd408..6d656231 100644 --- a/src/main/java/seng302/models/map/TestMapController.java +++ b/src/main/java/seng302/models/map/TestMapController.java @@ -16,7 +16,7 @@ public class TestMapController implements Initializable{ @Override public void initialize(URL location, ResourceBundle resources) { GraphicsContext gc = mapCanvas.getGraphicsContext2D(); - Bound bound = new Bound(57.662943, 11.848501, 57.673945, 11.824966); + Boundary bound = new Boundary(57.662943, 11.848501, 57.673945, 11.824966); CanvasMap canvasMap = new CanvasMap(bound, 1280, 960); gc.drawImage(canvasMap.getMapImage(), 0, 0, 1280, 960); } From 8a2f0a9f45b9b4a99ac1e5bb7b1c4570e4847503 Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Mon, 15 May 2017 13:23:04 +1200 Subject: [PATCH 08/14] Added unit tests for Mercator projection class. - changed its methods to static - add some documentation for its methods #story[928] --- .../java/seng302/models/map/Boundary.java | 32 +++++++------- .../models/map/MercatorProjection.java | 17 +++----- .../models/map/MercatorProjectionTest.java | 42 +++++++++++++++++++ 3 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 src/test/java/seng302/models/map/MercatorProjectionTest.java diff --git a/src/main/java/seng302/models/map/Boundary.java b/src/main/java/seng302/models/map/Boundary.java index f2d1302c..d39a60f0 100644 --- a/src/main/java/seng302/models/map/Boundary.java +++ b/src/main/java/seng302/models/map/Boundary.java @@ -9,36 +9,36 @@ package seng302.models.map; */ public class Boundary { - private double north, east, south, west; + private double northLat, eastLng, southLat, westLng; - public Boundary(double north, double east, double south, double west) { - this.north = north; - this.east = east; - this.south = south; - this.west = west; + public Boundary(double northLat, double eastLng, double southLat, double westLng) { + this.northLat = northLat; + this.eastLng = eastLng; + this.southLat = southLat; + this.westLng = westLng; } public double getCentreLat() { - return (north + south) / 2; + return (northLat + southLat) / 2; } public double getCentreLng() { - return (east + west) / 2; + return (eastLng + westLng) / 2; } - public double getNorth() { - return north; + public double getNorthLat() { + return northLat; } - public double getEast() { - return east; + public double getEastLng() { + return eastLng; } - public double getSouth() { - return south; + public double getSouthLat() { + return southLat; } - public double getWest() { - return west; + public double getWestLng() { + return westLng; } } diff --git a/src/main/java/seng302/models/map/MercatorProjection.java b/src/main/java/seng302/models/map/MercatorProjection.java index 4a442123..915712ba 100644 --- a/src/main/java/seng302/models/map/MercatorProjection.java +++ b/src/main/java/seng302/models/map/MercatorProjection.java @@ -2,21 +2,16 @@ package seng302.models.map; public class MercatorProjection { - private double MERCATOR_RANGE = 256; - private double pixelsPerLngDegree, pixelsPerLngRadian; - - - public MercatorProjection() { - pixelsPerLngDegree = MERCATOR_RANGE / 360.0; - pixelsPerLngRadian = MERCATOR_RANGE / (2 * Math.PI); - } + private static final double MERCATOR_RANGE = 256; + private static final double pixelsPerLngDegree = MERCATOR_RANGE / 360.0; + private static final double pixelsPerLngRadian = MERCATOR_RANGE / (2 * Math.PI); /** * A help function keeps the value in bound between -0.9999 and 0.9999. * @param value in bound value * @return the value in bound */ - private double bound(double value) { + private static double bound(double value) { return Math.min(Math.max(value, -0.9999), 0.9999); } @@ -25,7 +20,7 @@ public class MercatorProjection { * @param geo MapGeo (lat, lng) location to be projected * @return the projection GeoPoint (x, y) on planar */ - public MapPoint toMapPoint(MapGeo geo) { + public static MapPoint toMapPoint(MapGeo geo) { MapPoint point = new MapPoint(0, 0); MapPoint origin = new MapPoint(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0); point.setX(origin.getX() + geo.getLng() * pixelsPerLngDegree); @@ -42,7 +37,7 @@ public class MercatorProjection { * @param point MapPoint (x, y) to be converted back * @return the original Geo location converted from the given projection point */ - public MapGeo toMapGeo(MapPoint point) { + public static MapGeo toMapGeo(MapPoint point) { MapPoint origin = new MapPoint(MERCATOR_RANGE / 2.0, MERCATOR_RANGE / 2.0); double lng = (point.getX() - origin.getX()) / pixelsPerLngDegree; double latRadians = (point.getY() - origin.getY()) / (-pixelsPerLngRadian); diff --git a/src/test/java/seng302/models/map/MercatorProjectionTest.java b/src/test/java/seng302/models/map/MercatorProjectionTest.java new file mode 100644 index 00000000..a4350c18 --- /dev/null +++ b/src/test/java/seng302/models/map/MercatorProjectionTest.java @@ -0,0 +1,42 @@ +package seng302.models.map; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Unit test for Mercator Project class. + * Created by hyi25 on 15/05/17. + */ +public class MercatorProjectionTest { + @Test + public void toMapPoint() throws Exception { + MapGeo geo1 = new MapGeo(12.485394, 19.38947); + MapPoint actualPoint1 = MercatorProjection.toMapPoint(geo1); + MapPoint expectedPoint1 = new MapPoint(141.78806755555556, 119.0503853635612); + assertEquals(expectedPoint1.getX(), actualPoint1.getX(), 0.0001); + assertEquals(expectedPoint1.getY(), actualPoint1.getY(), 0.0001); + + MapGeo geo2 = new MapGeo(77.456432, -23.456462); + MapPoint actualPoint2 = MercatorProjection.toMapPoint(geo2); + MapPoint expectedPoint2 = new MapPoint(111.31984924444444, 38.03143323746788); + assertEquals(expectedPoint2.getX(), actualPoint2.getX(), 0.0001); + assertEquals(expectedPoint2.getY(), actualPoint2.getY(), 0.0001); + } + + @Test + public void toMapGeo() throws Exception { + MapPoint point1 = new MapPoint(123.1234, 25.4565); + MapGeo actualGeo1 = MercatorProjection.toMapGeo(point1); + MapGeo expectedGeo1 = new MapGeo(80.77043127275441, -6.857718749999995); + assertEquals(expectedGeo1.getLat(), actualGeo1.getLat(), 0.0001); + assertEquals(expectedGeo1.getLng(), actualGeo1.getLng(), 0.0001); + + MapPoint point2 = new MapPoint(1.235, 255.4565); + MapGeo actualGeo2 = MercatorProjection.toMapGeo(point2); + MapGeo expectedGeo2 = new MapGeo(-84.98475532898011, -178.26328125); + assertEquals(expectedGeo2.getLat(), actualGeo2.getLat(), 0.0001); + assertEquals(expectedGeo2.getLng(), actualGeo2.getLng(), 0.0001); + } + +} \ No newline at end of file From 4fe4ac1079cbfd31297fd46d1ffb2282d6cd5bc1 Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Mon, 15 May 2017 17:06:28 +1200 Subject: [PATCH 09/14] Added get map size (width and height) method in canvasMap with given boundary #story[928] --- .../java/seng302/models/map/CanvasMap.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/seng302/models/map/CanvasMap.java b/src/main/java/seng302/models/map/CanvasMap.java index a86be90b..104bb219 100644 --- a/src/main/java/seng302/models/map/CanvasMap.java +++ b/src/main/java/seng302/models/map/CanvasMap.java @@ -13,7 +13,6 @@ public class CanvasMap { private double width, height; // desired image size private int zoom; - private int MERCATOR_RANGE = 256; private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE"; public CanvasMap(Boundary bound, double width, double height) { @@ -46,4 +45,23 @@ public class CanvasMap { // sb.append(String.format("&key=%s", KEY)); return sb.toString(); } + + private MapSize getMapSize(int zoom, Boundary boundary) { + double scale = Math.pow(2, zoom); + MapGeo geoSW = new MapGeo(boundary.getSouthLat(), boundary.getWestLng()); + MapGeo geoNE = new MapGeo(boundary.getNorthLat(), boundary.getEastLng()); + MapPoint pointSW = MercatorProjection.toMapPoint(geoSW); + MapPoint pointNE = MercatorProjection.toMapPoint(geoNE); + return new MapSize(Math.abs(pointNE.getX() - pointSW.getX()), + Math.abs(pointNE.getY() - pointSW.getY())); + } + + class MapSize { + long width, height; + + MapSize(double width, double height) { + this.width = (long) width; + this.height = (long) height; + } + } } From 8f93956ff1dd07ddf87467ad040b2619ad22e7d6 Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Mon, 15 May 2017 19:57:23 +1200 Subject: [PATCH 10/14] Added methods to calculate optimal map size given a geo boundary. - From zoom level 20 to 1, once find a size that contains the whole boundary, then the size will be used to retrieve map image from google #story[928] --- .../java/seng302/models/map/Boundary.java | 18 +++--- .../java/seng302/models/map/CanvasMap.java | 64 +++++++++++++++---- src/main/java/seng302/models/map/MapGeo.java | 4 ++ .../java/seng302/models/map/MapPoint.java | 4 ++ .../models/map/MercatorProjection.java | 5 ++ .../seng302/models/map/TestMapController.java | 4 +- 6 files changed, 75 insertions(+), 24 deletions(-) diff --git a/src/main/java/seng302/models/map/Boundary.java b/src/main/java/seng302/models/map/Boundary.java index d39a60f0..4396d95d 100644 --- a/src/main/java/seng302/models/map/Boundary.java +++ b/src/main/java/seng302/models/map/Boundary.java @@ -1,9 +1,9 @@ package seng302.models.map; /** - * The Boundary class represents a square territorial bound on a map. It contains - * four extremity double values(N, E, S, W). N and S are represented as latitudes - * in radians. E and W are represented as longitudes in radians. + * The Boundary class represents a rectangle territorial boundary on a map. It + * contains four extremity double values(N, E, S, W). N and S are represented as + * latitudes in radians. E and W are represented as longitudes in radians. * * Created by Haoming on 10/5/17 */ @@ -18,27 +18,27 @@ public class Boundary { this.westLng = westLng; } - public double getCentreLat() { + double getCentreLat() { return (northLat + southLat) / 2; } - public double getCentreLng() { + double getCentreLng() { return (eastLng + westLng) / 2; } - public double getNorthLat() { + double getNorthLat() { return northLat; } - public double getEastLng() { + double getEastLng() { return eastLng; } - public double getSouthLat() { + double getSouthLat() { return southLat; } - public double getWestLng() { + double getWestLng() { return westLng; } } diff --git a/src/main/java/seng302/models/map/CanvasMap.java b/src/main/java/seng302/models/map/CanvasMap.java index 104bb219..3302112c 100644 --- a/src/main/java/seng302/models/map/CanvasMap.java +++ b/src/main/java/seng302/models/map/CanvasMap.java @@ -7,18 +7,25 @@ import java.net.URL; import java.lang.Math; +/** + * CanvasMap retrieves a map image with given geo boundary from Google Map server. + * By passing a rectangle like geo boundary, it returns a map image with the + * highest resolution. However, due to free quote account usage limit, the maximum + * resolution is only 1280 * 1280. + * + * Created by Haoming on 15/5/2017 + */ public class CanvasMap { - private Boundary bound; - private double width, height; // desired image size + private Boundary boundary; + private long width, height; // desired image size private int zoom; private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE"; - public CanvasMap(Boundary bound, double width, double height) { - this.bound = bound; - this.width = width; - this.height = height; + public CanvasMap(Boundary boundary) { + this.boundary = boundary; + calculateOptimalMapSize(); } public Image getMapImage() { @@ -35,33 +42,64 @@ public class CanvasMap { } private String getRequest() { - zoom = 15; StringBuilder sb = new StringBuilder(); sb.append("https://maps.googleapis.com/maps/api/staticmap?"); - sb.append(String.format("center=%f,%f", bound.getCentreLat(), bound.getCentreLng())); + sb.append(String.format("center=%f,%f", boundary.getCentreLat(), boundary.getCentreLng())); sb.append(String.format("&zoom=%d", zoom)); - sb.append(String.format("&size=%.0fx%.0f&scale=2", width / 2, height / 2)); + sb.append(String.format("&size=%dx%d&scale=2", width, height)); sb.append("&style=feature:all|element:labels|visibility:off"); // hide all labels on map +// sb.append(String.format("&markers=%f,%f", boundary.getSouthLat(), boundary.getWestLng())); // sb.append(String.format("&key=%s", KEY)); return sb.toString(); } + private void calculateOptimalMapSize() { + for (int z = 20; z > 0; z--) { + MapSize mapSize = getMapSize(z, boundary); + zoom = z; + width = mapSize.width; + height = mapSize.height; + // if map size is valid, exit the loop as we have the highest resolution + if (mapSize.isValid()) break; + } + } + private MapSize getMapSize(int zoom, Boundary boundary) { double scale = Math.pow(2, zoom); MapGeo geoSW = new MapGeo(boundary.getSouthLat(), boundary.getWestLng()); MapGeo geoNE = new MapGeo(boundary.getNorthLat(), boundary.getEastLng()); MapPoint pointSW = MercatorProjection.toMapPoint(geoSW); MapPoint pointNE = MercatorProjection.toMapPoint(geoNE); - return new MapSize(Math.abs(pointNE.getX() - pointSW.getX()), - Math.abs(pointNE.getY() - pointSW.getY())); + return new MapSize(Math.abs(pointNE.getX() - pointSW.getX()) * scale, + Math.abs(pointNE.getY() - pointSW.getY()) * scale); } class MapSize { long width, height; MapSize(double width, double height) { - this.width = (long) width; - this.height = (long) height; + this.width = Math.round(width); + this.height = Math.round(height); + } + + /** + * Map size is valid when width and height are both less than 640 pixels + * @return true if both dimensions are less than 640px + */ + boolean isValid() { + return Math.max(width, height) <= 640; } } + + public long getWidth() { + return width; + } + + public long getHeight() { + return height; + } + + public int getZoom() { + return zoom; + } } diff --git a/src/main/java/seng302/models/map/MapGeo.java b/src/main/java/seng302/models/map/MapGeo.java index 96af1dd5..43d02565 100644 --- a/src/main/java/seng302/models/map/MapGeo.java +++ b/src/main/java/seng302/models/map/MapGeo.java @@ -1,5 +1,9 @@ package seng302.models.map; +/** + * A class represent Geo location (latitude, longitude). + * Created by Haoming on 15/5/2017 + */ class MapGeo { private double lat, lng; diff --git a/src/main/java/seng302/models/map/MapPoint.java b/src/main/java/seng302/models/map/MapPoint.java index aa0e55d0..41be919a 100644 --- a/src/main/java/seng302/models/map/MapPoint.java +++ b/src/main/java/seng302/models/map/MapPoint.java @@ -1,5 +1,9 @@ package seng302.models.map; +/** + * A class represent euclidean planar point (x, y) + * Created by Haoming on 15/5/2017 + */ class MapPoint { private double x, y; diff --git a/src/main/java/seng302/models/map/MercatorProjection.java b/src/main/java/seng302/models/map/MercatorProjection.java index 915712ba..b4bf647d 100644 --- a/src/main/java/seng302/models/map/MercatorProjection.java +++ b/src/main/java/seng302/models/map/MercatorProjection.java @@ -1,5 +1,10 @@ package seng302.models.map; +/** + * An utility class useful to convert between Geo locations and Mercator projection + * planar coordinates. + * Created by Haoming on 15/5/2017 + */ public class MercatorProjection { private static final double MERCATOR_RANGE = 256; diff --git a/src/main/java/seng302/models/map/TestMapController.java b/src/main/java/seng302/models/map/TestMapController.java index 6d656231..fc319bcc 100644 --- a/src/main/java/seng302/models/map/TestMapController.java +++ b/src/main/java/seng302/models/map/TestMapController.java @@ -17,7 +17,7 @@ public class TestMapController implements Initializable{ public void initialize(URL location, ResourceBundle resources) { GraphicsContext gc = mapCanvas.getGraphicsContext2D(); Boundary bound = new Boundary(57.662943, 11.848501, 57.673945, 11.824966); - CanvasMap canvasMap = new CanvasMap(bound, 1280, 960); - gc.drawImage(canvasMap.getMapImage(), 0, 0, 1280, 960); + CanvasMap canvasMap = new CanvasMap(bound); + gc.drawImage(canvasMap.getMapImage(), 0, 0, canvasMap.getWidth(), canvasMap.getHeight()); } } From f85d3bf5fe921bccd2b1bf7aa1e3bb9fcc56366c Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Wed, 24 May 2017 13:52:49 +1200 Subject: [PATCH 11/14] Plugged Canvas map to canvas view controller to display map - rebase on the latest develop status - optimised the scaling factor the map to fit the canvas view - a new image containing map image is displayed under race canvas #story[928] --- src/main/java/seng302/App.java | 7 +- .../seng302/controllers/CanvasController.java | 89 ++++++++++++++++--- 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/main/java/seng302/App.java b/src/main/java/seng302/App.java index ac264db6..20c01788 100644 --- a/src/main/java/seng302/App.java +++ b/src/main/java/seng302/App.java @@ -15,8 +15,10 @@ public class App extends Application { public void start(Stage primaryStage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("/views/MainView.fxml")); primaryStage.setTitle("RaceVision"); - primaryStage.setScene(new Scene(root)); - primaryStage.setMaximized(true); + primaryStage.setScene(new Scene(root, 1530, 960)); + primaryStage.setMaxWidth(1530); + primaryStage.setMaxHeight(960); +// primaryStage.setMaximized(true); primaryStage.show(); primaryStage.setOnCloseRequest(e -> { @@ -64,6 +66,7 @@ public class App extends Application { else{ // sr = new StreamReceiver("localhost", 4949, "RaceStream"); sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream"); +// sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941, "RaceStream"); } sr.start(); diff --git a/src/main/java/seng302/controllers/CanvasController.java b/src/main/java/seng302/controllers/CanvasController.java index 0bdeae25..a1a83e8b 100644 --- a/src/main/java/seng302/controllers/CanvasController.java +++ b/src/main/java/seng302/controllers/CanvasController.java @@ -12,12 +12,15 @@ import javafx.geometry.Point2D; import javafx.scene.Group; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; +import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.paint.Color; import javafx.scene.text.Font; import seng302.models.BoatGroup; import seng302.models.Colors; import seng302.models.Yacht; +import seng302.models.map.Boundary; +import seng302.models.map.CanvasMap; import seng302.models.mark.GateMark; import seng302.models.mark.Mark; import seng302.models.mark.MarkGroup; @@ -28,6 +31,8 @@ import seng302.models.stream.XMLParser; import seng302.models.stream.XMLParser.RaceXMLObject.Limit; import seng302.models.stream.XMLParser.RaceXMLObject.Participant; import seng302.models.stream.packets.BoatPositionPacket; +import seng302.server.simulator.GeoUtility; +import seng302.server.simulator.mark.Position; /** * Created by ptg19 on 15/03/17. @@ -42,15 +47,18 @@ public class CanvasController { private ResizableCanvas canvas; private Group group; private GraphicsContext gc; + private ImageView mapImage; private final int MARK_SIZE = 10; private final int BUFFER_SIZE = 50; + private final int PANEL_WIDTH = 1260; // it should be 1280 but, minors 40 to cancel the bias. + private final int PANEL_HEIGHT = 960; private final int CANVAS_WIDTH = 720; private final int CANVAS_HEIGHT = 720; private final int LHS_BUFFER = BUFFER_SIZE; - private final int RHS_BUFFER = BUFFER_SIZE + MARK_SIZE / 2; + private final int RHS_BUFFER = BUFFER_SIZE; private final int TOP_BUFFER = BUFFER_SIZE; - private final int BOT_BUFFER = TOP_BUFFER + MARK_SIZE / 2; + private final int BOT_BUFFER = TOP_BUFFER; private boolean horizontalInversion = false; private double distanceScaleFactor; @@ -61,6 +69,8 @@ public class CanvasController { private Mark maxLonPoint; private double referencePointX; private double referencePointY; + private double metersPerPixelX; + private double metersPerPixelY; private List markGroups = new ArrayList<>(); private List boatGroups = new ArrayList<>(); @@ -87,6 +97,12 @@ public class CanvasController { canvas = new ResizableCanvas(); group = new Group(); + // create image view for map, bind panel size to image + mapImage = new ImageView(); + canvasPane.getChildren().add(mapImage); + mapImage.fitWidthProperty().bind(canvasPane.widthProperty()); + mapImage.fitHeightProperty().bind(canvasPane.heightProperty()); + canvasPane.getChildren().add(canvas); canvasPane.getChildren().add(group); // Bind canvas size to stack pane size. @@ -97,11 +113,13 @@ public class CanvasController { public void initializeCanvas (){ gc = canvas.getGraphicsContext2D(); - gc.save(); - gc.setFill(Color.SKYBLUE); - gc.fillRect(0,0, CANVAS_WIDTH, CANVAS_HEIGHT); - gc.restore(); +// gc.save(); +// gc.setFill(Color.SKYBLUE); +// gc.fillRect(0,0, CANVAS_WIDTH, CANVAS_HEIGHT); +// gc.restore(); + gc.setGlobalAlpha(0.5); fitMarksToCanvas(); + drawGoogleMap(); // TODO: 1/05/17 wmu16 - Change this call to now draw the marks as from the xml @@ -137,6 +155,30 @@ public class CanvasController { }; } + /** + * First find the top right and bottom left points' geo locations, then retrieve + * map from google to display on image view. - Haoming 22/5/2017 + */ + private void drawGoogleMap() { + findMetersPerPixel(); + Point2D topLeftPoint = findScaledXY(maxLatPoint.getLatitude(), minLonPoint.getLongitude()); + // distance from top left extreme to panel origin (top left corner) + double distanceFromTopLeftToOrigin = Math.sqrt(Math.pow(topLeftPoint.getX() * metersPerPixelX, 2) + Math.pow(topLeftPoint.getY() * metersPerPixelY, 2)); + // angle from top left extreme to panel origin + double bearingFromTopLeftToOrigin = Math.toDegrees(Math.atan2(-topLeftPoint.getX(), topLeftPoint.getY())); + // the top left extreme + Position topLeftPos = new Position(maxLatPoint.getLatitude(), minLonPoint.getLongitude()); + Position originPos = GeoUtility.getGeoCoordinate(topLeftPos, bearingFromTopLeftToOrigin, distanceFromTopLeftToOrigin); + + // distance from origin corner to bottom right corner of the panel + double distanceFromOriginToBottomRight = Math.sqrt(Math.pow(PANEL_HEIGHT* metersPerPixelY, 2) + Math.pow(PANEL_WIDTH * metersPerPixelX, 2)); + double bearingFromOriginToBottomRight = Math.toDegrees(Math.atan2(PANEL_WIDTH, -PANEL_HEIGHT)); + Position bottomRightPos = GeoUtility.getGeoCoordinate(originPos, bearingFromOriginToBottomRight, distanceFromOriginToBottomRight); + + Boundary boundary = new Boundary(originPos.getLat(), bottomRightPos.getLng(), bottomRightPos.getLat(), originPos.getLng()); + CanvasMap canvasMap = new CanvasMap(boundary); + mapImage.setImage(canvasMap.getMapImage()); + } /** * Adds border marks to the canvas, taken from the XML file @@ -174,8 +216,8 @@ public class CanvasController { borderPoint2.getX(), borderPoint2.getY()); xBoundaryPoints[courseLimits.size()-1] = borderPoint1.getX(); yBoundaryPoints[courseLimits.size()-1] = borderPoint1.getY(); - gc.setFill(Color.LIGHTBLUE); - gc.fillPolygon(xBoundaryPoints,yBoundaryPoints,yBoundaryPoints.length); +// gc.setFill(Color.LIGHTBLUE); +// gc.fillPolygon(xBoundaryPoints,yBoundaryPoints,yBoundaryPoints.length); } private void updateGroups(){ @@ -201,11 +243,13 @@ public class CanvasController { private void checkForCourseChanges() { if (StreamParser.isNewRaceXmlReceived()){ - gc.setFill(Color.SKYBLUE); - gc.fillRect(0,0, CANVAS_WIDTH, CANVAS_HEIGHT); - gc.restore(); +// gc.setFill(Color.SKYBLUE); +// gc.fillRect(0,0, CANVAS_WIDTH, CANVAS_HEIGHT); +// gc.restore(); + gc.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + drawGoogleMap(); addRaceBorder(); - canvas.toBack(); +// canvas.toBack(); } } @@ -465,6 +509,27 @@ public class CanvasController { return new Point2D(xAxisLocation, yAxisLocation); } + /** + * Find the number of meters per pixel. + */ + private void findMetersPerPixel () { + Point2D p1, p2; + Mark m1, m2; + double theta, distance, dx, dy, dHorizontal, dVertical; + m1 = new SingleMark("m1", maxLatPoint.getLatitude(), minLonPoint.getLongitude(), 1); + m2 = new SingleMark("m2", minLatPoint.getLatitude(), maxLonPoint.getLongitude(), 2); + p1 = findScaledXY(m1); + p2 = findScaledXY(m2); + theta = Mark.calculateHeadingRad(m1, m2); + distance = Mark.calculateDistance(m1, m2); + dHorizontal = Math.abs(Math.sin(theta) * distance); + dVertical = Math.abs(Math.cos(theta) * distance); + dx = Math.abs(p1.getX() - p2.getX()); + dy = Math.abs(p1.getY() - p2.getY()); + metersPerPixelX = dHorizontal / dx; + metersPerPixelY = dVertical / dy; + } + List getBoatGroups() { return boatGroups; } From 397f7d003a34d85986891a5ac2ac3e3e65f85d31 Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Wed, 24 May 2017 13:54:05 +1200 Subject: [PATCH 12/14] Fixed the size of race canvas and race view so that canvas won't be stretched - canvas view is set to 1280 * 960 #story[928] --- src/main/resources/views/CanvasView.fxml | 2 +- src/main/resources/views/RaceView.fxml | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/resources/views/CanvasView.fxml b/src/main/resources/views/CanvasView.fxml index bc16ad7e..94c08695 100644 --- a/src/main/resources/views/CanvasView.fxml +++ b/src/main/resources/views/CanvasView.fxml @@ -4,4 +4,4 @@ - + diff --git a/src/main/resources/views/RaceView.fxml b/src/main/resources/views/RaceView.fxml index 7b81dd19..46cc0481 100644 --- a/src/main/resources/views/RaceView.fxml +++ b/src/main/resources/views/RaceView.fxml @@ -1,11 +1,12 @@ + - + @@ -16,7 +17,7 @@ - + - + From c42942430f43dac29bc1f607b8e876cc5eca5433 Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Wed, 24 May 2017 14:57:06 +1200 Subject: [PATCH 13/14] Fixed bug that grid panel pushes annotation panel up out of window. #story[928] --- src/main/resources/views/RaceView.fxml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/views/RaceView.fxml b/src/main/resources/views/RaceView.fxml index 46cc0481..f43099a1 100644 --- a/src/main/resources/views/RaceView.fxml +++ b/src/main/resources/views/RaceView.fxml @@ -17,7 +17,7 @@ - +