attention to detail

I went thru and corrected numerus inspection warnings.  I'd say most of it is correcting typos.  I also removed a bunch of unused variables and methods (and even a cupple of files), adjusted varius stylings, improved a few docstrings, made local variables where one quantity was getting calculated multiple times, and cetera.  the spiciest thing was that I refatord the part where it saves a screenshot of the graphs in the MapAnalyzer; previusly it was bizzy-waiting for the GUI thread to finish taking the screenshot, which was probably fine, but now it uses FutureTask.get(), which is probably faster and definitely cleaner.  I also deleted the code for the two-point equalized projection because it was bad and didn't work very well (to be precise it wasn't in use after I removed it from the projection list a few days ago so this is just codifying that renunciation).  I also made AuthaGraph fixd instead of parameterized because I think that was just a mistake.
This commit is contained in:
Justin Kunimune 2023-12-14 14:08:14 -08:00
parent 2dec0a91c0
commit 2e5205e379
40 changed files with 203 additions and 437 deletions

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="2048"
height="1024"
@ -15,7 +14,7 @@
<rdf:Description
about="https://github.com/jkunimune/Map-Projections/"
dc:title="Blank world map (de facto country borders)"
dc:description="A map of the world, showing all countries' defacto borders, with major lakes overlaid. Borders precise to 110km. Equirectangular projection."
dc:description="A map of the world, showing all countries' de facto borders, with major lakes overlaid. Borders precise to 110km. Equirectangular projection."
dc:creator="Justin Kunimune"
dc:source="http://www.naturalearthdata.com/"
dc:date="2018-02-04"
@ -35,7 +34,7 @@
</metadata>
<title>Blank world map (de facto country borders)</title>
<desc>
A map of the world, showing all countries' defacto borders, with major lakes overlaid. Borders precise to 110km. Equirectangular projection.
A map of the world, showing all countries' de facto borders, with major lakes overlaid. Borders precise to 110km. Equirectangular projection.
</desc>
<style type="text/css">
.country {

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="2048"
height="1024"

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="1600"
height="800"

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 290 KiB

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="1600"
height="800"

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 290 KiB

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="2048"
height="1024"

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="2048"
height="1024"

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="2048"
height="1024"

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="2048"
height="1024"
@ -14,8 +13,8 @@
<cc:work>
<rdf:Description
about="https://github.com/jkunimune/Map-Projections/"
dc:title="Icosohedral orthodromic mesh - Equirectangular projection"
dc:description="A mesh of great circles that intersect with 10-fold symmetry at 12 principal nodes and 6-fold symmetry at 20 minor nodes. Aligns with the edges of an icosohedron and its dual dodecahedron."
dc:title="Icosahedral orthodromic mesh - Equirectangular projection"
dc:description="A mesh of great circles that intersect with 10-fold symmetry at 12 principal nodes and 6-fold symmetry at 20 minor nodes. Aligns with the edges of an icosahedron and its dual dodecahedron."
dc:creator="Justin Kunimune"
dc:source="http://www.naturalearthdata.com/"
dc:date="2018-02-04"
@ -33,9 +32,9 @@
</cc:License>
</rdf:RDF>
</metadata>
<title>Icosohedral orthodromic mesh &#8211; Equirectangular projection</title>
<title>Icosahedral orthodromic mesh &#8211; Equirectangular projection</title>
<desc>
A mesh of great circles that intersect with 10-fold symmetry at 12 principal nodes and 6-fold symmetry at 20 minor nodes. Aligns with the edges of an icosohedron and its dual dodecahedron.
A mesh of great circles that intersect with 10-fold symmetry at 12 principal nodes and 6-fold symmetry at 20 minor nodes. Aligns with the edges of an icosahedron and its dual dodecahedron.
</desc>
<style type="text/css">
.lines {

Before

Width:  |  Height:  |  Size: 438 KiB

After

Width:  |  Height:  |  Size: 438 KiB

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="2048"
height="1024"

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 336 KiB

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="2048"
height="1024"

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 250 KiB

View File

@ -3,7 +3,6 @@
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="2048"
height="1024"

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -25,11 +25,11 @@ package apps;
import image.ImageUtils;
import image.SavableImage;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.SnapshotResult;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
@ -38,6 +38,7 @@ 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.WritableImage;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
@ -53,6 +54,7 @@ import utils.Procedure;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.concurrent.ExecutionException;
import java.util.function.DoubleUnaryOperator;
import static java.lang.Double.isFinite;
@ -194,20 +196,16 @@ public class MapAnalyzer extends MapApplication {
private Task<SavableImage> calculatePlotTask() {
Task<SavableImage> task = new Task<SavableImage>() {
private SnapshotResult snapRes = null;
protected void running() {
charts.snapshot((snapRes) -> {
this.snapRes = snapRes;
return null;
}, null, null);
}
protected SavableImage call() { //yeah, this is weird and dumb, but only because the
updateProgress(-1, 1); //snapshot method is dumb. Why does it have to be called on
updateMessage("Converting graph\u2026"); //the GUI thread? It's clearly doing Thread
while (!isCancelled() && snapRes == null) {} //things, judging by the fact that it
return SavableImage.savable(snapRes.getImage()); //_has_ a Callback version
protected SavableImage call() throws ExecutionException, InterruptedException {
updateProgress(-1, 1);
updateMessage("Converting graph\u2026");
Task<WritableImage> snapshotTask = new Task<>() {
protected WritableImage call() {
return charts.snapshot(null, null);
}
};
Platform.runLater(snapshotTask);
return SavableImage.savable(snapshotTask.get());
}
};
disableWhile(task.runningProperty(), ButtonType.SAVE_GRAPH);

View File

@ -74,7 +74,6 @@ import maps.EqualEarth;
import maps.Gyorffy;
import maps.Lenticular;
import maps.Misc;
import maps.MyProjections;
import maps.Octahedral;
import maps.Polyhedral;
import maps.Projection;
@ -224,7 +223,7 @@ public abstract class MapApplication extends Application {
FileChooser.ExtensionFilter defaultExtension,
Function<File, Task<Void>> inputSetter) {
final Label label = new Label("Current input:");
final Text inputLabel = new Text("Basic"+defaultExtension.getExtensions().get(0).substring(1)); //this line kind of cheats, since it assumes the first input will be called "Basic", but I couldn't figure out a good way for this to update when the subclass programatically sets the input
final Text inputLabel = new Text("Basic"+defaultExtension.getExtensions().get(0).substring(1)); //this line kind of cheats, since it assumes the first input will be called "Basic", but I couldn't figure out a good way for this to update when the subclass programmatically sets the input
final FileChooser inputChooser = new FileChooser();
inputChooser.setInitialDirectory(new File("input"));
@ -415,9 +414,7 @@ public abstract class MapApplication extends Application {
final CheckBox cropBox = new CheckBox("Crop at antimeridian"); //the CheckBox for whether there should be shown imagery beyond += 180 degrees
cropBox.setSelected(cropAtIDL.isSet());
cropBox.setTooltip(new Tooltip("Hide points of extreme longitude."));
cropBox.selectedProperty().addListener((observable, old, now) -> {
cropAtIDL.set(now);
});
cropBox.selectedProperty().addListener((observable, old, now) -> cropAtIDL.set(now));
final ObservableList<Double> factorsOf90 = FXCollections.observableArrayList();
for (double f = 1; f <= 90; f += 0.5)
@ -533,9 +530,7 @@ public abstract class MapApplication extends Application {
"Could not access "+f.getAbsolutePath()+". It's possible that another program has it open.");
}
pBar.close();
// try {
// Desktop.getDesktop().open(f.getParentFile()); // TODO: show a popup here with a button to open the folder inste of of doing it automatically
// } catch (IOException e) {} //if you can't open the directory for some reason, don't worry about it.
// // TODO: show a popup here with a button to open the folder
});
disableWhile(task.runningProperty(), buttonType);
new Thread(task).start(); //now execute!
@ -698,7 +693,7 @@ public abstract class MapApplication extends Application {
protected enum ButtonType {
LOAD_INPUT, UPDATE_MAP, SAVE_MAP, SAVE_GRAPH;
LOAD_INPUT, UPDATE_MAP, SAVE_MAP, SAVE_GRAPH
}
}

View File

@ -49,41 +49,42 @@ public class MapConverter {
// the desired map projection
Projection projection = Polyhedral.DYMAXION;
Stream<Path> paths = Files.walk(Paths.get(directory));
Iterable<Path> pathIterable = paths::iterator;
try (Stream<Path> paths = Files.walk(Paths.get(directory))) {
Iterable<Path> pathIterable = paths::iterator;
// iterate thru the directory
for (Path inputPath: pathIterable) {
// look for images that are not dymaxion projections
if (inputPath.toString().endsWith(".jpg") ||
inputPath.toString().endsWith(".jpeg") ||
inputPath.toString().endsWith(".tif") ||
inputPath.toString().endsWith(".tiff") ||
inputPath.toString().endsWith(".png") &&
!inputPath.toString().endsWith(".dymaxion.png")) {
System.out.println(inputPath);
PixelMap inputImage = new PixelMap(inputPath.toFile());
// iterate thru the directory
for (Path inputPath : pathIterable) {
// look for images that are not dymaxion projections
if (inputPath.toString().endsWith(".jpg") ||
inputPath.toString().endsWith(".jpeg") ||
inputPath.toString().endsWith(".tif") ||
inputPath.toString().endsWith(".tiff") ||
inputPath.toString().endsWith(".png") &&
!inputPath.toString().endsWith(".dymaxion.png")) {
System.out.println(inputPath);
PixelMap inputImage = new PixelMap(inputPath.toFile());
// reduce the area by 2 to avoid pixelation
double area = inputImage.getWidth() * inputImage.getHeight() / 2.;
// and calculate the new dimensions according to Dymaxion's aspect ratio
int width = (int) sqrt(area * projection.getShape().aspectRatio);
int height = (int) sqrt(area / projection.getShape().aspectRatio);
// reduce the area by 2 to avoid pixelation
double area = inputImage.getWidth()*inputImage.getHeight()/2.;
// and calculate the new dimensions according to Dymaxion's aspect ratio
int width = (int) sqrt(area*projection.getShape().aspectRatio);
int height = (int) sqrt(area/projection.getShape().aspectRatio);
// generate the new map
BufferedImage outputImage = MapDesignerRaster.calculate(
width, height, 2, inputImage, projection,
null, true, 0,
null, null, null);
// generate the new map
BufferedImage outputImage = MapDesignerRaster.calculate(
width, height, 2, inputImage, projection,
null, true, 0,
null, null, null);
// update the filename and save to disk
String outputPath = inputPath.toString();
outputPath = outputPath.replace(".jpg", ".png");
outputPath = outputPath.replace(".jpeg", ".png");
outputPath = outputPath.replace(".tif", ".png");
outputPath = outputPath.replace(".tiff", ".png");
outputPath = outputPath.replace(".png", ".dymaxion.png");
SavableImage.savable(outputImage).save(new File(outputPath));
// update the filename and save to disk
String outputPath = inputPath.toString();
outputPath = outputPath.replace(".jpg", ".png");
outputPath = outputPath.replace(".jpeg", ".png");
outputPath = outputPath.replace(".tif", ".png");
outputPath = outputPath.replace(".tiff", ".png");
outputPath = outputPath.replace(".png", ".dymaxion.png");
SavableImage.savable(outputImage).save(new File(outputPath));
}
}
}

View File

@ -69,7 +69,7 @@ public class MapEvaluator {
};
public static void main(String[] args) throws IOException {
BufferedImage img = ImageIO.read(new File("input/Sillouette.png")); // start by loading the mask
BufferedImage img = ImageIO.read(new File("input/Silhouette.png")); // start by loading the mask
boolean[][] land = new boolean[img.getHeight()][img.getWidth()];
for (int i = 0; i < land.length; i ++)
for (int j = 0; j < land[i].length; j ++)
@ -142,7 +142,7 @@ public class MapEvaluator {
}
System.out.printf("Usable area: %.6f sr\n", totalArea);
System.out.printf("Anglular: %8.4f %%\n", 100*shearedArea/totalArea);
System.out.printf("Angular: %8.4f %%\n", 100*shearedArea/totalArea);
System.out.printf("Areal: %8.4f %%\n", 100*swollenArea/totalArea);
System.out.printf("Precision: %8.4f %%\n", 100*maxArea/totalArea);
System.out.println();

View File

@ -141,17 +141,17 @@ public class MapExplainer extends Application {
out.print("&#9734;");
out.println("</dt>");
out.println(" <dt>Geometry:&nbsp;</dt>");
out.println(" <dd>"+proj.getType().getName()+"</dd>");
out.println(" <dd>" + proj.getType().getName() + "</dd>");
out.println(" <dt>Property:&nbsp;</dt>");
out.println(" <dd>"+proj.getProperty().getName()+"</dd>");
out.println(" <dd>" + proj.getProperty().getName() + "</dd>");
out.println(" <dt>Uninterrupted:&nbsp;</dt>");
out.println(" <dd>"+ (proj.isContinuous() ? "Yes":"No") +"</dd>");
out.println(" <dd>" + (proj.isContinuous() ? "Yes":"No") + "</dd>");
out.println(" <dt>Shows entire world:&nbsp;</dt>");
out.println(" <dd>" + (proj.isComprehensive() ? "Yes" : "No") + "</dd>");
out.println(" <dt>Closed-form solution:&nbsp;</dt>");
out.println(" <dd>"+ (proj.isSolveable() ? "Yes":"No") +"</dd>");
out.println(" <dd>" + (proj.isSolvable() ? "Yes" : "No") + "</dd>");
out.println(" <dt>Closed-form inverse:&nbsp;</dt>");
out.println(" <dd>"+ (proj.isInvertable() ? "Yes":"No") +"</dd>");
out.println(" <dd>" + (proj.isInvertible() ? "Yes" : "No") + "</dd>");
out.println(" </dl>");
out.println(" <br>");
out.println(" </div>");

View File

@ -93,7 +93,7 @@ public class MapOptimizer extends Application {
chart.setCreateSymbols(true);
chart.setAxisSortingPolicy(SortingPolicy.NONE);
PrintStream log = new PrintStream(new File("output/parameters.txt"));
PrintStream log = new PrintStream("output/parameters.txt");
chart.getData().add(analyzeAll(EXISTING_PROJECTIONS));
for (Projection p: PROJECTIONS_TO_OPTIMIZE)
@ -119,13 +119,13 @@ public class MapOptimizer extends Application {
for (Projection proj : projs)
if (!proj.isParametrized())
output.getData().add(plotDistortion(proj, new double[0]));
output.getData().add(plotDistortion(proj));
return output;
}
private static Data<Number, Number> plotDistortion(Projection proj, double[] params) {
private static Data<Number, Number> plotDistortion(Projection proj) {
double[] distortion = proj.avgDistortion(GLOBE, proj.getDefaultParameters());
return new Data<Number, Number>(distortion[0], distortion[1]);
}
@ -193,8 +193,8 @@ public class MapOptimizer extends Application {
bestValue = avgDist;
bestParams = params.clone();
}
for (int i = 0; i < params.length; i ++)
System.out.print(params[i]+", ");
for (double param: params)
System.out.print(param + ", ");
System.out.println(avgDist+";");
int i;
@ -222,7 +222,7 @@ public class MapOptimizer extends Application {
* a backtracking line search.
* @param arrFunction - The function that takes a parameter array and returns a double value.
* @param x0 - The initial guess.
* @return The array of parameters that mimimise arrFunction.
* @return The array of parameters that minimise arrFunction.
*/
private static double[] bfgsMinimise(Function<double[], Double> arrFunction, double[] x0) { //The Broyden-Fletcher-Goldfarb-Shanno algorithm
System.out.println("BFGS = [");

View File

@ -155,7 +155,7 @@ public class MapPlotter extends Application {
for (Projection projection: projections) {
System.out.print(projection+": ");
final double[] params = projection.getDefaultParameters();
final double distortion[] = projection.avgDistortion(points, params);
final double[] distortion = projection.avgDistortion(points, params);
final Data<Number, Number> datum = new Data<Number, Number>(
distortion[0]/DECIBEL, distortion[1]/DECIBEL);
series.getData().add(datum);

View File

@ -144,8 +144,8 @@ public class ProjectionSelectionDialog extends Dialog<Projection> {
text.addRow(1, new Label("Property:"), new Label(p.getProperty().getName()));
text.addRow(2, new Label("Uninterrupted:"), new Label(p.isContinuous() ? "Yes" : "No"));
text.addRow(3, new Label("Shows entire world:"), new Label(p.isComprehensive() ? "Yes" : "No"));
text.addRow(4, new Label("Closed-form solution:"), new Label(p.isSolveable() ? "Yes" : "No"));
text.addRow(5, new Label("Closed-form inverse:"), new Label(p.isInvertable() ? "Yes" : "No"));
text.addRow(4, new Label("Closed-form solution:"), new Label(p.isSolvable() ? "Yes" : "No"));
text.addRow(5, new Label("Closed-form inverse:"), new Label(p.isInvertible() ? "Yes" : "No"));
for (Node label: text.getChildren())
((Label)label).setFont(body.getFont());
}

View File

@ -77,14 +77,9 @@ public class ImageUtils {
Path2D awtPath = new Path2D.Double(Path2D.WIND_NON_ZERO, path.size());
for (Path.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();
case 'M' -> awtPath.moveTo(svgCmd.args[0], svgCmd.args[1]);
case 'L' -> awtPath.lineTo(svgCmd.args[0], svgCmd.args[1]);
case 'Z' -> awtPath.closePath();
}
}
g.draw(awtPath);

View File

@ -243,7 +243,7 @@ public class SVGMap implements Iterable<SVGMap.SVGElement>, SavableImage {
y = y*transform[1] + transform[3];
width = width*transform[0];
height = height*transform[1];
// check if it exactly covers the viewbox (it's okay if it's vertically inverted)
// check if it exactly covers the viewBox (it's okay if it's vertically inverted)
if ((x == header.vbMinX && y == header.vbMinY && width == header.vbWidth && height == header.vbHeight) ||
(x == header.vbMinX && y == header.vbMinY + header.vbHeight && width == header.vbWidth && height == -header.vbHeight)) {
// if so, remove the dimensions and stick the remainder in a Background Object
@ -516,7 +516,7 @@ public class SVGMap implements Iterable<SVGMap.SVGElement>, SavableImage {
partI.remove(0); //remove the useless moveto
partJ.addAll(partI); //combine them
partI = partJ;
parts.remove(j); //don't look at J anymone; it has been absorbed.
parts.remove(j); //don't look at J anymore; it has been absorbed.
break;
}
}
@ -553,10 +553,6 @@ public class SVGMap implements Iterable<SVGMap.SVGElement>, SavableImage {
this.content = content;
}
public Content projected() {
return this;
}
public String toString() {
return this.content;
}
@ -623,10 +619,6 @@ public class SVGMap implements Iterable<SVGMap.SVGElement>, SavableImage {
this.y = y;
}
public GeographicPoint projected() {
return this;
}
public String toString() {
return format(formatSpecifier, x, y);
}

View File

@ -42,7 +42,8 @@ import static utils.Math2.tand;
/**
* A truncated octahedral 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
*
* <a href="http://www.genekeyes.com/CKOG-OOo/7-CKOG-illus-&-coastline.html">genekeyes.com/CKOG-OOo/7-CKOG-illus-&-coastline</a>
*
* @author jkunimune
*/
@ -74,7 +75,7 @@ public class CahillKeyes {
public static final double POLE_OFFSET = lMA*SCALE_FACTOR; //lMA expressed in output coordinates
public static Projection FACE = new Projection(
public static final Projection FACE = new Projection(
"CahillKeyes (face)", "A single face of Gene Keyes's octahedral projection",
Shape.polygon(new double[][] {{0., 0.}, {0., -sqrt(3)/2.}, {1/2., -sqrt(3)/2.}}),
true, true, true, false, Projection.Type.OTHER, Projection.Property.COMPROMISE, 2) {

View File

@ -189,7 +189,9 @@ public class Elastic {
/**
* convert a location on the map plane to a location on the globe by iteratively searching
* for the coordinates that project to x and y on a given section. the algorithm is
* Levenberg-Marquardt; see <i>J. Res. NIST</i> 103, p. 633 (1998).
* Levenberg-Marquardt; see <i>J. Res. NIST</i> 103, p. 633 (1998). this procedure could
* probably be a lot simpler (or omitted entirely) depending on how high quality an inverse
* you want.
* @param x the x value, in the same units as this.width
* @param y the y value, in the same units as this.height
* @param section_index the index of the section to search
@ -484,7 +486,7 @@ public class Elastic {
/**
* determine whether the given point falls within this polygon or not. the method used is a typical even/odd
* algorithm, with the caveat that the directionality of the polygon is significant. if the vertices go
* counterclockwise, the contianed points are everything inside the polygon as one would expect, but if the
* counterclockwise, the contained points are everything inside the polygon as one would expect, but if the
* vertices go clockwise, the contained points are actually everything <i>outside</i> the polygon.
* @param ф the latitude of the point
* @param λ the longitude of the point
@ -681,7 +683,7 @@ public class Elastic {
* recenter a set of spherical coordinates so that the latitude is in the range [-PI/2, PI/2]
* and the longitude is in the range [-PI, PI]. if it's outside of the latitude bounds, it
* will get reflected about the poles until it isn't. if it's out of the longitude bounds,
* it will get modulused such that it isn't.
* it will get moduloed such that it isn't.
* @param ф the latitude (radians)
* @param λ the longitude (radians)
* @return the equivalent in-bounds latitude and longitude in an array

View File

@ -170,7 +170,9 @@ public class Misc {
double phi1 = PI/2 - hypot(x, y);
if (phi1 < -PI/2) return null;
double lam1 = atan2(x, -y);
double phiP = asin(sin(phi0)/hypot(sin(phi1),cos(phi1)*cos(lam1))) - atan2(cos(phi1)*cos(lam1),sin(phi1));
double y1 = cos(phi1)*cos(lam1);
double z1 = sin(phi1);
double phiP = asin(sin(phi0)/hypot(z1, y1)) - atan2(y1, z1);
if (abs(phiP) > PI/2)
phiP = signum(phiP)*PI - phiP;
double delL = acos(sin(phi1)/cos(phiP)/cos(phi0) - tan(phiP)*tan(phi0));

View File

@ -1,92 +0,0 @@
/**
* 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.
*/
package maps;
import maps.Projection.Property;
import maps.Projection.Type;
import utils.Shape;
import static java.lang.Math.PI;
import static java.lang.Math.acos;
import static java.lang.Math.asin;
import static java.lang.Math.cos;
import static java.lang.Math.hypot;
import static java.lang.Math.pow;
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.toRadians;
/**
* All of the projections I invented, save the tetrahedral ones, because
* those have so much in common with other tetrahedral projections.
*
* @author jkunimune
*/
public class MyProjections {
public static final Projection TWO_POINT_EQUALIZED = new Projection("Two-Point Equalized",
"A projection I invented specifically for viewing small elliptical regions of the Earth",
null, true, true, true, true, Type.OTHER, Property.EQUIDISTANT, 2,
new String[] {"Width"}, new double[][] { {0, 180, 120} }) {
private double theta;
public void initialize(double... params) {
theta = toRadians(params[0])/2;
if (theta != 0)
this.shape = Shape.ellipse(
sqrt(pow(PI - theta, 2) - pow(theta, 2))*
sqrt(tan(theta)/theta), //minor axis
PI - theta //major axis
);
else
this.shape = Shape.circle(PI);
}
public double[] project(double lat, double lon) {
if (theta == 0) return Azimuthal.POLAR.project(lat, lon);
final double d1 = acos(
sin(lat)*cos(theta) - cos(lat)*sin(theta)*cos(lon));
final double d2 = acos(
sin(lat)*cos(theta) + cos(lat)*sin(theta)*cos(lon));
final double k = signum(lon)*sqrt(tan(theta)/theta);
return new double[] {
k*sqrt(d1*d1 - pow((d1*d1-d2*d2+4*theta*theta)/(4*theta), 2)),
(d2*d2-d1*d1)/(4*theta) };
}
public double[] inverse(double x, double y) {
if (theta == 0) return Azimuthal.POLAR.inverse(x, y);
final double d1 = hypot(x/sqrt(tan(theta)/theta), y - theta);
final double d2 = hypot(x/sqrt(tan(theta)/theta), y + theta);
if (d1 + d2 > 2*shape.yMax) return null;
final double phi = asin((cos(d1)+cos(d2))/(2*cos(theta)));
final double lam = signum(x)*acos(
(sin(phi)*cos(theta) - cos(d1))/(cos(phi)*sin(theta)));
return new double[] { phi, lam };
}
};
}

View File

@ -182,7 +182,7 @@ public class Octahedral {
public OctahedralProjection(String name, String desc, int rating, double tipOffset,
Projection faceProj, Configuration config) {
super(name, desc, null, false, config.comprehensive, faceProj.isSolveable(), faceProj.isInvertable(),
super(name, desc, null, false, config.comprehensive, faceProj.isSolvable(), faceProj.isInvertible(),
(tipOffset == 0) ? Type.OCTAHEDRAL : Type.TETRADECAHEDRAL, faceProj.getProperty(), rating,
new String[] {}, new double[][] {}, config.hasAspect);
this.octants = config.placeOctants(tipOffset);

View File

@ -113,18 +113,14 @@ public class Polyhedral {
public static final PolyhedralProjection AUTHAGRAPH = new PolyhedralProjection(
"IMAGO (AuthaGraph)",
"Authagraph is a hip new Japanese map that would be super great if they actually " +
"AuthaGraph is a hip new Japanese map that would be super great if they actually " +
"published their equations. This is technically just an approximation, also known as " +
"the Infinitessimal Mutated AuthaGraph Offspring.",
"the Infinitesimal Mutated AuthaGraph Offspring.",
true, true, false, Polyhedron.AUTHAGRAPH, Property.COMPROMISE, 3,
new String[] {"Power"}, new double[][] {{.5,1,.68}}) {
private final double[] POLE = {toRadians(77), toRadians(143), toRadians(17)};
private double k;
public void initialize(double... params) {
this.k = params[0];
}
private final double k = .68;
@Override
public double[] project(double lat, double lon) { //apply a pole shift to AuthaGraph
@ -137,21 +133,14 @@ public class Polyhedral {
return transformToOblique(super.inverse(x, y), POLE);
}
public double[] faceProject(double lat, double lon) {
final double tht = atan((lon - asin(sin(lon)/sqrt(3)))/PI*sqrt(12));
final double p = (PI/2 - lat) / atan(sqrt(2)/cos(lon));
return new double[] { pow(p,k)*sqrt(3)/cos(tht), tht };
AUTHAPOWER.initialize(k);
return AUTHAPOWER.faceProject(lat, lon);
}
protected double[] faceInverse(double r, double th) {
final double lon = NumericalAnalysis.newtonRaphsonApproximation(th, th*2,
(l) -> atan((l - asin(sin(l)/sqrt(3)))/PI*sqrt(12)),
(l) -> (1-1/sqrt(1+2*pow(cos(l),-2)))/sqrt(pow(PI,2)/12+pow(l-asin(sin(l)/sqrt(3)),2)),
.001);
final double R = r / (sqrt(3)/cos(th));
return new double[] {
PI/2 - pow(R,1/k)*atan(sqrt(2)/cos(lon)), lon };
public double[] faceInverse(double r, double th) {
AUTHAPOWER.initialize(k);
return AUTHAPOWER.faceInverse(r, th);
}
};
@ -173,7 +162,7 @@ public class Polyhedral {
return new double[] { pow(p,k)*sqrt(3)/cos(tht), tht };
}
protected double[] faceInverse(double r, double th) {
public double[] faceInverse(double r, double th) {
final double lon = NumericalAnalysis.newtonRaphsonApproximation(th, th*2,
(l) -> atan((l - asin(sin(l)/sqrt(3)))/PI*sqrt(12)),
(l) -> (1-1/sqrt(1+2*pow(cos(l),-2)))/sqrt(pow(PI,2)/12+pow(l-asin(sin(l)/sqrt(3)),2)),
@ -310,9 +299,9 @@ public class Polyhedral {
double y = r*sin(th)*atan(2);
double a = sqrt(3)*x + y; //angular distance up each side of the triangle
double b = sqrt(3)*x - y;
double xG = cos36*(sin(a) + sin(b))/(1 + cos(a) + cos(b)); //unnormalised gnomonic coordinates
double yG = sin36*
(sin(a) - sin(b) + 2*sin(a-b))/(1 + cos(a) + cos(b));
double correction = 1 + cos(a) + cos(b);
double xG = cos36*(sin(a) + sin(b))/correction; //unnormalised gnomonic coordinates
double yG = sin36*(sin(a) - sin(b) + 2*sin(a-b))/correction;
return new double[] {atan(1/hypot(xG, yG)), atan2(yG, xG)}; //inverse gnomonic projection
}
};
@ -530,7 +519,7 @@ public class Polyhedral {
new Facet(-0.5, -0.5*sqrt(3), PI/2, -PI/2, 0 , -3*PI/5, -PI/5, 2*PI/5), //West Pacific O.
new Facet(-1.5, -0.5*sqrt(3), PI/2, -PI/2, 0 , -PI/5, 0 , 3*PI/5), //Melanesia
});
/** the number of the rotational spmmetry in the spherical coordinate system */
/** the number of the rotational symmetry in the spherical coordinate system */
public final int sphereSym;
/** the number of the rotational symmetry in the planar coordinate system */
public final int planarSym;
@ -549,7 +538,7 @@ public class Polyhedral {
if (sphereSym == 3)
this.type = Type.TETRAHEDRAL;
else
this.type = Type.ICOSOHEDRAL;
this.type = Type.ICOSAHEDRAL;
}
public double[] rotateOOB(double x, double y, double xCen, double yCen) { //move points that are out of bounds for project()

View File

@ -70,8 +70,8 @@ public abstract class Projection {
private final boolean continuous; //is the interruption kept to no more than one meridian's equivalent?
private final boolean comprehensive; //does it display the entire world?
private final boolean solveable; //is the solution closed-form?
private final boolean invertable; //is the inverse solution closed-form?
private final boolean solvable; //is the solution closed-form?
private final boolean invertible; //is the inverse solution closed-form?
private final Type type; //the geometry of the projection
private final Property property; //what it is good for
private final int rating; //how good I think it is
@ -87,15 +87,15 @@ public abstract class Projection {
protected Projection(
String name, String description, Shape shape, boolean continuous, boolean comprehensive,
boolean solveable, boolean invertable, Type type, Property property, int rating,
boolean solvable, boolean invertible, Type type, Property property, int rating,
String[] paramNames, double[][] paramValues) {
this(name, description, shape, continuous, comprehensive, solveable, invertable, type, property, rating,
this(name, description, shape, continuous, comprehensive, solvable, invertible, type, property, rating,
paramNames, paramValues, true);
}
protected Projection (
String name, String description, Shape shape, boolean continuous, boolean comprehensive,
boolean solveable, boolean invertable, Type type, Property property, int rating,
boolean solvable, boolean invertible, Type type, Property property, int rating,
String[] paramNames, double[][] paramValues, boolean hasAspect) {
this.name = name;
this.description = description;
@ -105,16 +105,16 @@ public abstract class Projection {
this.shape = shape;
this.continuous = continuous;
this.comprehensive = comprehensive;
this.solveable = solveable;
this.invertable = invertable;
this.solvable = solvable;
this.invertible = invertible;
this.type = type;
this.property = property;
this.rating = rating;
}
protected Projection(String name, Projection base) {
this(name, base.description, base.shape, base.comprehensive, base.invertable,
base.solveable, base.continuous, base.type, base.property, base.rating,
this(name, base.description, base.shape, base.comprehensive, base.invertible,
base.solvable, base.continuous, base.type, base.property, base.rating,
base.paramNames, base.paramValues, base.hasAspect);
}
@ -188,30 +188,7 @@ public abstract class Projection {
public double[] inverse(double[] coords) {
return inverse(coords[0], coords[1]);
}
/**
* convert a location on the map plane to a location on the rotated globe
* @param coords the x and y value, in the same units as this.bounds
* @param pole the desired aspect: the latitude and longitude of the location that should appear as the North Pole
* and the angle to rotate the globe about its new axis
* @return the latitude and longitude in radians
*/
public double[] inverse(double[] coords, double[] pole) {
return inverse(coords[0], coords[1], pole);
}
/**
* convert a location on the map plane to a location on the rotated globe
* @param x the x value in the same units as this.bounds
* @param y the y value in the same units as this.bounds
* @param pole the desired aspect: the latitude and longitude of the location that should appear as the North Pole
* and the angle to rotate the globe about its new axis
* @return the latitude and longitude in radians
*/
public double[] inverse(double x, double y, double[] pole) {
return inverse(x, y, pole, false);
}
/**
* convert a location on the map plane to a location on the rotated globe
* @param x the x value in the same units as this.bounds
@ -233,19 +210,8 @@ public abstract class Projection {
else
return transformToOblique(relCoords, hasAspect ? pole : null);
}
/**
* perform the inverse projection on a 2D array of points
* @param size the maximum linear dimension of the 2D array
* @return an array of latitude-longitude pairs, where each row is at a particular x value,
* and each column is at a particular y value. y decreases with increasing row index.
* elements corresponding to points not on the map will be set to zero.
*/
public double[][][] map(int size) {
return map(size, false);
}
/**
* perform the inverse projection on a 2D array of points
* @param size the maximum linear dimension of the 2D array
@ -431,11 +397,11 @@ public abstract class Projection {
}
public double[][][] calculateDistortion(double[][][] points,
BooleanSupplier cancelation, DoubleConsumer progressTracker) { //calculate both kinds of distortion over the given region
BooleanSupplier cancellation, DoubleConsumer progressTracker) { //calculate both kinds of distortion over the given region
double[][][] output = new double[2][points.length][points[0].length]; //the distortion matrix
for (int y = 0; y < points.length; y ++) {
if (cancelation.getAsBoolean()) return null;
if (cancellation.getAsBoolean()) return null;
progressTracker.accept((double)y/points.length);
for (int x = 0; x < points[y].length; x ++) {
if (points[y][x] != null) {
@ -632,12 +598,12 @@ public abstract class Projection {
return this.comprehensive;
}
public final boolean isInvertable() {
return this.invertable;
public final boolean isInvertible() {
return this.invertible;
}
public final boolean isSolveable() {
return this.solveable;
public final boolean isSolvable() {
return this.solvable;
}
public final boolean isContinuous() {
@ -692,7 +658,7 @@ public abstract class Projection {
PSEUDOCYLINDRICAL("Pseudocylindrical"), PSEUDOCONIC("Pseudoconic"),
PSEUDOAZIMUTHAL("Pseudoazimuthal"),
TETRAHEDRAL("Tetrahedral"), OCTAHEDRAL("Octahedral"),
TETRADECAHEDRAL("Truncated Octahedral"), ICOSOHEDRAL("Icosohedral"),
TETRADECAHEDRAL("Truncated Octahedral"), ICOSAHEDRAL("Icosahedral"),
POLYNOMIAL("Polynomial"), STREBE("Strebe blend"), PLANAR("Planar"), OTHER("Other");
private final String name;

View File

@ -35,8 +35,9 @@ import static utils.Math2.linInterp;
/**
* A class of methods pertaining to the Waterman projection.
* http://www.watermanpolyhedron.com/methodp.html
*
*
* <a href="http://www.watermanpolyhedron.com/methodp.html">watermanpolyhedron.com/methodp</a>
*
* @author jkunimune
*/
public class Waterman {
@ -50,7 +51,7 @@ public class Waterman {
private static final double cos15 = (sqrt(3)+1)/sqrt(8); // cos(15°)
public static Projection FACE = new Projection(
public static final Projection FACE = new Projection(
"Waterman (face)", "A single face of Waterman's octahedral projection",
Shape.polygon(new double[][] {{0., 0.}, {0., -sqrt(3)/2.}, {1/2., -sqrt(3)/2.}}),
true, true, true, false, Type.OTHER, Property.COMPROMISE, 2) {

View File

@ -74,7 +74,7 @@ public final class WinkelTripel {
public double[] inverse(double x, double y) {
return NumericalAnalysis.newtonRaphsonApproximation(
x, y,
y/2, x*(1 + cos(y*PI/2))/(2 + 2*cos(stdParallel)), //inital guess is Eckert V
y/2, x*(1 + cos(y*PI/2))/(2 + 2*cos(stdParallel)), //initial guess is Eckert V
this::f1pX, this::f2pY,
this::df1dphi, this::df1dlam, this::df2dphi, this::df2dlam, .001);
}

View File

@ -41,7 +41,7 @@ public class Dixon {
/**
* One third of the period of a dixon sm or cm function
*/
public static double PERIOD_THIRD = 1.7666387503;
public static final double PERIOD_THIRD = 1.7666387503;
private static final double[] COEF = { 1.000000e0, .625000e-1, .223214e-2, .069754e-3,
.020121e-4/*, .005539e-5, .001477e-6, .000385e-7, .000099e-8, .000025e-9*/ };

View File

@ -154,52 +154,8 @@ public class Math2 {
public static double linInterp(double x, double a0, double a1, double b0, double b1) {
return (x-a0)*(b1-b0)/(a1-a0) + b0;
}
public static double[] linInterp(double[] xs, double[][] A, double[][] B) {
double[] out = new double[xs.length];
for (int i = 0; i < xs.length; i ++)
out[i] = linInterp(xs[i], A[0][i], A[1][i], B[0][i], B[1][i]);
return out;
}
public static boolean outOfBoundsInSameDirection(double[][] range, double[]... xs) {
boolean allOutOnLeft = true;
for (double[] x : xs)
if (x[0] >= range[0][0]) {
allOutOnLeft = false;
break;
}
if (allOutOnLeft)
return true;
boolean allOutOnRight = true;
for (double[] x : xs)
if (x[0] <= range[1][0]) {
allOutOnRight = false;
break;
}
if (allOutOnRight)
return true;
boolean allOutOnBottom = true;
for (double[] x : xs)
if (x[1] >= range[0][1]) {
allOutOnBottom = false;
break;
}
if (allOutOnBottom)
return true;
boolean allOutOnTop = true;
for (double[] x : xs)
if (x[1] <= range[1][1]) {
allOutOnTop = false;
break;
}
return allOutOnTop;
}
public static double hypot(double[] a, double[] b) {
return Math.hypot(a[0] - b[0], a[1] - b[1]);
}

View File

@ -44,11 +44,7 @@ public class MutableDouble {
public double get() {
return this.value;
}
public boolean isNaN() {
return Double.isNaN(this.value);
}
public void set(double value) {
this.value = value;
}

View File

@ -1,46 +0,0 @@
/**
* 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.
*/
package utils;
/**
* A class for working with and manipulating XML stuff.
*
* @author jkunimune
*/
public class SAXUtils {
public static String encode(String s0) { //encode with the ampersand notation
StringBuilder s1 = new StringBuilder();
for (int i = 0; i < s0.length(); i ++) {
if (s0.charAt(i) >= 128)
s1.append("&#").append((int) s0.charAt(i)).append(";");
else if (s0.charAt(i) == '&')
s1.append("&amp;");
else
s1.append(s0.charAt(i));
}
return s1.toString();
}
}

View File

@ -2,7 +2,7 @@ import math
AXIAL_TILT = 23.43694
ANTI_TILT = 66.56306 #I have both of these because it's the easiest way to deal with roundoff
ANTI_TILT = 66.56306 # I have both of these because it's the easiest way to deal with roundoff
def plot_meridian(lamd, granularity, cut=0, clazz=None) -> str:
@ -27,7 +27,7 @@ def generate_graticule(spacing, granularity, include_tropics=False, adjust_poles
result = ""
NUM_BASE = 90//spacing
cuts = [0]*(4*NUM_BASE)
if adjust_poles: #if this is True, reduce the number of meridians as you approach the pole
if adjust_poles: # if this is True, reduce the number of meridians as you approach the pole
old_num = 1
for p in range(0, 90, spacing):
new_num = old_num*int(1/math.cos(math.radians(p+spacing/2))/old_num)

View File

@ -15,7 +15,7 @@ def plot_orthodrome(phi0, lam0, tht0) -> str:
def generate_orthodromes() -> str:
"""generate an icosohedral orthodromic mesh, like the Brilliant logo (#notsponsored)"""
"""generate an icosahedral orthodromic mesh, like the Brilliant logo (#notsponsored)"""
result = ""
for l in range(-180, 180, 36):
result += plot_orthodrome(pi/2, 0, radians(l))

View File

@ -67,7 +67,7 @@ def plot_political_shapes(filename, mode="normal",
else:
regions = load_shapes_from_one_place_and_records_from_another(filename, include_circles_from)
# go thru the records and sort them into a dictionary by ISO 3166 codes
hierarchially_arranged_regions: dict[tuple[tuple[str, ...], str], ShapeRecord] = {}
hierarchically_arranged_regions: dict[tuple[tuple[str, ...], str], ShapeRecord] = {}
for shape, record in regions:
# if it Antarctica, trim it
if trim_antarctica:
@ -84,25 +84,25 @@ def plot_political_shapes(filename, mode="normal",
if province_code.endswith("~"):
province_code = province_code[:-1]
country_code = province_code[:province_code.index("-")]
hierarchial_identifier = (sovereign_code, country_code, province_code)
hierarchical_identifier = (sovereign_code, country_code, province_code)
unique_identifier = record["adm1_code"]
else: # or by sovereign, admin0
country_code = record["adm0_a3"]
hierarchial_identifier = (sovereign_code, country_code)
hierarchical_identifier = (sovereign_code, country_code)
unique_identifier = country_code
if hierarchial_identifier[0] == hierarchial_identifier[1]: # remove duplicate layers
hierarchial_identifier = hierarchial_identifier[1:]
key = (hierarchial_identifier, unique_identifier)
hierarchially_arranged_regions[key] = (shape, record)
if hierarchical_identifier[0] == hierarchical_identifier[1]: # remove duplicate layers
hierarchical_identifier = hierarchical_identifier[1:]
key = (hierarchical_identifier, unique_identifier)
hierarchically_arranged_regions[key] = (shape, record)
# next, go thru and plot the borders
current_state = []
result = ""
already_titled = set()
# for each item
for key in sorted(hierarchially_arranged_regions.keys()):
for key in sorted(hierarchically_arranged_regions.keys()):
hierarchy, identifier = key
shape, record = hierarchially_arranged_regions[key]
shape, record = hierarchically_arranged_regions[key]
# decide whether it's "small"
is_small = True
@ -179,7 +179,7 @@ def plot_political_shapes(filename, mode="normal",
current_state.pop()
# remove any groups with no elements
for i in range(3):
for _ in range(3):
result = re.sub(r'(\t)*<g class="([A-Za-z_-]+)">(\s*)</g>\n',
'', result)
# simplify any groups with only a single element

View File

@ -6,10 +6,11 @@ from helpers import plot, trim_edges, lengthen_edges, load_shaperecords
def plot_shapes(
filename, max_rank=float('inf'), clazz=None,
trim_antarctica=False, flesh_out_antarctica=False, mark_antarctica=False,
filter_field=None, filter_values=['']) -> str:
filter_field=None, filter_values=None) -> str:
result = ""
for shape, record in load_shaperecords(filename):
if filter_field is not None:
assert filter_values is not None
filter_value = record[filter_field]
if filter_value not in filter_values:
continue # skip it if it is filtered out

View File

@ -1,10 +1,10 @@
from __future__ import annotations
import math
import random as rng
from typing import Any, Iterable
from typing import Any, Iterable, Optional
import shapefile
from numpy import pi, sin, cos, tan, arcsin, arccos, degrees, ceil, radians, arctan2, hypot
def load_shaperecords(filename) -> Iterable[ShapeRecord]:
@ -49,31 +49,46 @@ def load_shapes_from_one_place_and_records_from_another(shape_filename, record_f
def obliquify(lat1, lon1, lat0, lon0):
""" go from relative to absolute coordinates """
latf = math.asin(math.sin(lat0)*math.sin(lat1) - math.cos(lat0)*math.cos(lon1)*math.cos(lat1))
innerFunc = math.sin(lat1)/math.cos(lat0)/math.cos(latf) - math.tan(lat0)*math.tan(latf)
if lat0 == math.pi/2: # accounts for special case when lat0 = pi/2
latf = arcsin(sin(lat0)*sin(lat1) - cos(lat0)*cos(lon1)*cos(lat1))
innerFunc = sin(lat1)/cos(lat0)/cos(latf) - tan(lat0)*tan(latf)
if lat0 == pi/2: # accounts for special case when lat0 = pi/2
lonf = lon1+lon0
elif lat0 == -math.pi/2: # accounts for special case when lat0 = -pi/2
lonf = -lon1+lon0 + math.pi
elif lat0 == -pi/2: # accounts for special case when lat0 = -pi/2
lonf = -lon1+lon0 + pi
elif abs(innerFunc) > 1: # accounts for special case when cos(lat1) -> 0
if (lon1 == 0 and lat1 < -lat0) or (lon1 != 0 and lat1 < lat0):
lonf = lon0 + math.pi
lonf = lon0 + pi
else:
lonf = lon0
elif math.sin(lon1) > 0:
lonf = lon0 + math.acos(innerFunc)
elif sin(lon1) > 0:
lonf = lon0 + arccos(innerFunc)
else:
lonf = lon0 - math.acos(innerFunc)
lonf = lon0 - arccos(innerFunc)
while lonf > math.pi:
lonf -= 2*math.pi
while lonf < -math.pi:
lonf += 2*math.pi
while lonf > pi:
lonf -= 2*pi
while lonf < -pi:
lonf += 2*pi
return latf, lonf
def plot(coords, midx=[0], close=True, fourmat='pr', clazz=None, ident=None, tabs=3) -> str:
def plot(coords: list[tuple[float, float]], midx: Optional[list[int]] = None, close=True, fourmat='pr', clazz=None, ident=None, tabs=3) -> str:
"""
express a list of 2D points as an SVG <path> tag
:param coords: the coordinate pairs of the vertices
:param midx: the indices at which each part of the path starts (that is, the indices of the movetos in the path)
:param close: whether to toss a Z on the end of the path string
:param fourmat: the order and units of the coordinates in each pair. probably "pr" if it's (ф,λ) coordinates in
radians, or "xd" if it's (λ,ф) in degrees. the output will always be expressed as (λ,ф) in degrees,
so that's why I ask.
:param clazz: the class attribute to give the <path>, if any
:param ident: the id attribute to give the <path>, if any
:param tabs: the number of tab characters to prepend to the <path> element
:return: a string describing the <path> in SVG syntax, including some tab characters on the front and a newline on the back
"""
if midx is None:
midx = [0]
class_attr = f'class="{clazz}" ' if clazz is not None else ''
ident_attr = f'id="{ident}" ' if ident is not None else ''
tag = '\t'*tabs + f'<path {class_attr}{ident_attr}d="'
@ -92,11 +107,13 @@ def plot(coords, midx=[0], close=True, fourmat='pr', clazz=None, ident=None, tab
letter = 'L'
if 'r' in fourmat:
coord = (math.degrees(c) for c in coord)
coord = (degrees(c) for c in coord)
if 'p' in fourmat:
y, x = coord
elif 'x' in fourmat:
x, y = coord
else:
raise ValueError(f"unrecognized format string: '{fourmat}'")
tag += '{}{:.3f},{:.3f} '.format(letter, x, y)
if close:
@ -126,11 +143,11 @@ def lengthen_edges(coast):
x1, y1 = coast[i+1]
if x0 == x1 and abs(y1-y0) > 1:
step = 1 if y0 > y1 else -1
for j in range(math.ceil(abs(y1-y0))-1):
for j in range(int(ceil(abs(y1-y0))-1)):
coast.insert(i+1, (x1, y1+step*(j+1)))
elif y0 == y1 and abs(x1-x0) > 1:
step = 1 if x0 > x1 else -1
for j in range(math.ceil(abs(x1-x0))-1):
for j in range(int(ceil(abs(x1-x0))-1)):
coast.insert(i+1, (x1+step*(j+1), y1))
return coast
@ -143,10 +160,10 @@ def get_centroid(points, parts=None):
maxP = max([p[1] for p in points])
if maxL-minL < 15 and maxP-minP < 60:
return ((maxL+minL)/2, (maxP+minP)/2)
elif parts: #if there are multiple parts, try guessing the centroid of just one; the biggest one by bounding box
return (maxL + minL)/2, (maxP + minP)/2
elif parts: # if there are multiple parts, try guessing the centroid of just one; the biggest one by bounding box
parts.append(len(points))
max_area = 0
max_area, best_part = 0, None
for i in range(1, len(parts)):
part = points[parts[i-1]:parts[i]]
minL = min([p[0] for p in part])
@ -156,7 +173,10 @@ def get_centroid(points, parts=None):
if (maxL-minL)*(maxP-minP) > max_area:
max_area = (maxL-minL)*(maxP-minP)
best_part = (parts[i-1], parts[i])
return get_centroid(points[best_part[0]:best_part[1]])
if best_part is not None:
return get_centroid(points[best_part[0]:best_part[1]])
else:
raise ValueError("no parts from which to take the centroid were found")
lines = []
for i in range(1, len(points)):
@ -164,30 +184,30 @@ def get_centroid(points, parts=None):
xc, yc, zc = 0, 0, 0
j = 0
num_in = 0
min_latnum = math.sin(math.radians(minP))
max_latnum = math.sin(math.radians(maxP))
min_lonnum = math.radians(minL)
max_lonnum = math.radians(maxL)
min_latnum = sin(radians(minP))
max_latnum = sin(radians(maxP))
min_lonnum = radians(minL)
max_lonnum = radians(maxL)
rng.seed(0)
while j < 4000 or num_in < 10: #monte carlo
while j < 4000 or num_in < 10: # monte carlo
j += 1
latr = math.asin(rng.random()*(max_latnum-min_latnum)+min_latnum)
latr = arcsin(rng.random()*(max_latnum-min_latnum)+min_latnum)
lonr = rng.random()*(max_lonnum-min_lonnum) + min_lonnum
latd, lond = math.degrees(latr), math.degrees(lonr)
latd, lond = degrees(latr), degrees(lonr)
num_crosses = 0
for l1, p1, l2, p2 in lines: #count the lines a northward ray crosses
for l1, p1, l2, p2 in lines: # count the lines a northward ray crosses
if ((l1 <= lond and l2 > lond) or (l2 <= lond and l1 > lond)) and (p1*(l2-lond)/(l2-l1) + p2*(l1-lond)/(l1-l2) > latd):
num_crosses += 1
if num_crosses%2 == 1: #if odd,
if num_crosses%2 == 1: # if odd,
num_in += 1
xc += math.cos(latr)*math.cos(lonr) #this point is in.
yc += math.cos(latr)*math.sin(lonr) #update the centroid
zc += math.sin(latr)
xc += cos(latr)*cos(lonr) # this point is in.
yc += cos(latr)*sin(lonr) # update the centroid
zc += sin(latr)
loncr = math.atan2(yc, xc)
latcr = math.atan2(zc, math.hypot(xc, yc))
return (math.degrees(loncr), math.degrees(latcr))
loncr = arctan2(yc, xc)
latcr = arctan2(zc, hypot(xc, yc))
return degrees(loncr), degrees(latcr)
def normalize_shaperecord(shaperecord: shapefile.ShapeRecord) -> ShapeRecord: