Got a dead lawn but to lazy to do anything?

Hire a Water Man!

Get it? Waterman?

I implemented the Waterman projection. Well, mostly. The raster part doesn't actually work for what would correspond to Gene Keyes's "equatorial supple zone". The math got too complicated, so I want to push what I have. It also doesn't detatch Antarctica, but I probably just won't do that; I think it detracts from the value of the map.
I renamed Tetrahedral to Polyhedral in the process, because I had originally intended to use that framework before I decided Waterman needed its own class. I still might put Dymaxion in Polyhedral.
I also fixed a bug with Cahill-Keyes that was cutting off the top and bottom and removed the Butterfly variant of Cahill-Keyes.
This commit is contained in:
Justin Kunimune 2018-01-18 10:56:39 -10:00
parent f8dcf34d94
commit d5a792cdb7
8 changed files with 270 additions and 136 deletions

View File

@ -79,8 +79,9 @@ import maps.Projection;
import maps.Pseudocylindrical;
import maps.Arbitrary;
import maps.Snyder;
import maps.Tetrahedral;
import maps.Polyhedral;
import maps.Tobler;
import maps.Waterman;
import maps.WinkelTripel;
import utils.Flag;
import utils.Math2;
@ -106,12 +107,12 @@ public abstract class MapApplication extends Application {
private static final KeyCombination CTRL_ENTER = new KeyCodeCombination(KeyCode.ENTER, KeyCodeCombination.CONTROL_DOWN);
public static final Projection[] FEATURED_PROJECTIONS = { Cylindrical.MERCATOR,
public static final Projection[] FEATURED_PROJECTIONS = { Waterman.BUTTERFLY, Cylindrical.MERCATOR,
Cylindrical.EQUIRECTANGULAR, Cylindrical.EQUAL_AREA, Cylindrical.GALL_STEREOGRAPHIC,
Azimuthal.STEREOGRAPHIC, Azimuthal.POLAR, Azimuthal.EQUAL_AREA, Azimuthal.GNOMONIC,
Azimuthal.PERSPECTIVE, Conic.LAMBERT, Conic.EQUIDISTANT, Conic.ALBERS,
Tetrahedral.LEE_TETRAHEDRAL_RECTANGULAR, Tetrahedral.ACTUAUTHAGRAPH,
Tetrahedral.AUTHAPOWER, CahillKeyes.M_MAP, Pseudocylindrical.SINUSOIDAL,
Polyhedral.LEE_TETRAHEDRAL_RECTANGULAR, Polyhedral.ACTUAUTHAGRAPH,
Polyhedral.AUTHAPOWER, CahillKeyes.M_MAP, Pseudocylindrical.SINUSOIDAL,
Pseudocylindrical.MOLLWEIDE, Tobler.TOBLER, Lenticular.AITOFF,
Lenticular.VAN_DER_GRINTEN, Arbitrary.ROBINSON, WinkelTripel.WINKEL_TRIPEL,
Misc.PEIRCE_QUINCUNCIAL, Misc.TWO_POINT_EQUIDISTANT, Pseudocylindrical.LEMONS }; //the set of featured projections for the ComboBox
@ -125,9 +126,8 @@ public abstract class MapApplication extends Application {
{ Azimuthal.EQUAL_AREA, Azimuthal.POLAR, Azimuthal.GNOMONIC, Azimuthal.ORTHOGRAPHIC,
Azimuthal.PERSPECTIVE, Azimuthal.STEREOGRAPHIC },
{ Conic.ALBERS, Conic.LAMBERT, Conic.EQUIDISTANT },
{ Tetrahedral.AUTHAGRAPH, CahillKeyes.BUTTERFLY, CahillKeyes.M_MAP,
CahillKeyes.HALF_OCTANT, Tetrahedral.LEE_TETRAHEDRAL_RECTANGULAR,
Tetrahedral.LEE_TETRAHEDRAL_TRIANGULAR },
{ Polyhedral.AUTHAGRAPH, CahillKeyes.M_MAP, Polyhedral.LEE_TETRAHEDRAL_RECTANGULAR,
Polyhedral.LEE_TETRAHEDRAL_TRIANGULAR, Waterman.BUTTERFLY },
{ Pseudocylindrical.ECKERT_IV, Pseudocylindrical.KAVRAYSKIY_VII,
Pseudocylindrical.MOLLWEIDE, Arbitrary.NATURAL_EARTH, Arbitrary.ROBINSON,
Pseudocylindrical.SINUSOIDAL, Tobler.TOBLER },
@ -135,9 +135,9 @@ public abstract class MapApplication extends Application {
Lenticular.VAN_DER_GRINTEN, WinkelTripel.WINKEL_TRIPEL },
{ Snyder.GS50, Misc.GUYOU, Misc.HAMMER_RETROAZIMUTHAL, Pseudocylindrical.LEMONS,
Misc.PEIRCE_QUINCUNCIAL, Misc.TWO_POINT_EQUIDISTANT, Misc.FLAT_EARTH },
{ MyProjections.EXPERIMENT, Tetrahedral.AUTHAPOWER, Tetrahedral.ACTUAUTHAGRAPH,
{ MyProjections.EXPERIMENT, Polyhedral.AUTHAPOWER, Polyhedral.ACTUAUTHAGRAPH,
MyProjections.MAGNIFIER, MyProjections.PSEUDOSTEREOGRAPHIC,
Tetrahedral.TETRAGRAPH, Tetrahedral.TETRAPOWER,
Polyhedral.TETRAGRAPH, Polyhedral.TETRAPOWER,
MyProjections.TWO_POINT_EQUALIZED } }; // every projection I have programmed
private static final String[] ASPECT_NAMES = { "Standard", "Transverse", "Cassini", "Atlantis",

View File

@ -31,7 +31,6 @@ import java.io.PrintStream;
import javax.imageio.ImageIO;
import maps.Azimuthal;
import maps.CahillKeyes;
import maps.Conic;
import maps.Cylindrical;
import maps.Lenticular;
@ -41,8 +40,9 @@ import maps.Projection;
import maps.Pseudocylindrical;
import maps.Arbitrary;
import maps.Snyder;
import maps.Tetrahedral;
import maps.Polyhedral;
import maps.Tobler;
import maps.Waterman;
import maps.WinkelTripel;
import utils.ImageUtils;
import utils.PixelMap;
@ -63,14 +63,14 @@ public class MapExplainer {
Azimuthal.STEREOGRAPHIC.transverse(), Azimuthal.POLAR.transverse(),
Azimuthal.EQUAL_AREA.transverse(), Azimuthal.GNOMONIC.transverse(),
Azimuthal.ORTHOGRAPHIC.transverse(), Conic.LAMBERT, Conic.EQUIDISTANT, Conic.ALBERS,
Tetrahedral.LEE_TETRAHEDRAL_RECTANGULAR, Tetrahedral.AUTHAGRAPH,
Polyhedral.LEE_TETRAHEDRAL_RECTANGULAR, Polyhedral.AUTHAGRAPH,
Pseudocylindrical.SINUSOIDAL, Pseudocylindrical.MOLLWEIDE, Tobler.TOBLER,
Lenticular.HAMMER, Lenticular.AITOFF, Lenticular.VAN_DER_GRINTEN, Arbitrary.ROBINSON,
WinkelTripel.WINKEL_TRIPEL, CahillKeyes.BUTTERFLY, Misc.PEIRCE_QUINCUNCIAL.transverse(),
WinkelTripel.WINKEL_TRIPEL, Waterman.BUTTERFLY, Misc.PEIRCE_QUINCUNCIAL.transverse(),
Snyder.GS50, Misc.TWO_POINT_EQUIDISTANT, Misc.HAMMER_RETROAZIMUTHAL, Misc.FLAT_EARTH },
{ MyProjections.PSEUDOSTEREOGRAPHIC, Tetrahedral.TETRAGRAPH, Tetrahedral.AUTHAPOWER,
Tetrahedral.TETRAPOWER, MyProjections.TWO_POINT_EQUALIZED.transverse() } };
{ MyProjections.PSEUDOSTEREOGRAPHIC, Polyhedral.TETRAGRAPH, Polyhedral.AUTHAPOWER,
Polyhedral.TETRAPOWER, MyProjections.TWO_POINT_EQUALIZED.transverse() } };
public static void main(String[] args) throws IOException {

View File

@ -43,7 +43,7 @@ import maps.Lenticular;
import maps.Misc;
import maps.Projection;
import maps.Arbitrary;
import maps.Tetrahedral;
import maps.Polyhedral;
import maps.Tobler;
import maps.WinkelTripel;
@ -57,7 +57,7 @@ public class MapOptimizer extends Application {
private static final Projection[] EXISTING_PROJECTIONS = { Cylindrical.HOBO_DYER,
Arbitrary.ROBINSON, Lenticular.VAN_DER_GRINTEN, Misc.PEIRCE_QUINCUNCIAL };
private static final Projection[] PROJECTIONS_TO_OPTIMIZE = { Tobler.TOBLER,
WinkelTripel.WINKEL_TRIPEL, Tetrahedral.TETRAPOWER, Tetrahedral.AUTHAPOWER };
WinkelTripel.WINKEL_TRIPEL, Polyhedral.TETRAPOWER, Polyhedral.AUTHAPOWER };
private static final double[] WEIGHTS = { 0., .125, .25, .375, .5, .625, .75, .875, 1. };
private static final int NUM_BRUTE_FORCE = 100;
private static final int NUM_DESCENT = 30;

View File

@ -55,7 +55,7 @@ import maps.MyProjections;
import maps.Projection;
import maps.Pseudocylindrical;
import maps.Arbitrary;
import maps.Tetrahedral;
import maps.Polyhedral;
import maps.Tobler;
import maps.WinkelTripel;
@ -76,8 +76,8 @@ public class MapPlotter extends Application {
Arbitrary.NATURAL_EARTH, Pseudocylindrical.KAVRAYSKIY_VII, Tobler.TOBLER };
private static final Projection[] PSEUDOAZM = { Lenticular.AITOFF,
MyProjections.PSEUDOSTEREOGRAPHIC };
private static final Projection[] TETRAHEDRAL = { Tetrahedral.LEE_TETRAHEDRAL_RECTANGULAR,
Tetrahedral.ACTUAUTHAGRAPH, Tetrahedral.AUTHAGRAPH, Tetrahedral.TETRAPOWER };
private static final Projection[] TETRAHEDRAL = { Polyhedral.LEE_TETRAHEDRAL_RECTANGULAR,
Polyhedral.ACTUAUTHAGRAPH, Polyhedral.AUTHAGRAPH, Polyhedral.TETRAPOWER };
private static final Projection[] CHEATY = { Pseudocylindrical.LEMONS,
CahillKeyes.HALF_OCTANT };
private static final Projection[] OTHER = { Lenticular.VAN_DER_GRINTEN,

View File

@ -29,9 +29,8 @@ import utils.Math2;
import utils.NumericalAnalysis;
/**
* A truncated octohedral map. The projection is Cahill-Keyes, because out of it, Cahill, and Waterman,
* it was by far the best-documented, and it's the newest of them so it's presumably best (not that
* Tobler World in a Square wasn't invented after Lambert EAC).
* A truncated octohedral map. The projection is Cahill-Keyes, because it was invented after Cahill,
* so it is presumably better (not that Tobler World in a Square wasn't invented after Lambert EAC).
* http://www.genekeyes.com/CKOG-OOo/7-CKOG-illus-&-coastline.html
*
* @author jkunimune
@ -41,7 +40,7 @@ public class CahillKeyes {
private static final double lMA = 940; //the distance from triangle vertex M to octant vertex A (the red length)
private static final double lMG = 10000; //the altitude of the triangle
private static final double lNG = lMG/Math.sqrt(3); //the height of the triangle
private static final double lENy = lMA*(Math.sqrt(3)+1)/Math.sqrt(8); //the height difference between the triangle and the octant
private static final double lENy = lMA*Math.sqrt(3)/2; //the height difference between the triangle and the octant
private static final double lMB = lMA*2/(Math.sqrt(3)-1); //the distance from triangle vertex M to octant vertex B (the blue length)
private static final double lGF = lMG/Math.sqrt(3) - lMB; //the distance from triangle vertex G to octant vertex F
private static final double lFE = lMA*Math.sqrt(2)/(Math.sqrt(3)-1); //the distance from octant vertex A to octant vertex B (the green length)
@ -61,60 +60,15 @@ public class CahillKeyes {
private static final double TOLERANCE = 10; //this is a reasonable tolerance when you recall that we're operating on the order of 10,000 units
public static final Projection BUTTERFLY =
new Projection(
"Cahill-Keyes Butterfly", "An aesthetically pleasing octohedral map arrangement.",
4*lMG-2*lMA, 4*lNG-2*lENy, 0b1110, Type.TETRADECAHEDRAL, Property.COMPROMISE) {
private final double OFFSET_Y = 10000/Math.sqrt(3);
public double[] project(double lat, double lon) {
final double centralLon = Math.floor(lon/(Math.PI/2))*Math.PI/2 + Math.PI/4; //get the octant longitude
final double[] mjCoords = innerProjectD(
Math.toDegrees(Math.abs(lat)), Math.toDegrees(Math.abs(lon - centralLon)));
double mjX = mjCoords[0];
double mjY = mjCoords[1];
if (lat < 0) //if it's in the souther hemisphere
mjX = 2*lMG - mjX; //flip it around
if (lon < centralLon) //if the relative longitude is negative
mjY = -mjY; //flip it the other way
return new double[] {
mjX*Math.sin(centralLon*2/3) + mjY*Math.cos(centralLon*2/3),
-mjX*Math.cos(centralLon*2/3) + mjY*Math.sin(centralLon*2/3) + OFFSET_Y };
}
public double[] inverse(double x, double y) {
y -= OFFSET_Y;
double tht = Math.atan2(x, -y);
if (Math.abs(tht) > 5*Math.PI/6) return null; //only show a little bit of extra
double quadrAngle = (Math.floor(tht/(Math.PI/3))+2)*Math.PI/3; //the angle of the centre of the quadrant, measured widdershins from -y
double centralLon = quadrAngle*1.5 - 3*Math.PI/4; //the central meridian of this quadrant
double mjX = -x*Math.cos(quadrAngle) - y*Math.sin(quadrAngle);
double mjY = x*Math.sin(quadrAngle) - y*Math.cos(quadrAngle);
double[] relCoords = innerInverseD(Math.min(mjX, 2*lMG-mjX), Math.abs(mjY));
if (relCoords == null)
return null;
if (mjY < 0) relCoords[1] *= -1; //the left half of the octant gets shifted west
if (mjX > lMG) relCoords[0] *= -1; //the outer rim of the map is the southern hemisphere
return new double[] { Math.toRadians(relCoords[0]),
Math.toRadians(relCoords[1]) + centralLon };
}
};
public static final Projection M_MAP =
new Projection(
"Cahill-Keyes M-Profile", "A horizontally arranged octohedral map designed to end all maps.",
4*lMG, 3*lNG-2*lENy, 0b1110, Type.TETRADECAHEDRAL, Property.COMPROMISE) {
"Cahill-Keyes", "A horizontally arranged octohedral map designed to end all maps.",
4*lMG, 3*lNG-2*lENy, 0b1010, Type.TETRADECAHEDRAL, Property.COMPROMISE) {
public double[] project(double lat, double lon) {
final int octantNum = (int)Math.floor(lon/(Math.PI/2));
final double centralLon = octantNum*Math.PI/2 + Math.PI/4; //get the octant longitude
final double[] mjCoords = innerProjectD(
final double[] mjCoords = faceProjectD(
Math.toDegrees(Math.abs(lat)), Math.toDegrees(Math.abs(lon - centralLon)));
double mjX = 10000 - mjCoords[0];
double mjY = mjCoords[1];
@ -141,7 +95,7 @@ public class CahillKeyes {
double diagX = rotDirec*cosRot*(x-offsetX) + sinRot*y;
double diagY = sinRot*(x-offsetX) - rotDirec*cosRot*y;
double[] relCoords = innerInverseD(lMG - Math.abs(diagX), Math.abs(diagY));
double[] relCoords = faceInverseD(lMG - Math.abs(diagX), Math.abs(diagY));
if (relCoords == null)
return null;
@ -156,19 +110,19 @@ public class CahillKeyes {
public static final Projection HALF_OCTANT =
new Projection(
"Cahill-Keyes Half-Octant", "A single sixteenth of a Cahill-Keyes projection.",
lMG-lMA, lNG, 0b0111, Type.TETRADECAHEDRAL, Property.COMPROMISE) {
lMG-lMA, lNG, 0b0011, Type.TETRADECAHEDRAL, Property.COMPROMISE) {
public double[] project(double lat, double lon) {
lat = Math.max(0, lat);
lon = Math.max(0, Math.min(Math.PI/4, lon));
final double[] mjCoords = innerProjectD(
final double[] mjCoords = faceProjectD(
Math2.round(Math.toDegrees(Math.abs(lat)),9), //I apply the round so that graticule lines exactly on the thing get done properly
Math2.round(Math.toDegrees(Math.abs(lon)),9));
return new double[] { mjCoords[0] - (lMG+lMA)/2, mjCoords[1] - lNG/2 };
}
public double[] inverse(double x, double y) {
double[] coordsD = innerInverseD(x + (lMG+lMA)/2, y + lNG/2);
double[] coordsD = faceInverseD(x + (lMG+lMA)/2, y + lNG/2);
if (coordsD == null)
return null;
else
@ -178,7 +132,7 @@ public class CahillKeyes {
private static final double[] innerProjectD(double latD, double lonD) { //convert adjusted lat and lon in degrees to Mary Jo's coordinates
private static final double[] faceProjectD(double latD, double lonD) { //convert adjusted lat and lon in degrees to Mary Jo's coordinates
final double[][] mer = meridian(lonD);
if (latD >= 75) { //zone c (frigid zone)
@ -208,7 +162,7 @@ public class CahillKeyes {
}
private static final double[] innerInverseD(double x, double y) { //convert Mary Jo's coordinates to relative lat and lon in degrees
private static final double[] faceInverseD(double x, double y) { //convert Mary Jo's coordinates to relative lat and lon in degrees
if (y > x-lMA || y > x/Math.sqrt(3) || y > x*(2-Math.sqrt(3))+bDE ||
y > (lMG-x)*(2+Math.sqrt(3))+lGF || x > lMG) //this describes the footprint of the octant
return null;

View File

@ -29,27 +29,27 @@ import utils.Math2;
import utils.NumericalAnalysis;
/**
* Projections created by projecting onto and then unfolding a regular tetrahedron
* Projections created by projecting onto and then unfolding some kind of polyhedron
*
* @author jkunimune
*/
public class Tetrahedral {
public class Polyhedral {
private static final double ASIN_ONE_THD = Math.asin(1/3.); //the complement of the angular radius of a face
public static final TetrahedralProjection LEE_TETRAHEDRAL_RECTANGULAR =
new TetrahedralProjection(
"Lee Tetrahedral", 0b1001, Configuration.WIDE_FACE, Property.CONFORMAL,
public static final PolyhedralProjection LEE_TETRAHEDRAL_RECTANGULAR =
new PolyhedralProjection(
"Lee Tetrahedral", 0b1001, Configuration.TETRAHEDRON_WIDE_FACE, Property.CONFORMAL,
null, "that really deserves more attention") {
public double[] innerProject(double lat, double lon) {
public double[] faceProject(double lat, double lon) {
final de.jtem.mfc.field.Complex z = de.jtem.mfc.field.Complex.fromPolar(
Math.pow(2, 5/6.)*Math.tan(Math.PI/4-lat/2), lon);
final de.jtem.mfc.field.Complex w = Dixon.invFunc(z);
return new double[] { w.abs()*1.132, w.arg() }; //I don't understand Dixon functions well enough to say whence the 1.132 comes
}
public double[] innerInverse(double r, double tht) {
public double[] faceInverse(double r, double tht) {
final de.jtem.mfc.field.Complex w = de.jtem.mfc.field.Complex.fromPolar(
r/1.132, tht);
final de.jtem.mfc.field.Complex ans = Dixon.leeFunc(w).times(Math.pow(2, -5/6.));
@ -60,34 +60,34 @@ public class Tetrahedral {
};
public static final TetrahedralProjection LEE_TETRAHEDRAL_TRIANGULAR =
new TetrahedralProjection(
public static final PolyhedralProjection LEE_TETRAHEDRAL_TRIANGULAR =
new PolyhedralProjection(
"Lee Tetrahedral (triangular)", 0b1001, Configuration.TRIANGLE_FACE, Property.CONFORMAL,
null, "in a triangle, because this is the form in which is was published, even though the rectangle is clearly better") {
public double[] innerProject(double lat, double lon) {
return LEE_TETRAHEDRAL_RECTANGULAR.innerProject(lat, lon);
public double[] faceProject(double lat, double lon) {
return LEE_TETRAHEDRAL_RECTANGULAR.faceProject(lat, lon);
}
public double[] innerInverse(double r, double tht) {
return LEE_TETRAHEDRAL_RECTANGULAR.innerInverse(r, tht);
public double[] faceInverse(double r, double tht) {
return LEE_TETRAHEDRAL_RECTANGULAR.faceInverse(r, tht);
}
};
public static final TetrahedralProjection TETRAGRAPH =
new TetrahedralProjection(
"TetraGraph", 0b1111, Configuration.WIDE_FACE, Property.EQUIDISTANT,
public static final PolyhedralProjection TETRAGRAPH =
new PolyhedralProjection(
"TetraGraph", 0b1111, Configuration.TETRAHEDRON_WIDE_FACE, Property.EQUIDISTANT,
null, "that I invented") {
public double[] innerProject(double lat, double lon) {
public double[] faceProject(double lat, double lon) {
final double tht = lon - Math.floor((lon+Math.PI/3)/(2*Math.PI/3))*(2*Math.PI/3);
return new double[] {
Math.atan(1/Math.tan(lat)*Math.cos(tht))/Math.cos(tht) /Math.atan(Math.sqrt(2)),
lon };
}
public double[] innerInverse(double r, double tht) {
public double[] faceInverse(double r, double tht) {
final double t0 = Math.floor((tht+Math.PI/3)/(2*Math.PI/3))*(2*Math.PI/3);
final double dt = tht-t0;
return new double[] {
@ -97,8 +97,8 @@ public class Tetrahedral {
};
public static final TetrahedralProjection AUTHAGRAPH =
new TetrahedralProjection(
public static final PolyhedralProjection AUTHAGRAPH =
new PolyhedralProjection(
"AuthaGraph", "A hip new Japanese map that is almost equal-area.",
0b1011, Configuration.AUTHAGRAPH, Property.COMPROMISE) {
@ -116,14 +116,14 @@ public class Tetrahedral {
}
public double[] innerProject(double lat, double lon) {
public double[] faceProject(double lat, double lon) {
final double lam = lon - (Math.floor(lon/(2*Math.PI/3))*(2*Math.PI/3)+Math.PI/3);
final double tht = Math.atan((lam - Math.asin(Math.sin(lam)/Math.sqrt(3)))/Math.PI*Math.sqrt(12));
final double p = (Math.PI/2 - lat) / Math.atan(Math.sqrt(2)/Math.cos(lam));
return new double[] { Math.pow(p,.707)*Math.sqrt(3)/Math.cos(tht), (lon - lam)/2 + tht };
}
protected double[] innerInverse(double r, double th) {
protected double[] faceInverse(double r, double th) {
final double dt = th - (Math.floor(th/(Math.PI/3))*(Math.PI/3)+Math.PI/6);
final double dl = NumericalAnalysis.newtonRaphsonApproximation(dt, dt*2,
(l) -> Math.atan((l - Math.asin(Math.sin(l)/Math.sqrt(3)))/Math.PI*Math.sqrt(12)),
@ -137,10 +137,10 @@ public class Tetrahedral {
};
public static final TetrahedralProjection AUTHAPOWER =
new TetrahedralProjection(
public static final PolyhedralProjection AUTHAPOWER =
new PolyhedralProjection(
"AuthaPower", "A parametrised, rearranged version of my AuthaGraph approximation.",
0b1011, Configuration.WIDE_VERTEX, Property.COMPROMISE,
0b1011, Configuration.TETRAHEDRON_WIDE_VERTEX, Property.COMPROMISE,
new String[] {"Power"}, new double[][] {{.25,1,.7}}) {
private double k;
@ -149,14 +149,14 @@ public class Tetrahedral {
this.k = params[0];
}
public double[] innerProject(double lat, double lon) {
public double[] faceProject(double lat, double lon) {
final double lam = lon - (Math.floor(lon/(2*Math.PI/3))*(2*Math.PI/3)+Math.PI/3);
final double tht = Math.atan((lam - Math.asin(Math.sin(lam)/Math.sqrt(3)))/Math.PI*Math.sqrt(12));
final double p = (Math.PI/2 - lat) / Math.atan(Math.sqrt(2)/Math.cos(lam));
return new double[] { Math.pow(p,k)*Math.sqrt(3)/Math.cos(tht), (lon - lam)/2 + tht };
}
protected double[] innerInverse(double r, double th) {
protected double[] faceInverse(double r, double th) {
final double dt = th - (Math.floor(th/(Math.PI/3))*(Math.PI/3)+Math.PI/6);
final double dl = NumericalAnalysis.newtonRaphsonApproximation(dt, dt*2,
(l) -> Math.atan((l - Math.asin(Math.sin(l)/Math.sqrt(3)))/Math.PI*Math.sqrt(12)),
@ -170,10 +170,10 @@ public class Tetrahedral {
};
public static final TetrahedralProjection ACTUAUTHAGRAPH =
new TetrahedralProjection(
public static final PolyhedralProjection ACTUAUTHAGRAPH =
new PolyhedralProjection(
"EquaHedral", "A holey authagraphic tetrahedral projection to put AuthaGraph to shame.",
0b1010, Configuration.WIDE_VERTEX, Property.EQUAL_AREA,
0b1010, Configuration.TETRAHEDRON_WIDE_VERTEX, Property.EQUAL_AREA,
new String[] {"Rho"}, new double[][] {{0,.5,.25}}) {
private double r02;
@ -182,7 +182,7 @@ public class Tetrahedral {
this.r02 = 3*Math.pow(params[0], 2);
}
public double[] innerProject(double lat, double lon) {
public double[] faceProject(double lat, double lon) {
final double lam = lon - (Math.floor(lon/(2*Math.PI/3))*(2*Math.PI/3)+Math.PI/3);
final double tht = Math.atan((lam - Math.asin(Math.sin(lam)/Math.sqrt(3)))/Math.PI*Math.sqrt(12));
return new double[] {
@ -190,7 +190,7 @@ public class Tetrahedral {
(lon - lam)/2 + tht };
}
public double[] innerInverse(double r, double th) {
public double[] faceInverse(double r, double th) {
final double dt = th - (Math.floor(th/(Math.PI/3))*(Math.PI/3)+Math.PI/6);
final double R2 = Math.pow(r*Math.cos(dt), 2);
if (R2 < r02) return null;
@ -204,10 +204,10 @@ public class Tetrahedral {
};
public static final TetrahedralProjection TETRAPOWER =
new TetrahedralProjection(
public static final PolyhedralProjection TETRAPOWER =
new PolyhedralProjection(
"TetraPower", "A parameterised tetrahedral projection that I invented.", 0b1111,
Configuration.WIDE_FACE, Property.COMPROMISE, new String[] {"k1","k2","k3"},
Configuration.TETRAHEDRON_WIDE_FACE, Property.COMPROMISE, new String[] {"k1","k2","k3"},
new double[][] {{.01,2.,.98},{.01,2.,1.2},{.01,2.,.98}}) {
private double k1, k2, k3;
@ -218,7 +218,7 @@ public class Tetrahedral {
this.k3 = params[2];
}
public double[] innerProject(double lat, double lon) {
public double[] faceProject(double lat, double lon) {
final double t0 = Math.floor((lon+Math.PI/3)/(2*Math.PI/3))*(2*Math.PI/3);
final double tht = lon - t0;
final double thtP = Math.PI/3*(1 - Math.pow(1-Math.abs(tht)/(Math.PI/2),k1))/(1 - 1/Math.pow(3,k1))*Math.signum(tht);
@ -230,7 +230,7 @@ public class Tetrahedral {
thtP + t0 };
}
public double[] innerInverse(double r, double tht) {
public double[] faceInverse(double r, double tht) {
final double t0 = Math.floor((tht+Math.PI/3)/(2*Math.PI/3))*(2*Math.PI/3);
final double thtP = tht-t0;
final double lamS = (1-Math.pow(1-Math.abs(thtP)*(1-1/Math.pow(3,k1))/(Math.PI/3), 1/k1))*Math.PI/2*Math.signum(thtP);
@ -250,12 +250,12 @@ public class Tetrahedral {
*
* @author jkunimune
*/
private static abstract class TetrahedralProjection extends Projection {
private static abstract class PolyhedralProjection extends Projection {
private final Configuration configuration;
public TetrahedralProjection(
public PolyhedralProjection(
String name, int fisc, Configuration config, Property property,
String adjective, String addendum) {
super(name, config.width, config.height, fisc, Type.TETRAHEDRAL, property,
@ -263,13 +263,13 @@ public class Tetrahedral {
this.configuration = config;
}
public TetrahedralProjection(
public PolyhedralProjection(
String name, String description, int fisc, Configuration config, Property property) {
super(name, description, config.width, config.height, fisc, Type.TETRAHEDRAL, property);
this.configuration = config;
}
public TetrahedralProjection(
public PolyhedralProjection(
String name, String description, int fisc, Configuration config, Property property,
String[] paramNames, double[][] paramValues) {
super(name, description, config.width, config.height, fisc, Type.TETRAHEDRAL, property,
@ -278,9 +278,9 @@ public class Tetrahedral {
}
protected abstract double[] innerProject(double lat, double lon); //the projection from spherical to polar within a face
protected abstract double[] faceProject(double lat, double lon); //the projection from spherical to polar within a face
protected abstract double[] innerInverse(double x, double y); //I think you can guess
protected abstract double[] faceInverse(double x, double y); //I think you can guess
public double[] project(double lat, double lon) {
@ -297,7 +297,7 @@ public class Tetrahedral {
}
}
final double[] rth = innerProject(latR, lonR); //apply the projection to the relative coordinates
final double[] rth = faceProject(latR, lonR); //apply the projection to the relative coordinates
final double r = rth[0];
final double th = rth[1] + centrum[3];
final double x0 = centrum[4];
@ -329,7 +329,7 @@ public class Tetrahedral {
final double y0 = centrum[5];
double[] relCoords =
innerInverse(Math.hypot(x-x0, y-y0), Math.atan2(y-y0, x-x0) - th0);
faceInverse(Math.hypot(x-x0, y-y0), Math.atan2(y-y0, x-x0) - th0);
if (relCoords == null)
return null;
@ -351,7 +351,7 @@ public class Tetrahedral {
private static enum Configuration {
/* LATITUDE, LONGITUDE, STD_PRLL, PLANE_ROT, X, Y */
WIDE_FACE(6, 2*Math.sqrt(3), new double[][] { // [<|>] arrangement, face-centred
TETRAHEDRON_WIDE_FACE(6, 2*Math.sqrt(3), new double[][] { // [<|>] arrangement, face-centred
{ ASIN_ONE_THD, Math.PI, -2*Math.PI/3, -2*Math.PI/3, 2, Math.sqrt(3) },
{ ASIN_ONE_THD, Math.PI, 2*Math.PI/3, -Math.PI/3, -2, Math.sqrt(3) },
{-Math.PI/2, 0, 2*Math.PI/3, 2*Math.PI/3, 2, -Math.sqrt(3) },
@ -371,7 +371,7 @@ public class Tetrahedral {
return y > Math.sqrt(3)*Math.abs(x) - 3;
}
},
WIDE_VERTEX(6, 2*Math.sqrt(3), new double[][] { // [<|>] arrangement, vertex-centred
TETRAHEDRON_WIDE_VERTEX(6, 2*Math.sqrt(3), new double[][] { // [<|>] arrangement, vertex-centred
{ Math.PI/2, 0, Math.PI/3, -Math.PI/3, 0, Math.sqrt(3) },
{-ASIN_ONE_THD, 0, 2*Math.PI/3, Math.PI/3, 0, -Math.sqrt(3) },
{-ASIN_ONE_THD, 2*Math.PI/3, -2*Math.PI/3, Math.PI, 3, 0 },
@ -382,23 +382,35 @@ public class Tetrahedral {
},
AUTHAGRAPH(4*Math.sqrt(3), 3, new double[][] { // |\/\/`| arrangement, vertex-centred
{-ASIN_ONE_THD, Math.PI, 0, -Math.PI/2,-2*Math.sqrt(3)-.6096, 1.5 },
{-ASIN_ONE_THD,-Math.PI/3,-2*Math.PI/3, Math.PI/2,-Math.sqrt(3)-.6096, -1.5 },
{ Math.PI/2, 0, 0, -Math.PI/2, 0-.6096, 1.5 },
{-ASIN_ONE_THD, Math.PI/3, 2*Math.PI/3, Math.PI/2, Math.sqrt(3)-.6096, -1.5 },
{-ASIN_ONE_THD,-Math.PI/3, -2*Math.PI/3, Math.PI/2,-Math.sqrt(3)-.6096, -1.5 },
{ Math.PI/2, 0, 0, -Math.PI/2, 0-.6096, 1.5 },
{-ASIN_ONE_THD, Math.PI/3, 2*Math.PI/3, Math.PI/2, Math.sqrt(3)-.6096, -1.5 },
{-ASIN_ONE_THD, Math.PI, 0, -Math.PI/2, 2*Math.sqrt(3)-.6096, 1.5 },
{-ASIN_ONE_THD,-Math.PI/3,-2*Math.PI/3, Math.PI/2, 3*Math.sqrt(3)-.6096, -1.5}}) {
{-ASIN_ONE_THD,-Math.PI/3, -2*Math.PI/3, Math.PI/2, 3*Math.sqrt(3)-.6096, -1.5}}) {
@Override public double[] rotateOOB(double[] oob) {
return new double[] { (oob[0]+3*width/2)%width-width/2, oob[1] };
}
};
/* LATITUDE, LONGITUDE, STD_PRLL, PLANE_ROT, X, Y */
},
WATERMAN_5(-2*Math.sqrt(3), 2*Math.sqrt(3), -6, 2, new double[][] { // <vv> butterfly arrangement, W5 polyhedron
{ Math.PI/2, 0, -3*Math.PI/4, -Math.PI, 0, 0 },
{ Math.PI/2, 0, -Math.PI/4, -2*Math.PI/3, 0, 0 },
{ Math.PI/2, 0, Math.PI/4, -Math.PI/3, 0, 0 },
{ Math.PI/2, 0, 3*Math.PI/4, 0, 0, 0 }});
/* LATITUDE, LONGITUDE, STD_PRLL, PLANE_ROT, X, Y */
public final double width, height; //the width and height of a map with this configuration
public final double xMin, yMin, width, height; //the width and height of a map with this configuration
public final double[][] centrumSet; //the mathematical information about this configuration
private Configuration(double width, double height, double[][] centrumSet) {
this.width = width;
this.height = height;
this(-width/2, width/2, -height/2, height/2, centrumSet);
}
private Configuration(
double xMin, double xMax, double yMin, double yMax, double[][] centrumSet) {
this.xMin = xMin;
this.yMin = yMin;
this.width = xMax - xMin;
this.height = yMax - yMin;
this.centrumSet = centrumSet;
}

View File

@ -561,8 +561,8 @@ public abstract class Projection {
CYLINDRICAL("Cylindrical"), CONIC("Conic"), AZIMUTHAL("Azimuthal"),
PSEUDOCYLINDRICAL("Pseudocylindrical"), PSEUDOCONIC("Pseudoconic"),
PSEUDOAZIMUTHAL("Pseudoazimuthal"), QUASIAZIMUTHAL("Quasiazimuthal"),
TETRAHEDRAL("Tetrahedral"), TETRADECAHEDRAL("Tetradecahedral"), POLYNOMIAL("Polynomial"),
STREBE("Strebe"), PLANAR("Planar"), OTHER("Other");
TETRAHEDRAL("Tetrahedral"), TETRADECAHEDRAL("Truncated Octohedral"),
POLYNOMIAL("Polynomial"), STREBE("Strebe Blend"), PLANAR("Planar"), OTHER("Other");
private String name;

168
src/maps/Waterman.java Normal file
View File

@ -0,0 +1,168 @@
package maps;
import java.util.Arrays;
import maps.Projection.Property;
import maps.Projection.Type;
import utils.Math2;
/**
* MIT License
*
* Copyright (c) 2017 Justin Kunimune
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* A class devoted to the Waterman butterfly, because it doesn't really fit into Polyhedral.java or
* CahillKeyes.java, and Misc.java has too many projections already.
* http://www.watermanpolyhedron.com/methodp.html
*
* @author jkunimune
*/
public class Waterman {
private static final double[] xELD =
{(Math.sqrt(3)-1)/2, .5*Math.sqrt(3), 1.5*Math.sqrt(3), Double.NaN};
private static final double[] dYdL = {0, 2/Math.PI, 6/Math.PI, (4+Math.sqrt(8))/Math.PI};
private static final double lossX = (Math.sqrt(3)-1)/2; //horizontal dimension interruption loss
private static final double lossY = lossX*Math.sqrt(3)/2; //vertical dimension interruption loss
private static final double lonDiag = Math.PI/(4+Math.sqrt(8));
private static final double sin15 = (Math.sqrt(3)-1)/Math.sqrt(8);
private static final double cos15 = (Math.sqrt(3)+1)/Math.sqrt(8);
public static final Projection BUTTERFLY =
new Projection(
"Waterman", "An aesthetically pleasing octohedral map arrangement.",
8*Math.sqrt(3)-2*lossX, 8-2*lossY, 0b1010,
Type.TETRADECAHEDRAL, Property.COMPROMISE) {
public double[] project(double lat, double lon) {
double centralLon = Math.floor(lon/(Math.PI/2))*Math.PI/2 + Math.PI/4; //get the octant longitude
if (lon == Math.PI) centralLon = 3*Math.PI/4; //a hack to fix a minor issue with the IDL
double[] mjCoords = faceProject(Math.abs(lat), Math.abs(lon - centralLon));
double mjX = mjCoords[0]; //"mj" stands for "Mary Jo Graca". I know she had nothing to do with the Waterman projection, but it means the same thing here as it does in Cahill-Keyes
double mjY = mjCoords[1];
if (lat < 0) //if it's in the southern hemisphere
mjX = 4*Math.sqrt(3) - mjX; //flip it around
if (lon < centralLon) //if the relative longitude is negative
mjY = -mjY; //flip it the other way
return new double[] {
mjX*Math.sin(centralLon*2/3) + mjY*Math.cos(centralLon*2/3),
-mjX*Math.cos(centralLon*2/3) + mjY*Math.sin(centralLon*2/3) + 2 };
}
public double[] inverse(double x, double y) {
y -= 2;
double tht = Math.atan2(x, -y);
if (Math.abs(tht) > 5*Math.PI/6) return null; //only show a little bit of extra
double quadrAngle = (Math.floor(tht/(Math.PI/3))+2)*Math.PI/3; //the angle of the centre of the quadrant, measured widdershins from -y
double centralLon = quadrAngle*1.5 - 3*Math.PI/4; //the central meridian of this quadrant
double mjX = -x*Math.cos(quadrAngle) - y*Math.sin(quadrAngle);
double mjY = x*Math.sin(quadrAngle) - y*Math.cos(quadrAngle);
double[] relCoords = faceInverse(Math.min(mjX, 4*Math.sqrt(3)-mjX), Math.abs(mjY));
if (relCoords == null)
return null;
if (mjY < 0) relCoords[1] *= -1; //the left half of the octant gets shifted west
if (mjX > 2*Math.sqrt(3)) relCoords[0] *= -1; //the outer rim of the map is the southern hemisphere
return new double[] { relCoords[0], relCoords[1] + centralLon };
}
};
private static double[] faceProject(double lat, double lon) {
double[] yELD = jointHeights(lon);
double desirLength = Math2.linInterp(lat, Math.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 = Math.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[] {
Math2.linInterp(desirLength, 0, l, xELD[i-1], xELD[i]),
Math2.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;
}
private static double[] faceInverse(double x, double y) {
if (y > x-(Math.sqrt(3)-1)/2 || y > x/Math.sqrt(3) || y > (2-Math.sqrt(3))*(x+3) ||
y > (7+4*Math.sqrt(3))-(2+Math.sqrt(3))*x || x > 2*Math.sqrt(3)) //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/Math2.linInterp(x, xELD[i-1], xELD[i], dYdL[i-1], dYdL[i]);
}
else { //the well-behaved part of the last segment?
longitude = y/Math2.linInterp(x, xELD[i-1], 2*Math.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 is quadratic here
double b = (dYdL[3]*cos15-dYdL[2])*(1.5*Math.sqrt(3)-x) - dYdL[3]*sin15*y - Math.sqrt(3)/2*dYdL[2];
double c = Math.sqrt(3)/2*y - x + 1.5*Math.sqrt(3);
longitude = (-b - Math.sqrt(b*b - 4*a*c))/(2*a);
}
}
double[] yELD = jointHeights(longitude);
double totalLength = totalLength(xELD, yELD);
double pointLength = Math.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 += Math.hypot(xELD[j]-xELD[j-1], yELD[j]-yELD[j-1]);
double latitude = Math2.linInterp(pointLength, 0, totalLength, Math.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 (Math.abs(lon) <= lonDiag) { //if it hits the equatorial ELD on the vertical (hexagon) part
xELD[3] = 2*Math.sqrt(3);
}
else { //if it hits it on the diagonal (square) part
xELD[3] = -(Math.abs(lon)-lonDiag)*dYdL[3]*sin15 + 2*Math.sqrt(3);
yELD[3] = (Math.abs(lon)-lonDiag)*dYdL[3]*cos15 + 1;
}
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 += Math.hypot(xs[i]-xs[i-1], ys[i]-ys[i-1]);
return totalLength;
}
}