diff --git a/src/apps/MapApplication.java b/src/apps/MapApplication.java index 1ad6f2d..9fcc1e8 100644 --- a/src/apps/MapApplication.java +++ b/src/apps/MapApplication.java @@ -145,9 +145,9 @@ public abstract class MapApplication extends Application { { Conic.ALBERS, Misc.BRAUN_CONIC, Conic.LAMBERT, Conic.EQUIDISTANT }, { Polyhedral.AUTHAGRAPH, Polyhedral.LEE_TETRAHEDRAL_RECTANGULAR, Polyhedral.LEE_TETRAHEDRAL_TRIANGULAR, Polyhedral.VAN_LEEUWEN }, - { Octohedral.CONFORMAL_CAHILL, Octohedral.CAHILL_CONCIALDI, Octohedral.KEYES_STANDARD, - Octohedral.KEYES_BASIC_M, Octohedral.KEYES_OCTANT, Polyhedral.DYMAXION, - Octohedral.WATERMAN }, + { Octohedral.CONFORMAL_CAHILL_BUTTERFLY, Octohedral.CAHILL_CONCIALDI, + Octohedral.KEYES_STANDARD, Octohedral.KEYES_BASIC_M, Octohedral.KEYES_OCTANT, + Polyhedral.DYMAXION, Octohedral.WATERMAN }, { Pseudocylindrical.ECKERT_IV, EqualEarth.EQUAL_EARTH, Pseudocylindrical.HOMOLOSINE_INTERRUPTED, Pseudocylindrical.HOMOLOSINE, Gyorffy.B, Gyorffy.D, Pseudocylindrical.KAVRAYSKIY_VII, Pseudocylindrical.MOLLWEIDE, @@ -421,7 +421,7 @@ public abstract class MapApplication extends Application { final ObservableList factorsOf90 = FXCollections.observableArrayList(); for (double f = 1; f <= 90; f += 0.5) if (90%f == 0) - factorsOf90.add((double)f); + factorsOf90.add(f); final Spinner gratSpinner = new Spinner(factorsOf90); //spinner for the graticule value gratSpinner.getValueFactory().setConverter(new DoubleStringConverter()); gratSpinner.getValueFactory().setValue(15.); diff --git a/src/apps/MapProducer.java b/src/apps/MapProducer.java index eb149e2..71d0bd3 100644 --- a/src/apps/MapProducer.java +++ b/src/apps/MapProducer.java @@ -81,7 +81,7 @@ public class MapProducer extends Application { Lenticular.WAGNER_VIII, Pseudocylindrical.HOMOLOSINE_INTERRUPTED }, { Misc.PEIRCE_QUINCUNCIAL, Misc.GUYOU, Polyhedral.LEE_TETRAHEDRAL_TRIANGULAR, - Octohedral.CONFORMAL_CAHILL, Octohedral.WATERMAN } }; + Octohedral.CONFORMAL_CAHILL_BUTTERFLY, Octohedral.WATERMAN } }; public static final double[] ctrMerids = {0, toRadians(-20)}; diff --git a/src/maps/CahillKeyes.java b/src/maps/CahillKeyes.java index 10f8555..ae0f52e 100644 --- a/src/maps/CahillKeyes.java +++ b/src/maps/CahillKeyes.java @@ -24,6 +24,7 @@ package maps; import utils.NumericalAnalysis; +import utils.Shape; import static java.lang.Math.atan2; import static java.lang.Math.hypot; @@ -71,12 +72,36 @@ public class CahillKeyes { private static final double TOLERANCE = 5; //this is a reasonable tolerance when you recall that we're operating on the order of 10,000 units public static final double POLE_OFFSET = lMA*SCALE_FACTOR; //lMA expressed in output coordinates - - + + + public static Projection FACE = new Projection( + "Cahill–Keyes (face)", "A single face of Gene Keyes's octohedral projection", + Shape.polygon(new double[][] {{0., 0.}, {0., -sqrt(3)/2.}, {1/2., -sqrt(3)/2.}}), + 0b1011, Projection.Type.OTHER, Projection.Property.COMPROMISE, 2) { + + public double[] project(double lat, double lon) { + // Mary-Jo's coordinates put the pole at the origin and everything else to the right + double[] coordsMj = projectMj(toDegrees(lat), toDegrees(lon)); + if (coordsMj == null) + return null; + // rotate so that the equator is below the pole instead + return new double[] {coordsMj[1], -coordsMj[0]}; + } + + public double[] inverse(double x, double y) { + // the generic coordinates here use degrees, and assume the equator is to the right of the pole + double[] coordsD = inverseMj(-y, x); + if (coordsD == null) + return null; + return new double[] {toRadians(coordsD[0]), toRadians(coordsD[1])}; + } + }; + + /** * convert adjusted lat and lon in degrees to Mary Jo's coordinates */ - public static double[] faceProjectD(double latD, double lonD) { + public static double[] projectMj(double latD, double lonD) { final double[][] mer = meridian(lonD); final double[] output; if (latD >= 75) { //zone c (frigid zone) @@ -114,13 +139,13 @@ public class CahillKeyes { } - public static double[] faceInverseD(double x, double y) { //convert Mary Jo's coordinates to relative lat and lon in degrees + public static double[] inverseMj(double x, double y) { //convert Mary Jo's coordinates to relative lat and lon in degrees // start by scaling it so a side length of 1 reads as the correct length y /= SCALE_FACTOR; x /= SCALE_FACTOR; if (y > x-lMA || y > x/sqrt(3) || y > x*(2-sqrt(3))+bDE || - y > (lMG-x)*(2+sqrt(3))+lGF || x > lMG) //this describes the footprint of the octant + y > (lMG-x)*(2+sqrt(3))+lGF || x > lMG) //this describes the footprint of the octant return null; double lonD = longitudeD(x, y); diff --git a/src/maps/Octohedral.java b/src/maps/Octohedral.java index 0294fbf..5625b74 100644 --- a/src/maps/Octohedral.java +++ b/src/maps/Octohedral.java @@ -41,12 +41,11 @@ import static java.lang.Math.signum; import static java.lang.Math.sin; import static java.lang.Math.sqrt; import static java.lang.Math.tan; -import static java.lang.Math.toDegrees; import static java.lang.Math.toRadians; import static utils.Math2.cosd; import static utils.Math2.cotd; import static utils.Math2.floorMod; -import static utils.Math2.min; +import static utils.Math2.max; import static utils.Math2.sind; /** @@ -57,116 +56,40 @@ import static utils.Math2.sind; */ public class Octohedral { - public static final Projection WATERMAN = new OctohedralProjection( - "Waterman Butterfly", "A simple Cahill-esque octohedral map arrangement, with Antarctica left on.", - (sqrt(3)-1)/8, 0b1010, Property.COMPROMISE, 3, - Configuration.BUTTERFLY) { + public static final Projection CONFORMAL_CAHILL_FACE = new Projection( + "Cahill Conformal (face)", "The conformal projection from an octant to an equilateral triangle", + Shape.polygon(new double[][] {{0., 0.}, {0., -sqrt(3)/2.}, {1/2., -sqrt(3)/2.}}), + 0b1001, Projection.Type.OCTOHEDRAL, Property.CONFORMAL, 3) { - protected double[] faceProject(double lat, double lon) { - return Waterman.faceProject(lat, lon); - } - - protected double[] faceInverse(double x, double y) { - return Waterman.faceInverse(x, y); - } - }; - - - public static final Projection KEYES_BASIC_M = new OctohedralProjection( - "Cahill\u2013Keyes (simplified)", "A simple M-shaped octohedral projection, with Antarctica broken into three pieces.", - CahillKeyes.POLE_OFFSET, 0b1010, Property.COMPROMISE, 3, - Configuration.M_PROFILE) { - - protected double[] faceProject(double lat, double lon) { - return CahillKeyes.faceProjectD(toDegrees(lat), toDegrees(lon)); - } - - protected double[] faceInverse(double x, double y) { - double[] coords = CahillKeyes.faceInverseD(x, y); - return (coords == null) ? null : - new double[] {toRadians(coords[0]), toRadians(coords[1])}; - } - }; - - - public static final Projection KEYES_STANDARD = new OctohedralProjection( - "Cahill\u2013Keyes", "An M-shaped octohedral projection with Antarctica assembled in the center.", - CahillKeyes.POLE_OFFSET, 0b1010, Property.COMPROMISE, 4, - Configuration.M_W_S_POLE) { - - public double[] project(double lat, double lon) { - return super.project(lat, lon + PI/9); // apply the central meridian manually - } - - protected double[] faceProject(double lat, double lon) { - return CahillKeyes.faceProjectD(toDegrees(lat), toDegrees(lon)); - } - - public double[] inverse(double x, double y) { - double[] coords = super.inverse(x, y); - if (coords == null) return null; - coords[1] = floorMod(coords[1] - PI/9 + PI, 2*PI) - PI; // apply the central meridian manually - return coords; - } - - protected double[] faceInverse(double x, double y) { - double[] coords = CahillKeyes.faceInverseD(x, y); - return (coords == null) ? null : - new double[] {toRadians(coords[0]), toRadians(coords[1])}; - } - }; - - - public static final Projection KEYES_OCTANT = new OctohedralProjection( - "Cahill\u2013Keyes (single octant)", "A single octant of the Cahill\u2013Keyes projection (for memory economization in the case of very large maps).", - CahillKeyes.POLE_OFFSET, 0b1010, Property.COMPROMISE, 3, - Configuration.SINGLE_OCTANT) { - - protected double[] faceProject(double lat, double lon) { - return CahillKeyes.faceProjectD(toDegrees(lat), toDegrees(lon)); - } - - protected double[] faceInverse(double x, double y) { - double[] coords = CahillKeyes.faceInverseD(x, y); - return (coords == null) ? null : - new double[] {toRadians(coords[0]), toRadians(coords[1])}; - } - }; - - - public static final OctohedralProjection CONFORMAL_CAHILL = new OctohedralProjection( - "Cahill Conformal", "The conformal and only reproducible variant of Cahill's original map.", - 0, 0b1000, Property.CONFORMAL, 3, Configuration.BUTTERFLY) { - private final double HEXAGON_SCALE = 1.112913; //this is 2^(2/3)/6*\int_0^\pi sin^(-1/3) x dx private final double TOLERANCE = 1e-3; private final double[] VERTEX = {0, PI/4, -3*PI/4}; - protected double[] faceProject(double lat, double lon) { + public double[] project(double lat, double lon) { double[] poleCoords = {lat, lon}; double[] vertCoords = transformFromOblique(lat, lon, VERTEX); //look at an oblique aspect from the nearest vertex if (poleCoords[0] > vertCoords[0]) { //if this point is closer to the pole Complex w = Complex.fromPolar(pow(tan(PI/4-lat/2), 2/3.), lon*2/3.); Complex z = polynomial(w); //project it as normal - return new double[] {z.getRe(), z.getIm()}; + return new double[] {z.getIm(), -z.getRe()}; //rotate 90° } else { //if it is closer to the vertex Complex w = Complex.fromPolar( pow(tan(PI/4-vertCoords[0]/2), 2/3.), vertCoords[1]*2/3.); Complex zSkew = polynomial(w); //use the maclaurin series centred there - return new double[] { - -1/2.*zSkew.getRe() + sqrt(3)/2*zSkew.getIm() + sqrt(3)/2, - -sqrt(3)/2*zSkew.getRe() - 1/2.*zSkew.getIm() + 1/2. }; + return new double[] { //rotate and translate in the plane appropriately + -sqrt(3)/2*zSkew.getRe() - 1/2.*zSkew.getIm() + 1/2., + 1/2.*zSkew.getRe() - sqrt(3)/2*zSkew.getIm() - sqrt(3)/2 }; } } - protected double[] faceInverse(double x, double y) { + public double[] inverse(double x, double y) { Complex z; - if (x < (1-y)/sqrt(3)) //do the Newton Raphson from whichever vertex to which it is closest - z = new Complex(x, y); + if (y > (x-1)/sqrt(3)) //do the Newton Raphson from whichever vertex to which it is closest + z = new Complex(-y, x); //applying a transformation in the plane as appropriate else - z = new Complex(-1/2.*(x-sqrt(3)/2) - sqrt(3)/2*(y-1/2.), - sqrt(3)/2*(x-sqrt(3)/2) - 1/2.*(y-1/2.)); + z = new Complex(-sqrt(3)/2*(x-1/2.) + 1/2.*(y+sqrt(3)/2), + -1/2.*(x-1/2.) - sqrt(3)/2*(y+sqrt(3)/2)); Complex w = z.divide(HEXAGON_SCALE); Complex error = polynomial(w).minus(z); for (int i = 0; i < 8 && error.abs() > TOLERANCE; i ++) { @@ -175,7 +98,7 @@ public class Octohedral { error = polynomial(w).minus(z); } double[] latLon = { PI/2 - 2*atan(pow(w.abs(), 3/2.)), w.arg()*3/2. }; //inverse conic it back to spherical coordinates - if (x < (1-y)/sqrt(3)) //if it was closest to that vertex, the result is easy + if (y > (x-1)/sqrt(3)) //if it was closest to that vertex, the result is easy return latLon; else //if it was closer to the other vertex, do some obliquifying return transformToOblique(latLon, VERTEX); @@ -197,55 +120,59 @@ public class Octohedral { }; + public static final OctohedralProjection CONFORMAL_CAHILL_BUTTERFLY = new OctohedralProjection( + "Cahill Conformal", "The conformal and only reproducible variant of Cahill's original map.", + 0, 0b1000, Property.CONFORMAL, 3, CONFORMAL_CAHILL_FACE, Configuration.BUTTERFLY); + + public static final Projection CAHILL_CONCIALDI = new OctohedralProjection( "Cahill\u2013Concialdi", "A conformal octohedral projection with no extra cuts and a unique arrangement.", - 0, 0b1000, Property.CONFORMAL, 4, Configuration.BAT_SHAPE) { - - private final double lon0 = toRadians(20); // TODO: it would be better for this 20° shift to go in the configuration - - public double[] project(double lat, double lon) { - lon = floorMod(lon - lon0 + PI, 2*PI) - PI; // change the central meridian - return super.project(lat, lon); - } - - protected double[] faceProject(double lat, double lon) { - return CONFORMAL_CAHILL.faceProject(lat, lon); - } - - public double[] inverse(double x, double y) { - double[] coords = super.inverse(x, y); - if (coords != null) - coords[1] = floorMod(coords[1] + lon0 + PI, 2*PI) - PI; // change the central meridian - return coords; - } - - protected double[] faceInverse(double x, double y) { - return CONFORMAL_CAHILL.faceInverse(x, y); - } - }; + 0, 0b1000, Property.CONFORMAL, 4, CONFORMAL_CAHILL_FACE, Configuration.BAT_SHAPE); + + + public static final Projection WATERMAN = new OctohedralProjection( + "Waterman Butterfly", "A simple Cahill-esque octohedral map arrangement, with Antarctica left on.", + (sqrt(3)-1)/8, 0b1010, Property.COMPROMISE, 3, + Waterman.FACE, Configuration.BUTTERFLY); + + + public static final Projection KEYES_BASIC_M = new OctohedralProjection( + "Cahill\u2013Keyes (simplified)", "A simple M-shaped octohedral projection, with Antarctica broken into three pieces.", + CahillKeyes.POLE_OFFSET, 0b1010, Property.COMPROMISE, 3, + CahillKeyes.FACE, Configuration.M_PROFILE); + + + public static final Projection KEYES_STANDARD = new OctohedralProjection( + "Cahill\u2013Keyes", "An M-shaped octohedral projection with Antarctica assembled in the center.", + CahillKeyes.POLE_OFFSET, 0b1010, Property.COMPROMISE, 4, + CahillKeyes.FACE, Configuration.M_W_S_POLE); + + + public static final Projection KEYES_OCTANT = new OctohedralProjection( + "Cahill\u2013Keyes (single octant)", "A single octant of the Cahill\u2013Keyes projection (for memory economization in the case of very large maps).", + CahillKeyes.POLE_OFFSET, 0b1010, Property.COMPROMISE, 3, + CahillKeyes.FACE, Configuration.SINGLE_OCTANT); - private static abstract class OctohedralProjection extends Projection { + private static class OctohedralProjection extends Projection { + private final Projection faceProj; private final Octant[] octants; public OctohedralProjection(String name, String desc, double tipOffset, - int fisc, Property property, int rating, Configuration config) { + int fisc, Property property, int rating, + Projection faceProj, Configuration config) { super(name, desc, null, fisc, (tipOffset == 0) ? Type.OCTOHEDRAL : Type.TETRADECAHEDRAL, property, rating, new String[] {}, new double[][] {}, config.hasAspect); this.octants = config.placeOctants(tipOffset); - this.shape = Shape.polygon(config.drawShape(tipOffset, this)); + this.faceProj = faceProj; + this.shape = Shape.polygon(config.drawShape(tipOffset, faceProj)); } - protected abstract double[] faceProject(double lat, double lon); - - protected abstract double[] faceInverse(double x, double y); - - public double[] project(double lat, double lon) { for (Octant octant: this.octants) { // try each octant double lonr = floorMod(lon - octant.centralLongitude + PI, 2*PI) - PI; // relative longitude @@ -255,17 +182,17 @@ public class Octohedral { continue; double th = octant.planeRotation; // rotation angle - double[] coords = this.faceProject(abs(lat), abs(lonr)); + double[] coords = this.faceProj.project(abs(lat), abs(lonr)); double xMj = coords[0], yMj = coords[1]; //relative octant coordinates (Mj stands for "Mary Jo Graca") if (lat < 0) //reflect the southern hemisphere over the equator - xMj = sqrt(3) - xMj; + yMj = -sqrt(3) - yMj; if (lonr < 0) //and reflect the western hemisphere over the central meridian - yMj = -yMj; + xMj = -xMj; return new double[] { - octant.x + sin(th)*xMj + cos(th)*yMj, - octant.y - cos(th)*xMj + sin(th)*yMj }; + octant.x + cos(th)*xMj - sin(th)*yMj, + octant.y + sin(th)*xMj + cos(th)*yMj }; } return new double[] {NaN, NaN}; // if none of the octants fit, return null } @@ -275,18 +202,18 @@ public class Octohedral { for (Octant octant: this.octants) { // try each octant double th = octant.planeRotation; // rotation angle - double xMj = sin(th)*(x - octant.x) - cos(th)*(y - octant.y); // do the coordinate change - double yMj = cos(th)*(x - octant.x) + sin(th)*(y - octant.y); - if (sqrt(3)*abs(yMj) > min(xMj, 2 - xMj) + 1e-12) // if the angle is wrong, + double xMj = cos(th)*(x - octant.x) + sin(th)*(y - octant.y); // do the coordinate change + double yMj = -sin(th)*(x - octant.x) + cos(th)*(y - octant.y); + if (abs(xMj) > -max(yMj, -sqrt(3) - yMj)/sqrt(3) + 1e-12) // if the angle is wrong, continue; // check the next one - double[] coords = this.faceInverse(min(xMj, 2 - xMj), abs(yMj)); + double[] coords = this.faceProj.inverse(abs(xMj), max(yMj, -sqrt(3) - yMj)); if (coords == null) continue; // if you got nothing, keep looking double lat = coords[0], lon = coords[1]; // project - lat *= signum(1 - xMj); // undo the reflections - lon *= signum(yMj); + lat *= signum(yMj + sqrt(3)/2); // undo the reflections + lon *= signum(xMj); if (lat < octant.minLatitude - 1e-6 || lat > octant.maxLatitude + 1e-6) // if the resulting coordinates are wrong continue; // move on if (lon < octant.minLongitude - 1e-6 || lon > octant.maxLongitude + 1e-6) // also check the longitude restriction @@ -364,44 +291,44 @@ public class Octohedral { double ySouthPole = -1.5 + tipOffset*sqrt(3)/2.; double quadrantLength = sqrt(3) - tipOffset; return new Octant[] { - new Octant(-sqrt(3)/2, 0., -PI/6, toRadians(-64), PI/2, -3*PI/4), - new Octant(-sqrt(3)/2, 0., PI/6, -PI/2, PI/2, -PI/4), - new Octant( sqrt(3)/2, 0., -PI/6, toRadians(-64), PI/2, PI/4), - new Octant( sqrt(3)/2, 0., PI/6, toRadians(-64), PI/2, 3*PI/4), + new Octant(-sqrt(3)/2, 0., -PI/6, toRadians(-64), PI/2, toRadians(-155)), + new Octant(-sqrt(3)/2, 0., PI/6, toRadians(-90), PI/2, toRadians( -65)), + new Octant( sqrt(3)/2, 0., -PI/6, toRadians(-64), PI/2, toRadians( 25)), + new Octant( sqrt(3)/2, 0., PI/6, toRadians(-64), PI/2, toRadians( 115)), new Octant(xSouthPole - quadrantLength*sqrt(3)/2, ySouthPole - quadrantLength/2, 2*PI/3, - -PI/2, toRadians(-64), -3*PI/4), + toRadians(-90), toRadians(-64), toRadians(-155)), new Octant(xSouthPole + quadrantLength*sqrt(3)/2, ySouthPole + quadrantLength/2, -PI/3, - -PI/2, toRadians(-64), PI/4), + toRadians(-90), toRadians(-64), toRadians(25)), new Octant(xSouthPole + quadrantLength/2, ySouthPole - quadrantLength*sqrt(3)/2, -5*PI/6, - -PI/2, toRadians(-64), 3*PI/4), + toRadians(-90), toRadians(-64), toRadians(115)), }; } double[][] drawShape(double tipOffset, Projection projection) { double xSouthPole = -tipOffset/2.; double ySouthPole = -1.5 + tipOffset*sqrt(3)/2.; double shortEdge = tipOffset/(2*sind(15)); - List parallelSegment = Path.translated( - -sqrt(3)/2., 0, projection.drawLoxodrome( - toRadians(64), toRadians(-20), toRadians(64), toRadians(70), .1)); + List parallelSegment = projection.drawLoxodrome( + toRadians(64), toRadians(45), toRadians(64), toRadians(0), .1); + parallelSegment.addAll(Path.scaled(-1, 1, Path.reversed(parallelSegment))); // expand the parallel segment to cover 90° List shape = new ArrayList<>(); shape.addAll(starShape( 0.0 , -0.5, 2*PI/3, -2*PI/3, 4, tipOffset)); shape.addAll(starShape(-0.5*sqrt(3), 0.0, PI/3, -PI/3, 2, tipOffset)); shape.addAll(starShape(-1.0*sqrt(3), -0.5, 2*PI/3, 0 , 2, tipOffset)); - shape.addAll(Path.translated(-sqrt(3), -1.5, Path.rotated(PI, Path.reversed(parallelSegment)))); + shape.addAll(Path.translated(-sqrt(3), -1.5, Path.rotated(5*PI/6, parallelSegment))); shape.addAll(starShape(-0.5*sqrt(3), -1.0, 5*PI/3, PI/3, 4, tipOffset)); shape.add(new Path.Command('L', xSouthPole - shortEdge*cosd(15), ySouthPole + shortEdge*sind(15))); shape.addAll(Path.translated(xSouthPole + tipOffset*sqrt(3)/2, ySouthPole + tipOffset/2, - Path.rotated(-PI/6, parallelSegment))); + Path.rotated(-PI/3, Path.reversed(parallelSegment)))); shape.add(new Path.Command('L', xSouthPole - shortEdge*sind(15), ySouthPole - shortEdge*cosd(15))); shape.addAll(Path.translated(xSouthPole - tipOffset/2, ySouthPole + tipOffset*sqrt(3)/2, - Path.rotated(PI/3, parallelSegment))); + Path.rotated(PI/6, Path.reversed(parallelSegment)))); shape.add(new Path.Command('L', xSouthPole + shortEdge*cosd(15), ySouthPole - shortEdge*sind(15))); shape.addAll(Path.translated(xSouthPole - tipOffset*sqrt(3)/2, ySouthPole - tipOffset/2, - Path.rotated(5*PI/6, parallelSegment))); + Path.rotated(2*PI/3, Path.reversed(parallelSegment)))); shape.add(new Path.Command('L', xSouthPole + shortEdge*sind(15), ySouthPole + shortEdge*cosd(15))); - shape.addAll(Path.translated(0, -1.5, Path.rotated(PI, Path.reversed(parallelSegment)))); + shape.addAll(Path.translated(0, -1.5, Path.rotated(5*PI/6, parallelSegment))); shape.addAll(starShape( 0.5*sqrt(3), -1.0, 5*PI/3, PI/3, 4, tipOffset)); - shape.addAll(Path.translated(sqrt(3), -1.5, Path.rotated(-2*PI/3, Path.reversed(parallelSegment)))); + shape.addAll(Path.translated(sqrt(3), -1.5, Path.rotated(-5*PI/6, parallelSegment))); shape.addAll(starShape( 1.0*sqrt(3), -0.5, 0 , -2*PI/3, 2, tipOffset)); shape.addAll(starShape( 0.5*sqrt(3), 0.0, PI/3, -PI/3, 2, tipOffset)); return Path.asArray(shape); @@ -412,20 +339,20 @@ public class Octohedral { BAT_SHAPE(false) { Octant[] placeOctants(double tipOffset) { return rotateOctants(toRadians(5), new Octant[]{ - new Octant( 0.0, 0.0 , -2*PI/3, 0 , PI/2, -PI , toRadians(-9), 1), // Alaska - new Octant(-1.5, 0.5*sqrt(3), 0 , -PI/2, 0 , -PI , toRadians(9), 1), // West Antarctica - new Octant( 0.0, 0.0 , -PI/3, -PI/2, PI/2, -PI/2), // America - new Octant( 0.0, 0.0 , 0 , -PI/4, PI/2, 0 ), // Africa - new Octant( 0.0, -1.0*sqrt(3), -2*PI/3, -PI/2, -PI/4, 0 , -1, 0), // Coats Land - new Octant( 0.0, -1.0*sqrt(3), 2*PI/3, -PI/2, -PI/4, 0 , 0, 1), // Dronning Maund Land - new Octant( 0.0, 0.0 , PI/3, -PI/2, PI/2, PI/2), // Asia - new Octant( 0.0, 0.0 , 2*PI/3, 0 , PI/2, PI , -1, toRadians(-9)),// Chukotka - new Octant( 1.5, 0.5*sqrt(3), 0 , -PI/2, 0 , PI , -1, toRadians(9)), // New Zealand + new Octant( 0.0, 0.0 , -2*PI/3, 0 , PI/2, toRadians(-160), toRadians(-9), 1), // Alaska + new Octant(-1.5, 0.5*sqrt(3), 0 , -PI/2, 0 , toRadians(-160), toRadians(9), 1), // West Antarctica + new Octant( 0.0, 0.0 , -PI/3, -PI/2, PI/2, toRadians( -70)), // America + new Octant( 0.0, 0.0 , 0 , -PI/4, PI/2, toRadians( 20)), // Africa + new Octant( 0.0, -1.0*sqrt(3), -2*PI/3, -PI/2, -PI/4, toRadians( 20), -1, 0), // Coats Land + new Octant( 0.0, -1.0*sqrt(3), 2*PI/3, -PI/2, -PI/4, toRadians( 20), 0, 1), // Dronning Maund Land + new Octant( 0.0, 0.0 , PI/3, -PI/2, PI/2, toRadians( 110)), // Asia + new Octant( 0.0, 0.0 , 2*PI/3, 0 , PI/2, toRadians( 200), -1, toRadians(-9)),// Chukotka + new Octant( 1.5, 0.5*sqrt(3), 0 , -PI/2, 0 , toRadians( 200), -1, toRadians(9)), // New Zealand }); } double[][] drawShape(double tipOffset, Projection projection) { - List meridian = projection.drawLoxodrome(0, toRadians(11), PI/2, toRadians(11), .1); - List parallel = projection.drawLoxodrome(PI/4, toRadians(20), PI/4, toRadians(65), .1); + List meridian = projection.drawLoxodrome(0, toRadians(-9), PI/2, toRadians(-9), .1); + List parallel = projection.drawLoxodrome(PI/4, toRadians(0), PI/4, toRadians(45), .1); List shape = new ArrayList<>(); shape.addAll(starShape( 0.0, 0.0 , PI/2, -PI/2, 3, tipOffset)); shape.addAll(Path.rotated(-2*PI/3, Path.reversed(meridian))); diff --git a/src/maps/Waterman.java b/src/maps/Waterman.java index f740bb9..7cc0790 100644 --- a/src/maps/Waterman.java +++ b/src/maps/Waterman.java @@ -23,9 +23,12 @@ */ package maps; +import maps.Projection.Property; +import maps.Projection.Type; +import utils.Shape; + import static java.lang.Double.NaN; import static java.lang.Math.PI; -import static java.lang.Math.abs; import static java.lang.Math.hypot; import static java.lang.Math.sqrt; import static utils.Math2.linInterp; @@ -38,84 +41,91 @@ import static utils.Math2.linInterp; */ public class Waterman { - private static final double[] xELD = - {(sqrt(3)-1)/8, sqrt(3)/8, 3*sqrt(3)/8, NaN}; - private static final double[] dYdL = {0, 1/(2*PI), 3/(2*PI), (2+sqrt(2))/(2*PI)}; + private static final double[] yELD = + {-(sqrt(3)-1)/8, -sqrt(3)/8, -3*sqrt(3)/8, NaN}; + private static final double[] dXdL = {0, 1/(2*PI), 3/(2*PI), (2+sqrt(2))/(2*PI)}; private static final double lonDiag = PI/(4+sqrt(8)); - private static final double sin15 = (sqrt(3)-1)/sqrt(8); - private static final double cos15 = (sqrt(3)+1)/sqrt(8); + private static final double sin15 = (sqrt(3)-1)/sqrt(8); // sin(15°) + private static final double cos15 = (sqrt(3)+1)/sqrt(8); // cos(15°) - public static double[] faceProject(double lat, double lon) { - double[] yELD = jointHeights(lon); - double desirLength = linInterp(lat, PI/2, 0, 0, totalLength(xELD, yELD)); + public static Projection FACE = new Projection( + "Waterman (face)", "A single face of Waterman's octohedral projection", + Shape.polygon(new double[][] {{0., 0.}, {0., -sqrt(3)/2.}, {1/2., -sqrt(3)/2.}}), + 0b1011, Type.OTHER, Property.COMPROMISE, 2) { - for (int i = 1; i < xELD.length; i ++) { //now that we've established the meridian, lets place our point on it - double l = hypot(xELD[i]-xELD[i-1], yELD[i]-yELD[i-1]); //inch up the meridian - if (l >= desirLength || i == xELD.length-1) //if it fits on this segment - return new double[] { - linInterp(desirLength, 0, l, xELD[i-1], xELD[i]), - linInterp(desirLength, 0, l, yELD[i-1], yELD[i]) }; //interpolate and return - else //if we have further to go - desirLength -= l; //mark off this length and continue - } - return null; - } - - public static double[] faceInverse(double x, double y) { - if (y > x-(sqrt(3)-1)/8 || y > x/sqrt(3) || y > (2-sqrt(3))*(x+3/4.) || - y > (7/4.+sqrt(3))-(2+sqrt(3))*x || x > sqrt(3)/2) //this describes the footprint of the octant - return null; - - double longitude; - int i = 0; - while (i < xELD.length-1 && xELD[i] < x) - i ++; //figure out in which segment it is - if (i <= 2) { //well-behaved segments? - longitude = y/linInterp(x, xELD[i-1], xELD[i], dYdL[i-1], dYdL[i]); - } - else { //the well-behaved part of the last segment? - longitude = y/linInterp(x, xELD[i-1], 2*sqrt(3), dYdL[i-1], dYdL[i]); - if (longitude > lonDiag) { //the diagonal part of the last segment? - double a = dYdL[2]*dYdL[3]*sin15; //surprisingly, the equation becomes quadratic here - double b = (dYdL[3]*cos15-dYdL[2])*(3*sqrt(3)/8-x) - dYdL[3]*sin15*y - dYdL[2]*(sqrt(3)/8+dYdL[3]*lonDiag*sin15); - double c = (sqrt(3)/8+dYdL[3]*lonDiag*sin15)*y + (1-dYdL[3]*lonDiag*cos15)*(3*sqrt(3)/8-x); - longitude = (-b - sqrt(b*b - 4*a*c))/(2*a); + public double[] project(double lat, double lon) { + double[] xELD = jointPositions(lon); + double desirLength = linInterp(lat, PI/2, 0, 0, totalLength(xELD, yELD)); + + for (int i = 1; i < xELD.length; i ++) { //now that we've established the meridian, lets place our point on it + double l = hypot(xELD[i]-xELD[i-1], yELD[i]-yELD[i-1]); //inch up the meridian + if (l >= desirLength || i == xELD.length-1) //if it fits on this segment + return new double[] { + linInterp(desirLength, 0, l, xELD[i-1], xELD[i]), + linInterp(desirLength, 0, l, yELD[i-1], yELD[i])}; //interpolate and return + else //if we have further to go + desirLength -= l; //mark off this length and continue } + return null; } - double[] yELD = jointHeights(longitude); - double totalLength = totalLength(xELD, yELD); - double pointLength = hypot(x-xELD[i-1], y-yELD[i-1]); - for (int j = 1; j < i; j ++) //add in the lengths of all previous segments - pointLength += hypot(xELD[j]-xELD[j-1], yELD[j]-yELD[j-1]); - double latitude = linInterp(pointLength, 0, totalLength, PI/2, 0); - return new double[] {latitude, longitude}; - } - - - private static double[] jointHeights(double lon) { //POSTCONDITION: this also sets the value of xELD[3] based on the longitude. An odd place for a side-effect? Bad coding practice to manipulate static variables? meh. I documented it, didn't I? - double[] yELD = new double[xELD.length]; //the height of the intersections of this meridian with the Equal Line Delineations - for (int i = 0; i < xELD.length; i ++) - yELD[i] = dYdL[i]*lon; - if (abs(lon) <= lonDiag) { //if it hits the equatorial ELD on the vertical (hexagon) part - xELD[3] = sqrt(3)/2.; + public double[] inverse(double x, double y) { + if (x > -y-(sqrt(3)-1)/8 || x > -y/sqrt(3) || x > (2-sqrt(3))*(3/4.-y) || + x > (7/4.+sqrt(3))+(2+sqrt(3))*y || y < -sqrt(3)/2) //this describes the footprint of the octant + return null; + + double longitude; + int i = 0; + while (i < yELD.length-1 && yELD[i] > y) + i ++; //figure out in which segment it is + if (i <= 2) { //well-behaved segments? + longitude = x/linInterp(y, yELD[i-1], yELD[i], dXdL[i-1], dXdL[i]); + } + else { //the well-behaved part of the last segment? + longitude = x/linInterp(y, yELD[i-1], -2*sqrt(3), dXdL[i-1], dXdL[i]); + if (longitude > lonDiag) { //the diagonal part of the last segment? + double a = dXdL[2]*dXdL[3]*sin15; //surprisingly, the equation becomes quadratic here + double b = (dXdL[3]*cos15-dXdL[2])*(y-yELD[2]) - dXdL[3]*sin15*x - dXdL[2]*(dXdL[3]*lonDiag*sin15-yELD[1]); + double c = (dXdL[3]*lonDiag*sin15-yELD[1])*x + (1/4.-dXdL[3]*lonDiag*cos15)*(y-yELD[2]); + longitude = (-b - sqrt(b*b - 4*a*c))/(2*a); + } + } + + double[] xELD = jointPositions(longitude); + double totalLength = totalLength(xELD, yELD); + double pointLength = hypot(x-xELD[i-1], y-yELD[i-1]); + for (int j = 1; j < i; j ++) //add in the lengths of all previous segments + pointLength += hypot(xELD[j]-xELD[j-1], yELD[j]-yELD[j-1]); + double latitude = linInterp(pointLength, 0, totalLength, PI/2, 0); + + return new double[] {latitude, longitude}; } - else { //if it hits it on the diagonal (square) part - xELD[3] = -(abs(lon)-lonDiag)*dYdL[3]*sin15 + sqrt(3)/2.; - yELD[3] = (abs(lon)-lonDiag)*dYdL[3]*cos15 + 1/4.; + + + private double[] jointPositions(double lon) { //POSTCONDITION: this also sets the value of yELD[3] based on the longitude. An odd place for a side-effect? Bad coding practice to manipulate static variables? meh. I documented it, didn't I? + double[] xELD = new double[yELD.length]; //the positions of the intersections of this meridian with the Equal Line Delineations + for (int i = 0; i < xELD.length; i ++) + xELD[i] = dXdL[i]*lon; + if (lon <= lonDiag) { //if it hits the equatorial ELD on the horizontal (hexagon) part + yELD[3] = -sqrt(3)/2.; + } + else { //if it hits it on the diagonal (square) part + xELD[3] = (lon-lonDiag)*dXdL[3]*cos15 + 1/4.; + yELD[3] = (lon-lonDiag)*dXdL[3]*sin15 - sqrt(3)/2.; + } + return xELD; } - return yELD; - } - - - private static double totalLength(double[] xs, double[] ys) { - double totalLength = 0; //compute meridian length - for (int i = 1; i < xELD.length; i ++) - totalLength += hypot(xs[i]-xs[i-1], ys[i]-ys[i-1]); - return totalLength; - } + + + private double totalLength(double[] xs, double[] ys) { + double totalLength = 0; //compute meridian length + for (int i = 1; i < xs.length; i ++) + totalLength += hypot(xs[i]-xs[i-1], ys[i]-ys[i-1]); + return totalLength; + } + }; }