diff --git a/src/main/org/insa/graph/GraphStatistics.java b/src/main/org/insa/graph/GraphStatistics.java index db575b5..91d1656 100644 --- a/src/main/org/insa/graph/GraphStatistics.java +++ b/src/main/org/insa/graph/GraphStatistics.java @@ -51,6 +51,36 @@ public class GraphStatistics { return topLeft; } + /** + * Create a new bounding box by extending the current one according to the given + * value for each side. + * + * @param left Extra size to add to the left of the box. + * @param top Extra size to add to the top of the box. + * @param right Extra size to add to the right of the box. + * @param bottom Extra size to add to the bottom of the box. + * + * @return New bounding box corresponding to an extension of the current one. + */ + public BoundingBox extend(float left, float top, float right, float bottom) { + return new BoundingBox( + new Point(this.topLeft.getLongitude() - left, this.topLeft.getLatitude() + top), + new Point(this.bottomRight.getLongitude() + right, + this.bottomRight.getLatitude() - bottom)); + } + + /** + * Create a new bounding box by extending the current one according by the given + * value on each side. + * + * @param size Extra size to add to each side of this box. + * + * @return New bounding box corresponding to an extension of the current one. + */ + public BoundingBox extend(float size) { + return this.extend(size, size, size, size); + } + } // Bounding box for this graph. diff --git a/src/main/org/insa/graphics/drawing/MercatorProjection.java b/src/main/org/insa/graphics/drawing/MercatorProjection.java new file mode 100644 index 0000000..f5facc5 --- /dev/null +++ b/src/main/org/insa/graphics/drawing/MercatorProjection.java @@ -0,0 +1,144 @@ +package org.insa.graphics.drawing; + +import java.awt.Dimension; + +import org.insa.graph.GraphStatistics.BoundingBox; + +public class MercatorProjection { + + public static final double MAX_LATITUDE = 82; + + public static final double MIN_LATITUDE = -MAX_LATITUDE; + + // From Wikipedia... for the above max/min latitude. + private static final double IMAGE_WIDTH = 2058, IMAGE_HEIGHT = 1746; + + private static final double MAX_LATITUDE_PROJ = projectY(MAX_LATITUDE); + private static final double MIN_LATITUDE_PROJ = projectY(MIN_LATITUDE); + + // Bounding box + private final float minLatitude, minLongitude, maxLatitude, maxLongitude; + + // Projection of min and max latitude. + private final double minLatitudeProj, maxLatitudeProj; + + // Dimension of the image + private final double width, height; + + /** + * Create a new MercatorProjection corresponding to the given BoundingBox and + * maxSize. + * + * @param boundingBox Box for this projection. + * @param maxSize Maximum size of any side (width / height) of the image to + * which this projection should draw. + */ + public MercatorProjection(BoundingBox boundingBox, int maxSize) { + // Find minimum/maximum longitude and latitude. + this.minLongitude = boundingBox.getTopLeftPoint().getLongitude(); + this.maxLongitude = boundingBox.getBottomRightPoint().getLongitude(); + this.minLatitude = boundingBox.getBottomRightPoint().getLatitude(); + this.maxLatitude = boundingBox.getTopLeftPoint().getLatitude(); + + // Compute projection + this.minLatitudeProj = projectY(this.minLatitude); + this.maxLatitudeProj = projectY(this.maxLatitude); + + Dimension imageDimension = computeImageSize(maxSize); + this.width = imageDimension.getWidth(); + this.height = imageDimension.getHeight(); + } + + /** + * @return Image width for this projection to work properly. + */ + public double getImageWidth() { + return this.width; + } + + /** + * @return Image weight for this projection to work properly. + */ + public double getImageHeight() { + return this.height; + } + + /** + * Compute the projection (without scaling) of the given latitude. + * + * @param latitude Latitude to project. + * + * @return Projection of the given latitude (without scaling). + */ + private static double projectY(double latitude) { + double sinLatitude = Math.sin(latitude * Math.PI / 180.0); + return Math.log((1 + sinLatitude) / (1 - sinLatitude)) / 2; + } + + /** + * Compute the dimension required for drawing a projection of the given box on + * an image, ensuring that none of the side of image is greater than maxSize. + * + * @param maxSize Maximum side of any side of the image. + * + * @return Dimension corresponding to the preferred size for the image. + */ + protected Dimension computeImageSize(int maxSize) { + double propWidth = (maxLongitude - minLongitude) * IMAGE_WIDTH / 360.0; + double propHeight = (this.maxLatitudeProj - this.minLatitudeProj) + / (MAX_LATITUDE_PROJ - MIN_LATITUDE_PROJ) * IMAGE_HEIGHT; + + return propWidth < propHeight + ? new Dimension((int) (maxSize * propWidth / propHeight), maxSize) + : new Dimension(maxSize, (int) (maxSize * propHeight / propWidth)); + } + + /** + * Project the given latitude on the image. + * + * @param latitude Latitude to project. + * + * @return Projected position of the latitude on the image. + */ + public int latitudeToPixelY(float latitude) { + return (int) ((this.maxLatitudeProj - projectY(latitude)) + / (this.maxLatitudeProj - this.minLatitudeProj) * this.height); + } + + /** + * Project the given longitude on the image. + * + * @param longitude Longitude to project. + * + * @return Projected position of the longitude on the image. + */ + public int longitudeToPixelX(float longitude) { + return (int) (width * (longitude - minLongitude) / (maxLongitude - minLongitude)); + } + + /** + * Retrieve the latitude associated to the given projected point. + * + * @param py Projected y-position for which latitude should be retrieved. + * + * @return The original latitude of the point. + */ + public float pixelYToLatitude(double py) { + float y = (float) (this.maxLatitudeProj + - (py / this.height) * (this.maxLatitudeProj - this.minLatitudeProj)); + return (float) (180 * (2 * Math.atan(Math.exp(y)) - Math.PI / 2) / Math.PI); + } + + /** + * Retrieve the longitude associated to the given projected point. + * + * @param px Projected x-position for which longitude should be retrieved. + * + * @return The original longitude of the point. + */ + public float pixelXToLongitude(double px) { + return (float) ((px / this.width) * (this.maxLongitude - this.minLongitude) + + this.minLongitude); + } + +} diff --git a/src/main/org/insa/graphics/drawing/components/BasicDrawing.java b/src/main/org/insa/graphics/drawing/components/BasicDrawing.java index ea1f297..a182bba 100644 --- a/src/main/org/insa/graphics/drawing/components/BasicDrawing.java +++ b/src/main/org/insa/graphics/drawing/components/BasicDrawing.java @@ -31,6 +31,7 @@ import org.insa.graphics.drawing.BasicGraphPalette; import org.insa.graphics.drawing.Drawing; import org.insa.graphics.drawing.DrawingClickListener; import org.insa.graphics.drawing.GraphPalette; +import org.insa.graphics.drawing.MercatorProjection; import org.insa.graphics.drawing.overlays.MarkerOverlay; import org.insa.graphics.drawing.overlays.MarkerUtils; import org.insa.graphics.drawing.overlays.Overlay; @@ -146,8 +147,8 @@ public class BasicDrawing extends JPanel implements Drawing { @Override public void drawImpl(Graphics2D graphics) { - int px = BasicDrawing.this.projx(getPoint().getLongitude()); - int py = BasicDrawing.this.projy(getPoint().getLatitude()); + int px = projection.longitudeToPixelX(getPoint().getLongitude()); + int py = projection.latitudeToPixelY(getPoint().getLatitude()); graphics.drawImage(this.image, px - MARKER_WIDTH / 2, py - MARKER_HEIGHT, MARKER_WIDTH, MARKER_HEIGHT, BasicDrawing.this); @@ -193,10 +194,10 @@ public class BasicDrawing extends JPanel implements Drawing { while (itPoint.hasNext()) { Point curr = itPoint.next(); - int x1 = BasicDrawing.this.projx(prev.getLongitude()); - int x2 = BasicDrawing.this.projx(curr.getLongitude()); - int y1 = BasicDrawing.this.projy(prev.getLatitude()); - int y2 = BasicDrawing.this.projy(curr.getLatitude()); + int x1 = projection.longitudeToPixelX(prev.getLongitude()); + int x2 = projection.longitudeToPixelX(curr.getLongitude()); + int y1 = projection.latitudeToPixelY(prev.getLatitude()); + int y2 = projection.latitudeToPixelY(curr.getLatitude()); graphics.drawLine(x1, y1, x2, y2); @@ -253,8 +254,8 @@ public class BasicDrawing extends JPanel implements Drawing { @Override public void addPoint(Point point) { - int x = BasicDrawing.this.projx(point.getLongitude()) - this.width / 2; - int y = BasicDrawing.this.projy(point.getLatitude()) - this.width / 2; + int x = projection.longitudeToPixelX(point.getLongitude()) - this.width / 2; + int y = projection.latitudeToPixelY(point.getLatitude()) - this.width / 2; this.graphics.fillOval(x, y, this.width, this.width); BasicDrawing.this.repaint(); } @@ -294,7 +295,7 @@ public class BasicDrawing extends JPanel implements Drawing { // Maximum width for the drawing (in pixels). private static final int MAXIMUM_DRAWING_WIDTH = 2000; - private double long1, long2, lat1, lat2; + private MercatorProjection projection; // Width and height of the image private int width, height; @@ -429,22 +430,6 @@ public class BasicDrawing extends JPanel implements Drawing { return this.zoomAndPanListener; } - /** - * @param lon - * @return - */ - private int projx(double lon) { - return (int) (width * (lon - this.long1) / (this.long2 - this.long1)); - } - - /** - * @param lat - * @return - */ - private int projy(double lat) { - return (int) (height * (1 - (lat - this.lat1) / (this.lat2 - this.lat1))); - } - /** * Return the longitude and latitude corresponding to the given position of the * MouseEvent. @@ -465,13 +450,8 @@ public class BasicDrawing extends JPanel implements Drawing { .inverseTransform(event.getPoint(), null); // Inverse the "projection" on x/y to get longitude and latitude. - double lon = ptDst.getX(); - double lat = ptDst.getY(); - lon = (lon / this.width) * (this.long2 - this.long1) + this.long1; - lat = (1 - lat / this.height) * (this.lat2 - this.lat1) + this.lat1; - - // Return a new point. - return new Point((float) lon, (float) lat); + return new Point(projection.pixelXToLongitude(ptDst.getX()), + projection.pixelYToLatitude(ptDst.getY())); } /* @@ -546,10 +526,10 @@ public class BasicDrawing extends JPanel implements Drawing { while (it1.hasNext()) { Point curr = it1.next(); - int x1 = this.projx(prev.getLongitude()); - int x2 = this.projx(curr.getLongitude()); - int y1 = this.projy(prev.getLatitude()); - int y2 = this.projy(curr.getLatitude()); + int x1 = projection.longitudeToPixelX(prev.getLongitude()); + int x2 = projection.longitudeToPixelX(curr.getLongitude()); + int y1 = projection.latitudeToPixelY(prev.getLatitude()); + int y2 = projection.latitudeToPixelY(curr.getLatitude()); graphGraphics.drawLine(x1, y1, x2, y2); prev = curr; @@ -573,30 +553,20 @@ public class BasicDrawing extends JPanel implements Drawing { BoundingBox box = graph.getGraphInformation().getBoundingBox(); // Find minimum/maximum longitude and latitude. - double minLon = box.getTopLeftPoint().getLongitude(), + float minLon = box.getTopLeftPoint().getLongitude(), maxLon = box.getBottomRightPoint().getLongitude(), minLat = box.getBottomRightPoint().getLatitude(), maxLat = box.getTopLeftPoint().getLatitude(); // Add a little delta to avoid drawing on the edge... - double diffLon = maxLon - minLon, diffLat = maxLat - minLat; - double deltaLon = 0.01 * diffLon, deltaLat = 0.01 * diffLat; + float diffLon = maxLon - minLon, diffLat = maxLat - minLat; + float deltaLon = 0.01f * diffLon, deltaLat = 0.01f * diffLat; - this.long1 = minLon - deltaLon; - this.long2 = maxLon + deltaLon; - this.lat1 = minLat - deltaLat; - this.lat2 = maxLat + deltaLat; - - // Compute width/height for the image - - if (diffLat < diffLon) { - this.width = MAXIMUM_DRAWING_WIDTH; - this.height = (int) (this.width * diffLat / diffLon); - } - else { - this.height = MAXIMUM_DRAWING_WIDTH; - this.width = (int) (this.height * diffLon / diffLat); - } + // Create the projection and retrieve width and height for the box. + projection = new MercatorProjection(box.extend(deltaLon, deltaLat, deltaLon, deltaLat), + MAXIMUM_DRAWING_WIDTH); + this.width = (int) projection.getImageWidth(); + this.height = (int) projection.getImageHeight(); // Create the image BufferedImage img = new BufferedImage(this.width, this.height,