diff --git a/input/Advanced/Tissot Not.jpg b/input/Advanced/Tissot Not.jpg new file mode 100644 index 0000000..da44e7d Binary files /dev/null and b/input/Advanced/Tissot Not.jpg differ diff --git a/input/Advanced/Tissot Shift East.jpg b/input/Advanced/Tissot Shift East.jpg new file mode 100644 index 0000000..e9fa7db Binary files /dev/null and b/input/Advanced/Tissot Shift East.jpg differ diff --git a/input/Advanced/Tissot Shift West.jpg b/input/Advanced/Tissot Shift West.jpg new file mode 100644 index 0000000..d80e366 Binary files /dev/null and b/input/Advanced/Tissot Shift West.jpg differ diff --git a/output/graph - plotter.png b/output/graph - plotter.png index b7af387..b223edc 100644 Binary files a/output/graph - plotter.png and b/output/graph - plotter.png differ diff --git a/src/apps/MapAnalyzer.java b/src/apps/MapAnalyzer.java index 3cc3145..5f22544 100644 --- a/src/apps/MapAnalyzer.java +++ b/src/apps/MapAnalyzer.java @@ -22,10 +22,15 @@ * SOFTWARE. */ package apps; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; import java.util.function.DoubleUnaryOperator; +import image.ImageUtils; import image.SavableImage; import javafx.concurrent.Task; +import javafx.embed.swing.SwingFXUtils; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.SnapshotResult; @@ -37,8 +42,6 @@ import javafx.scene.chart.XYChart.Series; import javafx.scene.control.Label; import javafx.scene.control.Separator; import javafx.scene.image.ImageView; -import javafx.scene.image.PixelWriter; -import javafx.scene.image.WritableImage; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; @@ -76,6 +79,9 @@ public class MapAnalyzer extends MapApplication { new FileChooser.ExtensionFilter("JPG", "*.jpg","*.jpeg","*.jpe","*.jfif"), new FileChooser.ExtensionFilter("GIF", "*.gif") }; + private static final Color GRATICULE_COLOR = Color.BLACK; + private static final float GRATICULE_WIDTH = 0.5f; + private Flag cropAtIDL; private MutableDouble graticuleSpacing; private Text avgSizeDistort, avgShapeDistort; @@ -224,7 +230,7 @@ public class MapAnalyzer extends MapApplication { * @param imgSize - The desired graphic size. * @param proj - The projection to analyze. * @param crop - Should points at extreme longitudes be hidden? - * @param gratSpace - The number of degrees between graticule lines, or 0 if no graticule. + * @param gratSpacing - The number of degrees between graticule lines, or 0 if no graticule. * @param mapDisplay - The optional ImageView into which to put the new Image. * @param sizeChart - The optional BarChart to update with the area histogram. * @param shapeChart - The optional BarChart to update with the stretch histogram. @@ -233,13 +239,13 @@ public class MapAnalyzer extends MapApplication { * @return The new graphic as a SavableImage. */ public static Task calculateGraphicTask(int imgSize, - Projection proj, boolean crop, double gratSpace, ImageView mapDisplay, + Projection proj, boolean crop, double gratSpacing, ImageView mapDisplay, BarChart sizeChart, BarChart shapeChart, Text avgSizeDistort, Text avgShapeDistort) { //TODO graticule still does nothing; just paste it in! return new Task() { double[][][] distortionG; //some variables that might get used later double sizeDistort, shapeDistort; - WritableImage graphic; + BufferedImage graphic; protected SavableImage call() { updateProgress(-1, 1); @@ -257,22 +263,23 @@ public class MapAnalyzer extends MapApplication { updateProgress(1, 2); updateMessage("Generating graphic\u2026"); - graphic = new WritableImage( - distortionM[0][0].length, distortionM[0].length); - PixelWriter writer = graphic.getPixelWriter(); - for (int y = 0; y < distortionM[0].length; y ++) { + int width = distortionM[0][0].length; + int height = distortionM[0].length; + graphic = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < height; y ++) { if (isCancelled()) return null; updateProgress(1 + (double)y/distortionM[0].length, 2); - for (int x = 0; x < distortionM[0][y].length; x ++) { - final double sizeDistort = distortionM[0][y][x], shapeDistort = distortionM[1][y][x]; - final double sizeContour = Math.round(sizeDistort/(LN_10/10))*LN_10/10; //contour the size by decibels - final double shapeContour = Math.round(shapeDistort/(LN_10/20))*LN_10/20; //contour the size by semidecibels + for (int x = 0; x < width; x ++) { + double sizeDistort = distortionM[0][y][x]; + double shapeDistort = distortionM[1][y][x]; + double sizeContour = Math.round(sizeDistort/(LN_10/10))*LN_10/10; //contour the size by decibels + double shapeContour = Math.round(shapeDistort/(LN_10/20))*LN_10/20; //contour the size by semidecibels if (Double.isNaN(sizeDistort) || Double.isNaN(shapeDistort)) { - writer.setArgb(x, y, 0); + graphic.setRGB(x, y, 0); continue; } - final int r, g, b; + int r, g, b; if (sizeDistort < 0) { //if compressing r = (int)(255.9*Math.exp(-shapeContour*.6)); g = (int)(255.9*Math.exp(-shapeContour*.6)*Math.exp(sizeContour*.6)); @@ -285,16 +292,27 @@ public class MapAnalyzer extends MapApplication { } final int argb = ((((((0xFF)<<8)+r)<<8)+g)<<8)+b; - writer.setArgb(x, y, argb); + graphic.setRGB(x, y, argb); } } + if (gratSpacing != 0) { //draw the graticule, if desired + if (isCancelled()) return null; + updateProgress(-1, 1); + updateMessage("Drawing graticule\u2026"); + ImageUtils.drawSVGPath( + proj.drawGraticule(Math.toRadians(gratSpacing), .02, + width, height, Math.PI/2, Math.PI, null), + GRATICULE_COLOR, GRATICULE_WIDTH, true, + (Graphics2D)graphic.getGraphics()); + } + return SavableImage.savable(graphic); } protected void succeeded() { if (mapDisplay != null) - mapDisplay.setImage(graphic); + mapDisplay.setImage(SwingFXUtils.toFXImage(graphic, null)); if (sizeChart != null) { sizeChart.getData().clear(); diff --git a/src/apps/MapDesignerRaster.java b/src/apps/MapDesignerRaster.java index 2e56330..0a6825f 100644 --- a/src/apps/MapDesignerRaster.java +++ b/src/apps/MapDesignerRaster.java @@ -23,11 +23,8 @@ */ package apps; -import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.geom.Path2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -36,8 +33,6 @@ import dialogs.MapConfigurationDialog; import image.ImageUtils; import image.PixelMap; import image.SavableImage; -import image.SVGMap.Command; -import image.SVGMap.Path; import javafx.concurrent.Task; import javafx.embed.swing.SwingFXUtils; import javafx.geometry.Insets; @@ -78,8 +73,8 @@ public class MapDesignerRaster extends MapApplication { new FileChooser.ExtensionFilter("JPG", "*.jpg"), new FileChooser.ExtensionFilter("GIF", "*.gif") }; - private static final float GRATICULE_WIDTH = 0.5f; private static final Color GRATICULE_COLOR = Color.WHITE; + private static final float GRATICULE_WIDTH = 0.5f; private Region aspectSelector; private double[] aspect; @@ -244,28 +239,11 @@ public class MapDesignerRaster extends MapApplication { if (isCancelled()) return null; updateProgress(-1, 1); updateMessage("Drawing graticule\u2026"); - - Graphics2D g = (Graphics2D)theMap.getGraphics(); - g.setStroke(new BasicStroke(GRATICULE_WIDTH)); - g.setColor(GRATICULE_COLOR); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - Path svgPath = proj.drawGraticule( - Math.toRadians(gratSpacing), .02, width, height, Math.PI/2, Math.PI, aspect); - Path2D awtPath = new Path2D.Double(Path2D.WIND_NON_ZERO, svgPath.size()); - for (Command svgCmd: svgPath) { - if (isCancelled()) return null; - switch (svgCmd.type) { - case 'M': - awtPath.moveTo(svgCmd.args[0], svgCmd.args[1]); - break; - case 'L': - awtPath.lineTo(svgCmd.args[0], svgCmd.args[1]); - break; - case 'Z': - awtPath.closePath(); - } - } - g.draw(awtPath); + ImageUtils.drawSVGPath( + proj.drawGraticule(Math.toRadians(gratSpacing), .02, + width, height, Math.PI/2, Math.PI, aspect), + GRATICULE_COLOR, GRATICULE_WIDTH, true, + (Graphics2D)theMap.getGraphics()); } return SavableImage.savable(theMap); diff --git a/src/apps/MapExplainer.java b/src/apps/MapExplainer.java index b1e715e..be65856 100644 --- a/src/apps/MapExplainer.java +++ b/src/apps/MapExplainer.java @@ -67,11 +67,13 @@ public class MapExplainer extends Application { 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, Octohedral.WATERMAN, - Misc.PEIRCE_QUINCUNCIAL.transverse(), Snyder.GS50, Misc.TWO_POINT_EQUIDISTANT, - Misc.HAMMER_RETROAZIMUTHAL, Misc.FLAT_EARTH }, + Arbitrary.ROBINSON, WinkelTripel.WINKEL_TRIPEL, Octohedral.CAHILL_KEYES, + Octohedral.CAHILL_CONCIALDI, Misc.PEIRCE_QUINCUNCIAL.transverse(), Snyder.GS50, + Misc.TWO_POINT_EQUIDISTANT, Misc.HAMMER_RETROAZIMUTHAL, Misc.FLAT_EARTH }, { MyProjections.PSEUDOSTEREOGRAPHIC, Polyhedral.TETRAGRAPH, Polyhedral.AUTHAPOWER, - MyProjections.TWO_POINT_EQUALIZED.transverse() } }; + Polyhedral.ACTUAUTHAGRAPH, MyProjections.TWO_POINT_EQUALIZED.transverse() } }; + + private PixelMap inputSkew, inputPole, inputNone, inputEast, inputWest; public static void main(String[] args) { @@ -81,19 +83,39 @@ public class MapExplainer extends Application { public void start(Stage stage) throws Exception { new File("images").mkdirs(); - final PixelMap inputSkew = new PixelMap(new File("input/Tissot-alt2.jpg")); - final PixelMap inputPole = new PixelMap(new File("input/Tissot-alt1.jpg")); - final PrintStream out = System.out; + try { + inputSkew = new PixelMap(new File("input/Advanced/Tissot Oblique.jpg")); + inputPole = new PixelMap(new File("input/Advanced/Tissot Standard.jpg")); + inputEast = new PixelMap(new File("input/Advanced/Tissot Shift East.jpg")); + inputWest = new PixelMap(new File("input/Advanced/Tissot Shift West.jpg")); + inputNone = new PixelMap(new File("input/Advanced/Tissot Not.jpg")); + } catch(IOException e) { + e.printStackTrace(); + stop(); + return; + } + final PrintStream out = System.out; for (Projection[] projs: ALL_PROJECTIONS) { out.println("

Map Projections

"); for (Projection proj: projs) { + proj.setParameters(proj.getDefaultParameters()); out.println("

"+proj.getName()+"

"); + PixelMap input = inputSkew; + if (!proj.hasAspect() || proj == Polyhedral.AUTHAGRAPH) + input = inputPole; + if (proj == Octohedral.CAHILL_KEYES) //some projections only look good in standard aspect + input = inputWest; + if (proj == Octohedral.CAHILL_CONCIALDI) + input = inputEast; + if (proj == Misc.FLAT_EARTH) //or can't be seen with indicatrices + input = inputNone; + Task task = MapDesignerRaster.calculateTask( - IMG_WIDTH, (int)(IMG_WIDTH/proj.getAspectRatio()), 2, - proj.hasAspect() ? inputSkew : inputPole, proj, null, false, 0, null); + IMG_WIDTH, (int)(IMG_WIDTH/proj.getAspectRatio()), 2, input, proj, null, + false, 0, null); task.setOnSucceeded((event) -> { try { task.getValue().save(new File("images/"+proj+".gif")); @@ -108,6 +130,13 @@ public class MapExplainer extends Application { out.println("

"+proj.getDescription()+"

"); out.println("
"); + out.println("
Rating: 
"); + out.print("
"); + for (int i = 0; i < proj.getRating(); i ++) + out.print("★"); + for (int i = 0; i < 4-proj.getRating(); i ++) + out.print("☆"); + out.println(""); out.println("
Geometry: 
"); out.println("
"+proj.getType().getName()+"
"); out.println("
Property: 
"); diff --git a/src/apps/MapPlotter.java b/src/apps/MapPlotter.java index f611dea..7993e53 100644 --- a/src/apps/MapPlotter.java +++ b/src/apps/MapPlotter.java @@ -81,7 +81,7 @@ public class MapPlotter extends Application { private static final Projection[] TETRAHEDRAL = { Polyhedral.LEE_TETRAHEDRAL_RECTANGULAR, Polyhedral.AUTHAGRAPH, Polyhedral.ACTUAUTHAGRAPH }; private static final Projection[] CHEATY = { Pseudocylindrical.LEMONS, - Octohedral.KEYES_BASIC_M, Polyhedral.DYMAXION }; + Octohedral.KEYES_BASIC_M, Polyhedral.DYMAXION, Octohedral.CAHILL_CONCIALDI }; private static final Projection[] OTHER = { Misc.PEIRCE_QUINCUNCIAL }; diff --git a/src/apps/MapProducer.java b/src/apps/MapProducer.java index 6efeb73..f073649 100644 --- a/src/apps/MapProducer.java +++ b/src/apps/MapProducer.java @@ -111,6 +111,6 @@ public class MapProducer extends Application { } } - stage.close(); + stop(); } } diff --git a/src/image/ImageUtils.java b/src/image/ImageUtils.java index 7786f27..dee0dc9 100644 --- a/src/image/ImageUtils.java +++ b/src/image/ImageUtils.java @@ -23,6 +23,15 @@ */ package image; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.Path2D; + +import image.SVGMap.Command; +import image.SVGMap.Path; + /** * A collection of (currently just one) methods for dealing with images and coordinates * @@ -34,6 +43,7 @@ public class ImageUtils { return blend(colors, 2.2); } + public static final int blend(int[] colors, double gamma) { int a_tot = 0; int r_tot = 0; @@ -53,4 +63,30 @@ public class ImageUtils { ((int)Math.pow(g_tot/a_tot, 1/gamma) << 8) + ((int)Math.pow(b_tot/a_tot, 1/gamma) << 0); } + + + public static final void drawSVGPath(Path path, Color stroke, float strokeWidth, boolean antialias, Graphics2D g) { + g.setStroke(new BasicStroke(strokeWidth)); + g.setColor(stroke); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + antialias ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); + drawSVGPath(path, g); + } + + public static final void drawSVGPath(Path path, Graphics2D g) { + Path2D awtPath = new Path2D.Double(Path2D.WIND_NON_ZERO, path.size()); + for (Command svgCmd: path) { + switch (svgCmd.type) { + case 'M': + awtPath.moveTo(svgCmd.args[0], svgCmd.args[1]); + break; + case 'L': + awtPath.lineTo(svgCmd.args[0], svgCmd.args[1]); + break; + case 'Z': + awtPath.closePath(); + } + } + g.draw(awtPath); + } } diff --git a/src/maps/Oblique.java b/src/maps/Oblique.java index e8459c8..c2c6fcb 100644 --- a/src/maps/Oblique.java +++ b/src/maps/Oblique.java @@ -51,7 +51,9 @@ public class Oblique extends Projection { @Override public double[] inverse(double x, double y) { - return obliquifyPlnr(base.inverse(x, y), axis); + double[] coords = base.inverse(x, y); + if (coords == null) return null; + return obliquifyPlnr(coords, axis); } diff --git a/src/maps/Octohedral.java b/src/maps/Octohedral.java index 0610521..4b896b5 100644 --- a/src/maps/Octohedral.java +++ b/src/maps/Octohedral.java @@ -214,7 +214,7 @@ public class Octohedral { double xMj = Math.sin(tht0)*(x-x0) - Math.cos(tht0)*(y-y0); double yMj = Math.cos(tht0)*(x-x0) + Math.sin(tht0)*(y-y0); - if (Math.abs(yMj) > Math.min(xMj, 2*size-xMj)/Math.sqrt(3)) return null; //restrict to one rhombus + if (Math.abs(yMj) > Math.min(xMj, 2*size-xMj)/Math.sqrt(3)+1e-12) return null; //restrict to one rhombus (plus a little, to account for roundoff) double[] coords = this.faceInverse(Math.min(xMj, 2*size-xMj), Math.abs(yMj)); if (coords == null) return null; double lat = coords[0], lon = coords[1]; @@ -356,7 +356,7 @@ public class Octohedral { if (y > -.5) return null; //more empty space return new double[] { sign*Math.sqrt(3), .5, 0, sign*Math.PI }; //the outer wings } - else if (y <= -1.5 && Math.abs(x) > 1) { + else if (y <= -1.5 && Math.abs(x) >= 1) { if (y+2.5 >= Math.abs(x)/Math.sqrt(3)) return new double[] { 0, -2.5, Math.signum(x)*2*Math.PI/3, 0, -Math.PI/2, -Math.PI/4 }; //the bottoms of the wings diff --git a/src/maps/Polyhedral.java b/src/maps/Polyhedral.java index 1603efd..2dd1ac3 100644 --- a/src/maps/Polyhedral.java +++ b/src/maps/Polyhedral.java @@ -436,10 +436,12 @@ public class Polyhedral { } }, AUTHAGRAPH(3, 6, 4*Math.sqrt(3), 3, new double[][] { // |\/\/`| arrangement, vertex-centred + {-ASIN_ONE_THD, Math.PI, Math.PI, 0, -2*Math.sqrt(3)-.6096, 1.5 }, {-ASIN_ONE_THD, -Math.PI/3, Math.PI/3, 0, -Math.sqrt(3)-.6096, -1.5 }, { Math.PI/2, 0, Math.PI, 0, 0-.6096, 1.5 }, {-ASIN_ONE_THD, Math.PI/3, -Math.PI/3, 0, Math.sqrt(3)-.6096, -1.5 }, - {-ASIN_ONE_THD, Math.PI, Math.PI, 0, 2*Math.sqrt(3)-.6096, 1.5 }}) { + {-ASIN_ONE_THD, Math.PI, Math.PI, 0, 2*Math.sqrt(3)-.6096, 1.5 }, + {-ASIN_ONE_THD, -Math.PI/3, Math.PI/3, 0, 3*Math.sqrt(3)-.6096, -1.5 }}) { @Override public double[] rotateOOB(double x, double y, double xCen, double yCen) { if (Math.abs(y) > height/2) { x = 2*xCen - x; diff --git a/src/maps/Projection.java b/src/maps/Projection.java index 2df1a9b..0b80522 100644 --- a/src/maps/Projection.java +++ b/src/maps/Projection.java @@ -549,6 +549,10 @@ public abstract class Projection { return this.property; } + public final int getRating() { + return this.rating; + } + public final double getWidth() { return this.width; }