mirror of
https://github.com/csharpee/Map-Projections.git
synced 2025-12-10 00:00:19 -05:00
I (finally) made it automatically snip paths that are obviously crossing interruptions. This should make vector maps look world better straight out of the program, and will be crucial to mass-producing vector maps. There's some room for improvement regarding the handling of closepaths, but all in all, this is a big step up. I also got a new input, fixed some minor issues with the new Cahill-Keyes (did you know it's pronounced /kais/?), and might have fixed the sporadic BufferOverflowExceptions, but I'm not sure about the last one, as that problem is extremely difficult to reproduce, but I have a strong suspicion it has something to do with threads, and I added a bunch of Platform.runLater()s, so we'll see.
686 lines
26 KiB
Java
686 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.Node;
|
|
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.text.Text;
|
|
import javafx.stage.FileChooser;
|
|
import javafx.stage.Stage;
|
|
import javafx.util.converter.DoubleStringConverter;
|
|
import maps.Arbitrary;
|
|
import maps.Azimuthal;
|
|
import maps.Conic;
|
|
import maps.Cylindrical;
|
|
import maps.Lenticular;
|
|
import maps.Misc;
|
|
import maps.MyProjections;
|
|
import maps.Octohedral;
|
|
import maps.Polyhedral;
|
|
import maps.Projection;
|
|
import maps.Pseudocylindrical;
|
|
import maps.Snyder;
|
|
import maps.Tobler;
|
|
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, Octohedral.KEYES_BASIC_M, 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, Octohedral.CAHILL_CONCIALDI, Octohedral.CAHILL_KEYES,
|
|
Octohedral.KEYES_BASIC_M, Octohedral.KEYES_BUTTERFLY, Polyhedral.DYMAXION,
|
|
Polyhedral.LEE_TETRAHEDRAL_RECTANGULAR, Polyhedral.LEE_TETRAHEDRAL_TRIANGULAR,
|
|
Octohedral.WATERMAN },
|
|
{ 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())));
|
|
this.root.show();
|
|
|
|
this.suppressListeners.set();
|
|
this.projectionChooser.setValue(FEATURED_PROJECTIONS[0]);
|
|
this.suppressListeners.clear();
|
|
}
|
|
|
|
|
|
protected abstract Node 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("Hide points of extreme longitude."));
|
|
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(15.);
|
|
gratSpinner.setDisable(true);
|
|
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(false);
|
|
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) {
|
|
Platform.runLater(() -> {
|
|
for (ButtonType bt: buttons)
|
|
this.buttons.get(bt).setDisable(true);
|
|
});
|
|
}
|
|
|
|
|
|
protected void enable(ButtonType... buttons) {
|
|
Platform.runLater(() -> {
|
|
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;
|
|
}
|
|
}
|
|
|