All of the projections!

I added a nifty projection selection dialog so that the user now has
access to every projection I have programmed, while the combobox list is
now shorter. It was more difficult than I anticipated, but it works and
looks great!
This commit is contained in:
Justin Kunimune 2018-01-02 11:04:40 -10:00
parent 91b71314f9
commit da163a45bc
14 changed files with 422 additions and 213 deletions

View File

@ -53,18 +53,7 @@ import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import maps.Azimuthal;
import maps.Conic;
import maps.Cylindrical;
import maps.Misc;
import maps.MyProjections;
import maps.Projection;
import maps.Pseudocylindrical;
import maps.Robinson;
import maps.Snyder;
import maps.Tetrahedral;
import maps.Tobler;
import maps.WinkelTripel;
import utils.Math2;
import utils.Procedure;
@ -93,20 +82,6 @@ public class MapAnalyzer extends MapApplication {
new FileChooser.ExtensionFilter("JPG", "*.jpg","*.jpeg","*.jpe","*.jfif"),
new FileChooser.ExtensionFilter("GIF", "*.gif") };
private static final Projection[] PROJ_ARR = { Cylindrical.MERCATOR, Cylindrical.PLATE_CARREE,
Cylindrical.EQUIRECTANGULAR, Cylindrical.GALL_PETERS, Cylindrical.HOBO_DYER,
Cylindrical.BEHRMANN, Cylindrical.LAMBERT, Cylindrical.EQUAL_AREA, Cylindrical.GALL,
Azimuthal.STEREOGRAPHIC, Azimuthal.POLAR, Azimuthal.EQUAL_AREA, Azimuthal.GNOMONIC,
Azimuthal.ORTHOGRAPHIC, Azimuthal.PERSPECTIVE, Conic.LAMBERT, Conic.EQUIDISTANT,
Conic.ALBERS, Tetrahedral.LEE, Tetrahedral.TETRAGRAPH, Tetrahedral.ACTUAUTHAGRAPH,
Tetrahedral.AUTHAGRAPH, Tetrahedral.AUTHAPOWER, Pseudocylindrical.SINUSOIDAL,
Pseudocylindrical.MOLLWEIDE, Tobler.TOBLER, Misc.HAMMER, Misc.AITOFF,
Misc.VAN_DER_GRINTEN, Robinson.ROBINSON, WinkelTripel.WINKEL_TRIPEL,
Misc.PEIRCE_QUINCUNCIAL, Misc.TWO_POINT_EQUIDISTANT, Misc.HAMMER_RETROAZIMUTHAL, Snyder.GS50,
Pseudocylindrical.LEMONS, MyProjections.EXPERIMENT, MyProjections.PSEUDOSTEREOGRAPHIC,
MyProjections.TWO_POINT_EQUALIZED };
private Button updateBtn;
private Text avgSizeDistort, avgShapeDistort;
private ImageView mapDisplay;
@ -132,7 +107,7 @@ public class MapAnalyzer extends MapApplication {
@Override
protected Node makeWidgets() {
final Node projectionSelector = buildProjectionSelector(PROJ_ARR, Procedure.NONE);
final Node projectionSelector = buildProjectionSelector(Procedure.NONE);
final Node parameterSelector = buildParameterSelector(Procedure.NONE);
final Node textDisplay = buildTextDisplay();
this.updateBtn = buildUpdateButton(this::calculateAndUpdate);

View File

@ -5,12 +5,14 @@ package apps;
import java.io.File;
import java.lang.reflect.Array;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.DoubleUnaryOperator;
import dialogs.ProgressBarDialog;
import dialogs.ProjectionSelectionDialog;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
@ -43,8 +45,18 @@ import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import maps.Azimuthal;
import maps.Conic;
import maps.Cylindrical;
import maps.Lenticular;
import maps.Misc;
import maps.MyProjections;
import maps.Projection;
import maps.Pseudocylindrical;
import maps.Robinson;
import maps.Tetrahedral;
import maps.Tobler;
import maps.WinkelTripel;
import utils.Math2;
import utils.Procedure;
@ -59,9 +71,19 @@ public abstract class MapApplication extends Application {
protected static final int GUI_WIDTH = 350;
protected static final int IMG_WIDTH = 500;
private static final KeyCombination ctrlO = new KeyCodeCombination(KeyCode.O, KeyCodeCombination.CONTROL_DOWN);
private static final KeyCombination ctrlS = new KeyCodeCombination(KeyCode.S, KeyCodeCombination.CONTROL_DOWN);
private static final KeyCombination ctrlEnter = new KeyCodeCombination(KeyCode.ENTER, KeyCodeCombination.CONTROL_DOWN);
private static final KeyCombination CTRL_O = new KeyCodeCombination(KeyCode.O, KeyCodeCombination.CONTROL_DOWN);
private static final KeyCombination CTRL_S = new KeyCodeCombination(KeyCode.S, KeyCodeCombination.CONTROL_DOWN);
private static final KeyCombination CTRL_ENTER = new KeyCodeCombination(KeyCode.ENTER, KeyCodeCombination.CONTROL_DOWN);
protected static final Projection[] PROJECTIONS = { Cylindrical.MERCATOR,
Cylindrical.EQUIRECTANGULAR, Cylindrical.EQUAL_AREA, Cylindrical.GALL,
Azimuthal.STEREOGRAPHIC, Azimuthal.POLAR, Azimuthal.EQUAL_AREA, Azimuthal.GNOMONIC,
Azimuthal.PERSPECTIVE, Conic.LAMBERT, Conic.EQUIDISTANT, Conic.ALBERS, Tetrahedral.LEE,
Tetrahedral.ACTUAUTHAGRAPH, Tetrahedral.AUTHAGRAPH, Pseudocylindrical.SINUSOIDAL,
Pseudocylindrical.MOLLWEIDE, Tobler.TOBLER, Lenticular.AITOFF,
Lenticular.VAN_DER_GRINTEN, Robinson.ROBINSON, WinkelTripel.WINKEL_TRIPEL,
Misc.PEIRCE_QUINCUNCIAL, Misc.TWO_POINT_EQUIDISTANT, Pseudocylindrical.LEMONS }; //the set of featured projections for the ComboBox
private static final String[] ASPECT_NAMES = { "Standard", "Transverse", "Cassini",
"Atlantis", "AuthaGraph", "Jerusalem", "Point Nemo", "Longest Line",
@ -82,7 +104,7 @@ public abstract class MapApplication extends Application {
private double[] currentParams;
private Flag isChanging = new Flag();
private Flag suppressListeners = new Flag();
private Flag suppressListeners = new Flag(); //a flag to prevent events from triggering projection setting
@ -148,7 +170,7 @@ public abstract class MapApplication extends Application {
loadButton.setTooltip(new Tooltip(
"Change the input image"));
root.addEventHandler(KeyEvent.KEY_PRESSED, (event) -> { // ctrl-O opens
if (ctrlO.match(event)) {
if (CTRL_O.match(event)) {
loadButton.requestFocus();
loadButton.fire();
}
@ -162,25 +184,30 @@ public abstract class MapApplication extends Application {
/**
* Create a set of widgets to choose a Projection
* @param projections The Projections from which the user may choose
* @param defProj The default projection, before the user chooses anything
* @return the full formatted Region
*/
protected Region buildProjectionSelector(Projection[] projections, Procedure projectionSetter) {
protected Region buildProjectionSelector(Procedure projectionSetter) {
final Label label = new Label("Projection:");
projectionChooser =
new ComboBox<Projection>(FXCollections.observableArrayList(projections));
new ComboBox<Projection>(FXCollections.observableArrayList(PROJECTIONS));
projectionChooser.getItems().add(Projection.NULL_PROJECTION);
projectionChooser.setPrefWidth(210);
final Text description = new Text();
description.setWrappingWidth(GUI_WIDTH);
projectionChooser.setOnAction((event) -> {
final boolean suppressedListeners = suppressListeners.isSet(); //save this value, because
description.setText(projectionChooser.getValue().getDescription()); //revealParameters
revealParameters(projectionChooser.getValue()); //clears suppressListeners. That's fine,
if (!suppressedListeners) //because suppressListeners is only needed here for that one case.
projectionSetter.execute();
projectionChooser.valueProperty().addListener((observable, old, now) -> {
final boolean suppressedListeners = suppressListeners.isSet(); //save this value, because revealParameters()...
if (projectionChooser.getValue() == Projection.NULL_PROJECTION) {
chooseProjectionFromExpandedList(old); //<aside>NULL_PROJECTION is the "More..." button. It triggers the expanded list</aside>
}
else {
description.setText(projectionChooser.getValue().getDescription());
revealParameters(projectionChooser.getValue()); //...clears suppressListeners. That's fine,
if (!suppressedListeners) //because suppressListeners is only needed here for that one case.
projectionSetter.execute();
}
});
return new VBox(5, new HBox(3, label, projectionChooser), description);
@ -286,6 +313,12 @@ public abstract class MapApplication extends Application {
}
protected Node buildOptionPane(Flag cropAtIDL, Flag graticule) {
//TODO: checkboxes for cropping and graticule
return null;
}
/**
* Create a default button that will update the map
* @return the button
@ -300,7 +333,7 @@ public abstract class MapApplication extends Application {
updateButton.setDefaultButton(true);
root.addEventHandler(KeyEvent.KEY_PRESSED, (event) -> {
if (ctrlEnter.match(event)) {
if (CTRL_ENTER.match(event)) {
updateButton.requestFocus();
updateButton.fire();
}
@ -362,7 +395,7 @@ public abstract class MapApplication extends Application {
if (bindCtrlS) // ctrl+S saves
root.addEventHandler(KeyEvent.KEY_PRESSED, (event) -> {
if (ctrlS.match(event)) {
if (CTRL_S.match(event)) {
saveButton.requestFocus();
saveButton.fire();
}
@ -387,6 +420,29 @@ public abstract class MapApplication extends Application {
}
private void chooseProjectionFromExpandedList(Projection lastProjection) {
final ProjectionSelectionDialog selectDialog = new ProjectionSelectionDialog();
do {
final Optional<Projection> result = selectDialog.showAndWait();
if (result.isPresent()) {
if (result.get() == Projection.NULL_PROJECTION) {
showError("No projection chosen", "Please select a projection.");
}
else {
Platform.runLater(() -> projectionChooser.setValue(result.get()));
break;
}
}
else {
Platform.runLater(() -> projectionChooser.setValue(lastProjection)); //I need these runLater()s because JavaFX is dumb and throws an obscure error on recursive edits to ComboBoxes
break;
}
} while (true);
}
private void setAspectByPreset(String presetName,
Slider[] sliders, Spinner<Double>[] spinners) {
this.suppressListeners.set();
@ -432,8 +488,7 @@ public abstract class MapApplication extends Application {
paramSpinVF.setMax(paramValues[i][1]);
paramSpinVF.setValue(paramValues[i][2]);
final Tooltip tt = new Tooltip(
"Change the "+paramNames[i]+" of the map (default is "+
paramValues[i][2]+")");
"Change the "+paramNames[i]+" of the map (default is " + paramValues[i][2]+")");
paramSliders[i].setTooltip(tt);
paramSpinners[i].setTooltip(tt);
paramGrid.addRow(i, paramLabels[i], paramSliders[i], paramSpinners[i]);

View File

@ -45,18 +45,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import maps.Azimuthal;
import maps.Conic;
import maps.Cylindrical;
import maps.Misc;
import maps.MyProjections;
import maps.Projection;
import maps.Pseudocylindrical;
import maps.Robinson;
import maps.Snyder;
import maps.Tetrahedral;
import maps.Tobler;
import maps.WinkelTripel;
import utils.ImageUtils;
import utils.PixelMap;
import utils.Procedure;
@ -84,18 +73,6 @@ public class MapDesignerRaster extends MapApplication {
new FileChooser.ExtensionFilter("JPG", "*.jpg"),
new FileChooser.ExtensionFilter("GIF", "*.gif") };
private static final Projection[] PROJ_ARR = { Cylindrical.MERCATOR,
Cylindrical.EQUIRECTANGULAR, Cylindrical.EQUAL_AREA, Cylindrical.GALL,
Azimuthal.STEREOGRAPHIC, Azimuthal.POLAR, Azimuthal.EQUAL_AREA, Azimuthal.GNOMONIC,
Azimuthal.PERSPECTIVE, Conic.LAMBERT, Conic.EQUIDISTANT, Conic.ALBERS, Tetrahedral.LEE,
Tetrahedral.ACTUAUTHAGRAPH, Tetrahedral.AUTHAPOWER, Tetrahedral.AUTHAGRAPH,
Pseudocylindrical.SINUSOIDAL, Pseudocylindrical.MOLLWEIDE, Tobler.TOBLER, Misc.AITOFF,
Misc.VAN_DER_GRINTEN, Robinson.ROBINSON, WinkelTripel.WINKEL_TRIPEL,
Misc.PEIRCE_QUINCUNCIAL, Misc.TWO_POINT_EQUIDISTANT, Misc.HAMMER_RETROAZIMUTHAL, Snyder.GS50,
Pseudocylindrical.LEMONS, MyProjections.EXPERIMENT, MyProjections.PSEUDOSTEREOGRAPHIC,
MyProjections.TWO_POINT_EQUALIZED };
private Node aspectSelector;
private Button updateBtn, saveMapBtn;
private double[] aspect;
@ -126,8 +103,7 @@ public class MapDesignerRaster extends MapApplication {
this.aspect = new double[3];
final Node inputSelector = buildInputSelector(READABLE_TYPES,
RASTER_TYPES[0], this::setInput);
final Node projectionSelector = buildProjectionSelector(PROJ_ARR,
this::hideAspect);
final Node projectionSelector = buildProjectionSelector(this::hideAspect);
this.aspectSelector = buildAspectSelector(this.aspect, Procedure.NONE);
final Node parameterSelector = buildParameterSelector(Procedure.NONE);
this.updateBtn = buildUpdateButton(this::updateMap);

View File

@ -45,19 +45,6 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import maps.Azimuthal;
import maps.CahillKeyes;
import maps.Conic;
import maps.Cylindrical;
import maps.Misc;
import maps.MyProjections;
import maps.Projection;
import maps.Pseudocylindrical;
import maps.Robinson;
import maps.Snyder;
import maps.Tetrahedral;
import maps.Tobler;
import maps.WinkelTripel;
import utils.Math2;
import utils.Procedure;
import utils.SVGMap;
@ -77,23 +64,11 @@ public class MapDesignerVector extends MapApplication {
private static final FileChooser.ExtensionFilter[] VECTOR_TYPES = {
new FileChooser.ExtensionFilter("SVG", "*.svg") };
private static final Projection[] PROJ_ARR = { Cylindrical.MERCATOR,
Cylindrical.EQUIRECTANGULAR, Cylindrical.EQUAL_AREA, Cylindrical.GALL,
Azimuthal.STEREOGRAPHIC, Azimuthal.POLAR, Azimuthal.EQUAL_AREA, Azimuthal.GNOMONIC,
Azimuthal.PERSPECTIVE, Conic.LAMBERT, Conic.EQUIDISTANT, Conic.ALBERS, Tetrahedral.LEE,
Tetrahedral.ACTUAUTHAGRAPH, Tetrahedral.AUTHAPOWER, Tetrahedral.AUTHAGRAPH,
Pseudocylindrical.SINUSOIDAL, Pseudocylindrical.MOLLWEIDE, Tobler.TOBLER, Misc.AITOFF,
Misc.VAN_DER_GRINTEN, Robinson.ROBINSON, WinkelTripel.WINKEL_TRIPEL,
Misc.PEIRCE_QUINCUNCIAL, CahillKeyes.BUTTERFLY, Misc.TWO_POINT_EQUIDISTANT,
Misc.HAMMER_RETROAZIMUTHAL, Snyder.GS50, Pseudocylindrical.LEMONS, MyProjections.EXPERIMENT,
MyProjections.PSEUDOSTEREOGRAPHIC, MyProjections.TWO_POINT_EQUALIZED };
private static final int DEF_MAX_VTX = 5000;
private static final int FAST_MAX_VTX = 2000;
private static final FileChooser.ExtensionFilter[] VECTOR_TYPES = {
new FileChooser.ExtensionFilter("SVG", "*.svg") };
private Node aspectSelector;
private Button saveBtn;
@ -123,7 +98,7 @@ public class MapDesignerVector extends MapApplication {
this.aspect = new double[3];
final Node inputSelector = buildInputSelector(VECTOR_TYPES,
VECTOR_TYPES[0], this::setInput);
final Node projectionSelector = buildProjectionSelector(PROJ_ARR,
final Node projectionSelector = buildProjectionSelector(
Procedure.concat(this::updateMap, this::hideAspect));
this.aspectSelector = buildAspectSelector(this.aspect, this::updateMap);
final Node parameterSelector = buildParameterSelector(this::updateMap);

View File

@ -34,6 +34,7 @@ import maps.Azimuthal;
import maps.CahillKeyes;
import maps.Conic;
import maps.Cylindrical;
import maps.Lenticular;
import maps.Misc;
import maps.MyProjections;
import maps.Projection;
@ -64,8 +65,8 @@ public class MapExplainer {
Azimuthal.GNOMONIC.transverse(), Azimuthal.ORTHOGRAPHIC.transverse(),
Conic.LAMBERT, Conic.EQUIDISTANT, Conic.ALBERS, Tetrahedral.LEE,
Tetrahedral.AUTHAGRAPH, Pseudocylindrical.SINUSOIDAL,
Pseudocylindrical.MOLLWEIDE, Tobler.TOBLER, Misc.HAMMER, Misc.AITOFF,
Misc.VAN_DER_GRINTEN, Robinson.ROBINSON, WinkelTripel.WINKEL_TRIPEL,
Pseudocylindrical.MOLLWEIDE, Tobler.TOBLER, Lenticular.HAMMER, Lenticular.AITOFF,
Lenticular.VAN_DER_GRINTEN, Robinson.ROBINSON, WinkelTripel.WINKEL_TRIPEL,
CahillKeyes.BUTTERFLY, Misc.PEIRCE_QUINCUNCIAL.transverse(), Snyder.GS50,
Misc.TWO_POINT_EQUIDISTANT, Misc.HAMMER_RETROAZIMUTHAL, Misc.FLAT_EARTH },
@ -100,7 +101,7 @@ public class MapExplainer {
out.println(" <dt>Property:&nbsp;</dt>");
out.println(" <dd>"+proj.getProperty().getName()+"</dd>");
out.println(" <dt>Uninterrupted:&nbsp;</dt>");
out.println(" <dd>"+ (proj.isConvex() ? "Yes":"No") +"</dd>");
out.println(" <dd>"+ (proj.isContinuous() ? "Yes":"No") +"</dd>");
out.println(" <dt>Shows entire world:&nbsp;</dt>");
out.println(" <dd>"+ (proj.isFinite() ? "Yes":"No") +"</dd>");
out.println(" <dt>Closed-form solution:&nbsp;</dt>");

View File

@ -39,6 +39,7 @@ import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.stage.Stage;
import maps.Cylindrical;
import maps.Lenticular;
import maps.Misc;
import maps.Projection;
import maps.Robinson;
@ -54,7 +55,7 @@ import maps.WinkelTripel;
public class MapOptimizer extends Application {
private static final Projection[] EXISTING_PROJECTIONS = { Cylindrical.HOBO_DYER,
Robinson.ROBINSON, Misc.VAN_DER_GRINTEN, Misc.PEIRCE_QUINCUNCIAL };
Robinson.ROBINSON, Lenticular.VAN_DER_GRINTEN, Misc.PEIRCE_QUINCUNCIAL };
private static final Projection[] PROJECTIONS_TO_OPTIMIZE = { Tobler.TOBLER,
WinkelTripel.WINKEL_TRIPEL, Tetrahedral.TETRAPOWER, Tetrahedral.AUTHAPOWER };
private static final double[] WEIGHTS = { 0., .125, .25, .375, .5, .625, .75, .875, 1. };

View File

@ -49,6 +49,7 @@ import javafx.stage.Stage;
import maps.Azimuthal;
import maps.CahillKeyes;
import maps.Cylindrical;
import maps.Lenticular;
import maps.Misc;
import maps.MyProjections;
import maps.Projection;
@ -73,13 +74,13 @@ public class MapPlotter extends Application {
private static final Projection[] AZIMUTHAL = { Azimuthal.POLAR };
private static final Projection[] PSEUDOCYL = { Pseudocylindrical.MOLLWEIDE, Robinson.ROBINSON,
Tobler.TOBLER };
private static final Projection[] PSEUDOAZM = { Misc.AITOFF,
private static final Projection[] PSEUDOAZM = { Lenticular.AITOFF,
MyProjections.PSEUDOSTEREOGRAPHIC };
private static final Projection[] TETRAHEDRAL = { Tetrahedral.LEE, Tetrahedral.ACTUAUTHAGRAPH,
Tetrahedral.AUTHAGRAPH, Tetrahedral.TETRAPOWER };
private static final Projection[] CHEATY = { Pseudocylindrical.LEMONS,
CahillKeyes.OCTANT };
private static final Projection[] OTHER = { Misc.VAN_DER_GRINTEN, WinkelTripel.WINKEL_TRIPEL,
private static final Projection[] OTHER = { Lenticular.VAN_DER_GRINTEN, WinkelTripel.WINKEL_TRIPEL,
Misc.PEIRCE_QUINCUNCIAL };

View File

@ -96,8 +96,7 @@ public class MapConfigurationDialog extends Dialog<Boolean> {
this.smoothBox.setValue("Low");
this.smoothBox.setMaxWidth(Double.MAX_VALUE);
this.gui = new VBox();
this.gui.setSpacing(20);
this.gui = new VBox(20);
pane.contentTextProperty().addListener((arg0) -> { // set it to refresh the gui when... the content texts?
this.updateGUI();
@ -107,12 +106,8 @@ public class MapConfigurationDialog extends Dialog<Boolean> {
pane.getButtonTypes().addAll(new ButtonType[] { ButtonType.OK, ButtonType.CANCEL }); // add buttons
this.updateGUI();
this.setResultConverter((btn) -> {
if (btn != null && btn.getButtonData() == ButtonData.OK_DONE)
return true;
else
return false;
});
this.setResultConverter(
(btn) -> (btn != null && btn.getButtonData() == ButtonData.OK_DONE));
realEdit = true;
}
@ -127,12 +122,9 @@ public class MapConfigurationDialog extends Dialog<Boolean> {
grid.setMaxWidth(Double.MAX_VALUE);
grid.setAlignment(Pos.CENTER_LEFT);
grid.getChildren().clear();
grid.add(new Label("Width:"), 0, 0);
grid.add(this.widthBox, 1, 0);
grid.add(new Label("Height:"), 0, 1);
grid.add(this.heightBox, 1, 1);
grid.add(new Label("Smoothing:"), 0, 2);
grid.add(this.smoothBox, 1, 2);
grid.addRow(0, new Label("Width:"), this.widthBox);
grid.addRow(1, new Label("Height:"), this.heightBox);
grid.addRow(2, new Label("Smoothing:"), this.smoothBox);
this.gui.getChildren().add(grid);
this.getDialogPane().setContent(this.gui);

View File

@ -0,0 +1,183 @@
/**
* 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 dialogs;
import java.util.HashMap;
import java.util.Map;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.control.TableView;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.TextFieldTreeCell;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import maps.Azimuthal;
import maps.CahillKeyes;
import maps.Conic;
import maps.Cylindrical;
import maps.Lenticular;
import maps.Misc;
import maps.MyProjections;
import maps.Projection;
import maps.Pseudocylindrical;
import maps.Robinson;
import maps.Snyder;
import maps.Tetrahedral;
import maps.Tobler;
import maps.WinkelTripel;
/**
* A dialog for perusing all of the map projections I have to offer in an ordered drop-down-type
* thing.
*
* @author jkunimune
*/
public class ProjectionSelectionDialog extends Dialog<Projection> {
private static final double MENU_WIDTH = 250;
private static final double TEXT_WIDTH = 300;
private static final String[] CATEGORIES = { "Cylindrical", "Azimuthal", "Conic", "Polyhedral",
"Pseudocylindrical", "Lenticular", "Other", "Invented by Justin" };
private static final Projection[][] PROJECTIONS = {
{ Cylindrical.MERCATOR, Cylindrical.PLATE_CARREE, Cylindrical.EQUIRECTANGULAR,
Cylindrical.GALL_PETERS, Cylindrical.HOBO_DYER, Cylindrical.BEHRMANN,
Cylindrical.LAMBERT, Cylindrical.EQUAL_AREA, Cylindrical.GALL,
Cylindrical.MILLER },
{ Azimuthal.STEREOGRAPHIC, Azimuthal.POLAR, Azimuthal.EQUAL_AREA, Azimuthal.GNOMONIC,
Azimuthal.ORTHOGRAPHIC, Azimuthal.PERSPECTIVE },
{ Conic.LAMBERT, Conic.EQUIDISTANT, Conic.ALBERS },
{ Tetrahedral.LEE, Tetrahedral.AUTHAGRAPH, CahillKeyes.M_MAP, CahillKeyes.BUTTERFLY },
{ Pseudocylindrical.SINUSOIDAL, Pseudocylindrical.MOLLWEIDE, Tobler.TOBLER,
Robinson.ROBINSON },
{ Lenticular.HAMMER, Lenticular.AITOFF, Lenticular.VAN_DER_GRINTEN,
WinkelTripel.WINKEL_TRIPEL },
{ Misc.PEIRCE_QUINCUNCIAL, Misc.GUYOU, Pseudocylindrical.LEMONS,
Misc.TWO_POINT_EQUIDISTANT, Misc.HAMMER_RETROAZIMUTHAL, Snyder.GS50,
Misc.FLAT_EARTH },
{ Tetrahedral.AUTHAPOWER, Tetrahedral.ACTUAUTHAGRAPH, Tetrahedral.TETRAGRAPH,
Tetrahedral.TETRAPOWER, MyProjections.EXPERIMENT,
MyProjections.PSEUDOSTEREOGRAPHIC, MyProjections.TWO_POINT_EQUALIZED,
MyProjections.MAGNIFIER } };
private Map<TreeItem<String>, Projection> projMap;
private TreeView<String> menu;
private TextFlow flow;
private GridPane text;
public ProjectionSelectionDialog() {
projMap = new HashMap<TreeItem<String>, Projection>();
final TreeItem<String> root = new TreeItem<String>();
menu = new TreeView<String>(root);
menu.setShowRoot(false); //create and configure the TreeView of options
menu.setPrefWidth(MENU_WIDTH);
flow = new TextFlow(); //create and configure the description area
flow.setPrefWidth(TEXT_WIDTH);
text = new GridPane();
text.setHgap(10);
menu.getSelectionModel().selectedItemProperty().addListener((observable, old, now) -> {
if (projMap.containsKey(now)) //selection callback to describe each projection
describe(projMap.get(now));
else if (now != null) {
now.setExpanded(!now.isExpanded());
Platform.runLater(() -> menu.getSelectionModel().select(old)); //prevent them from selecting headers
}
});
menu.setCellFactory((tView) -> { //factoring cells to detect double-clicks
final TreeCell<String> cell = new TextFieldTreeCell<String>();
cell.setOnMouseClicked((event) -> { //on double click, close dialog
if (event.getClickCount() >= 2 && projMap.containsKey(cell.getTreeItem())) {
this.setResult(projMap.get(cell.getTreeItem()));
}
});
return cell;
});
for (int i = 0; i < CATEGORIES.length; i ++) {
final TreeItem<String> header = new TreeItem<String>(CATEGORIES[i]);
root.getChildren().add(header);
for (int j = 0; j < PROJECTIONS[i].length; j ++) {
final TreeItem<String> leaf = new TreeItem<String>(PROJECTIONS[i][j].getName());
projMap.put(leaf, PROJECTIONS[i][j]);
header.getChildren().add(leaf);
}
}
this.setTitle("Projection selection"); //set general properties for the dialog
final DialogPane pane = this.getDialogPane();
pane.setHeaderText("Choose a projection from the list below.");
pane.getButtonTypes().addAll(new ButtonType[] { ButtonType.OK, ButtonType.CANCEL }); //add buttons
pane.setContent(new HBox(10, menu, new VBox(10, flow, text)));
this.setResultConverter((btn) -> { //how to return a result:
if (btn != null && btn.getButtonData() == ButtonData.OK_DONE) {
final TreeItem<String> selection = menu.getSelectionModel().getSelectedItem();
return projMap.getOrDefault(selection, Projection.NULL_PROJECTION); //return the corresponding projection
} //or NULL_PROJECTION if the user never chose anything
else {
return null;
}
});
}
private void describe(Projection p) {
final Text head = new Text(p.getName()+"\n");
head.setFont(Font.font(head.getFont().getFamily(), FontWeight.BOLD, 18));
final Text body = new Text(p.getDescription());
flow.getChildren().setAll(head, body);
text.getChildren().clear();
text.addRow(0, new Label("Geometry:"), new Label(p.getType().getName()));
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.isFinite() ? "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"));
for (Node label: text.getChildren())
((Label)label).setFont(body.getFont());
}
}

117
src/maps/Lenticular.java Normal file
View File

@ -0,0 +1,117 @@
/**
* 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;
/**
* TODO: Write description
*
* @author jkunimune
*/
public class Lenticular {
public static final Projection AITOFF =
new Projection("Aitoff", "A compromise projection shaped like an ellipse.",
2*Math.PI, Math.PI, 0b1111, Type.PSEUDOAZIMUTHAL, Property.COMPROMISE) {
public double[] project(double lat, double lon) {
final double a = Math.acos(Math.cos(lat)*Math.cos(lon/2));
return new double[] {
2*Math.cos(lat)*Math.sin(lon/2)*a/Math.sin(a),
Math.sin(lat)*a/Math.sin(a)};
}
public double[] inverse(double x, double y) {
final double[] intermediate = Azimuthal.POLAR.inverse(x/2, y);
double[] transverse = obliquifyPlnr(intermediate, new double[] {0,0,0});
if (transverse != null) transverse[1] *= 2;
return transverse;
}
};
public static final Projection HAMMER =
new Projection("Hammer", "An equal-area projection shaped like an ellipse.",
4, 2, 0b1111, Type.PSEUDOAZIMUTHAL, Property.EQUAL_AREA) {
public double[] project(double lat, double lon) {
return new double[] {
2*Math.cos(lat)*Math.sin(lon/2)/Math.sqrt(1+Math.cos(lat)*Math.cos(lon/2)),
Math.sin(lat)/Math.sqrt(1+Math.cos(lat)*Math.cos(lon/2)) };
}
public double[] inverse(double x, double y) {
final double z = Math.sqrt(1 - x*x/8 - y*y/2);
final double shift = (Math.hypot(x/2, y) > 1) ? 2*Math.PI*Math.signum(x) : 0;
return new double[] {
Math.asin(z*y*Math.sqrt(2)),
2*Math.atan(Math.sqrt(.5)*z*x / (2*z*z - 1)) + shift};
}
};
public static final Projection VAN_DER_GRINTEN =
new Projection(
"Van der Grinten", "A circular compromise map that is popular for some reason.",
2, 2, 0b1111, Type.OTHER, Property.COMPROMISE) {
public double[] project(double lat, double lon) {
if (lat == 0) //special case 1: equator
return new double[] {lon/Math.PI, 0};
if (lon == 0 || lat >= Math.PI/2 || lat <= -Math.PI/2) //special case 3: prime meridian
return new double[] {0, Math.tan(Math.asin(2*lat/Math.PI)/2)};
final double t = Math.abs(Math.asin(2*lat/Math.PI));
final double A = Math.abs(Math.PI/lon - lon/Math.PI)/2;
final double G = Math.cos(t)/(Math.sin(t)+Math.cos(t)-1);
final double P = G*(2/Math.sin(t) - 1);
final double Q = A*A + G;
return new double[] {
Math.signum(lon)*(A*(G-P*P)+Math.sqrt(A*A*(G-P*P)*(G-P*P)-(P*P+A*A)*(G*G-P*P)))/(P*P+A*A),
Math.signum(lat)*(P*Q-A*Math.sqrt((A*A+1)*(P*P+A*A)-Q*Q))/(P*P+A*A)};
}
public double[] inverse(double x, double y) {
if (y == 0) // special case 1: equator
return new double[] {0, x*Math.PI};
if (x == 0) // special case 3: prime meridian
return new double[] {Math.PI/2 * Math.sin(2*Math.atan(y)), 0};
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 t1 = Math.acos(3*d / (a1 * m1)) / 3;
return new double[] {
Math.signum(y) * Math.PI * (-m1 * Math.cos(t1 + 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)};
}
};
}

View File

@ -37,88 +37,6 @@ import utils.Elliptic;
*/
public class Misc {
public static final Projection AITOFF =
new Projection("Aitoff", "A compromise projection shaped like an ellipse.",
2*Math.PI, Math.PI, 0b1111, Type.PSEUDOAZIMUTHAL, Property.COMPROMISE) {
public double[] project(double lat, double lon) {
final double a = Math.acos(Math.cos(lat)*Math.cos(lon/2));
return new double[] {
2*Math.cos(lat)*Math.sin(lon/2)*a/Math.sin(a),
Math.sin(lat)*a/Math.sin(a)};
}
public double[] inverse(double x, double y) {
final double[] intermediate = Azimuthal.POLAR.inverse(x/2, y);
double[] transverse = obliquifyPlnr(intermediate, new double[] {0,0,0});
if (transverse != null) transverse[1] *= 2;
return transverse;
}
};
public static final Projection HAMMER =
new Projection("Hammer", "An equal-area projection shaped like an ellipse.",
4, 2, 0b1111, Type.PSEUDOAZIMUTHAL, Property.EQUAL_AREA) {
public double[] project(double lat, double lon) {
return new double[] {
2*Math.cos(lat)*Math.sin(lon/2)/Math.sqrt(1+Math.cos(lat)*Math.cos(lon/2)),
Math.sin(lat)/Math.sqrt(1+Math.cos(lat)*Math.cos(lon/2)) };
}
public double[] inverse(double x, double y) {
final double z = Math.sqrt(1 - x*x/8 - y*y/2);
final double shift = (Math.hypot(x/2, y) > 1) ? 2*Math.PI*Math.signum(x) : 0;
return new double[] {
Math.asin(z*y*Math.sqrt(2)),
2*Math.atan(Math.sqrt(.5)*z*x / (2*z*z - 1)) + shift};
}
};
public static final Projection VAN_DER_GRINTEN =
new Projection(
"Van der Grinten", "A circular compromise map that is popular for some reason.",
2, 2, 0b1111, Type.OTHER, Property.COMPROMISE) {
public double[] project(double lat, double lon) {
if (lat == 0) //special case 1: equator
return new double[] {lon/Math.PI, 0};
if (lon == 0 || lat >= Math.PI/2 || lat <= -Math.PI/2) //special case 3: prime meridian
return new double[] {0, Math.tan(Math.asin(2*lat/Math.PI)/2)};
final double t = Math.abs(Math.asin(2*lat/Math.PI));
final double A = Math.abs(Math.PI/lon - lon/Math.PI)/2;
final double G = Math.cos(t)/(Math.sin(t)+Math.cos(t)-1);
final double P = G*(2/Math.sin(t) - 1);
final double Q = A*A + G;
return new double[] {
Math.signum(lon)*(A*(G-P*P)+Math.sqrt(A*A*(G-P*P)*(G-P*P)-(P*P+A*A)*(G*G-P*P)))/(P*P+A*A),
Math.signum(lat)*(P*Q-A*Math.sqrt((A*A+1)*(P*P+A*A)-Q*Q))/(P*P+A*A)};
}
public double[] inverse(double x, double y) {
if (y == 0) // special case 1: equator
return new double[] {0, x*Math.PI};
if (x == 0) // special case 3: prime meridian
return new double[] {Math.PI/2 * Math.sin(2*Math.atan(y)), 0};
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 t1 = Math.acos(3*d / (a1 * m1)) / 3;
return new double[] {
Math.signum(y) * Math.PI * (-m1 * Math.cos(t1 + 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)};
}
};
public static final Projection PEIRCE_QUINCUNCIAL =
new Projection(
"Peirce Quincuncial", "A conformal projection that uses complex elliptic functions.",
@ -324,4 +242,5 @@ public class Misc {
return null;
}
};
}

View File

@ -61,7 +61,7 @@ public class MyProjections {
public static final Projection EXPERIMENT =
new Projection(
"Complex Sine", "What happens when you apply a complex differentiable function to a stereographic projection?",
"Complex Arcsine", "What happens when you apply a complex differentiable function to a stereographic projection?",
6, 6, 0b0000, Type.OTHER, Property.CONFORMAL) {
public double[] project(double lat, double lon) {

View File

@ -47,7 +47,7 @@ public abstract class Projection {
private final boolean finite; //does it display the entire world?
private final boolean invertable; //is the inverse solution closed-form?
private final boolean solveable; //is the solution closed-form?
private final boolean convex; //is it convex?
private final boolean continuous; //does a random continuous path cross outside of the map?
private final Type type; //the geometry of the projection
private final Property property; //what it is good for
@ -93,7 +93,7 @@ public abstract class Projection {
protected Projection(Projection base) {
this( base.name, base.description, base.width, base.height, base.finite, base.invertable,
base.solveable, base.convex, base.type, base.property, base.paramNames,
base.solveable, base.continuous, base.type, base.property, base.paramNames,
base.paramValues, base.hasAspect);
}
@ -111,7 +111,7 @@ public abstract class Projection {
this.finite = f;
this.invertable = i;
this.solveable = s;
this.convex = c;
this.continuous = c;
this.type = type;
this.property = property;
}
@ -398,8 +398,8 @@ public abstract class Projection {
return this.solveable;
}
public final boolean isConvex() {
return this.convex;
public final boolean isContinuous() {
return this.continuous;
}
public final Type getType() {
@ -432,6 +432,21 @@ public abstract class Projection {
public static final Projection NULL_PROJECTION = //this exists solely for the purpose of a "More..." option at the end of menus
new Projection("More...", null, 0, 0, 0, null, null) {
public double[] project(double lat, double lon) {
return null;
}
public double[] inverse(double x, double y) {
return null;
}
};
/**
* The most common geometric configurations of projections
* @author jkunimune
@ -439,8 +454,7 @@ public abstract class Projection {
public static enum Type {
CYLINDRICAL("Cylindrical"), CONIC("Conic"), AZIMUTHAL("Azimuthal"),
PSEUDOCYLINDRICAL("Pseudocylindrical"), PSEUDOAZIMUTHAL("Pseudoazimuthal"),
QUASIAZIMUTHAL("Quasiazimuthal"), TETRAHEDRAL("Tetrahedral"), POLYHEDRAL("Polyhedral"),
OTHER("Other");
QUASIAZIMUTHAL("Quasiazimuthal"), POLYHEDRAL("Polyhedral"), OTHER("Other");
private String name;

View File

@ -83,7 +83,7 @@ public class Tetrahedral {
public static final Projection AUTHAGRAPH =
new TetrahedralProjection(
"AuthaGraph", "A hip new Japanese map that is not authagraphic.",
"AuthaGraph", "A hip new Japanese map that is almost equal-area.",
0b1011, Configuration.AUTHAGRAPH, Property.COMPROMISE) {
public double[] innerProject(double lat, double lon) {
@ -109,7 +109,7 @@ public class Tetrahedral {
public static final Projection AUTHAPOWER =
new TetrahedralProjection(
"AuthaPower", "A parametrised, rearranged version of my AuthaGraph approximation",
"AuthaPower", "A parametrised, rearranged version of my AuthaGraph approximation.",
0b1011, Configuration.WIDE_VERTEX, Property.COMPROMISE,
new String[] {"Power"}, new double[][] {{.25,1,.7}}) {
@ -142,7 +142,7 @@ public class Tetrahedral {
public static final Projection ACTUAUTHAGRAPH =
new TetrahedralProjection(
"EquaHedral", "A holey authagraphic tetrahedral projection to put AuthaGraph to shame",
"EquaHedral", "A holey authagraphic tetrahedral projection to put AuthaGraph to shame.",
0b1010, Configuration.WIDE_VERTEX, Property.EQUAL_AREA,
new String[] {"Rho"}, new double[][] {{0,.5,.25}}) {
@ -228,21 +228,21 @@ public class Tetrahedral {
public TetrahedralProjection(
String name, int fisc, Configuration config, Property property,
String adjective, String addendum) {
super(name, config.width, config.height, fisc, Type.TETRAHEDRAL, property,
super(name, config.width, config.height, fisc, Type.POLYHEDRAL, property,
adjective, addendum);
this.configuration = config;
}
public TetrahedralProjection(
String name, String description, int fisc, Configuration config, Property property) {
super(name, description, config.width, config.height, fisc, Type.TETRAHEDRAL, property);
super(name, description, config.width, config.height, fisc, Type.POLYHEDRAL, property);
this.configuration = config;
}
public TetrahedralProjection(
String name, String description, int fisc, Configuration config, Property property,
String[] paramNames, double[][] paramValues) {
super(name, description, config.width, config.height, fisc, Type.TETRAHEDRAL, property,
super(name, description, config.width, config.height, fisc, Type.POLYHEDRAL, property,
paramNames, paramValues);
this.configuration = config;
}