Map-Projections/src/MapProjections.java
jkunimune cc1ab0b27b Bug fixes
Yep. I'm too lazy to write a more descriptive message.
2016-10-11 19:49:23 -04:00

534 lines
19 KiB
Java

import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import ellipticFunctions.Jacobi;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Slider;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import mfc.field.Complex;
/**
*
*/
/**
* @author Justin Kunimune
*
*/
public class MapProjections extends Application {
private static final int CONT_WIDTH = 300;
private static final int IMG_WIDTH = 500;
private static final String[] PROJ_ARR = { "Equirectangular", "Mercator", "Gall Stereographic",
"Cylindrical Equal-Area", "Polar", "Stereographic", "Azimuthal Equal-Area",
"Orthographic", "Gnomonic", "Lambert Conical", "Winkel Tripel", "Van der Grinten", "Mollweide", "Hammer",
"Sinusoidal", "Lemons", "Pierce Quincuncial", "Guyou Hemisphere-in-a-Square", "Magnifier" };
private static final double[] DEFA = { 2, 1, 4/3.0, 2, 1, 1, 1, 1, 1, 2, Math.PI/2, 1, 2, 2,
2, 2, 1, 2, 1 };
private static final String[] DESC = { "An equidistant cylindrical map", "A conformal cylindrical map",
"A compromising cylindrical map", "An equal-area cylindrical map", "An equidistant azimuthal map",
"A conformal azimuthal map", "An equal-area azimuthal map",
"Represents earth viewed from an infinite distance",
"Every straight line on the map is a straight line on the sphere", "A conformal conical map",
"The compromise map used by National Geographic (caution: very slow)", "A circular compromise map",
"An equal-area map shaped like an elipse", "An equal-area map shaped like an elipse",
"An equal-area map shaped like a sinusoid", "BURN LIFE'S HOUSE DOWN!",
"A conformal square map that uses complex math",
"A reorganized version of Pierce Quincuncial and actually the best map ever",
"A novelty map that swells the center to disproportionate scale" };
private static final String[] AXES = { "Standard", "Transverse", "Center of Mass", "Jerusalem", "Point Nemo",
"Longest Line", "Longest Line Transverse", "Cylindrical", "Conical", "Quincuncial" };
private static final double[] DEF_LATS = { 90, 0, 29.9792, 31.7833, 48.8767, -28.5217, -46.4883, -35, -10, 60 };
private static final double[] DEF_LONS = { 0, 0, 31.1344, 35.216, 56.6067, 141.451, 16.5305, -13.6064, 65, -6 };
private static final double[] DEF_THTS = { 0, 0, -32, -35, -45, 71.5, 137, 145, -150, -10 };
private FileChooser inputChooser, saver;
private Text inputLabel;
private ComboBox<String> projectionChooser;
private Text projectionDesc;
private Slider latSlider, lonSlider, thtSlider;
private Button update;
private Image input;
private int outputWidth, outputHeight;
private ImageView output;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
stage.setTitle("Map Designer");
final VBox layout = new VBox();
layout.setSpacing(5);
layout.setAlignment(Pos.CENTER);
layout.setPrefWidth(CONT_WIDTH);
Label lbl = new Label("Current input:");
inputLabel = new Text("None");
layout.getChildren().add(new HBox(3, lbl, inputLabel));
inputChooser = new FileChooser();
inputChooser.setInitialDirectory(new File("input"));
inputChooser.setTitle("Choose an input map");
inputChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Images", "*.*"),
new FileChooser.ExtensionFilter("JPG", "*.jpg"),
new FileChooser.ExtensionFilter("PNG", "*.png"));
final Button changeInput = new Button("Choose input...");
changeInput.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
final File f = inputChooser.showOpenDialog(stage);
if (f == null) return;
input = new Image("file:"+f.getAbsolutePath());
inputLabel.setText(f.getName());
update.setDisable(false);
}
});
changeInput.setTooltip(new Tooltip(
"Choose the image to determine your map's color scheme"));
layout.getChildren().add(changeInput);
lbl = new Label("Projection:");
ObservableList<String> items = FXCollections.observableArrayList(PROJ_ARR);
projectionChooser = new ComboBox<String>(items);
projectionChooser.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
for (int i = 0; i < PROJ_ARR.length; i ++) {
if (PROJ_ARR[i].equals(projectionChooser.getValue())) {
projectionDesc.setText(DESC[i]);
break;
}
}
}
});
projectionChooser.setPrefWidth(200);
projectionChooser.setValue("Equirectangular");
layout.getChildren().add(new HBox(3, lbl, projectionChooser));
projectionDesc = new Text("Please choose a projection.");
projectionDesc.setWrappingWidth(CONT_WIDTH);
layout.getChildren().add(projectionDesc);
final MenuButton defAxes = new MenuButton("Axis Presets");
for (String preset: AXES) {
MenuItem m = new MenuItem(preset);
m.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
setAxisByPreset(((MenuItem) event.getSource()).getText());
}
});
defAxes.getItems().add(m);
}
defAxes.setTooltip(new Tooltip(
"Set the axis 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);
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);
layout.getChildren().add(grid);
update = new Button("Update Map");
update.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
output.setImage(map(projectionChooser.getValue(),
latSlider.getValue(),
lonSlider.getValue(),
thtSlider.getValue()));
}
});
update.setTooltip(new Tooltip(
"Update the current map with your parameters."));
update.setDisable(true);
update.setDefaultButton(true);
layout.getChildren().add(update);
saver = new FileChooser();
saver.setInitialDirectory(new File("output"));
saver.setInitialFileName("myMap.png");
saver.setTitle("Save Map");
saver.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("PNG", "*.png"));
final Button saveMap = new Button("Save Map...");
saveMap.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
final File f = saver.showSaveDialog(stage);
if (f == null) return;
try {
ImageIO.write(
SwingFXUtils.fromFXImage(output.getImage(),null),
"png", f);
} catch (IOException e) {}
}
});
saveMap.setTooltip(new Tooltip("Save your new custom map!"));
layout.getChildren().add(saveMap);
output = new ImageView();
output.setFitWidth(IMG_WIDTH);
output.setFitHeight(IMG_WIDTH);
output.setPreserveRatio(true);
final HBox gui = new HBox(layout, output);
gui.setAlignment(Pos.CENTER);
StackPane.setMargin(gui, new Insets(10));
stage.setScene(new Scene(new StackPane(gui)));
stage.show();
}
private void setAxisByPreset(String preset) {
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]);
break;
}
}
}
public Image map(String projName,
double latD, double lonD, double thtD) {
int p = 0;
for (int i = 0; i < PROJ_ARR.length; i ++)
if (PROJ_ARR[i].equals(projName))
p = i;
outputWidth = (int)(600*Math.sqrt(DEFA[p]));
outputHeight = (int)(600/Math.sqrt(DEFA[p]));
WritableImage img = new WritableImage(outputWidth, outputHeight);
for (int x = 0; x < outputWidth; x ++)
for (int y = 0; y < outputHeight; y ++)
img.getPixelWriter().setArgb(x, y, getArgb(x, y));
return img;
}
public int getArgb(int x, int y) {
final double lat0 = Math.toRadians(latSlider.getValue());
final double lon0 = Math.toRadians(lonSlider.getValue());
final double tht0 = Math.toRadians(thtSlider.getValue());
final double pole[] = {lat0, lon0, tht0};
final double width = input.getWidth();
final double height = input.getHeight();
final int[] refDims = {(int)width, (int)height};
final String p = projectionChooser.getValue();
final double X = 2.0*x/outputWidth-1;
final double Y = 2.0*y/outputHeight-1;
if (p.equals("Pierce Quincuncial"))
return quincuncial(pole, X, Y, refDims, input);
else if (p.equals("Equirectangular"))
return equirectangular(pole, X, Y, refDims, input);
else if (p.equals("Mercator"))
return mercator(pole, X, Y, refDims, input);
else if (p.equals("Polar"))
return polar(pole, X, Y, refDims, input);
else if (p.equals("Gall Stereographic"))
return gall(pole, X, Y, refDims, input);
else if (p.equals("Sinusoidal"))
return sinusoidal(pole, X, Y, refDims, input);
else if (p.equals("Stereographic"))
return stereographic(pole, X, Y, refDims, input);
else if (p.equals("Gnomonic"))
return gnomonic(pole, X, Y, refDims, input);
else if (p.equals("Orthographic"))
return orthographic(pole, X, Y, refDims, input);
else if (p.equals("Cylindrical Equal-Area"))
return eaCylindrical(pole, X, Y, refDims, input);
else if (p.equals("Lambert Conical"))
return lambert(pole, X, Y, refDims, input);
else if (p.equals("Lemons"))
return lemons(pole, X, Y, refDims, input);
else if (p.equals("Azimuthal Equal-Area"))
return eaAzimuth(pole, X, Y, refDims, input);
else if (p.equals("Guyou Hemisphere-in-a-Square"))
return quinshift(pole, X, Y, refDims, input);
else if (p.equals("Mollweide"))
return mollweide(pole, X, Y, refDims, input);
else if (p.equals("Winkel Tripel"))
return winkel_tripel(pole, X, Y, refDims, input);
else if (p.equals("Van der Grinten"))
return grinten(pole, X, Y, refDims, input);
else if (p.equals("Magnifier"))
return magnus(pole, X, Y, refDims, input);
else if (p.equals("Hammer"))
return hammer(pole, X, Y, refDims, input);
else
throw new IllegalArgumentException(p);
}
private static int quincuncial(final double[] pole, double x, double y,
int[] refDims, Image ref) { // a tessalatable square map
Complex u = new Complex(1.8558 * (x+1), 1.8558 * y); // don't ask me where 3.7116 comes from
Complex k = new Complex(Math.sqrt(0.5)); // the rest comes from some fancy complex calculus
Complex ans = Jacobi.cn(u, k);
double p = 2 * Math.atan(ans.abs());
double theta = Math.atan2(ans.getRe(), ans.getIm()) + Math.PI;
double lambda = p - Math.PI / 2;
return getColor(pole, lambda, theta, refDims, ref);
}
private static int equirectangular(final double[] pole, double x, double y,
int[] refDims, Image ref) { // a basic scale
return getColor(pole, y*Math.PI/2, x*Math.PI, refDims, ref);
}
private static int mercator(final double[] pole, double x, double y,
int[] refDims, Image ref) { // a popular shape-preserving map
double phi = Math.atan(Math.sinh(y*Math.PI));
return getColor(pole, phi, x*Math.PI, refDims, ref);
}
private int polar(final double[] pole, double x, double y,
int[] refDims, Image ref) { // the projection used on the UN flag
double phi = Math.PI * Math.hypot(x, y) - Math.PI/2;
if (Math.abs(phi) < Math.PI/2)
return getColor(pole, phi, Math.atan2(x, y), refDims, ref);
else
return 0;
}
private int gall(final double[] pole, double x, double y,
int[] refDims, Image ref) { // a compromise map, similar to mercator
return getColor(pole, 2*Math.atan(y), x*Math.PI, refDims, ref);
}
private int sinusoidal(final double[] pole, double x, double y,
int[] refDims, Image ref) { // a map shaped like a sinusoid
return getColor(pole, y*Math.PI/2,
x*Math.PI / Math.cos(y*Math.PI/2), refDims, ref);
}
private static int stereographic(final double[] pole, double x, double y,
int[] refDims, Image ref) { // a shape-preserving infinite map
return getColor(pole, 2*Math.atan(2*Math.hypot(x, y)) - Math.PI/2,
Math.atan2(x, y), refDims, ref);
}
private static int gnomonic(final double[] pole, double x, double y,
int[] refDims, Image ref) { // map where straight lines are straight
return getColor(pole, Math.atan(2*Math.hypot(x, y)) - Math.PI/2,
Math.atan2(x, y), refDims, ref);
}
private static int orthographic(final double[] pole, double x, double y,
int[] refDims, Image ref) { // a map that mimics the view from space
double R = Math.hypot(x, y);
if (R <= 1)
return getColor(pole, -Math.acos(R), Math.atan2(x, y),
refDims, ref);
else
return 0;
}
private static int eaCylindrical(final double[] pole, double x, double y,
int[] refDims, Image ref) { // an equal-area cylindrical map
return getColor(pole, Math.asin(y), x*Math.PI, refDims, ref);
}
private static int lambert(final double[] pole, double x, double y,
int[] refDims, Image ref) { // a conical projection
y = (y+1)/2;
return getColor(pole,
2*Math.atan(Math.pow(1.5*Math.hypot(x, y), 2)) - Math.PI/2,
2*Math.atan2(x, y), refDims, ref);
}
private static int lemons(final double[] pole, double x, double y,
int[] refDims, Image ref) { // a simple map that is shaped like lemons
x = x+2;
final double lemWdt = 1/6.0;
if (Math.abs(x % lemWdt - lemWdt / 2.0) < Math.cos(y*Math.PI/2) * lemWdt/2.0) // if it is in
return getColor(pole, y*Math.PI/2, // a sine curve
Math.PI * (x%lemWdt - lemWdt/2.0) / (Math.cos(y*Math.PI/2))
+ (int)(x/lemWdt) * Math.PI/6,
refDims, ref);
else
return 0;
}
private static int eaAzimuth(final double[] pole, double x, double y,
final int[] refDims, Image ref) { // the lambert azimuthal equal area projection
double R = Math.hypot(x, y);
if (R <= 1)
return getColor(pole, Math.asin(2*R*R - 1), Math.atan2(x, y),
refDims, ref);
else
return 0;
}
private static int quinshift(final double[] pole, double x, double y,
final int[] refDims, Image ref) { // a tessalatable rectangle map
Complex u = new Complex(1.8558*(x - y/2 - 0.5), 1.8558*(x + y/2 + 0.5)); // don't ask me where 3.7116 comes from
Complex k = new Complex(Math.sqrt(0.5)); // the rest comes from some fancy complex calculus
Complex ans = Jacobi.cn(u, k);
double p = 2 * Math.atan(ans.abs());
double theta = Math.atan2(ans.getRe(), ans.getIm());
double lambda = p - Math.PI / 2;
return getColor(pole, lambda, theta, refDims, ref);
}
private static int mollweide(final double[] pole, double x, double y,
int[] refDims, Image ref) {
double tht = Math.asin(y);
return getColor(pole, Math.asin((2*tht + Math.sin(2*tht)) / Math.PI),
Math.PI * x / Math.cos(tht), refDims, ref);
}
private static int winkel_tripel(final double[] pole, double x, double y,
int[] refDims, Image ref) {
final double tolerance = 0.001;
double phi = y * Math.PI/2;
double lam = x * Math.PI; // I used equirectangular for my initial guess
double xf = x * (2 + Math.PI);
double yf = y * Math.PI;
double error = Math.PI;
for (int i = 0; i < 100 && error > tolerance; i++) {
final double X = WinkelTripel.X(phi, lam);
final double Y = WinkelTripel.Y(phi, lam);
final double dXdP = WinkelTripel.dXdphi(phi, lam);
final double dYdP = WinkelTripel.dYdphi(phi, lam);
final double dXdL = WinkelTripel.dXdlam(phi, lam);
final double dYdL = WinkelTripel.dYdlam(phi, lam);
phi -= (dYdL*(X - xf) - dXdL*(Y - yf)) / (dXdP*dYdL - dXdL*dYdP);
lam -= (dXdP*(Y - yf) - dYdP*(X - xf)) / (dXdP*dYdL - dXdL*dYdP);
error = Math.hypot(X - xf, Y - yf);
}
if (error >= tolerance) // if it aborted due to timeout
return 0;
else // if it aborted due to convergence
return getColor(pole, phi, lam + Math.PI, refDims, ref);
}
private static int grinten(final double[] pole, double x, double y,
int[] refDims, Image ref) {
if (y == 0) // special case 1: equator
return getColor(pole, 0, x*Math.PI, refDims, ref);
if (x == 0) // special case 3: meridian
return getColor(pole, Math.PI/2 * Math.sin(2*Math.atan(y)),
0, refDims, ref);
double c1 = -Math.abs(y) * (1 + x*x + y*y);
double c2 = c1 - 2*y*y + x*x;
double c3 = -2 * c1 + 1 + 2*y*y + Math.pow(x*x + y*y, 2);
double d = y*y / c3 + 1 / 27.0 * (2*Math.pow(c2 / c3, 3) - 9*c1*c2 / (c3*c3));
double a1 = 1 / c3*(c1 - c2*c2 / (3*c3));
double m1 = 2 * Math.sqrt(-a1 / 3);
double tht1 = Math.acos(3*d / (a1 * m1)) / 3;
return getColor(pole,
Math.signum(y) * Math.PI * (-m1 * Math.cos(tht1 + Math.PI/3) - c2 / (3*c3)),
Math.PI*(x*x + y*y - 1 + Math.sqrt(1 + 2*(x*x - y*y) + Math.pow(x*x + y*y, 2)))
/ (2*x),
refDims, ref);
}
private static int magnus(final double[] pole, double x, double y,
int[] refDims, Image ref) { // a novelty map that magnifies the center profusely
double R = Math.hypot(x, y);
if (R <= 1)
return getColor(pole, Math.PI/2 * (R*R*R*1.8 + R*.2 - 1),
Math.atan2(x, y), refDims, ref);
else
return 0;
}
private static int hammer(final double[] pole, double x, double y,
int[] refDims, Image ref) { // similar to Mollweide, but moves distortion from the poles to the edges
final double X = x * Math.sqrt(8);
final double Y = y * Math.sqrt(2);
final double z = Math.sqrt(1 - Math.pow(X/4, 2) - Math.pow(Y/2, 2));
return getColor(pole, Math.asin(z * Y),
2*Math.atan(0.5*z*X / (2*z*z - 1)), refDims, ref);
}
public static int getColor(final double[] pole, double lat1, double lon1,
int[] refDims, Image input) { // returns the color of any coordinate on earth
final double lat0 = pole[0];
final double lon0 = pole[1];
final double tht0 = pole[2];
lon1 += tht0;
double latitude = Math.asin(Math.sin(lat0) * Math.sin(lat1) + Math.cos(lat0) * Math.cos(lon1) * Math.cos(lat1));
double longitude;
double innerFunc = Math.sin(lat1) / Math.cos(lat0) / Math.cos(latitude) - Math.tan(lat0) * Math.tan(latitude); // used
if (lat0 == Math.PI / 2) // accounts for special case when lat0 = pi/2
longitude = lon1 + Math.PI;
else if (lat0 == -Math.PI / 2) // accounts for special case when lat0 =
// -pi/2
longitude = -lon1;
else if (Math.abs(innerFunc) > 1) { // accounts for special case when
// cos(lat) = --> 0
if ((lon1 == Math.PI && lat1 < -lat0) || (lon1 != Math.PI && lat1 < lat0))
longitude = Math.PI + lon0;
else
longitude = lon0;
} else if (Math.sin(lon1) < 0)
longitude = lon0 + Math
.acos(Math.sin(lat1) / Math.cos(lat0) / Math.cos(latitude) - Math.tan(lat0) * Math.tan(latitude));
else
longitude = lon0 - Math
.acos(Math.sin(lat1) / Math.cos(lat0) / Math.cos(latitude) - Math.tan(lat0) * Math.tan(latitude));
double x = longitude / (2*Math.PI);
double y = latitude * refDims[1] / Math.PI + refDims[1]/2.0;
x = (x - Math.floor(x)) * refDims[0];
if (y < 0)
y = 0;
else if (y >= refDims[1])
y = refDims[1] - 1;
return input.getPixelReader().getArgb((int)x, (int)y);
}
}