Explain. Explain! EXPLAIN!

I fixed up and ran the MapExplainer, to update the website. I added more maps to it and made it handle threads properly. I also updated the MapPlotter to have that neat Bat Map, and fixed minor issues with Polyhedral and Octohedral.
This commit is contained in:
Justin Kunimune 2018-02-21 20:22:53 -05:00
parent 31b1f98e23
commit aa34b76190
14 changed files with 129 additions and 60 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -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<SavableImage> calculateGraphicTask(int imgSize,
Projection proj, boolean crop, double gratSpace, ImageView mapDisplay,
Projection proj, boolean crop, double gratSpacing, ImageView mapDisplay,
BarChart<String, Number> sizeChart, BarChart<String, Number> shapeChart,
Text avgSizeDistort, Text avgShapeDistort) { //TODO graticule still does nothing; just paste it in!
return new Task<SavableImage>() {
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();

View File

@ -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);

View File

@ -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("<h1>Map Projections</h1>");
for (Projection proj: projs) {
proj.setParameters(proj.getDefaultParameters());
out.println("<h2>"+proj.getName()+"</h2>");
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<SavableImage> 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(" <p>"+proj.getDescription()+"</p>");
out.println(" <dl>");
out.println(" <dt>Rating:&nbsp;</dt>");
out.print(" <dd>");
for (int i = 0; i < proj.getRating(); i ++)
out.print("&#9733;");
for (int i = 0; i < 4-proj.getRating(); i ++)
out.print("&#9734;");
out.println("</dt>");
out.println(" <dt>Geometry:&nbsp;</dt>");
out.println(" <dd>"+proj.getType().getName()+"</dd>");
out.println(" <dt>Property:&nbsp;</dt>");

View File

@ -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 };

View File

@ -111,6 +111,6 @@ public class MapProducer extends Application {
}
}
stage.close();
stop();
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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;

View File

@ -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;
}