mirror of
https://github.com/csharpee/Map-Projections.git
synced 2025-12-24 00:00:03 -05:00
I've finally made something that's not trash!
My tetrafillet projection is actually kind of good! It's not really continuous like most of the projections I make, but I like it. I think it's cool. And it's finally objectively better than Winkel Tripel.
This commit is contained in:
parent
4080a8be6d
commit
1ff29b1405
BIN
output/graph.png
Normal file
BIN
output/graph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 212 B |
@ -1,12 +1,16 @@
|
||||
package mapAnalyzer;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.BinaryOperator;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.ScatterChart;
|
||||
import javafx.scene.chart.XYChart.Data;
|
||||
import javafx.scene.chart.XYChart.Series;
|
||||
import javafx.stage.Stage;
|
||||
@ -21,11 +25,11 @@ import vectormaps.MapProjections;
|
||||
*/
|
||||
public class MapOptimizer extends Application {
|
||||
|
||||
|
||||
private static final String[] EXISTING_PROJECTIONS = { "Hobo-Dyer", "Mollweide", "TetraGraph", "Robinson", "Lee" };
|
||||
private static final double[] WEIGHTS = { .11, .43, 1.0, 2.3, 9.0 };
|
||||
private static final int NUM_DESCENT = 10;//40;
|
||||
private ScatterChart<Number, Number> chart;
|
||||
// private static final String[] EXISTING_PROJECTIONS = { "Hobo-Dyer", "Mollweide", "EACyllindrical", "Robinson", "Winkel Tripel", "Van der Grinten", "Lee", "Mercator", "Orthographic" };
|
||||
private static final String[] EXISTING_PROJECTIONS = { "Hobo-Dyer", "Robinson", "Tetragraph", "Lee" };
|
||||
private static final double[] WEIGHTS = { .125, .5, 1.0, 2, 8 };//{ .11, .25
|
||||
private static final int NUM_DESCENT = 40;
|
||||
private LineChart<Number, Number> chart;
|
||||
|
||||
|
||||
|
||||
@ -38,21 +42,27 @@ public class MapOptimizer extends Application {
|
||||
public void start(Stage stage) throws Exception {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
|
||||
chart = new ScatterChart<Number, Number>(
|
||||
chart = new LineChart<Number, Number>(
|
||||
new NumberAxis("Size distortion", 0, 1, 0.2),
|
||||
new NumberAxis("Shape distortion", 0, 1, 0.2));
|
||||
chart.setCreateSymbols(true);
|
||||
double[][][] globe = MapAnalyzer.globe(0.02);
|
||||
|
||||
chart.getData().add(analyzeAll(globe, EXISTING_PROJECTIONS));
|
||||
//chart.getData().add(optimizeHyperelliptical(globe));
|
||||
// chart.getData().add(optimizeHyperelliptical(globe));
|
||||
chart.getData().add(optimizeTetrapower(globe));
|
||||
//chart.getData().add(optimizeTetrafilllet(globe));
|
||||
chart.getData().add(optimizeTetrafillet(globe));
|
||||
|
||||
System.out.println("Total time elapsed: "+
|
||||
(System.currentTimeMillis()-startTime)/1000.+"s");
|
||||
|
||||
stage.setTitle("Map Projections");
|
||||
stage.setScene(new Scene(chart));
|
||||
|
||||
ImageIO.write(
|
||||
SwingFXUtils.fromFXImage(chart.snapshot(null, null), null),
|
||||
"png", new File("output/graph.png"));
|
||||
|
||||
stage.show();
|
||||
}
|
||||
|
||||
@ -123,7 +133,6 @@ public class MapOptimizer extends Application {
|
||||
frd[j] = avgDistortion(projectionFam, params, points, WEIGHTS[k]); //and the distortion nearby
|
||||
params[j] -= h;
|
||||
}
|
||||
System.out.println(Arrays.toString(frd));
|
||||
for (int j = 0; j < params.length; j ++)
|
||||
params[j] -= (frd[j]-fr0)/h*delX; //use that to approximate the gradient and go in that direction
|
||||
}
|
||||
@ -139,7 +148,7 @@ public class MapOptimizer extends Application {
|
||||
System.out.print("\t");
|
||||
for (int i = 0; i < params.length; i ++)
|
||||
System.out.print("t"+i+"="+best[3+i]+"; ");
|
||||
System.out.println();
|
||||
System.out.println("\t("+best[1]+", "+best+")");
|
||||
output.getData().add(new Data<Number, Number>(best[1], best[2]));
|
||||
}
|
||||
return output;
|
||||
@ -154,7 +163,13 @@ public class MapOptimizer extends Application {
|
||||
|
||||
private static Series<Number, Number> optimizeTetrapower(double[][][] points) { //optimize and plot some hyperelliptical maps
|
||||
return optimizeFamily(MapOptimizer::tetrapower, "Tetrapower",
|
||||
new double[][] {{0.25,2.25}, {0.25,2.25}}, points);
|
||||
new double[][] {{0.25,2.25}, {0.25,2.25}, {.25,2.25}}, points);
|
||||
}
|
||||
|
||||
|
||||
private static Series<Number, Number> optimizeTetrafillet(double[][][] points) { //optimize and plot some hyperelliptical maps
|
||||
return optimizeFamily(MapOptimizer::tetrafillet, "Tetrafillet",
|
||||
new double[][] {{0.25,2.25}, {0.25,2.25}, {.25,2.25}}, points);
|
||||
}
|
||||
|
||||
|
||||
@ -177,17 +192,35 @@ public class MapOptimizer extends Application {
|
||||
|
||||
|
||||
private static final double[] tetrapower(double[] coords, double[] params) { //a tetragraph projection using a few power functions to spice it up
|
||||
final double k1 = params[0], k2 = params[0];
|
||||
final double k1 = params[0], k2 = params[1], k3 = params[2];
|
||||
|
||||
return MapProjections.tetrahedralProjection(coords[0], coords[1], (coordR) -> {
|
||||
final double t0 = Math.floor(coordR[1]/(2*Math.PI/3))*(2*Math.PI/3) + Math.PI/3;
|
||||
final double tht = coordR[1] - t0;
|
||||
final double thtP = Math.PI/3*(1 - Math.pow(1-Math.abs(tht)/(Math.PI/2),k1))/(1 - 1/Math.pow(3,k1))*Math.signum(tht);
|
||||
final double rmax = .5/Math.cos(thtP); //the max radius of this triangle (in the plane)
|
||||
final double kRad = k3*Math.abs(thtP)/(Math.PI/3) + k2*(1-Math.abs(thtP)/(Math.PI/3));
|
||||
final double rmax = .5/Math.cos(thtP); //the max normalized radius of this triangle (in the plane)
|
||||
final double rtgf = Math.atan(1/Math.tan(coordR[0])*Math.cos(tht))/Math.atan(Math.sqrt(2))*rmax; //normalized tetragraph radius
|
||||
return new double[] {
|
||||
(1 - Math.pow(1-rtgf,k2))/(1 - Math.pow(1-rmax,k2))*rmax*2*Math.PI/3,
|
||||
//rtgf*2*Math.PI/3,
|
||||
(1 - Math.pow(1-rtgf,kRad))/(1 - Math.pow(1-rmax,kRad))*rmax*2*Math.PI/3,
|
||||
thtP + t0
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static final double[] tetrafillet(double[] coords, double[] params) { //a tetragraph projection using a few power functions to spice it up and now with fillets
|
||||
final double k1 = params[0], k2 = params[1], k3 = params[2];
|
||||
|
||||
return MapProjections.tetrahedralProjection(coords[0], coords[1], (coordR) -> {
|
||||
final double t0 = Math.floor(coordR[1]/(2*Math.PI/3))*(2*Math.PI/3) + Math.PI/3;
|
||||
final double tht = coordR[1] - t0;
|
||||
final double thtP = Math.PI/3*(1 - Math.pow(1-Math.abs(tht)/(Math.PI/2),k1))/(1 - 1/Math.pow(3,k1))*Math.signum(tht);
|
||||
final double kRad = k3*Math.abs(thtP)/(Math.PI/3) + k2*(1-Math.abs(thtP)/(Math.PI/3));
|
||||
final double rmax = 1/2. + 1/4.*Math.pow(thtP,2) + 5/48.*Math.pow(thtP,4) - .132621*Math.pow(thtP,6); //the max normalized radius of this triangle (in the plane)
|
||||
final double rtgf = Math.atan(1/Math.tan(coordR[0])*Math.cos(tht))/Math.atan(Math.sqrt(2))*rmax; //normalized tetragraph radius
|
||||
return new double[] {
|
||||
(1 - Math.pow(1-rtgf,kRad))/(1 - Math.pow(1-rmax,kRad))*rmax*2*Math.PI/3,
|
||||
thtP + t0
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package rastermaps;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.Optional;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
@ -24,6 +25,7 @@ import javafx.scene.control.MenuButton;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
@ -85,9 +87,9 @@ public class MapProjections extends Application {
|
||||
|
||||
private static final String[] AXES = { "Standard", "Transverse", "Center of Mass", "Jerusalem", "Point Nemo",
|
||||
"Longest Line", "Longest Line Transverse", "Cylindrical", "Conic", "Tetrahedral", "Quincuncial", "Antipode", "Random" };
|
||||
private static final double[] DEF_LATS = { 90, 0, 29.9792, 31.7833, 48.8767, -28.5217, -46.4883, -35, -10, 47, 60 };
|
||||
private static final double[] DEF_LONS = { 0, 0, 31.1344, 35.216, 56.6067, 141.451, 16.5305, -13.6064, 65, -173, -6 };
|
||||
private static final double[] DEF_THTS = { 0, 0, -32, -35, -45, 161.5, 137, 145, -150, 138, -10 };
|
||||
private static final double[][] DEF_ASPECTS = { { 90, 0, 29.9792, 31.7833, 48.8767, -28.5217, -46.4883, -35, -10, 47, 60 },
|
||||
{ 0, 0, 31.1344, 35.216, 56.6067, 141.451, 16.5305, -13.6064, 65, -173, -6 },
|
||||
{ 0, 0, -32, -35, -45, 161.5, 137, 145, -150, 138, -10 } };
|
||||
|
||||
|
||||
private Stage stage;
|
||||
@ -96,7 +98,8 @@ public class MapProjections extends Application {
|
||||
private Button changeInput;
|
||||
private ComboBox<String> projectionChooser;
|
||||
private Text projectionDesc;
|
||||
private Slider latSlider, lonSlider, thtSlider;
|
||||
private Slider[] aspectSliders;
|
||||
private Spinner<Double>[] aspectSpinners;
|
||||
private Button update, saveMap;
|
||||
private Image input;
|
||||
private ImageView output;
|
||||
@ -108,6 +111,7 @@ public class MapProjections extends Application {
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
stage = primaryStage;
|
||||
@ -182,22 +186,53 @@ public class MapProjections extends Application {
|
||||
"Set the aspect sliders based on a preset"));
|
||||
layout.getChildren().add(defAxes);
|
||||
|
||||
latSlider = new Slider(-90, 90, 90);
|
||||
lonSlider = new Slider(-180,180,0);
|
||||
thtSlider = new Slider(-180,180,0);
|
||||
Tooltip aspTlTp = new Tooltip("Change the aspect of the map");
|
||||
latSlider.setTooltip(aspTlTp);
|
||||
lonSlider.setTooltip(aspTlTp);
|
||||
thtSlider.setTooltip(aspTlTp);
|
||||
aspectSliders = new Slider[] {
|
||||
new Slider(-90, 90, 90),
|
||||
new Slider(-180,180,0),
|
||||
new Slider(-180,180,0)
|
||||
};
|
||||
final Spinner<Double> latSpinner = new Spinner<Double>(-90, 90, 90.0);
|
||||
aspectSpinners = (Spinner<Double>[]) Array.newInstance(latSpinner.getClass(), 3);
|
||||
aspectSpinners[0] = latSpinner;
|
||||
aspectSpinners[1] = new Spinner<Double>(-180, 180, 0.0);
|
||||
aspectSpinners[2] = new Spinner<Double>(-180, 180, 0.0);
|
||||
|
||||
GridPane grid = new GridPane();
|
||||
grid.addRow(0, new Text("Latitude:"), latSlider);
|
||||
grid.addRow(1, new Text("Longitude:"), lonSlider);
|
||||
grid.addRow(2, new Text("Orientation:"), thtSlider);
|
||||
GridPane.setHgrow(latSlider, Priority.ALWAYS);
|
||||
GridPane.setHgrow(lonSlider, Priority.ALWAYS);
|
||||
GridPane.setHgrow(thtSlider, Priority.ALWAYS);
|
||||
//GridPane.setFillWidth(latSlider, Boolean.valueOf(true));
|
||||
grid.addRow(0, new Text("Latitude:"), aspectSliders[0], aspectSpinners[0]);
|
||||
grid.addRow(1, new Text("Longitude:"), aspectSliders[1], aspectSpinners[1]);
|
||||
grid.addRow(2, new Text("Orientation:"), aspectSliders[2], aspectSpinners[2]);
|
||||
|
||||
for (int i = 0; i < 3; i ++) {
|
||||
final Slider sld = aspectSliders[i];
|
||||
final Spinner<Double> spn = aspectSpinners[i];
|
||||
GridPane.setHgrow(sld, Priority.ALWAYS);
|
||||
sld.setTooltip(new Tooltip("Change the aspect of the map"));
|
||||
sld.valueChangingProperty().addListener(
|
||||
(observable, then, now) -> {
|
||||
if (spn.getValue() != sld.getValue())
|
||||
spn.getEditor().textProperty().set(Double.toString(sld.getValue()));
|
||||
});
|
||||
|
||||
spn.setTooltip(new Tooltip("Change the aspect of the map"));
|
||||
spn.setPrefWidth(100);
|
||||
spn.setEditable(true);
|
||||
spn.getEditor().textProperty().addListener((ov, pv, nv) -> { // link the Spinners
|
||||
if (spn.getEditor().textProperty().isEmpty().get()) return;
|
||||
try {
|
||||
Double.parseDouble(nv);
|
||||
spn.increment(0); // forces the spinner to commit its value
|
||||
if (spn.getValue() != sld.getValue())
|
||||
sld.setValue(spn.getValue());
|
||||
} catch (NumberFormatException e) {
|
||||
spn.getEditor().textProperty().set(pv); //yeah, this is all pretty jank. JavaFX spinners are just weird by default
|
||||
}
|
||||
});
|
||||
spn.setOnKeyPressed((event) -> {
|
||||
System.out.println(event);
|
||||
if (event.getCode() == KeyCode.ENTER)
|
||||
updateMap();
|
||||
});
|
||||
}
|
||||
layout.getChildren().add(grid);
|
||||
|
||||
layout.getChildren().add(new Separator());
|
||||
@ -288,22 +323,21 @@ public class MapProjections extends Application {
|
||||
|
||||
private void setAxisByPreset(String preset) {
|
||||
if (preset.equals("Antipode")) {
|
||||
latSlider.setValue(-latSlider.getValue());
|
||||
lonSlider.setValue((lonSlider.getValue()+360)%360-180);
|
||||
thtSlider.setValue(-thtSlider.getValue());
|
||||
for (Slider s: aspectSliders)
|
||||
s.setValue(-s.getValue());
|
||||
aspectSliders[1].setValue((-aspectSliders[1].getValue()+360)%360-180);
|
||||
return;
|
||||
}
|
||||
if (preset.equals("Random")) {
|
||||
latSlider.setValue(Math.toDegrees(Math.asin(Math.random()*2-1)));
|
||||
lonSlider.setValue(Math.random()*360-180);
|
||||
thtSlider.setValue(Math.random()*360-180);
|
||||
aspectSliders[0].setValue(Math.toDegrees(Math.asin(Math.random()*2-1)));
|
||||
aspectSliders[1].setValue(Math.random()*360-180);
|
||||
aspectSliders[2].setValue(Math.random()*360-180);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < AXES.length; i ++) {
|
||||
if (AXES[i].equals(preset)) {
|
||||
latSlider.setValue(DEF_LATS[i]);
|
||||
lonSlider.setValue(DEF_LONS[i]);
|
||||
thtSlider.setValue(DEF_THTS[i]);
|
||||
for (int j = 0; j < 3; j ++)
|
||||
aspectSliders[j].setValue(DEF_ASPECTS[j][i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -343,7 +377,7 @@ public class MapProjections extends Application {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
saveMap.setDisable(true);
|
||||
ImageIO.write(SwingFXUtils.fromFXImage(img,null), "png", f);
|
||||
ImageIO.write(SwingFXUtils.fromFXImage(img,null), "jpg", f);
|
||||
saveMap.setDisable(false);
|
||||
} catch (IOException e) {}
|
||||
}).start();
|
||||
@ -368,9 +402,9 @@ public class MapProjections extends Application {
|
||||
public Image map(int outputWidth, int outputHeight, int smoothing,
|
||||
ProgressBarDialog pbar) {
|
||||
final String proj = projectionChooser.getValue();
|
||||
final double[] pole = {Math.toRadians(latSlider.getValue()),
|
||||
Math.toRadians(lonSlider.getValue()),
|
||||
Math.toRadians(thtSlider.getValue())};
|
||||
final double[] pole = new double[3];
|
||||
for (int i = 0; i < 3; i ++)
|
||||
pole[i] = Math.toRadians(aspectSliders[i].getValue());
|
||||
final PixelReader ref = input.getPixelReader();
|
||||
final int[] refDims = {(int)input.getWidth(), (int)input.getHeight()};
|
||||
final int[] outDims = {outputWidth, outputHeight};
|
||||
@ -565,7 +599,9 @@ public class MapProjections extends Application {
|
||||
|
||||
private static double[] experiment(double x, double y) { // just some random complex plane stuff
|
||||
Complex z = new Complex(x*3, y*3);
|
||||
Complex ans = z.sin();
|
||||
// Complex ans = z.sin();
|
||||
Complex k = new Complex(Math.sqrt(.75));
|
||||
Complex ans = Jacobi.cn(z.plus(3), k);
|
||||
double p = 2 * Math.atan(ans.abs());
|
||||
double theta = ans.arg();
|
||||
double lambda = Math.PI/2 - p;
|
||||
|
||||
@ -860,18 +860,23 @@ public class MapProjections extends Application {
|
||||
}
|
||||
|
||||
private static double[] tetrapower(double lat, double lon) { // a tetrahedral parametric map
|
||||
final double k1 = .5;
|
||||
final double k2 = .8;
|
||||
// final double k1 = .5;
|
||||
// final double k2 = 1.1;
|
||||
// final double k3 = .7;
|
||||
final double k1 = 1;
|
||||
final double k2 = 1;
|
||||
final double k3 = 1;
|
||||
|
||||
return tetrahedralProjection(lat, lon, (coordR) -> {
|
||||
final double t0 = Math.floor(coordR[1]/(2*Math.PI/3))*(2*Math.PI/3) + Math.PI/3;
|
||||
final double tht = coordR[1] - t0;
|
||||
final double thtP = Math.PI/3*(1 - Math.pow(1-Math.abs(tht)/(Math.PI/2),k1))/(1 - 1/Math.pow(3,k1))*Math.signum(tht);
|
||||
final double rmax = .5/Math.cos(thtP); //the max radius of this triangle (in the plane)
|
||||
final double kRad = k3*Math.abs(thtP)/(Math.PI/3) + k2*(1-Math.abs(thtP)/(Math.PI/3));
|
||||
// final double rmax = .5/Math.cos(thtP); //the max normalized radius of this triangle (in the plane)
|
||||
final double rmax = 1/2. + 1/4.*Math.pow(thtP,2) + 5/48.*Math.pow(thtP,4) - .132621*Math.pow(thtP,6); //the max normalized radius of this triangle (in the plane)
|
||||
final double rtgf = Math.atan(1/Math.tan(coordR[0])*Math.cos(tht))/Math.atan(Math.sqrt(2))*rmax; //normalized tetragraph radius
|
||||
return new double[] {
|
||||
(1 - Math.pow(1-rtgf,k2))/(1 - Math.pow(1-rmax,k2))*rmax*2*Math.PI/3,
|
||||
//rtgf*2*Math.PI/3,
|
||||
(1 - Math.pow(1-rtgf,kRad))/(1 - Math.pow(1-rmax,kRad))*rmax*2*Math.PI/3,
|
||||
thtP + t0
|
||||
};
|
||||
});
|
||||
@ -895,7 +900,6 @@ public class MapProjections extends Application {
|
||||
Robinson.pdfeFromLat(Math.abs(lat))*Math.signum(lat)*Math.PI/2};
|
||||
}
|
||||
|
||||
|
||||
private static final double[] hyperelliptical(double lat, double lon) { //a hyperelliptic map projection with hyperellipse order k and lattitudinal spacind described by x^n/n
|
||||
final double k = 3.25, n = 1.25, a = 1.125;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user