mirror of
https://github.com/csharpee/Map-Projections.git
synced 2025-12-20 00:00:31 -05:00
I changed my shape distortion metric to be max angle displacement instead of stretch factor in decibels, hoping it would weigh moderate distortion higher and severe distortion lower. It didn't really do anything. I think I'll just set it back to how it was, because I liked it that way.
682 lines
26 KiB
Java
682 lines
26 KiB
Java
/**
|
|
* 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 apps;
|
|
|
|
import java.awt.Desktop;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.lang.reflect.Array;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
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;
|
|
import javafx.collections.ObservableList;
|
|
import javafx.geometry.Pos;
|
|
import javafx.scene.Scene;
|
|
import javafx.scene.control.Alert;
|
|
import javafx.scene.control.Button;
|
|
import javafx.scene.control.CheckBox;
|
|
import javafx.scene.control.ComboBox;
|
|
import javafx.scene.control.Control;
|
|
import javafx.scene.control.Label;
|
|
import javafx.scene.control.MenuButton;
|
|
import javafx.scene.control.MenuItem;
|
|
import javafx.scene.control.Slider;
|
|
import javafx.scene.control.Spinner;
|
|
import javafx.scene.control.SpinnerValueFactory;
|
|
import javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory;
|
|
import javafx.scene.control.Tooltip;
|
|
import javafx.scene.control.cell.ComboBoxListCell;
|
|
import javafx.scene.input.KeyCode;
|
|
import javafx.scene.input.KeyCodeCombination;
|
|
import javafx.scene.input.KeyCombination;
|
|
import javafx.scene.input.KeyEvent;
|
|
import javafx.scene.layout.ColumnConstraints;
|
|
import javafx.scene.layout.GridPane;
|
|
import javafx.scene.layout.HBox;
|
|
import javafx.scene.layout.Priority;
|
|
import javafx.scene.layout.Region;
|
|
import javafx.scene.layout.StackPane;
|
|
import javafx.scene.layout.VBox;
|
|
import javafx.scene.paint.Color;
|
|
import javafx.scene.text.Text;
|
|
import javafx.stage.FileChooser;
|
|
import javafx.stage.Stage;
|
|
import javafx.util.converter.DoubleStringConverter;
|
|
import maps.Arbitrary;
|
|
import maps.Azimuthal;
|
|
import maps.CahillKeyes;
|
|
import maps.Conic;
|
|
import maps.Cylindrical;
|
|
import maps.Lenticular;
|
|
import maps.Misc;
|
|
import maps.MyProjections;
|
|
import maps.Polyhedral;
|
|
import maps.Projection;
|
|
import maps.Pseudocylindrical;
|
|
import maps.Snyder;
|
|
import maps.Tobler;
|
|
import maps.Waterman;
|
|
import maps.WinkelTripel;
|
|
import utils.Flag;
|
|
import utils.Math2;
|
|
import utils.MutableDouble;
|
|
import utils.Procedure;
|
|
|
|
|
|
/**
|
|
* A base class for all GUI applications that deal with maps
|
|
*
|
|
* @author jkunimune
|
|
*/
|
|
public abstract class MapApplication extends Application {
|
|
|
|
protected static final int GUI_WIDTH = 300;
|
|
protected static final int IMG_WIDTH = 450;
|
|
protected static final int V_SPACE = 6;
|
|
protected static final int H_SPACE = 4;
|
|
protected static final int MARGIN = 10;
|
|
protected static final int COMBOBOX_WIDTH = 200;
|
|
protected static final int SPINNER_WIDTH = 80;
|
|
|
|
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);
|
|
|
|
|
|
public static final Projection[] FEATURED_PROJECTIONS = { Cylindrical.MERCATOR,
|
|
Cylindrical.EQUIRECTANGULAR, Cylindrical.EQUAL_AREA, Cylindrical.GALL_STEREOGRAPHIC,
|
|
Azimuthal.STEREOGRAPHIC, Azimuthal.POLAR, Azimuthal.EQUAL_AREA, Azimuthal.GNOMONIC,
|
|
Azimuthal.PERSPECTIVE, Conic.LAMBERT, Conic.EQUIDISTANT, Conic.ALBERS,
|
|
Polyhedral.LEE_TETRAHEDRAL_RECTANGULAR, Polyhedral.ACTUAUTHAGRAPH,
|
|
Polyhedral.AUTHAPOWER, CahillKeyes.M_MAP, Pseudocylindrical.SINUSOIDAL,
|
|
Pseudocylindrical.MOLLWEIDE, Tobler.TOBLER, Lenticular.AITOFF,
|
|
Lenticular.VAN_DER_GRINTEN, Arbitrary.ROBINSON, WinkelTripel.WINKEL_TRIPEL,
|
|
Misc.PEIRCE_QUINCUNCIAL, Misc.TWO_POINT_EQUIDISTANT, Pseudocylindrical.LEMONS }; //the set of featured projections for the ComboBox
|
|
|
|
public static final String[] PROJECTION_CATEGORIES = { "Cylindrical", "Azimuthal", "Conic", "Polyhedral",
|
|
"Pseudocylindrical", "Lenticular", "Other", "Invented by Justin" }; //the overarching categories by which I organise my projections
|
|
public static final Projection[][] ALL_PROJECTIONS = {
|
|
{ Cylindrical.EQUAL_AREA, Cylindrical.EQUIRECTANGULAR, Cylindrical.GALL_ORTHOGRAPHIC,
|
|
Cylindrical.GALL_STEREOGRAPHIC, Cylindrical.HOBO_DYER, Cylindrical.LAMBERT,
|
|
Cylindrical.MERCATOR, Cylindrical.MILLER, Cylindrical.PLATE_CARREE },
|
|
{ Azimuthal.EQUAL_AREA, Azimuthal.POLAR, Azimuthal.GNOMONIC, Azimuthal.ORTHOGRAPHIC,
|
|
Azimuthal.PERSPECTIVE, Azimuthal.STEREOGRAPHIC },
|
|
{ Conic.ALBERS, Conic.LAMBERT, Conic.EQUIDISTANT },
|
|
{ Polyhedral.AUTHAGRAPH, CahillKeyes.M_MAP, Polyhedral.DYMAXION,
|
|
Polyhedral.LEE_TETRAHEDRAL_RECTANGULAR, Polyhedral.LEE_TETRAHEDRAL_TRIANGULAR,
|
|
Waterman.BUTTERFLY },
|
|
{ Pseudocylindrical.ECKERT_IV, Pseudocylindrical.KAVRAYSKIY_VII,
|
|
Pseudocylindrical.MOLLWEIDE, Arbitrary.NATURAL_EARTH, Arbitrary.ROBINSON,
|
|
Pseudocylindrical.SINUSOIDAL, Tobler.TOBLER },
|
|
{ Lenticular.AITOFF, Lenticular.HAMMER, Lenticular.STREBE_95,
|
|
Lenticular.VAN_DER_GRINTEN, WinkelTripel.WINKEL_TRIPEL },
|
|
{ Snyder.GS50, Misc.GUYOU, Misc.HAMMER_RETROAZIMUTHAL, Pseudocylindrical.LEMONS,
|
|
Misc.PEIRCE_QUINCUNCIAL, Misc.TWO_POINT_EQUIDISTANT, Misc.FLAT_EARTH },
|
|
{ MyProjections.EXPERIMENT, Polyhedral.AUTHAPOWER, Polyhedral.ACTUAUTHAGRAPH,
|
|
MyProjections.MAGNIFIER, MyProjections.PSEUDOSTEREOGRAPHIC,
|
|
Polyhedral.TETRAGRAPH, Polyhedral.TETRAPOWER,
|
|
MyProjections.TWO_POINT_EQUALIZED } }; // every projection I have programmed
|
|
|
|
private static final String[] ASPECT_NAMES = { "Standard", "Transverse", "Cassini", "Atlantis",
|
|
"Jerusalem", "Point Nemo", "Longest Line", "Cylindrical", "Tetrahedral", "Antipode",
|
|
"Random" };
|
|
private static final double[][] ASPECT_VALS = { //the aspect presets (in degrees)
|
|
{ 90., 0., 0., -4., 31.78, 48.88, -28.52, 35., 57. },
|
|
{ 0., 0., 90., 65., 35.22, 56.61, 141.45, 166.5,-175.5 },
|
|
{ 0., 0.,-90.,-147.,-35., -45., 30., -145., 154. } };
|
|
|
|
|
|
private final Map<ButtonType, Button> buttons = new HashMap<ButtonType, Button>();
|
|
|
|
private String name;
|
|
private Stage root;
|
|
private ComboBox<Projection> projectionChooser;
|
|
private GridPane paramGrid;
|
|
private Label[] paramLabels;
|
|
private Slider[] paramSliders;
|
|
private Spinner<Double>[] paramSpinners;
|
|
private double[] currentParams;
|
|
|
|
private Flag isChanging = new Flag();
|
|
private Flag suppressListeners = new Flag(); //a flag to prevent events from triggering projection setting
|
|
|
|
|
|
|
|
public MapApplication(String name) {
|
|
super();
|
|
this.name = name;
|
|
}
|
|
|
|
|
|
@Override
|
|
public void start(Stage root) {
|
|
this.root = root;
|
|
this.root.setTitle(this.name);
|
|
this.root.setScene(new Scene(new StackPane(makeWidgets()), Color.ANTIQUEWHITE)); //Why can't I set this color?
|
|
this.root.show();
|
|
|
|
this.suppressListeners.set();
|
|
this.projectionChooser.setValue(FEATURED_PROJECTIONS[0]);
|
|
this.suppressListeners.clear();
|
|
}
|
|
|
|
|
|
protected abstract Region makeWidgets();
|
|
|
|
|
|
/**
|
|
* Create a set of widgets to select an input image
|
|
* @param allowedExtensions The List of possible file types for the input
|
|
* @param setInput The method to be called when an input file is loaded.
|
|
* This method will be called from a nongui thread.
|
|
* @return the full formatted Region
|
|
*/
|
|
protected Region buildInputSelector(
|
|
FileChooser.ExtensionFilter[] allowedExtensions,
|
|
FileChooser.ExtensionFilter defaultExtension,
|
|
Consumer<File> 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 FileChooser inputChooser = new FileChooser();
|
|
inputChooser.setInitialDirectory(new File("input"));
|
|
inputChooser.setTitle("Choose an input map");
|
|
inputChooser.getExtensionFilters().addAll(allowedExtensions);
|
|
inputChooser.setSelectedExtensionFilter(defaultExtension);
|
|
|
|
final Button loadButton = new Button("Choose input...");
|
|
loadButton.setTooltip(new Tooltip(
|
|
"Choose the image to determine the style of your map"));
|
|
loadButton.setOnAction((event) -> {
|
|
File file;
|
|
try {
|
|
file = inputChooser.showOpenDialog(root);
|
|
} catch (IllegalArgumentException e) {
|
|
inputChooser.setInitialDirectory(new File("."));
|
|
file = inputChooser.showOpenDialog(root);
|
|
}
|
|
if (file != null) {
|
|
inputChooser.setInitialDirectory(file.getParentFile()); //remember this directory for next time
|
|
final File f = file;
|
|
new Thread(() -> inputSetter.accept(f)).start();
|
|
inputLabel.setText(f.getName());
|
|
}
|
|
});
|
|
loadButton.setTooltip(new Tooltip(
|
|
"Change the input image"));
|
|
root.addEventHandler(KeyEvent.KEY_PRESSED, (event) -> { // ctrl-O opens
|
|
if (CTRL_O.match(event)) {
|
|
loadButton.requestFocus();
|
|
loadButton.fire();
|
|
}
|
|
});
|
|
|
|
buttons.put(ButtonType.LOAD_INPUT, loadButton);
|
|
VBox output = new VBox(V_SPACE, new HBox(H_SPACE, label, inputLabel), loadButton);
|
|
output.setAlignment(Pos.CENTER);
|
|
return output;
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a set of widgets to choose a Projection
|
|
* @param defProj The default projection, before the user chooses anything
|
|
* @return the full formatted Region
|
|
*/
|
|
protected Region buildProjectionSelector(Procedure projectionSetter) {
|
|
final Label label = new Label("Projection:");
|
|
projectionChooser =
|
|
new ComboBox<Projection>(FXCollections.observableArrayList(FEATURED_PROJECTIONS));
|
|
projectionChooser.getItems().add(Projection.NULL_PROJECTION);
|
|
projectionChooser.setPrefWidth(COMBOBOX_WIDTH);
|
|
|
|
final Text description = new Text();
|
|
description.setWrappingWidth(GUI_WIDTH);
|
|
|
|
projectionChooser.valueProperty().addListener((observable, old, now) -> {
|
|
projectionChooser.setButtonCell(new ComboBoxListCell<Projection>()); //This makes it properly display values not in the featured list
|
|
|
|
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();
|
|
}
|
|
});
|
|
|
|
HBox comboRow = new HBox(H_SPACE, label, projectionChooser);
|
|
comboRow.setAlignment(Pos.CENTER_LEFT);
|
|
return new VBox(V_SPACE, comboRow, description);
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a set of widgets to choose an aspect either from a preset or numbers
|
|
* Also bind aspectArr to the sliders
|
|
* @return the full formatted Region
|
|
*/
|
|
protected Region buildAspectSelector(double[] aspectArr, Procedure aspectSetter) {
|
|
final MenuButton presetChooser = new MenuButton("Aspect Presets");
|
|
presetChooser.setTooltip(new Tooltip(
|
|
"Set the aspect sliders based on a preset"));
|
|
|
|
final String[] labels = { "Latitude:", "Longitude:", "Ctr. Meridian:" };
|
|
final Slider[] sliders = new Slider[] {
|
|
new Slider(-90, 90, 0.),
|
|
new Slider(-180, 180, 0.),
|
|
new Slider(-180, 180, 0.) };
|
|
final Spinner<Double> spin0 = new Spinner<Double>(-90, 90, 0.); //yes, this is awkward. Java gets weird about arrays with generic types
|
|
@SuppressWarnings("unchecked")
|
|
final Spinner<Double>[] spinners = (Spinner<Double>[]) Array.newInstance(spin0.getClass(), 3);
|
|
spinners[0] = spin0;
|
|
spinners[1] = new Spinner<Double>(-180, 180, 0.);
|
|
spinners[2] = new Spinner<Double>(-180, 180, 0.);
|
|
|
|
for (int i = 0; i < 3; i ++) {
|
|
aspectArr[i] = Math.toRadians(sliders[i].getValue());
|
|
link(sliders[i], spinners[i], i, aspectArr, Math::toRadians,
|
|
aspectSetter, isChanging, suppressListeners);
|
|
}
|
|
setAspectByPreset("Standard", sliders, spinners);
|
|
|
|
for (String preset: ASPECT_NAMES) {
|
|
MenuItem m = new MenuItem(preset);
|
|
m.setOnAction((event) -> {
|
|
setAspectByPreset(((MenuItem) event.getSource()).getText(),
|
|
sliders, spinners);
|
|
for (int i = 0; i < 3; i ++)
|
|
aspectArr[i] = Math.toRadians(sliders[i].getValue());
|
|
if (!suppressListeners.isSet())
|
|
aspectSetter.execute();
|
|
});
|
|
presetChooser.getItems().add(m);
|
|
}
|
|
|
|
final GridPane grid = new GridPane();
|
|
grid.setVgap(V_SPACE);
|
|
grid.setHgap(H_SPACE);
|
|
grid.getColumnConstraints().addAll(
|
|
new ColumnConstraints(SPINNER_WIDTH,Control.USE_COMPUTED_SIZE,Control.USE_COMPUTED_SIZE),
|
|
new ColumnConstraints(), new ColumnConstraints(SPINNER_WIDTH));
|
|
for (int i = 0; i < 3; i ++) {
|
|
GridPane.setHgrow(sliders[i], Priority.ALWAYS);
|
|
sliders[i].setTooltip(new Tooltip("Change the aspect of the map"));
|
|
spinners[i].setTooltip(new Tooltip("Change the aspect of the map"));
|
|
spinners[i].setEditable(true);
|
|
grid.addRow(i, new Label(labels[i]), sliders[i], spinners[i]);
|
|
}
|
|
|
|
VBox all = new VBox(V_SPACE, presetChooser, grid);
|
|
all.setAlignment(Pos.CENTER);
|
|
all.managedProperty().bind(all.visibleProperty()); //make it hide when it is hiding
|
|
return all;
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a grid of sliders and spinners not unlike the aspectSelector
|
|
* @param parameterSetter The function to execute when the parameters change
|
|
* @return the full formatted Region
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
protected Region buildParameterSelector(Procedure parameterSetter) {
|
|
currentParams = new double[4];
|
|
paramLabels = new Label[4];
|
|
paramSliders = new Slider[4]; // I don't think any projection has more than four parameters
|
|
final Spinner<Double> spin0 = new Spinner<Double>(0.,0.,0.); //yes, this is awkward. Java gets weird about arrays with generic types
|
|
paramSpinners = (Spinner<Double>[])Array.newInstance(spin0.getClass(), 4);
|
|
paramSpinners[0] = spin0;
|
|
|
|
for (int i = 0; i < 4; i ++) {
|
|
paramLabels[i] = new Label();
|
|
paramSliders[i] = new Slider();
|
|
if (i != 0)
|
|
paramSpinners[i] = new Spinner<Double>(0.,0.,0.);
|
|
link(paramSliders[i], paramSpinners[i], i, currentParams, (d)->d, parameterSetter, isChanging, suppressListeners);
|
|
}
|
|
|
|
for (int i = 0; i < 4; i ++) {
|
|
GridPane.setHgrow(paramSliders[i], Priority.ALWAYS);
|
|
paramSpinners[i].setEditable(true);
|
|
}
|
|
|
|
paramGrid = new GridPane();
|
|
paramGrid.setVgap(V_SPACE);
|
|
paramGrid.setHgap(H_SPACE);
|
|
paramGrid.getColumnConstraints().addAll(
|
|
new ColumnConstraints(SPINNER_WIDTH,Control.USE_COMPUTED_SIZE,Control.USE_COMPUTED_SIZE),
|
|
new ColumnConstraints(), new ColumnConstraints(SPINNER_WIDTH));
|
|
return paramGrid;
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a block of niche options - specifically International Dateline cropping and whether
|
|
* there should be a graticule
|
|
* @param cropAtIDL The mutable boolean value to which to bind the "Crop at Dateline" CheckBox
|
|
* @param graticule The mutable double value to which to bind the "Graticule" Spinner
|
|
* @return the full formatted Region
|
|
*/
|
|
protected Region buildOptionPane(Flag cropAtIDL, MutableDouble graticule) {
|
|
final CheckBox cropBox = new CheckBox("Crop at International Dateline"); //the CheckBox for whether there should be shown imagery outside the International Dateline
|
|
cropBox.setSelected(cropAtIDL.isSet());
|
|
cropBox.setTooltip(new Tooltip("Show every point exactly once."));
|
|
cropBox.selectedProperty().addListener((observable, old, now) -> {
|
|
cropAtIDL.set(now);
|
|
});
|
|
|
|
final ObservableList<Double> factorsOf90 = FXCollections.observableArrayList();
|
|
for (double f = 1; f <= 90; f += 0.5)
|
|
if (90%f == 0)
|
|
factorsOf90.add((double)f);
|
|
final Spinner<Double> gratSpinner = new Spinner<Double>(factorsOf90); //spinner for the graticule value
|
|
gratSpinner.getValueFactory().setConverter(new DoubleStringConverter());
|
|
gratSpinner.getValueFactory().setValue(graticule.get());
|
|
gratSpinner.setDisable(graticule.isZero());
|
|
gratSpinner.setEditable(true);
|
|
gratSpinner.setTooltip(new Tooltip("The spacing in degrees between shown parallels and meridians."));
|
|
gratSpinner.setPrefWidth(SPINNER_WIDTH);
|
|
gratSpinner.valueProperty().addListener((observable, old, now) -> {
|
|
graticule.set(now); //which is tied to the mutable graticule spacing variable
|
|
});
|
|
|
|
final CheckBox gratBox = new CheckBox("Graticule: "); //the CheckBox for whether there should be a graticule
|
|
gratBox.setSelected(!graticule.isZero());
|
|
gratBox.setTooltip(new Tooltip("Overlay a mesh of parallels and meridians."));
|
|
gratBox.selectedProperty().addListener((observable, old, now) -> {
|
|
if (now)
|
|
graticule.set(gratSpinner.getValue()); //set the value of graticule appropriately when checked
|
|
else
|
|
graticule.set(0); //when not checked, represent "no graticule" as a spacing of 0
|
|
gratSpinner.setDisable(!now); //disable the graticule Spinner when appropriate
|
|
});
|
|
|
|
final HBox gratRow = new HBox(H_SPACE, gratBox, gratSpinner);
|
|
gratRow.setAlignment(Pos.CENTER_LEFT);
|
|
return new VBox(V_SPACE, cropBox, gratRow);
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a default button that will update the map
|
|
* @return the button
|
|
*/
|
|
protected Region buildUpdateButton(String text, Runnable mapUpdater) {
|
|
final Button updateButton = new Button(text);
|
|
updateButton.setOnAction((event) -> {
|
|
new Thread(mapUpdater).start();
|
|
});
|
|
updateButton.setTooltip(new Tooltip(
|
|
"Update the current map with your parameters"));
|
|
|
|
updateButton.setDefaultButton(true);
|
|
root.addEventHandler(KeyEvent.KEY_PRESSED, (event) -> {
|
|
if (CTRL_ENTER.match(event)) {
|
|
updateButton.requestFocus();
|
|
updateButton.fire();
|
|
}
|
|
});
|
|
|
|
this.buttons.put(ButtonType.UPDATE_MAP, updateButton);
|
|
return updateButton;
|
|
}
|
|
|
|
|
|
/**
|
|
* Build a button that will save something
|
|
* @param bindCtrlS Should ctrl+S trigger this button?
|
|
* @param savee The name of the thing being saved
|
|
* @param allowedExtensions The allowed file formats that can be saved
|
|
* @param defaultExtension The default file format to be saved
|
|
* @param settingCollector A callback to run just before the saving happens that returns true if it should commence
|
|
* @param calculateAndSaver The callback that saves the thing
|
|
* @return the button, ready to be pressed
|
|
*/
|
|
protected Region buildSaveButton(boolean bindCtrlS, String savee,
|
|
FileChooser.ExtensionFilter[] allowedExtensions,
|
|
FileChooser.ExtensionFilter defaultExtension,
|
|
BooleanSupplier settingCollector,
|
|
BiConsumer<File, ProgressBarDialog> calculateAndSaver) {
|
|
final FileChooser saver = new FileChooser();
|
|
saver.setInitialDirectory(new File("output"));
|
|
saver.setInitialFileName("my"+savee+defaultExtension.getExtensions().get(0).substring(1));
|
|
saver.setTitle("Save "+savee);
|
|
saver.getExtensionFilters().addAll(allowedExtensions);
|
|
saver.setSelectedExtensionFilter(defaultExtension);
|
|
try {
|
|
if (!saver.getInitialDirectory().exists())
|
|
saver.getInitialDirectory().mkdirs();
|
|
} catch (SecurityException e) {}
|
|
|
|
final Button saveButton = new Button("Save "+savee+"...");
|
|
saveButton.setOnAction((event) -> {
|
|
File file;
|
|
try {
|
|
file = saver.showSaveDialog(root);
|
|
} catch(IllegalArgumentException e) {
|
|
saver.setInitialDirectory(new File("."));
|
|
file = saver.showSaveDialog(root);
|
|
}
|
|
if (file != null) {
|
|
saver.setInitialDirectory(file.getParentFile()); //remember this directory for next time
|
|
final File f = file;
|
|
if (settingCollector.getAsBoolean()) {
|
|
final ProgressBarDialog pBar = new ProgressBarDialog();
|
|
pBar.setContentText("Finalizing "+savee+"...");
|
|
pBar.show();
|
|
new Thread(() -> {
|
|
calculateAndSaver.accept(f, pBar);
|
|
Platform.runLater(pBar::close);
|
|
try {
|
|
Desktop.getDesktop().open(f.getParentFile());
|
|
} catch (IOException e) {} //if you can't open the file for any reason, just don't worry about it
|
|
}).start();
|
|
}
|
|
}
|
|
});
|
|
saveButton.setTooltip(new Tooltip("Save the "+savee+" with current settings"));
|
|
|
|
if (bindCtrlS) // ctrl+S saves
|
|
root.addEventHandler(KeyEvent.KEY_PRESSED, (event) -> {
|
|
if (CTRL_S.match(event)) {
|
|
saveButton.requestFocus();
|
|
saveButton.fire();
|
|
}
|
|
});
|
|
|
|
if (savee.equals("map"))
|
|
this.buttons.put(ButtonType.SAVE_MAP, saveButton);
|
|
else
|
|
this.buttons.put(ButtonType.SAVE_GRAPH, saveButton);
|
|
return saveButton;
|
|
}
|
|
|
|
|
|
protected Projection getProjection() {
|
|
return projectionChooser.getValue();
|
|
}
|
|
|
|
|
|
protected boolean getParamsChanging() { //are the aspect or parameters actively changing?
|
|
return isChanging.isSet();
|
|
}
|
|
|
|
|
|
protected void loadParameters() {
|
|
getProjection().setParameters(currentParams);
|
|
}
|
|
|
|
|
|
protected void disable(ButtonType... buttons) {
|
|
for (ButtonType bt: buttons)
|
|
this.buttons.get(bt).setDisable(true);
|
|
}
|
|
|
|
|
|
protected void enable(ButtonType... buttons) {
|
|
for (ButtonType bt: buttons)
|
|
this.buttons.get(bt).setDisable(false);
|
|
}
|
|
|
|
|
|
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();
|
|
if (presetName.equals("Antipode")) {
|
|
sliders[0].setValue(-sliders[0].getValue());
|
|
sliders[1].setValue((sliders[1].getValue()+360)%360-180);
|
|
sliders[2].setValue(-sliders[2].getValue());
|
|
}
|
|
else if (presetName.equals("Random")) {
|
|
sliders[0].setValue(Math.toDegrees(Math.asin(Math.random()*2-1)));
|
|
sliders[1].setValue(Math.random()*360-180);
|
|
sliders[2].setValue(Math.random()*360-180);
|
|
}
|
|
else {
|
|
for (int i = 0; i < ASPECT_NAMES.length; i ++) {
|
|
if (ASPECT_NAMES[i].equals(presetName)) {
|
|
for (int j = 0; j < 3; j ++)
|
|
sliders[j].setValue(ASPECT_VALS[j][i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < 3; i ++)
|
|
spinners[i].getEditor().textProperty().set(
|
|
Double.toString(sliders[i].getValue()));
|
|
this.suppressListeners.clear();
|
|
}
|
|
|
|
|
|
private void revealParameters(Projection proj) {
|
|
this.suppressListeners.set();
|
|
final String[] paramNames = proj.getParameterNames();
|
|
final double[][] paramValues = proj.getParameterValues();
|
|
paramGrid.getChildren().clear();
|
|
for (int i = 0; i < proj.getNumParameters(); i ++) {
|
|
paramLabels[i].setText(paramNames[i]+":");
|
|
paramSliders[i].setMin(paramValues[i][0]);
|
|
paramSliders[i].setMax(paramValues[i][1]);
|
|
paramSliders[i].setValue(paramValues[i][2]);
|
|
final SpinnerValueFactory.DoubleSpinnerValueFactory paramSpinVF =
|
|
(DoubleSpinnerValueFactory) paramSpinners[i].getValueFactory();
|
|
paramSpinVF.setMin(paramValues[i][0]);
|
|
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]+")");
|
|
paramSliders[i].setTooltip(tt);
|
|
paramSpinners[i].setTooltip(tt);
|
|
paramGrid.addRow(i, paramLabels[i], paramSliders[i], paramSpinners[i]);
|
|
}
|
|
this.suppressListeners.clear();
|
|
}
|
|
|
|
|
|
private static void link(Slider sld, Spinner<Double> spn, int i, double[] doubles,
|
|
DoubleUnaryOperator converter, Procedure callback, Flag isChanging, Flag suppressListeners) {
|
|
sld.valueChangingProperty().addListener((observable, prev, now) -> { //link spinner to slider
|
|
isChanging.set(now);
|
|
if (!now) {
|
|
if (spn.getValue() != sld.getValue())
|
|
spn.getValueFactory().setValue(sld.getValue());
|
|
doubles[i] = converter.applyAsDouble(Math2.round(sld.getValue(),3));
|
|
if (!suppressListeners.isSet())
|
|
callback.execute();
|
|
}
|
|
});
|
|
sld.valueProperty().addListener((observable, prev, now) -> {
|
|
if (spn.getValue() != sld.getValue())
|
|
spn.getValueFactory().setValue(sld.getValue());
|
|
doubles[i] = converter.applyAsDouble(Math2.round(now.doubleValue(),3));
|
|
if (!suppressListeners.isSet())
|
|
callback.execute();
|
|
});
|
|
|
|
spn.valueProperty().addListener((observable, prev, now) -> { //link slider to spinner
|
|
if (spn.getValue() != sld.getValue())
|
|
sld.setValue(spn.getValue());
|
|
});
|
|
|
|
spn.focusedProperty().addListener((observable, prev, now) -> { //make spinner act rationally
|
|
if (!now) spn.increment(0);
|
|
});
|
|
}
|
|
|
|
|
|
protected static void showError(String header, String message) { //a simple thread-safe error handling thing
|
|
Platform.runLater(() -> {
|
|
final Alert alert = new Alert(Alert.AlertType.ERROR);
|
|
alert.setHeaderText(header);
|
|
alert.setContentText(message);
|
|
alert.showAndWait();
|
|
});
|
|
}
|
|
|
|
|
|
|
|
protected enum ButtonType {
|
|
LOAD_INPUT, UPDATE_MAP, SAVE_MAP, SAVE_GRAPH;
|
|
}
|
|
}
|
|
|