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..6fa9b773 100644 --- a/src/main/java/seng302/controllers/CanvasController.java +++ b/src/main/java/seng302/controllers/CanvasController.java @@ -1,10 +1,5 @@ package seng302.controllers; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.PriorityBlockingQueue; import javafx.animation.AnimationTimer; import javafx.beans.property.SimpleDoubleProperty; import javafx.fxml.FXML; @@ -12,22 +7,29 @@ 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.mark.GateMark; -import seng302.models.mark.Mark; -import seng302.models.mark.MarkGroup; -import seng302.models.mark.MarkType; -import seng302.models.mark.SingleMark; +import seng302.models.map.Boundary; +import seng302.models.map.CanvasMap; +import seng302.models.mark.*; import seng302.models.stream.StreamParser; 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; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.PriorityBlockingQueue; /** * Created by ptg19 on 15/03/17. @@ -42,15 +44,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 +66,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 +94,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 +110,9 @@ 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.setGlobalAlpha(0.5); fitMarksToCanvas(); + drawGoogleMap(); // TODO: 1/05/17 wmu16 - Change this call to now draw the marks as from the xml @@ -137,6 +148,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 +209,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 +236,9 @@ public class CanvasController { private void checkForCourseChanges() { if (StreamParser.isNewRaceXmlReceived()){ - 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(); } } @@ -465,6 +498,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; } diff --git a/src/main/java/seng302/models/map/Boundary.java b/src/main/java/seng302/models/map/Boundary.java new file mode 100644 index 00000000..4396d95d --- /dev/null +++ b/src/main/java/seng302/models/map/Boundary.java @@ -0,0 +1,44 @@ +package seng302.models.map; + +/** + * 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 + */ +public class Boundary { + + private double northLat, eastLng, southLat, westLng; + + public Boundary(double northLat, double eastLng, double southLat, double westLng) { + this.northLat = northLat; + this.eastLng = eastLng; + this.southLat = southLat; + this.westLng = westLng; + } + + double getCentreLat() { + return (northLat + southLat) / 2; + } + + double getCentreLng() { + return (eastLng + westLng) / 2; + } + + double getNorthLat() { + return northLat; + } + + double getEastLng() { + return eastLng; + } + + double getSouthLat() { + return southLat; + } + + double getWestLng() { + return westLng; + } +} 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..162358a1 --- /dev/null +++ b/src/main/java/seng302/models/map/CanvasMap.java @@ -0,0 +1,104 @@ +package seng302.models.map; + +import javafx.scene.image.Image; + +import javax.net.ssl.HttpsURLConnection; +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 boundary; + private long width, height; // desired image size + private int zoom; + + private String KEY = "AIzaSyC-5oOShMCY5Oy_9L7guYMPUPFHDMr37wE"; + + public CanvasMap(Boundary boundary) { + this.boundary = boundary; + calculateOptimalMapSize(); + } + + public Image getMapImage() { + try { + 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() { + StringBuilder sb = new StringBuilder(); + sb.append("https://maps.googleapis.com/maps/api/staticmap?"); + sb.append(String.format("center=%f,%f", boundary.getCentreLat(), boundary.getCentreLng())); + sb.append(String.format("&zoom=%d", zoom)); + 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()) * scale, + Math.abs(pointNE.getY() - pointSW.getY()) * scale); + } + + class MapSize { + long width, height; + + MapSize(double width, double 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 new file mode 100644 index 00000000..43d02565 --- /dev/null +++ b/src/main/java/seng302/models/map/MapGeo.java @@ -0,0 +1,31 @@ +package seng302.models.map; + +/** + * A class represent Geo location (latitude, longitude). + * Created by Haoming on 15/5/2017 + */ +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..41be919a --- /dev/null +++ b/src/main/java/seng302/models/map/MapPoint.java @@ -0,0 +1,31 @@ +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; + + 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..b4bf647d --- /dev/null +++ b/src/main/java/seng302/models/map/MercatorProjection.java @@ -0,0 +1,52 @@ +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; + 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 static 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 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); + +// 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 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); + 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 new file mode 100644 index 00000000..fc319bcc --- /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(); + Boundary bound = new Boundary(57.662943, 11.848501, 57.673945, 11.824966); + CanvasMap canvasMap = new CanvasMap(bound); + gc.drawImage(canvasMap.getMapImage(), 0, 0, canvasMap.getWidth(), canvasMap.getHeight()); + } +} 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..f43099a1 100644 --- a/src/main/resources/views/RaceView.fxml +++ b/src/main/resources/views/RaceView.fxml @@ -1,11 +1,12 @@ + - + @@ -16,7 +17,7 @@ - + - + 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 @@ + + + + + + + + + + + + + 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