diff --git a/src/maps/Conic.java b/src/maps/Conic.java index 207b756..388ebd4 100644 --- a/src/maps/Conic.java +++ b/src/maps/Conic.java @@ -197,9 +197,11 @@ public class Conic { lat = -lat; lon = -lon; } - final double s = reversed ? -1 : 1; final double r = Math.sqrt(C - 2*n*Math.sin(lat)); - return new double[] { s*r*Math.sin(n*lon), s*(y0 - r*Math.cos(n*lon)) }; + final double x = r*Math.sin(n*lon); + final double y = -r*Math.cos(n*lon); + if (reversed) return new double[] {-x,-y}; + else return new double[] { x, y}; } public double[] inverse(double x, double y) { @@ -219,7 +221,6 @@ public class Conic { }; - /** * A base for all conic projections * @author jkunimune diff --git a/src/maps/Misc.java b/src/maps/Misc.java index 7f8ac04..fe3eae5 100644 --- a/src/maps/Misc.java +++ b/src/maps/Misc.java @@ -233,6 +233,89 @@ public class Misc { }; + public static final Projection BONNE = + new Projection( + "Bonne", "A traditional pseudoconic projection, also known as the Sylvanus projection.", + 10, 10, 0b1111, Type.PSEUDOCONIC, Property.EQUAL_AREA, 1, + new String[] {"Std. Parallel"}, new double[][] {{-90, 90, 45}}) { + + private double r0; + private double yC; // the y coordinate of the centers of the parallels + private boolean reversed; // if the standard parallel is southern + + public void setParameters(double... params) { + double lat0 = Math.toRadians(params[0]); + this.reversed = (lat0 < 0); + if (reversed) + lat0 = -lat0; + this.r0 = 1/Math.tan(lat0) + lat0; + if (Double.isInfinite(r0)) { + this.width = Pseudocylindrical.SINUSOIDAL.getWidth(); + this.height = Pseudocylindrical.SINUSOIDAL.getHeight(); + } + else { // for such a simple map projection... + double argmaxX = NumericalAnalysis.bisectionFind( + (p) -> (Math.PI*(1/(r0-p)*Math.cos(p) - Math.sin(p))*Math.cos(Math.PI/(r0-p)*Math.cos(p)) - Math.sin(Math.PI/(r0-p)*Math.cos(p))), + -Math.PI/2, 0, 1e-3); // it sure is complicated to find its dimensions! + double maxX = (r0 - argmaxX)*Math.sin(Math.PI/(r0 - argmaxX)*Math.cos(argmaxX)); + this.width = 2*maxX; + double argmaxY; + try { + argmaxY = NumericalAnalysis.bisectionFind( + (p) -> (Math.PI*(1/(r0-p)*Math.cos(p) - Math.sin(p))*Math.sin(Math.PI/(r0-p)*Math.cos(p)) + Math.cos(Math.PI/(r0-p)*Math.cos(p))), + 0, Math.PI/4, 1e-3); + } catch (IllegalArgumentException e) { + argmaxY = Math.PI/2; + } + double maxY = Math.max(-r0 + Math.PI/2, + -(r0 - argmaxY)*Math.cos(Math.PI/(r0 - argmaxY)*Math.cos(argmaxY))); + this.height = r0 + Math.PI/2 + maxY; + this.yC = (r0 + Math.PI/2 - maxY)/2; + } + } + + public double[] project(double lat, double lon) { + if (Double.isInfinite(r0)) + return Pseudocylindrical.SINUSOIDAL.project(lat, lon); + if (reversed) { + lat = -lat; + lon = -lon; + } + + double r = r0 - lat; + double th = lon*Math.cos(lat)/r; + double x = r*Math.sin(th); + double y = yC - r*Math.cos(th); + + if (reversed) + return new double[] {-x,-y}; + else + return new double[] { x, y}; + } + + public double[] inverse(double x, double y) { + if (Double.isInfinite(r0)) + return Pseudocylindrical.SINUSOIDAL.inverse(x, y); + if (reversed) { + x = -x; + y = -y; + } + + double r = Math.hypot(x, y-yC); + if (r < r0 - Math.PI/2 || r > r0 + Math.PI/2) + return null; + double th = Math.atan2(x, -(y-yC)); + double lat = r0 - r; + double lon = th*r/Math.cos(lat); + + if (reversed) + return new double[] {-lat,-lon}; + else + return new double[] { lat, lon}; + } + }; + + public static final Projection T_SHIRT = new Projection( "T-Shirt", "A conformal projection onto a torso.", diff --git a/src/maps/Projection.java b/src/maps/Projection.java index 30ee94f..b6e0a8d 100644 --- a/src/maps/Projection.java +++ b/src/maps/Projection.java @@ -560,6 +560,14 @@ public abstract class Projection { return this.property; } + /** + * @return the rating: + * 0 if I hate it with a burning passion; + * 1 if it is bad and you shouldn't use it;; + * 2 if it has its use cases but isn't very good outside of them; + * 3 if it is a solid, defensible choice; and + * 4 if I love it with a burning passion. + */ public final int getRating() { return this.rating; } @@ -612,10 +620,10 @@ public abstract class Projection { public static enum Type { CYLINDRICAL("Cylindrical"), CONIC("Conic"), AZIMUTHAL("Azimuthal"), PSEUDOCYLINDRICAL("Pseudocylindrical"), PSEUDOCONIC("Pseudoconic"), - PSEUDOAZIMUTHAL("Pseudoazimuthal"), QUASIAZIMUTHAL("Quasiazimuthal"), + PSEUDOAZIMUTHAL("Pseudoazimuthal"), TETRAHEDRAL("Tetrahedral"), OCTOHEDRAL("Octohedral"), TETRADECAHEDRAL("Truncated Octohedral"), ICOSOHEDRAL("Icosohedral"), - POLYNOMIAL("Polynomial"), STREBE("Strebe Blend"), PLANAR("Planar"), OTHER("Other"); + POLYNOMIAL("Polynomial"), STREBE("Strebe blend"), PLANAR("Planar"), OTHER("Other"); private String name; diff --git a/src/utils/NumericalAnalysis.java b/src/utils/NumericalAnalysis.java index 4f470c8..7d7aaa7 100644 --- a/src/utils/NumericalAnalysis.java +++ b/src/utils/NumericalAnalysis.java @@ -138,6 +138,36 @@ public class NumericalAnalysis { } + /** + * Applies a bisection zero-finding scheme to find x such that f(x)=y + * @param f The function whose zero must be found + * @param xMin The lower bound for the zero + * @param xMax The upper bound for the zero + * @param tolerence The maximum error in x that this can return + * @return The value of x that sets f to zero + */ + public static final double bisectionFind(DoubleUnaryOperator f, + double xMin, double xMax, double tolerance) { + double yMin = f.applyAsDouble(xMin); + double yMax = f.applyAsDouble(xMax); + if ((yMin < 0) == (yMax < 0)) + throw new IllegalArgumentException("Bisection failed; bounds "+xMin+" and "+xMax+" do not necessarily straddle a zero."); + while (Math.abs(xMax - xMin) > tolerance) { + double x = (xMax + xMin)/2; + double y = f.applyAsDouble(x); + if ((y < 0) == (yMin < 0)) { + xMin = x; + yMin = y; + } + else { + xMax = x; + yMax = y; + } + } + return (xMax + xMin)/2; + } + + /** * Applies Newton's method in one dimension to solve for x such that f(x)=y * @param y Desired value for f @@ -145,7 +175,7 @@ public class NumericalAnalysis { * @param f The error in terms of x * @param dfdx The derivative of f with respect to x * @param tolerance The maximum error that this can return - * @return The value of x that puts f near 0, or NaN if it does not converge in 5 iterations + * @return The value of x that puts f near y, or NaN if it does not converge in 5 iterations */ public static final double newtonRaphsonApproximation( double y, double x0, DoubleUnaryOperator f, DoubleUnaryOperator dfdx, double tolerance) {