diff --git a/input/Basic.jpg b/input/Basic.jpg index d048e86..c453246 100644 Binary files a/input/Basic.jpg and b/input/Basic.jpg differ diff --git a/input/Squares.jpg b/input/Squares.jpg deleted file mode 100644 index fa4cccb..0000000 Binary files a/input/Squares.jpg and /dev/null differ diff --git a/input/Squares.png b/input/Squares.png new file mode 100644 index 0000000..c2ccfd0 Binary files /dev/null and b/input/Squares.png differ diff --git a/input/Tissot-alt3.jpg b/input/Tissot-alt3.jpg new file mode 100644 index 0000000..4b1c782 Binary files /dev/null and b/input/Tissot-alt3.jpg differ diff --git a/input/Tissot.jpg b/input/Tissot.jpg index ef733ec..4a470d9 100644 Binary files a/input/Tissot.jpg and b/input/Tissot.jpg differ diff --git a/src/apps/MapAnalyzer.java b/src/apps/MapAnalyzer.java index 33fc398..c9e3eb1 100644 --- a/src/apps/MapAnalyzer.java +++ b/src/apps/MapAnalyzer.java @@ -54,7 +54,9 @@ import javafx.scene.text.Text; import javafx.stage.FileChooser; import javafx.stage.Stage; import maps.Projection; +import utils.Flag; import utils.Math2; +import utils.MutableDouble; import utils.Procedure; /** @@ -82,6 +84,8 @@ public class MapAnalyzer extends MapApplication { new FileChooser.ExtensionFilter("JPG", "*.jpg","*.jpeg","*.jpe","*.jfif"), new FileChooser.ExtensionFilter("GIF", "*.gif") }; + private Flag cropAtIDL; + private MutableDouble graticuleSpacing; private Button updateBtn; private Text avgSizeDistort, avgShapeDistort; private ImageView mapDisplay; @@ -107,8 +111,11 @@ public class MapAnalyzer extends MapApplication { @Override protected Node makeWidgets() { + this.cropAtIDL = new Flag(true); + this.graticuleSpacing = new MutableDouble(15); final Node projectionSelector = buildProjectionSelector(Procedure.NONE); final Node parameterSelector = buildParameterSelector(Procedure.NONE); + final Node optionPane = buildOptionPane(cropAtIDL, graticuleSpacing); final Node textDisplay = buildTextDisplay(); this.updateBtn = buildUpdateButton(this::calculateAndUpdate); this.updateBtn.setText("Calculate"); //I don't need to follow your darn conventions! @@ -121,7 +128,7 @@ public class MapAnalyzer extends MapApplication { final VBox layout = new VBox(5, projectionSelector, parameterSelector, new Separator(), - buttons, new Separator(), textDisplay); + optionPane, new Separator(), buttons, new Separator(), textDisplay); layout.setAlignment(Pos.CENTER); layout.setPrefWidth(GUI_WIDTH); @@ -191,7 +198,7 @@ public class MapAnalyzer extends MapApplication { loadParameters(); final Projection proj = this.getProjection(); - final double[][][] distortionM = proj.calculateDistortion(proj.map(ROUGH_SAMP_NUM)); + final double[][][] distortionM = proj.calculateDistortion(proj.map(ROUGH_SAMP_NUM, cropAtIDL.isSet())); mapDisplay.setImage(makeGraphic(distortionM)); diff --git a/src/apps/MapApplication.java b/src/apps/MapApplication.java index 1f62fb0..8ba8a77 100644 --- a/src/apps/MapApplication.java +++ b/src/apps/MapApplication.java @@ -16,11 +16,13 @@ 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; @@ -45,19 +47,21 @@ 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.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.Flag; import utils.Math2; +import utils.MutableDouble; import utils.Procedure; @@ -70,6 +74,9 @@ public abstract class MapApplication extends Application { protected static final int GUI_WIDTH = 350; protected static final int IMG_WIDTH = 500; + protected static final int V_SPACE = 5; + protected static final int H_SPACE = 3; + protected static final int SPINNER_WIDTH = 92; 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); @@ -80,7 +87,7 @@ public abstract class MapApplication extends Application { 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, + Tetrahedral.ACTUAUTHAGRAPH, Tetrahedral.AUTHAPOWER, 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 @@ -176,7 +183,7 @@ public abstract class MapApplication extends Application { } }); - VBox output = new VBox(5, new HBox(3, label, inputLabel), loadButton); + VBox output = new VBox(V_SPACE, new HBox(H_SPACE, label, inputLabel), loadButton); output.setAlignment(Pos.CENTER); return output; } @@ -210,7 +217,7 @@ public abstract class MapApplication extends Application { } }); - return new VBox(5, new HBox(3, label, projectionChooser), description); + return new VBox(V_SPACE, new HBox(H_SPACE, label, projectionChooser), description); } @@ -226,7 +233,7 @@ public abstract class MapApplication extends Application { final String[] labels = { "Latitude:", "Longitude:", "Ctr. Meridian:" }; final Slider[] sliders = new Slider[] { - new Slider(-90, 90, 0.), //TODO: can we call setAspectByPreset("Standard") instead of this? + new Slider(-90, 90, 0.), new Slider(-180, 180, 0.), new Slider(-180, 180, 0.) }; final Spinner spin0 = new Spinner(-90, 90, 0.); //yes, this is awkward. Java gets weird about arrays with generic types @@ -257,11 +264,11 @@ public abstract class MapApplication extends Application { } final GridPane grid = new GridPane(); - grid.setVgap(5); - grid.setHgap(3); + grid.setVgap(V_SPACE); + grid.setHgap(H_SPACE); grid.getColumnConstraints().addAll( - new ColumnConstraints(92,Control.USE_COMPUTED_SIZE,Control.USE_COMPUTED_SIZE), - new ColumnConstraints(), new ColumnConstraints(92)); + 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")); @@ -270,8 +277,9 @@ public abstract class MapApplication extends Application { grid.addRow(i, new Label(labels[i]), sliders[i], spinners[i]); } - VBox all = new VBox(5, presetChooser, grid); + 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; } @@ -282,12 +290,12 @@ public abstract class MapApplication extends Application { * @return the full formatted Region */ @SuppressWarnings("unchecked") - protected Node buildParameterSelector(Procedure parameterSetter) { + 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 spin0 = new Spinner(0.,0.,0.); //yes, this is awkward. Java gets weird about arrays with generic types - paramSpinners = (Spinner[]) Array.newInstance(spin0.getClass(), 4); + paramSpinners = (Spinner[])Array.newInstance(spin0.getClass(), 4); paramSpinners[0] = spin0; for (int i = 0; i < 4; i ++) { @@ -304,18 +312,59 @@ public abstract class MapApplication extends Application { } paramGrid = new GridPane(); - paramGrid.setVgap(5); - paramGrid.setHgap(3); + paramGrid.setVgap(V_SPACE); + paramGrid.setHgap(H_SPACE); paramGrid.getColumnConstraints().addAll( - new ColumnConstraints(92,Control.USE_COMPUTED_SIZE,Control.USE_COMPUTED_SIZE), - new ColumnConstraints(), new ColumnConstraints(92)); + new ColumnConstraints(SPINNER_WIDTH,Control.USE_COMPUTED_SIZE,Control.USE_COMPUTED_SIZE), + new ColumnConstraints(), new ColumnConstraints(SPINNER_WIDTH)); return paramGrid; } - protected Node buildOptionPane(Flag cropAtIDL, Flag graticule) { - //TODO: checkboxes for cropping and graticule - return null; + /** + * 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 factorsOf360 = FXCollections.observableArrayList(); + for (double f = 1; f <= 45; f += 0.5) + if (360%f == 0) + factorsOf360.add((double)f); + final Spinner gratSpinner = new Spinner(factorsOf360); //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); } @@ -550,43 +599,5 @@ public abstract class MapApplication extends Application { alert.showAndWait(); }); } - - - - /** - * Because Java apparently doesn't already have a mutable Bullion - * @author jkunimune - */ - private class Flag { - private boolean set; - - public Flag() { - this(false); - } - - public Flag(boolean set) { - this.set = set; - } - - public boolean isSet() { - return this.set; - } - - public void set(boolean set) { - this.set = set; - } - - public void set() { - this.set = true; - } - - public void clear() { - this.set = false; - } - - public String toString() { - return "Flag("+this.set+")"; - } - } } diff --git a/src/apps/MapDesignerRaster.java b/src/apps/MapDesignerRaster.java index 9aceb30..5586380 100644 --- a/src/apps/MapDesignerRaster.java +++ b/src/apps/MapDesignerRaster.java @@ -46,7 +46,9 @@ import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; import maps.Projection; +import utils.Flag; import utils.ImageUtils; +import utils.MutableDouble; import utils.PixelMap; import utils.Procedure; @@ -76,6 +78,8 @@ public class MapDesignerRaster extends MapApplication { private Node aspectSelector; private Button updateBtn, saveMapBtn; private double[] aspect; + private Flag cropAtIDL; + private MutableDouble graticuleSpacing; private PixelMap input; private ImageView display; private MapConfigurationDialog configDialog; @@ -101,22 +105,24 @@ public class MapDesignerRaster extends MapApplication { @Override protected Node makeWidgets() { this.aspect = new double[3]; + this.cropAtIDL = new Flag(false); + this.graticuleSpacing = new MutableDouble(15); final Node inputSelector = buildInputSelector(READABLE_TYPES, RASTER_TYPES[0], this::setInput); final Node projectionSelector = buildProjectionSelector(this::hideAspect); this.aspectSelector = buildAspectSelector(this.aspect, Procedure.NONE); final Node parameterSelector = buildParameterSelector(Procedure.NONE); + final Node optionPane = buildOptionPane(cropAtIDL, graticuleSpacing); this.updateBtn = buildUpdateButton(this::updateMap); this.saveMapBtn = buildSaveButton(true, "map", RASTER_TYPES, RASTER_TYPES[0], this::collectFinalSettings, this::calculateAndSaveMap); - final HBox buttons = new HBox(5, updateBtn, saveMapBtn); + final HBox buttons = new HBox(H_SPACE, updateBtn, saveMapBtn); buttons.setAlignment(Pos.CENTER); - aspectSelector.managedProperty().bind(aspectSelector.visibleProperty()); - final VBox layout = new VBox(5, - inputSelector, new Separator(), projectionSelector, - new Separator(), aspectSelector, parameterSelector, - new Separator(), buttons); + final VBox layout = new VBox(V_SPACE, + inputSelector, new Separator(), projectionSelector, new Separator(), + aspectSelector, parameterSelector, new Separator(), optionPane, new Separator(), + buttons); layout.setAlignment(Pos.CENTER); layout.setPrefWidth(GUI_WIDTH); @@ -202,8 +208,9 @@ public class MapDesignerRaster extends MapApplication { private BufferedImage makeImage(int width, int height, int step, ProgressBarDialog pBar) { final double[] pole = aspect.clone(); + final boolean crop = cropAtIDL.isSet(); final Projection pjc = this.getProjection(); - final BufferedImage out = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + BufferedImage out = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); for (int y = 0; y < out.getHeight(); y ++) { if (pBar != null) pBar.setProgress((double) y/out.getHeight()); @@ -211,9 +218,9 @@ public class MapDesignerRaster extends MapApplication { int[] colors = new int[step*step]; for (int dy = 0; dy < step; dy ++) { for (int dx = 0; dx < step; dx ++) { - final double X = ((x+(dx+.5)/step)/out.getWidth() - 1/2.) *pjc.getWidth(); - final double Y = (1/2. - (y+(dy+.5)/step)/out.getHeight()) *pjc.getHeight(); - final double[] coords = this.getProjection().inverse(X, Y, pole); + double X = ((x+(dx+.5)/step)/out.getWidth() - 1/2.) *pjc.getWidth(); + double Y = (1/2. - (y+(dy+.5)/step)/out.getHeight()) *pjc.getHeight(); + double[] coords = this.getProjection().inverse(X, Y, pole, crop); if (coords != null) //if it is null, the default (0:transparent) is used colors[step*dy+dx] = input.getArgb(coords[0], coords[1]); } diff --git a/src/apps/MapDesignerVector.java b/src/apps/MapDesignerVector.java index 8a62bd6..b77b581 100644 --- a/src/apps/MapDesignerVector.java +++ b/src/apps/MapDesignerVector.java @@ -104,7 +104,6 @@ public class MapDesignerVector extends MapApplication { final Node parameterSelector = buildParameterSelector(this::updateMap); this.saveBtn = buildSaveButton(true, "map", VECTOR_TYPES, VECTOR_TYPES[0], ()->true, this::calculateAndSaveMap); - aspectSelector.managedProperty().bind(aspectSelector.visibleProperty()); final VBox layout = new VBox(5, inputSelector, new Separator(), projectionSelector, diff --git a/src/dialogs/ProjectionSelectionDialog.java b/src/dialogs/ProjectionSelectionDialog.java index ea855bf..412ca99 100644 --- a/src/dialogs/ProjectionSelectionDialog.java +++ b/src/dialogs/ProjectionSelectionDialog.java @@ -33,7 +33,6 @@ 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; diff --git a/src/maps/Azimuthal.java b/src/maps/Azimuthal.java index a7388bd..a1488e4 100644 --- a/src/maps/Azimuthal.java +++ b/src/maps/Azimuthal.java @@ -38,9 +38,7 @@ public class Azimuthal { } public double[] inverse(double x, double y) { - return new double[] { - Math.PI/2 - 2*Math.atan(Math.hypot(x, y)), - Math.atan2(y, x) + Math.PI/2 }; + return new double[] { Math.PI/2 - 2*Math.atan(Math.hypot(x, y)), Math.atan2(x, -y) }; } }; @@ -58,7 +56,7 @@ public class Azimuthal { public double[] inverse(double x, double y) { double phi = Math.PI/2 - Math.hypot(x, y); if (phi > -Math.PI/2) - return new double[] {phi, Math.atan2(y, x) + Math.PI/2}; + return new double[] {phi, Math.atan2(x, -y)}; else return null; } @@ -77,7 +75,7 @@ public class Azimuthal { public double[] inverse(double x, double y) { double r = Math.hypot(x, y); if (r <= 1) - return new double[] {Math.asin(1-2*r*r), Math.atan2(y,x)+Math.PI/2}; + return new double[] { Math.asin(1-2*r*r), Math.atan2(x, -y) }; else return null; } @@ -97,7 +95,7 @@ public class Azimuthal { public double[] inverse(double x, double y) { double R = Math.hypot(x, y); if (R <= 1) - return new double[] { Math.acos(R), Math.atan2(y, x) + Math.PI/2 }; + return new double[] { Math.acos(R), Math.atan2(x, -y) }; else return null; } @@ -116,8 +114,7 @@ public class Azimuthal { } public double[] inverse(double x, double y) { - return new double[] { Math.PI/2 - Math.atan(Math.hypot(x, y)), - Math.atan2(y, x) + Math.PI/2 }; + return new double[] { Math.PI/2 - Math.atan(Math.hypot(x, y)), Math.atan2(x, -y) }; } }; diff --git a/src/maps/Misc.java b/src/maps/Misc.java index e79a64c..a5ef089 100644 --- a/src/maps/Misc.java +++ b/src/maps/Misc.java @@ -29,6 +29,7 @@ import de.jtem.ellipticFunctions.Jacobi; import maps.Projection.Property; import maps.Projection.Type; import utils.Elliptic; +import utils.Math2; /** * All the projections that don't fit into any of the other categories. @@ -71,7 +72,7 @@ public class Misc { de.jtem.mfc.field.Complex k = new de.jtem.mfc.field.Complex(Math.sqrt(0.5)); //the rest comes from some fancy complex calculus de.jtem.mfc.field.Complex ans = Jacobi.cn(u, k); double p = 2 * Math.atan(ans.abs()); - double theta = ans.arg() - Math.PI/2; + double theta = Math.atan2(-ans.getRe(), ans.getIm()); double lambda = Math.PI/2 - p; return new double[] {lambda, theta}; } @@ -147,15 +148,17 @@ public class Misc { } public double[] inverse(double x, double y) { - final double phi1 = Math.PI/2 - Math.hypot(x, y); + double phi1 = Math.PI/2 - Math.hypot(x, y); if (phi1 < -Math.PI/2) return null; - final double lam1 = Math.atan2(x, -y); + double lam1 = Math.atan2(x, -y); double phiP = Math.asin(Math.sin(phi0)/Math.hypot(Math.sin(phi1),Math.cos(phi1)*Math.cos(lam1))) - Math.atan2(Math.cos(phi1)*Math.cos(lam1),Math.sin(phi1)); if (Math.abs(phiP) > Math.PI/2) phiP = Math.signum(phiP)*Math.PI - phiP; - final double delL = Math.acos(Math.sin(phi1)/Math.cos(phiP)/Math.cos(phi0) - Math.tan(phiP)*Math.tan(phi0)); - final double lamP = lam0 + Math.signum(x)*delL; + double delL = Math.acos(Math.sin(phi1)/Math.cos(phiP)/Math.cos(phi0) - Math.tan(phiP)*Math.tan(phi0)); + double lamP = lam0 + Math.signum(x)*delL; if (Double.isNaN(phiP) || Double.isNaN(lamP)) return null; + if (lamP > Math.PI) lamP -= 2*Math.PI; + if (lamP < -Math.PI) lamP += 2*Math.PI; return new double[] {phiP, lamP}; } }; @@ -216,7 +219,7 @@ public class Misc { final double s1 = Math.signum(Math.sin(Math.acos(t1)-s0*Math.acos(t2))); final double PHI = Math.asin(Math.sin(lat1)*Math.cos(d1) - Math.cos(lat1)*Math.sin(d1)*casab); final double LAM = lon1 +s1* Math.acos((Math.cos(d1) - Math.sin(lat1)*Math.sin(PHI))/(Math.cos(lat1)*Math.cos(PHI))); - return new double[] {PHI, LAM}; + return new double[] { PHI, Math2.floorMod(LAM+Math.PI, 2*Math.PI) - Math.PI }; } private double dist(double lat1, double lon1, double lat2, double lon2) { diff --git a/src/maps/MyProjections.java b/src/maps/MyProjections.java index c9a0a32..59e328e 100644 --- a/src/maps/MyProjections.java +++ b/src/maps/MyProjections.java @@ -27,6 +27,7 @@ import org.apache.commons.math3.complex.Complex; import maps.Projection.Property; import maps.Projection.Type; +import utils.Math2; /** * All of the projections I invented, save the tetrahedral ones, because @@ -52,7 +53,7 @@ public class MyProjections { if (R <= 1) return new double[] { Math.PI/2 * (1 - R*.2 - R*R*R*1.8), - Math.atan2(y, x) + Math.PI/2}; + Math.atan2(x, -y)}; else return null; } @@ -76,9 +77,14 @@ public class MyProjections { Complex z = new Complex(x, y); Complex ans = z.sin(); double p = 2 * Math.atan(ans.abs()); - double theta = ans.getArgument() + Math.PI/2; + double theta = Math2.floorMod(ans.getArgument() + 3*Math.PI/2, 2*Math.PI) - Math.PI; double lambda = Math.PI/2 - p; - return new double[] {lambda, theta}; + if (x < -Math.PI/2) + return new double[] {lambda, theta-2*Math.PI}; + else if (x < Math.PI/2) + return new double[] {lambda, theta}; + else + return new double[] {lambda, theta+2*Math.PI}; } }; diff --git a/src/maps/Projection.java b/src/maps/Projection.java index 6309fe3..99eadab 100644 --- a/src/maps/Projection.java +++ b/src/maps/Projection.java @@ -158,7 +158,15 @@ public abstract class Projection { } public double[] inverse(double x, double y, double[] pole) { - return obliquifyPlnr(inverse(x, y), pole); + return inverse(x, y, pole, false); + } + + public double[] inverse(double x, double y, double[] pole, boolean cropAtPi) { + final double[] relCoords = inverse(x, y); + if (relCoords == null || (cropAtPi && Math.abs(relCoords[1]) > Math.PI)) + return null; //cropAtPi removes all points with longitudes outside +- PI + else + return obliquifyPlnr(relCoords, pole); } @@ -167,22 +175,27 @@ public abstract class Projection { public double[][][] map(int size) { - return map(size, new double[] {Math.PI/2,0,0}); + return map(size, false); } - public double[][][] map(int size, double[] pole) { + public double[][][] map(int size, boolean cropAtPi) { + return map(size, new double[] {Math.PI/2,0,0}, cropAtPi); + } + + public double[][][] map(int size, double[] pole, boolean cropAtPi) { if (width >= height) - return map(size, Math.max(Math.round(size*height/width),1), pole, null); + return map(size, Math.max(Math.round(size*height/width),1), pole, cropAtPi, null); else - return map(Math.max(Math.round(size*width/height),1), size, pole, null); + return map(Math.max(Math.round(size*width/height),1), size, pole, cropAtPi, null); } - public double[][][] map(double w, double h, double[] pole, DoubleConsumer tracker) { //generate a matrix of coordinates based on a map projection + public double[][][] map(double w, double h, double[] pole, boolean cropAtPi, + DoubleConsumer tracker) { //generate a matrix of coordinates based on a map projection final double[][][] output = new double[(int) h][(int) w][2]; for (int y = 0; y < h; y ++) { for (int x = 0; x < w; x ++) output[y][x] = inverse( - ((x+0.5)/w-1/2.)*width, (1/2.-(y+0.5)/h)*height, pole); + ((x+0.5)/w-1/2.)*width, (1/2.-(y+0.5)/h)*height, pole, cropAtPi); if (tracker != null) tracker.accept((double)y / (int)h); } @@ -295,7 +308,7 @@ public abstract class Projection { lon1 = 2*Math.PI-lon1; } lon1 = lon1-tht0; - lon1 = Math2.mod(lon1+Math.PI, 2*Math.PI) - Math.PI; + lon1 = Math2.floorMod(lon1+Math.PI, 2*Math.PI) - Math.PI; return new double[] {lat1, lon1}; } @@ -308,8 +321,6 @@ public abstract class Projection { * @return { LAT, LON } */ protected static final double[] obliquifyPlnr(double[] coords, double[] pole) { - if (coords == null) return null; - double lat1 = coords[0], lon1 = coords[1]; final double lat0 = pole[0], lon0 = pole[1], tht0 = pole[2]; lon1 += tht0; @@ -333,6 +344,9 @@ public abstract class Projection { lonf = lon0 - Math.acos(innerFunc); + if (Math.abs(lonf) > Math.PI) + lonf = Math2.floorMod(lonf+Math.PI, 2*Math.PI) - Math.PI; + double thtf = pole[2]; double[] output = {latf, lonf, thtf}; diff --git a/src/maps/Snyder.java b/src/maps/Snyder.java index b82dfd1..3581225 100644 --- a/src/maps/Snyder.java +++ b/src/maps/Snyder.java @@ -47,7 +47,7 @@ public class Snyder { private static final double TOLERANCE = 10e-4; private static final double[] LIMS = { - Math.toRadians(10), Math.toRadians(85), Math.toRadians(-195), Math.toRadians(-50) }; //trims the outside unsightly portions + Math.toRadians(5), Math.toRadians(85), Math.toRadians(-195), Math.toRadians(-50) }; //trims the outside unsightly portions public static final Projection GS50 = @@ -78,12 +78,13 @@ public class Snyder { z = z.minus(error.divide(deriv)); error = f(z).minus(p); } - final double r = z.abs(); - final double phi = 2*Math.atan(r/2); - final double lat = Math.asin(Math.cos(phi)*Math.sin(POLE[0]) + z.getIm()*Math.sin(phi)*Math.cos(POLE[0])/r); - final double lon = POLE[1] + Math.atan(z.getRe()*Math.sin(phi)/(r*Math.cos(POLE[0])*Math.cos(phi)-z.getIm()*Math.sin(POLE[0]*Math.sin(phi)))); + double r = z.abs(); + double phi = 2*Math.atan(r/2); + double lat = Math.asin(Math.cos(phi)*Math.sin(POLE[0]) + z.getIm()*Math.sin(phi)*Math.cos(POLE[0])/r); + double lon = POLE[1] + Math.atan(z.getRe()*Math.sin(phi)/(r*Math.cos(POLE[0])*Math.cos(phi)-z.getIm()*Math.sin(POLE[0]*Math.sin(phi)))); if (lat < LIMS[0] || lat > LIMS[1]) return null; if (lon < LIMS[2] || lon > LIMS[3]) return null; + if (lon < -Math.PI) lon += 2*Math.PI; return new double[] {lat, lon}; } }; diff --git a/src/maps/Tetrahedral.java b/src/maps/Tetrahedral.java index 389714f..d60440a 100644 --- a/src/maps/Tetrahedral.java +++ b/src/maps/Tetrahedral.java @@ -25,6 +25,7 @@ package maps; import maps.Projection.Property; import utils.Dixon; +import utils.Math2; import utils.NumericalAnalysis; /** @@ -296,8 +297,16 @@ public class Tetrahedral { final double x0 = centrum[4]; final double y0 = centrum[5]; - return obliquifyPlnr( - innerInverse(Math.hypot(x-x0, y-y0), Math.atan2(y-y0, x-x0) - th0), centrum); + double[] relCoords = + innerInverse(Math.hypot(x-x0, y-y0), Math.atan2(y-y0, x-x0) - th0); + + if (relCoords == null) + return null; + + double[] absCoords = obliquifyPlnr(relCoords, centrum); + if (Math.abs(absCoords[1]) > Math.PI) + absCoords[1] = Math2.floorMod(absCoords[1]+Math.PI, 2*Math.PI) - Math.PI; + return absCoords; } } diff --git a/src/maps/WinkelTripel.java b/src/maps/WinkelTripel.java index 879fb24..199313c 100644 --- a/src/maps/WinkelTripel.java +++ b/src/maps/WinkelTripel.java @@ -64,7 +64,7 @@ public final class WinkelTripel { public double[] inverse(double x, double y) { return NumericalAnalysis.newtonRaphsonApproximation( x, y, - y/2, x*(1 + Math.cos(y*Math.PI/2))/(2 + 2*Math.cos(stdParallel)), + y/2, x*(1 + Math.cos(y*Math.PI/2))/(2 + 2*Math.cos(stdParallel)), //inital guess is Eckert V this::f1pX, this::f2pY, this::df1dphi, this::df1dlam, this::df2dphi, this::df2dlam, .002); } diff --git a/src/utils/Flag.java b/src/utils/Flag.java new file mode 100644 index 0000000..af7a576 --- /dev/null +++ b/src/utils/Flag.java @@ -0,0 +1,63 @@ +/** + * 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 utils; + + +/** + * Because Java apparently doesn't already have a mutable Bullion + * + * @author jkunimune + */ +public class Flag { + + private boolean set; + + public Flag() { + this(false); + } + + public Flag(boolean set) { + this.set = set; + } + + public boolean isSet() { + return this.set; + } + + public void set(boolean set) { + this.set = set; + } + + public void set() { + this.set = true; + } + + public void clear() { + this.set = false; + } + + public String toString() { + return "Flag("+this.set+")"; + } +} \ No newline at end of file diff --git a/src/utils/Math2.java b/src/utils/Math2.java index a43c67e..8fb58cc 100644 --- a/src/utils/Math2.java +++ b/src/utils/Math2.java @@ -83,13 +83,13 @@ public class Math2 { } - public static final double determ(double a, double b, double c, double d) { - return a*d - b*c; + public static final double floorMod(double x, double y) { + return x - Math.floor(x / y) * y; } - public static final double mod(double x, double y) { - return x - Math.floor(x/y)*y; + public static final double determ(double a, double b, double c, double d) { + return a*d - b*c; } diff --git a/src/utils/MutableDouble.java b/src/utils/MutableDouble.java new file mode 100644 index 0000000..340da34 --- /dev/null +++ b/src/utils/MutableDouble.java @@ -0,0 +1,92 @@ +/** + * 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 utils; + + +/** + * Because Java apparently doesn't already have a mutable Double + * + * @author jkunimune + */ +public class MutableDouble { + + private double value; + + public MutableDouble() { + this(0.); + } + + public MutableDouble(double value) { + this.value = value; + } + + public double get() { + return this.value; + } + + public boolean isFinite() { + return Double.isFinite(this.value); + } + + public boolean isInfinite() { + return Double.isInfinite(this.value); + } + + public boolean isNaN() { + return Double.isNaN(this.value); + } + + public boolean isZero() { + return this.equals(0); + } + + public boolean equals(double value) { + return this.value == value; + } + + public void set(double value) { + this.value = value; + } + + public void increment() { + this.increment(1); + } + + public void increment(double value) { + this.value += value; + } + + public void decrement() { + this.decrement(1); + } + + public void decrement(double value) { + this.value -= value; + } + + public String toString() { + return "MutableDouble("+value+")"; + } + +} diff --git a/src/utils/Spinner.java b/src/utils/Spinner.java new file mode 100644 index 0000000..fde7871 --- /dev/null +++ b/src/utils/Spinner.java @@ -0,0 +1,43 @@ +/** + * 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 utils; + + +/** + * It's a JavaFX spinner, but with intuitive behaviour (i.e. it commits its value when it loses + * focus, not just when the user presses enter) + * + * @author jkunimune + */ +public class Spinner extends javafx.scene.control.Spinner { + + public Spinner() { + super(); + this.focusedProperty().addListener((observable, prev, now) -> { + if (!now) //why I'm not allowed to call commitEditorText() is beyond me, since it clearly doesn't do that enough + this.increment(0); //was that so hard? + }); + } + +}