Parsing, for real this time

I implemented more advanced SVG parsing and manipulation. It's somewhat
slower now, I think because I changed the way it skips vertices to save
time, but it was necessary to enable slightly more advanced SVG reading
(which I have yet to test). All the projections work, though. I also
changed my coordinate system. I also broke the bit where it does not
draw long lines.
This commit is contained in:
Justin Kunimune 2017-08-25 21:06:56 -10:00
parent 3c8150a276
commit 40201f6f46
24 changed files with 617 additions and 291 deletions

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
org.eclipse.ltk.core.refactoring.enable.project.refactoring.history=false

View File

@ -21,11 +21,9 @@ The executable applications all have similar layouts that let you select an inpu
## Dependencies
While the excecutables are standalone, and the Jars require only Java, the source code makes use of several external libraries. These are
* Apache commons `math3`
* "mfc"
* "ellipticFunctions"
To be perfectly honest, I don't remember where I got most of these. Oops. It looks like they might be German. I would recommend looking the `de.jtem` package from (math.tu-berlin.de)[www3.math.tu.berlin.de/jtem/].
* [Apache Commons Mathematics Library](https://commons.apache.org/proper/commons-math/)
* [Java Tools for Experimental Mathematics "ellipticFunctions" package](http://www3.math.tu-berlin.de/jtem/ellipticFunctions/), which requires their ["mfc" package](http://www3.math.tu-berlin.de/jtem/mfc/)
* [Apache Batik Java SVG Toolkit](https://xmlgraphics.apache.org/batik/)
## Wherefore?
I'll write a little blurb here later.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

45
input/Grid.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 163 KiB

View File

@ -68,7 +68,7 @@ public abstract class MapApplication extends Application {
private static final double[][] ASPECT_VALS = { //the aspect presets (in degrees)
{ 90., 0., 0., 29.98, 31.78, 48.88, -28.52,-35., 47., 60. },
{ 0., 0., 90., 31.13, 35.22, 56.61, 141.45,-13.61,-173., -6. },
{ 0., 0.,-90.,-32., -35., -45., 161.5, 145., 138.,-100. } };
{ 0., 0.,-90.,-32., -35., -45., 30., 145., 138.,-100. } };
final private String name;

View File

@ -57,6 +57,7 @@ import maps.Tetrahedral;
import maps.Tobler;
import maps.WinkelTripel;
import utils.ImageUtils;
import utils.PixelMap;
import utils.Procedure;
/**
@ -98,7 +99,7 @@ public class MapDesignerRaster extends MapApplication {
private Node aspectSelector;
private Button updateBtn, saveMapBtn;
private double[] aspect;
private BufferedImage input;
private PixelMap input;
private ImageView display;
private MapConfigurationDialog configDialog;
@ -163,7 +164,7 @@ public class MapDesignerRaster extends MapApplication {
saveMapBtn.setDisable(true);
try {
input = ImageIO.read(file);
input = new PixelMap(file);
} catch (IOException e) {
final Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("File not found!");
@ -230,8 +231,9 @@ public class MapDesignerRaster extends MapApplication {
for (int dx = 0; dx < step; dx ++) {
final double X = 2*(x+(dx+.5)/step)/out.getWidth()-1;
final double Y = 1-2*(y+(dy+.5)/step)/out.getHeight();
colors[step*dy+dx] = ImageUtils.getArgb(
this.getProjection().inverse(X, Y, pole), input);
final double[] coords = this.getProjection().inverse(X, Y, pole);
if (coords != null) //if it is null, the default (0:transparent) is used
colors[step*dy+dx] = input.getArgb(coords[0], coords[1]);
}
}
out.setRGB(x, y, ImageUtils.blend(colors));

View File

@ -22,17 +22,17 @@
* SOFTWARE.
*/
package apps;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.function.DoubleConsumer;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import dialogs.ProgressBarDialog;
import javafx.application.Platform;
import javafx.geometry.Insets;
@ -58,8 +58,10 @@ import maps.Robinson;
import maps.Tetrahedral;
import maps.Tobler;
import maps.WinkelTripel;
import utils.Math2;
import utils.Procedure;
import utils.SVGMap;
import utils.SVGMap.Command;
import utils.SVGMap.Path;
/**
* An application to make vector oblique aspects of map projections
@ -96,10 +98,7 @@ public class MapDesignerVector extends MapApplication {
private Node aspectSelector;
private Button saveBtn;
private double[] aspect;
private List<String> format;
private List<List<double[]>> input;
private double minX, maxX, minY, maxY;
private int numVtx;
private SVGMap input;
private Canvas viewer;
@ -154,13 +153,16 @@ public class MapDesignerVector extends MapApplication {
saveBtn.setDisable(true);
try {
input = loadSVG(file.getAbsolutePath());
} catch (IllegalArgumentException e) {
showError("Unreadable file!",
"We couldn't read "+file.getAbsolutePath()+". It may be corrupt or an unreadable format.");
input = new SVGMap(file);
} catch (IOException e) {
showError("File not found!",
"We couldn't find "+file.getAbsolutePath()+".");
} catch (SAXException e) {
showError("Unreadable file!",
"We couldn't read "+file.getAbsolutePath()+". It may be corrupt or an unreadable format.");
} catch (ParserConfigurationException e) {
// TODO: Handle this
e.printStackTrace();
} finally {
saveBtn.setDisable(false);
}
@ -169,71 +171,6 @@ public class MapDesignerVector extends MapApplication {
}
private List<List<double[]>> loadSVG(String filename) throws IOException { // this method is just awful.
input = new ArrayList<List<double[]>>();
format = new ArrayList<String>();
minX = minY = Integer.MAX_VALUE;
maxX = maxY = Integer.MIN_VALUE;
numVtx = 0;
BufferedReader in = new BufferedReader(new FileReader(filename));
String formatStuff = "";
int c = in.read();
do {
formatStuff += (char)c;
if (formatStuff.length() >= 4 &&
formatStuff.substring(formatStuff.length()-4).equals(" d=\"")) {
format.add(formatStuff);
formatStuff = "";
List<double[]> currentShape = new ArrayList<double[]>();
c = in.read();
do {
if (c == 'Z') {
input.add(currentShape);
currentShape = new ArrayList<double[]>();
format.add("");
}
if (isDigit((char) c)) {
String num = Character.toString((char)c);
while (isDigit((char) (c = in.read())))
num += (char) c;
double x = Double.parseDouble(num);
if (x < minX) minX = x;
if (x > maxX) maxX = x;
c = in.read();
num = Character.toString((char)c);
while (isDigit((char) (c = in.read())))
num += (char) c;
double y = Double.parseDouble(num);
if (y < minY) minY = y;
if (y > maxY) maxY = y;
currentShape.add(new double[] {x,y});
numVtx ++;
}
else {
c = in.read();
}
} while (c != '"');
}
else {
c = in.read();
}
} while (c >= 0);
format.add(formatStuff);
in.close();
for (List<double[]> curve: input) {
for (double[] coords: curve) {
final double[] radCoords = convCoordsToMathy(coords);
coords[0] = radCoords[0];
coords[1] = radCoords[1];
}
}
return input;
}
private void hideAspect() {
aspectSelector.setVisible(this.getProjection().hasAspect());
@ -243,112 +180,63 @@ public class MapDesignerVector extends MapApplication {
private void updateMap() {
loadParameters();
int maxVtx = this.getParamsChanging() ? FAST_MAX_VTX : DEF_MAX_VTX;
final List<List<double[]>> transformed = map(input, numVtx/maxVtx+1, aspect.clone(), null);
Platform.runLater(() -> drawImage(transformed, viewer));
final Iterable<Path> transformed = map(input, input.size()/maxVtx+1, aspect.clone(), null);
drawImage(transformed, viewer);
}
private void calculateAndSaveMap(File file, ProgressBarDialog pBar) {
loadParameters();
final List<List<double[]>> transformed = map(input, 1, aspect.clone(), pBar::setProgress); //calculate
saveToSVG(transformed, file, pBar); //save
final Iterable<Path> transformed = map(input, 1, aspect.clone(), pBar::setProgress); //calculate
try {
input.save(transformed, file, pBar::setProgress); //save
} catch (IOException e) {
showError("Failure!",
"Could not access "+file.getAbsolutePath()+". It's possible that another program has it open.");
}
}
public List<List<double[]>> map(List<List<double[]>> curves, int step,
double[] pole, DoubleConsumer tracker) {
List<List<double[]>> output = new LinkedList<List<double[]>>();
public Iterable<Path> map(SVGMap input, int step, double[] pole, DoubleConsumer tracker) {
List<Path> output = new LinkedList<Path>();
int i = 0;
for (List<double[]> curve0: curves) {
if (curve0.size() < step*3) continue;
for (Path path0: input) {
if (path0.length() < step*3) continue;
List<double[]> curve1 = new ArrayList<double[]>(curve0.size()/step);
for (int j = 0; j < curve0.size(); j += step)
curve1.add(this.getProjection().project(curve0.get(j), pole));
output.add(curve1);
Path path1 = new Path();
int counter = 0;
for (Command cmd0: path0) {
counter --;
if (counter > 0 && cmd0.type != 'M' && cmd0.type != 'Z') continue;
counter = step;
Command cmd1 = new Command(cmd0.type, new double[cmd0.args.length]);
for (int k = 0; k < cmd0.args.length; k += 2) {
System.arraycopy(
this.getProjection().project(cmd0.args[k+1], cmd0.args[k], pole), 0,
cmd1.args, k, 2);
}
path1.add(cmd1);
}
output.add(path1);
if (tracker != null) {
i ++;
tracker.accept((double)i/curves.size());
tracker.accept((double)i/input.numCurves());
}
}
return output;
}
private void drawImage(List<List<double[]>> img, Canvas c) {
private void drawImage(Iterable<Path> paths, Canvas c) {
GraphicsContext g = c.getGraphicsContext2D();
g.clearRect(0, 0, c.getWidth(), c.getHeight());
for (List<double[]> closedCurve: img) {
final double[] start = convCoordsToImg(closedCurve.get(0));
g.beginPath();
g.moveTo(start[0], start[1]);
for (int i = 1; i < closedCurve.size()+1; i ++) {
double[] p0 = convCoordsToImg(closedCurve.get(i-1));
double[] p1 = convCoordsToImg(closedCurve.get(i%closedCurve.size()));
if (Math.hypot(p1[0]-p0[0], p1[1]-p0[1]) >= c.getWidth()/8) {
g.stroke();
g.beginPath();
g.moveTo(p1[0], p1[1]);
}
else
g.lineTo(p1[0], p1[1]);
}
g.stroke();
g.beginPath();
for (Path path: paths) {
g.appendSVGPath(path.toString(0, 0, IMG_WIDTH, IMG_WIDTH));
}
}
private void saveToSVG(List<List<double[]>> curves, File file,
ProgressBarDialog pBar) {
try {
BufferedWriter out = new BufferedWriter(new FileWriter(file));
for (int i = 0; i < curves.size(); i ++) {
out.write(format.get(i));
String curveCode = "M";
for (double[] r: curves.get(i)) {
final double[] p = convCoordsToSVG(r);
curveCode += p[0]+" "+p[1]+"L";
}
out.write(curveCode.substring(0,curveCode.length()-2)+"Z");
pBar.setProgress((double)i/curves.size());
}
out.write(format.get(curves.size()));
out.close();
} catch (IOException e) {
showError("Failure!",
"Could not access "+file.getAbsolutePath()+". It's possible that another program has it open.");
}
saveBtn.setDisable(false);
}
private double[] convCoordsToMathy(double... coords) { // changes svg coordinates to radians
final double NORTHMOST = 1.459095; //TODO: use image height and width
final double SOUTHMOST = -1.4868809;
final double EASTMOST = -Math.PI;
final double WESTMOST = Math.PI;
return new double[] {Math2.linInterp(coords[1], minY,maxY, SOUTHMOST,NORTHMOST),
Math2.linInterp(coords[0], minX,maxX, EASTMOST,WESTMOST)};
}
private double[] convCoordsToImg(double... coords) { // changes [-1,1] coordinates to image coordinates
return new double[] {Math2.linInterp(coords[0], -Math.PI,Math.PI, 0,IMG_WIDTH),
Math2.linInterp(coords[1], -Math.PI,Math.PI, IMG_WIDTH,0)};
}
private double[] convCoordsToSVG(double... coords) { // changes image coordinates to svg coordinates
return new double[] {Math2.linInterp(coords[0], -Math.PI,Math.PI, -180,180), //TODO: use image height and width
Math2.linInterp(coords[1], -Math.PI,Math.PI, -180,180)};
}
private static final boolean isDigit(char c) {
return c >= '-' && c <= '9';
g.stroke();
}
}

View File

@ -42,6 +42,7 @@ import maps.Tetrahedral;
import maps.Tobler;
import maps.WinkelTripel;
import utils.ImageUtils;
import utils.PixelMap;
/**
* A program to generate and output an HTML snippet listing and explaining all of my maps
@ -74,8 +75,8 @@ public class MapExplainer {
public static void main(String[] args) throws IOException {
new File("images").mkdirs();
final BufferedImage inputSkew = ImageIO.read(new File("input/Tissot-alt2.jpg"));
final BufferedImage inputPole = ImageIO.read(new File("input/Tissot-alt1.jpg"));
final PixelMap inputSkew = new PixelMap(new File("input/Tissot-alt2.jpg"));
final PixelMap inputPole = new PixelMap(new File("input/Tissot-alt1.jpg"));
final PrintStream out = System.out;
for (Projection[] projs: ALL_PROJECTIONS) {
@ -124,7 +125,7 @@ public class MapExplainer {
}
private static BufferedImage makeImage(Projection proj, BufferedImage input) {
private static BufferedImage makeImage(Projection proj, PixelMap input) {
proj.setParameters(proj.getDefaultParameters());
final BufferedImage out = new BufferedImage(
IMG_WIDTH, (int) (IMG_WIDTH/proj.getAspectRatio()), BufferedImage.TYPE_INT_ARGB);
@ -135,8 +136,9 @@ public class MapExplainer {
for (int dx = 0; dx < SMOOTHING; dx ++) {
final double X = 2*(x+(dx+.5)/SMOOTHING)/out.getWidth()-1;
final double Y = 1-2*(y+(dy+.5)/SMOOTHING)/out.getHeight();
colors[SMOOTHING*dy+dx] = ImageUtils.getArgb(
proj.inverse(X, Y), input);
final double[] coords = proj.inverse(X, Y);
if (coords != null)
colors[SMOOTHING*dy+dx] = input.getArgb(coords[0], coords[1]);
}
}
out.setRGB(x, y, ImageUtils.blend(colors));

View File

@ -33,7 +33,7 @@ public class Azimuthal {
"mathematically important") {
public double[] project(double lat, double lon) {
final double r = Math.PI/2/(Math.tan(lat/2 + Math.PI/4));
final double r = .5/(Math.tan(lat/2 + Math.PI/4));
return new double[] {r*Math.sin(lon), -r*Math.cos(lon)};
}
@ -48,7 +48,7 @@ public class Azimuthal {
new Projection("Polar", 1., 0b1111, Type.AZIMUTHAL, Property.EQUIDISTANT) {
public double[] project(double lat, double lon) {
final double r = Math.PI/2 - lat;
final double r = .5 - lat/Math.PI;
return new double[] {r*Math.sin(lon), -r*Math.cos(lon)};
}
@ -67,7 +67,7 @@ public class Azimuthal {
"Azimuthal Equal-Area", 1., 0b1111, Type.AZIMUTHAL, Property.EQUAL_AREA) {
public double[] project(double lat, double lon) {
final double r = Math.PI*Math.cos((Math.PI/2+lat)/2);
final double r = Math.cos((Math.PI/2+lat)/2);
return new double[] {r*Math.sin(lon), -r*Math.cos(lon)};
}
@ -87,8 +87,7 @@ public class Azimuthal {
public double[] project(double lat, double lon) {
if (lat < 0) lat = 0;
final double r = Math.PI*Math.cos(lat);
return new double[] { r*Math.sin(lon), -r*Math.cos(lon) };
return new double[] { Math.cos(lat)*Math.sin(lon), -Math.cos(lat)*Math.cos(lon) };
}
public double[] inverse(double x, double y) {
@ -108,7 +107,7 @@ public class Azimuthal {
public double[] project(double lat, double lon) {
if (lat <= 0) lat = 1e-5;
final double r = Math.tan(Math.PI/2 - lat);
final double r = Math.tan(Math.PI/2 - lat)/2;
return new double[] { r*Math.sin(lon), -r*Math.cos(lon)};
}
@ -134,7 +133,7 @@ public class Azimuthal {
public double[] project(double lat, double lon) {
if (Double.isInfinite(d)) return ORTHOGRAPHIC.project(lat, lon);
if (lat < Math.asin(1/d)) lat = Math.asin(1/d);
final double r = Math.abs(Math.cos(lat)/(d - Math.sin(lat))) * Math.PI*Math.sqrt(d*d-1);
final double r = Math.abs(Math.cos(lat)/(d - Math.sin(lat))) * Math.sqrt(d*d-1);
return new double[] { r*Math.sin(lon), -r*Math.cos(lon) };
}

View File

@ -63,9 +63,9 @@ public class Conic {
}
final double r = F*Math.pow(Math.tan(Math.PI/4+lat/2), -n);
final double s = reversed ? -1 : 1;
final double x = s*Math.PI*r*Math.sin(n*lon);
final double y = s*Math.PI*(r0 - r*Math.cos(n*lon));
if (d > 0) return new double[] {x, y+Math.PI*d/2};
final double x = s*r*Math.sin(n*lon);
final double y = s*(r0 - r*Math.cos(n*lon));
if (d > 0) return new double[] {x, y+d/2};
else if (d < 0) return new double[] {-d*x, -d*y};
else return new double[] {x, y};
}
@ -96,8 +96,10 @@ public class Conic {
private double m, n, s;
public void setSpecificParameters() {
if (lat1 == -lat2) //degenerates into Equirectangular; indicate with m=0
if (lat1 == -lat2) { //degenerates into Equirectangular; indicate with m=0
this.m = 0;
Cylindrical.EQUIRECTANGULAR.setParameters(lat1);
}
else if (lat1 == lat2) //equation becomes indeterminate; use limit
this.m = 1/(1/Math.tan(lat1)/Math.PI + lat1/Math.PI + .5);
else //normal conic
@ -124,9 +126,9 @@ public class Conic {
if (reversed) {
lat = -lat; lon = -lon;
}
final double r = Math.PI - m*lat - Math.PI*m/2;
final double r = 1 - m*lat/Math.PI - m/2;
final double x = s*r*Math.sin(n*lon);
final double y = Math.PI*(s-1/Math.max(aspectRatio,1)) - s*r*Math.cos(n*lon);
final double y = (s-1/Math.max(aspectRatio,1)) - s*r*Math.cos(n*lon);
if (reversed) return new double[] { -x, -y };
else return new double[] { x, y };
}
@ -155,8 +157,10 @@ public class Conic {
private double n, C, s;
public void setSpecificParameters() {
if (lat1 == -lat2) //degenerates into Equirectangular; indicate with n=0
if (lat1 == -lat2) { //degenerates into Equirectangular; indicate with n=0
this.n = 0;
Cylindrical.EQUAL_AREA.setParameters(lat1);
}
else //normal conic
this.n = (Math.sin(lat1) + Math.sin(lat2))/2;
@ -183,8 +187,8 @@ public class Conic {
lat = -lat; lon = -lon;
}
final double r = Math.sqrt(C - 2*n*Math.sin(lat))/n;
final double x = Math.PI*s*r*Math.sin(n*lon);
final double y = Math.PI*(s*Math.sqrt(C+2*n)/n-1/Math.max(aspectRatio,1)) - Math.PI*s*r*Math.cos(n*lon);
final double x = s*r*Math.sin(n*lon);
final double y = s*Math.sqrt(C+2*n)/n - 1/Math.max(aspectRatio,1) - s*r*Math.cos(n*lon);
if (reversed) return new double[] { -x, -y };
else return new double[] { x, y };
}

View File

@ -38,7 +38,7 @@ public class Cylindrical {
"Mercator", 1., 0b0111, Type.CYLINDRICAL, Property.CONFORMAL, "very popular") {
public double[] project(double lat, double lon) {
return new double[] {lon, Math.log(Math.tan(Math.PI/4+lat/2))};
return new double[] {lon/Math.PI, Math.log(Math.tan(Math.PI/4+lat/2))/Math.PI};
}
public double[] inverse(double x, double y) {
@ -52,7 +52,7 @@ public class Cylindrical {
Type.CYLINDRICAL, Property.EQUIDISTANT, null, "focused on the equator"){
public double[] project(double lat, double lon) {
return new double[] {lon, lat};
return new double[] {lon/Math.PI, lat/Math.PI};
}
public double[] inverse(double x, double y) {
@ -73,9 +73,9 @@ public class Cylindrical {
public double[] project(double lat, double lon) {
if (aspectRatio >= 1)
return new double[] {lon, lat/aspectRatio*2};
return new double[] {lon/Math.PI, 2*lat/Math.PI/aspectRatio};
else
return new double[] {lon*aspectRatio, 2*lat};
return new double[] {lon/Math.PI*aspectRatio, 2*lat/Math.PI};
}
public double[] inverse(double x, double y) {
@ -85,11 +85,11 @@ public class Cylindrical {
public static final Projection GALL_PETERS =
new Projection("Gall-Peters", 1.571, 0b1111,
new Projection("Gall-Peters", Math.PI/2, 0b1111,
Type.CYLINDRICAL, Property.EQUAL_AREA, "somewhat controversial") {
public double[] project(double lat, double lon) {
return new double[] {lon, Math.sin(lat)*Math.PI/1.571};
return new double[] {lon/Math.PI, Math.sin(lat)/aspectRatio};
}
public double[] inverse(double x, double y) {
@ -103,7 +103,7 @@ public class Cylindrical {
"with least distortion at 37.5\u00B0") {
public double[] project(double lat, double lon) {
return new double[] {lon, Math.sin(lat)*Math.PI/1.977};
return new double[] {lon/Math.PI, Math.sin(lat)/aspectRatio};
}
public double[] inverse(double x, double y) {
@ -117,7 +117,7 @@ public class Cylindrical {
"with least distortion at 30\u00B0") {
public double[] project(double lat, double lon) {
return new double[] {lon, Math.sin(lat)*Math.PI/2.356};
return new double[] {lon/Math.PI, Math.sin(lat)/aspectRatio};
}
public double[] inverse(double x, double y) {
@ -131,7 +131,7 @@ public class Cylindrical {
Property.EQUAL_AREA, null, "with least distortion along the equator") {
public double[] project(double lat, double lon) {
return new double[] {lon, Math.sin(lat)};
return new double[] {lon/Math.PI, Math.sin(lat)/aspectRatio};
}
public double[] inverse(double x, double y) {
@ -152,9 +152,9 @@ public class Cylindrical {
public double[] project(double lat, double lon) {
if (aspectRatio >= 1)
return new double[] {lon, Math.sin(lat)/aspectRatio*Math.PI};
return new double[] {lon/Math.PI, Math.sin(lat)/aspectRatio};
else
return new double[] {lon*aspectRatio, Math.sin(lat)*Math.PI};
return new double[] {lon*aspectRatio/Math.PI, Math.sin(lat)};
}
public double[] inverse(double x, double y) {
@ -168,7 +168,7 @@ public class Cylindrical {
"Gall Stereographic", 4/3., 0b1111, Type.CYLINDRICAL, Property.COMPROMISE) {
public double[] project(double lat, double lon) {
return new double[] {lon, Math.tan(lat/2)*(1+Math.sqrt(2))};
return new double[] {lon/Math.PI, Math.tan(lat/2)*(1+Math.sqrt(2))/Math.PI};
}
public double[] inverse(double x, double y) {
@ -182,7 +182,7 @@ public class Cylindrical {
Type.CYLINDRICAL, Property.COMPROMISE) {
public double[] project(double lat, double lon) {
return new double[] {lon, 1.25*Math.log(Math.tan(Math.PI/4+.8*lat/2))};
return new double[] {lon/Math.PI, 1.25*Math.log(Math.tan(Math.PI/4+.8*lat/2))/Math.PI};
}
public double[] inverse(double x, double y) {
@ -190,4 +190,3 @@ public class Cylindrical {
}
};
}

View File

@ -44,8 +44,8 @@ public class Misc {
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)};
2*Math.cos(lat)*Math.sin(lon/2)*a/Math.sin(a)/Math.PI,
Math.sin(lat)*a/Math.sin(a)/Math.PI};
}
public double[] inverse(double x, double y) {
@ -63,8 +63,8 @@ public class Misc {
public double[] project(double lat, double lon) {
return new double[] {
Math.PI*Math.cos(lat)*Math.sin(lon/2)/Math.sqrt(1+Math.cos(lat)*Math.cos(lon/2)),
Math.PI/2*Math.sin(lat)/Math.sqrt(1+Math.cos(lat)*Math.cos(lon/2)) };
Math.cos(lat)*Math.sin(lon/2)/Math.sqrt(1+Math.cos(lat)*Math.cos(lon/2)),
1/2.*Math.sin(lat)/Math.sqrt(1+Math.cos(lat)*Math.cos(lon/2)) };
}
public double[] inverse(double x, double y) {
@ -94,8 +94,8 @@ public class Misc {
final double P = G*(2/Math.sin(t) - 1);
final double Q = A*A + G;
return new double[] {
Math.PI*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.PI*Math.signum(lat)*(P*Q-A*Math.sqrt((A*A+1)*(P*P+A*A)-Q*Q))/(P*P+A*A)};
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) {
@ -123,12 +123,14 @@ public class Misc {
new Projection("Peirce Quincuncial", "A conformal projection that uses complex elliptic functions",
1., 0b1001, Type.OTHER, Property.CONFORMAL) {
private static final double K_RT_HALF = 1.854; //1.854 is approx K(sqrt(1/2))
public double[] project(double lat, double lon) {
final double alat = Math.abs(lat);
final double wMag = Math.tan(Math.PI/4-alat/2);
final Complex w = new Complex(wMag*Math.sin(lon), -wMag*Math.cos(lon)); //this Complex comes from Apache Commons
final Complex k = new Complex(Math.sqrt(0.5));
Complex z = Elliptic.F(w.acos(),k).multiply(Math.PI/1.854).subtract(Math.PI).negate();
Complex z = Elliptic.F(w.acos(),k).multiply(1/K_RT_HALF).subtract(Math.PI).negate();
if (z.isInfinite() || z.isNaN()) z = new Complex(0);
double x = z.getReal(), y = z.getImaginary();
@ -146,7 +148,7 @@ public class Misc {
}
public double[] inverse(double x, double y) {
mfc.field.Complex u = new mfc.field.Complex(1.854*(x+1), 1.854*y); //1.854 is approx K(sqrt(1/2)
mfc.field.Complex u = new mfc.field.Complex(K_RT_HALF*(x+1), K_RT_HALF*y);
mfc.field.Complex k = new mfc.field.Complex(Math.sqrt(0.5)); //the rest comes from some fancy complex calculus
mfc.field.Complex ans = Jacobi.cn(u, k);
double p = 2 * Math.atan(ans.abs());
@ -161,6 +163,7 @@ public class Misc {
new Projection("Guyou", "Peirce Quincuncial, rearranged a bit", 2., 0b1001,
Type.OTHER, Property.CONFORMAL) {
private final double K_RT_HALF = 1.854; //1.854 is approx K(sqrt(1/2))
private final double[] POLE = {0, -Math.PI/2, Math.PI/4};
public double[] project(double lat, double lon) {
@ -169,15 +172,15 @@ public class Misc {
final double wMag = Math.tan(Math.PI/4-alat/2);
final Complex w = new Complex(wMag*Math.sin(coords[1]), -wMag*Math.cos(coords[1]));
final Complex k = new Complex(Math.sqrt(0.5));
Complex z = Elliptic.F(w.acos(),k).multiply(new Complex(Math.PI/3.708,Math.PI/3.708)).subtract(new Complex(0,Math.PI/2));
Complex z = Elliptic.F(w.acos(),k).multiply(2*K_RT_HALF).subtract(new Complex(0,0.5));
if (z.isInfinite() || z.isNaN()) z = new Complex(0);
if (coords[0] < 0) z = z.conjugate().negate();
return new double[] {z.getReal(), z.getImaginary()};
}
public double[] inverse(double x, double y) {
mfc.field.Complex u = new mfc.field.Complex(1.8558*(x - y/2 - 0.5), 1.8558*(x + y/2 + 0.5)); // don't ask me where 3.7116 comes from
mfc.field.Complex k = new mfc.field.Complex(Math.sqrt(0.5)); // the rest comes from some fancy complex calculus
mfc.field.Complex u = new mfc.field.Complex(x-y/2-.5, x+y/2+.5).times(K_RT_HALF);
mfc.field.Complex k = new mfc.field.Complex(Math.sqrt(0.5)); //just some fancy complex calculus stuff
mfc.field.Complex ans = Jacobi.cn(u, k);
double p = 2 * Math.atan(ans.abs());
double theta = ans.arg();
@ -205,10 +208,12 @@ public class Misc {
}
public double[] project(double lat, double lon) {
final double z = Math.acos(Math.sin(phi0)*Math.sin(lat) + Math.cos(phi0)*Math.cos(lat)*Math.cos(lon-lam0));
final double K = z/Math.sin(z);
final double z = Math.acos(Math.sin(phi0)*Math.sin(lat) +
Math.cos(phi0)*Math.cos(lat)*Math.cos(lon-lam0));
final double K = z/Math.sin(z)/Math.PI;
final double x = K*Math.cos(phi0)*Math.sin(lon-lam0);
final double y = -K*(Math.sin(phi0)*Math.cos(lat) - Math.cos(phi0)*Math.sin(lat)*Math.cos(lon-lam0));
final double y = -K*(Math.sin(phi0)*Math.cos(lat) -
Math.cos(phi0)*Math.sin(lat)*Math.cos(lon-lam0));
if (Math.cos(lon-lam0) < 0)
return new double[] {-x, -y};
else
@ -269,8 +274,8 @@ public class Misc {
Math.tan(lat1)*Math.sin(lon0-lon2) +
Math.tan(lat2)*Math.sin(lon1-lon0));
return new double[] {
(d1*d1-d2*d2)/(2*D) * Math.PI/a,
s*Math.sqrt(d1*d1 - Math.pow((d1*d1-d2*d2+D*D)/(2*D), 2)) * Math.PI/a };
(d1*d1-d2*d2)/(2*D)/a,
s*Math.sqrt(d1*d1 - Math.pow((d1*d1-d2*d2+D*D)/(2*D), 2))/a };
}
public double[] inverse(double x, double y, double[] pole) {

View File

@ -29,8 +29,8 @@ import maps.Projection.Property;
import maps.Projection.Type;
/**
* All of the projections I invented, save the tetrahedral ones,
* because those have so much in common with other tetrahedral projections.
* All of the projections I invented, save the tetrahedral ones, because
* those have so much in common with other tetrahedral projections.
*
* @author jkunimune
*/
@ -44,8 +44,7 @@ public class MyProjections {
public double[] project(double lat, double lon) {
final double p = 1/2.0+lat/Math.PI;
final double fp = 1 - 0.1*p - 0.9*Math.pow(p,7);
final double r = Math.PI*fp;
return new double[] { r*Math.sin(lon), -r*Math.cos(lon) };
return new double[] { fp*Math.sin(lon), -fp*Math.cos(lon) };
}
public double[] inverse(double x, double y) {
@ -67,13 +66,13 @@ public class MyProjections {
public double[] project(double lat, double lon) {
final double wMag = Math.tan(Math.PI/4-lat/2);
final Complex w = new Complex(wMag*Math.sin(lon), -wMag*Math.cos(lon));
Complex z = w.asin();
Complex z = w.asin().divide(3);
if (z.isInfinite() || z.isNaN()) z = new Complex(0);
return new double[] { z.getReal(), z.getImaginary() };
}
public double[] inverse(double x, double y) {
Complex z = new Complex((x+1)*Math.PI, y*Math.PI);
Complex z = new Complex(3*x+3, y*3);
Complex ans = z.sin();
double p = 2 * Math.atan(ans.abs());
double theta = ans.getArgument() - Math.PI/2;
@ -92,8 +91,8 @@ public class MyProjections {
final double a = Math.PI - Math.acos(Math.cos(lat)*Math.cos(lon/2));
final double b = Math.acos(Math.sin(lat)/Math.sin(a));
return new double[] {
Math.PI*Math.signum(lon)/Math.tan(a/2)*Math.sin(b),
Math.PI/2/Math.tan(a/2)*Math.cos(b) };
Math.signum(lon)/Math.tan(a/2)*Math.sin(b),
Math.cos(b)/Math.tan(a/2)/2 };
}
public double[] inverse(double x, double y) {
@ -123,8 +122,8 @@ public class MyProjections {
public double[] project(double lat, double lon) {
final double ynorm = (1-Math.pow(1-Math.abs(lat/(Math.PI/2)), n));
return new double[] {
Math.pow(1 - Math.pow(ynorm, k),1/k)*lon,
ynorm*Math.PI/2/Math.sqrt(n)*a*Math.signum(lat) };
Math.pow(1 - Math.pow(ynorm, k),1/k)*lon/Math.PI,
ynorm/2/Math.sqrt(n)*a*Math.signum(lat) };
}
public double[] inverse(double x, double y) {
@ -158,7 +157,7 @@ public class MyProjections {
final double d2 = Math.acos(
Math.sin(lat)*Math.cos(theta) + Math.cos(lat)*Math.sin(theta)*Math.cos(lon));
final double k = Math.signum(lon)*Math.sqrt(Math.tan(theta)/theta);
final double s = Math.PI/(Math.PI-theta/2);
final double s = 1/(Math.PI-theta/2);
return new double[] {
s*k*Math.sqrt(d1*d1 - Math.pow((d1*d1-d2*d2+4*theta*theta)/(4*theta), 2)),
s*(d2*d2-d1*d1)/(4*theta) };

View File

@ -24,6 +24,7 @@
package maps;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.DoubleConsumer;

View File

@ -38,7 +38,7 @@ public class Pseudocylindrical {
2., 0b1111, Type.PSEUDOCYLINDRICAL, Property.EQUAL_AREA) {
public double[] project(double lat, double lon) {
return new double[] { Math.cos(lat)*lon, lat };
return new double[] { Math.cos(lat)*lon/Math.PI, lat/Math.PI };
}
public double[] inverse(double x, double y) {
@ -56,7 +56,7 @@ public class Pseudocylindrical {
for (int i = 0; i < 10; i ++)
tht -= (2*tht+Math.sin(2*tht)-Math.PI*Math.sin(lat))/
(2+2*Math.cos(2*tht));
return new double[] { lon*Math.cos(tht), Math.PI/2*Math.sin(tht) };
return new double[] { lon*Math.cos(tht)/Math.PI, Math.sin(tht)/2 };
}
public double[] inverse(double x, double y) {
@ -81,15 +81,17 @@ public class Pseudocylindrical {
final int lemNum = (int)Math.floor(lon/LEM_SPAN);
final double dl = (lon+2*Math.PI) % LEM_SPAN - LEM_SPAN/2;
return new double[] {
Math.asin(Math.cos(lat)*Math.sin(dl)) + (lemNum+.5)*LEM_SPAN,
Math.asin(Math.sin(lat)/Math.sqrt(1-Math.pow(Math.cos(lat)*Math.sin(dl), 2))) };
Math.asin(Math.cos(lat)*Math.sin(dl))/Math.PI + (lemNum+.5)*LEM_WIDTH,
Math.asin(Math.sin(lat)/Math.sqrt(1-Math.pow(Math.cos(lat)*Math.sin(dl), 2)))
/Math.PI };
}
public double[] inverse(double x, double y) {
final int lemNum = (int)Math.floor(x/LEM_WIDTH);
final double dx = (x+2) % LEM_WIDTH * Math.PI - LEM_SPAN/2;
y = y*Math.PI/2;
final double dl = Math.asin(Math.sin(dx)/Math.sqrt(1-Math.pow(Math.cos(dx)*Math.sin(y), 2)));
final double dl = Math.asin(
Math.sin(dx)/Math.sqrt(1-Math.pow(Math.cos(dx)*Math.sin(y), 2)));
if (Math.abs(dl) > LEM_SPAN/2)
return null;
else

View File

@ -55,8 +55,8 @@ public class Robinson {
public double[] project(double lat, double lon) {
return new double[] {
lon*smartInterpolate(Math.toDegrees(lat), TABLE[0], TABLE[1], ORDER),
Math.PI/2*smartInterpolate(Math.toDegrees(lat), TABLE[0], TABLE[2], ORDER) };
lon/Math.PI*smartInterpolate(Math.toDegrees(lat), TABLE[0], TABLE[1], ORDER),
.5*smartInterpolate(Math.toDegrees(lat), TABLE[0], TABLE[2], ORDER) };
}
public double[] inverse(double x, double y) {

View File

@ -41,7 +41,7 @@ public class Tetrahedral {
final mfc.field.Complex z = mfc.field.Complex.fromPolar(
Math.pow(2, 5/6.)*Math.tan(Math.PI/4-lat/2), lon);
final mfc.field.Complex w = Dixon.invFunc(z);
return new double[] { w.abs()*1.186, w.arg() };
return new double[] { w.abs()*.3775, w.arg() };
}
public double[] innerInverse(double r, double tht) {
@ -62,7 +62,7 @@ public class Tetrahedral {
public double[] innerProject(double lat, double lon) {
final double tht = lon - Math.floor(lon/(2*Math.PI/3))*(2*Math.PI/3) - Math.PI/3;
return new double[] {
Math.atan(1/Math.tan(lat)*Math.cos(tht))/Math.cos(tht)*Math.PI/3/Math.atan(Math.sqrt(2)),
Math.atan(1/Math.tan(lat)*Math.cos(tht))/Math.cos(tht)/3/Math.atan(Math.sqrt(2)),
lon };
}
@ -98,7 +98,7 @@ public class Tetrahedral {
final double rmax = .5/Math.cos(thtP); //the max normalized radius of this triangle (in the plane)
final double rtgf = Math.atan(1/Math.tan(lat)*Math.cos(tht))/Math.atan(Math.sqrt(2))*rmax;
return new double[] {
(1 - Math.pow(1-rtgf,kRad))/(1 - Math.pow(1-rmax,kRad))*rmax*2*Math.PI/3,
(1 - Math.pow(1-rtgf,kRad))/(1 - Math.pow(1-rmax,kRad))*rmax*2/3,
thtP + t0 };
}
@ -140,7 +140,7 @@ public class Tetrahedral {
else rmax = .75 - 1.5972774*Math.pow(Math.PI/3-Math.abs(thtP),2)/2;
final double rtgf = Math.atan(1/Math.tan(lat)*Math.cos(tht))/Math.atan(Math.sqrt(2))*rmax; //normalized tetragraph radius
return new double[] {
(1 - Math.pow(1-rtgf,kRad))/(1 - Math.pow(1-rmax,kRad))*rmax*2*Math.PI/3,
(1 - Math.pow(1-rtgf,kRad))/(1 - Math.pow(1-rmax,kRad))*rmax*2/3,
thtP + t0
};
}
@ -185,7 +185,7 @@ public class Tetrahedral {
final double rmax = Math.min(.5/Math.cos(thtP), .75/Math.cos(Math.PI/3-Math.abs(thtP))); //the max normalized radius of this triangle (in the plane)
final double rtgf = Math.atan(1/Math.tan(lat)*Math.cos(tht))/Math.atan(Math.sqrt(2))*rmax; //normalized tetragraph radius
return new double[] {
(1 - Math.pow(1-rtgf,kRad))/(1 - Math.pow(1-rmax,kRad))*rmax*2*Math.PI/3,
(1 - Math.pow(1-rtgf,kRad))/(1 - Math.pow(1-rmax,kRad))*rmax*2/3,
thtP + t0 };
}
@ -322,23 +322,23 @@ public class Tetrahedral {
case 0:
if (Math.sin(lon) < 0)
return new double[]
{-2*Math.PI/3 + rtht[0]*Math.sin(rtht[1]-Math.PI/6), -Math.PI/Math.sqrt(3) - rtht[0]*Math.cos(rtht[1]-Math.PI/6)}; // lower left
{-2/3. + rtht[0]*Math.sin(rtht[1]-Math.PI/6), -1/Math.sqrt(3) - rtht[0]*Math.cos(rtht[1]-Math.PI/6)}; // lower left
else
return new double[]
{2*Math.PI/3 - rtht[0]*Math.sin(rtht[1]-Math.PI/6), -Math.PI/Math.sqrt(3) + rtht[0]*Math.cos(rtht[1]-Math.PI/6)}; // lower right
{2/3. - rtht[0]*Math.sin(rtht[1]-Math.PI/6), -1/Math.sqrt(3) + rtht[0]*Math.cos(rtht[1]-Math.PI/6)}; // lower right
case 1:
if (Math.sin(lon) < 0)
return new double[]
{-2*Math.PI/3 + rtht[0]*Math.sin(rtht[1]-Math.PI/6), Math.PI/Math.sqrt(3) - rtht[0]*Math.cos(rtht[1]-Math.PI/6)}; // upper left
{-2/3. + rtht[0]*Math.sin(rtht[1]-Math.PI/6), 1/Math.sqrt(3) - rtht[0]*Math.cos(rtht[1]-Math.PI/6)}; // upper left
else
return new double[]
{2*Math.PI/3 - rtht[0]*Math.sin(rtht[1]-Math.PI/6), Math.PI/Math.sqrt(3) + rtht[0]*Math.cos(rtht[1]-Math.PI/6)}; // upper right
{2/3. - rtht[0]*Math.sin(rtht[1]-Math.PI/6), 1/Math.sqrt(3) + rtht[0]*Math.cos(rtht[1]-Math.PI/6)}; // upper right
case 2:
return new double[]
{Math.PI/3 + rtht[0]*Math.cos(rtht[1]), rtht[0]*Math.sin(rtht[1])}; // right
{1/3. + rtht[0]*Math.cos(rtht[1]), rtht[0]*Math.sin(rtht[1])}; // right
case 3:
return new double[]
{-Math.PI/3 - rtht[0]*Math.cos(rtht[1]), -rtht[0]*Math.sin(rtht[1])}; // left
{-1/3. - rtht[0]*Math.cos(rtht[1]), -rtht[0]*Math.sin(rtht[1])}; // left
default:
return null;
}

View File

@ -70,8 +70,8 @@ public class Tobler {
y = Math2.linInterp(z0, Z[-i-2], Z[-i-1], -i-2, -i-1)/
(Z.length-1.);
return new double[] {
lon * Math.abs(alpha + (1-alpha)*hyperEllipse(y)),
y * Math.signum(lat)/aspectRatio*Math.PI };
lon * Math.abs(alpha + (1-alpha)*hyperEllipse(y))/Math.PI,
y * Math.signum(lat)/aspectRatio };
}
public double[] inverse(double x, double y) {

View File

@ -27,14 +27,15 @@ import maps.Projection.Property;
import maps.Projection.Type;
/**
* TODO: Write description
* A polyhedral projection that's so interrupted it can get away with gnomonic
*
* @author jkunimune
*/
public class Waterman {
public static final Projection WATERMAN =
new Projection("Waterman Butterfly", "An aesthetically pleasing octohedral map arrangement",
new Projection(
"Waterman Butterfly", "An aesthetically pleasing octohedral map arrangement",
0, 0b1110, Type.POLYHEDRAL, Property.COMPROMISE) {
public double[] project(double lat, double lon) {

View File

@ -36,10 +36,10 @@ import utils.NumericalAnalysis;
*
* Ipb&uuml;ker, Cengizhan; Bildirici, I.&Ouml;ztug (2002). "A General Algorithm for the
* Inverse Transformation of Map Projections Using Jacobian Matrices".
* Proceedings of the Third International Symposium Mathematical &
* Computational Applications. Third International Symposium Mathematical &
* Proceedings of the Third International Symposium Mathematical &amp;
* Computational Applications. Third International Symposium Mathematical &amp;
* Computational Applications September 4-6, 2002. Konya, Turkey. Selcuk,
* Turkey. pp. 175&mdash;182. Archived from the original on 20 October 2014.
* Turkey. pp. 175-182. Archived from the original on 20 October 2014.
*
* @author jkunimune
*/
@ -60,8 +60,7 @@ public final class WinkelTripel {
public double[] inverse(double x, double y) {
return NumericalAnalysis.newtonRaphsonApproximation(
x*Math.PI*aspectRatio, y*Math.PI,
y*Math.PI/2, x*Math.PI*(1 + Math.cos(y*Math.PI/2))/2,
x*aspectRatio, y, y/2, x*(1 + Math.cos(y*Math.PI/2))/2,
this::f1pX, this::f2pY,
this::df1dphi, this::df1dlam, this::df2dphi, this::df2dlam, .002);
}
@ -69,13 +68,14 @@ public final class WinkelTripel {
private double f1pX(double phi, double lam) {
final double d = D(phi,lam);
final double c = C(phi,lam);
return 2*d/Math.sqrt(c)*Math.cos(phi)*Math.sin(lam/2) + lam*(aspectRatio-1);
return 2/Math.PI*d/Math.sqrt(c)*Math.cos(phi)*Math.sin(lam/2)
+ lam/Math.PI*(aspectRatio-1);
}
private double f2pY(double phi, double lam) {
final double d = D(phi,lam);
final double c = C(phi,lam);
return d/Math.sqrt(c)*Math.sin(phi) + phi;
return d/Math.sqrt(c)*Math.sin(phi)/Math.PI + phi/Math.PI;
}
private double df1dphi(double phi, double lam) {

View File

@ -30,8 +30,8 @@ import mfc.field.Complex;
* All the algorithms here came directly from L.P. Lee's paper,
* "Some Conformal Projections Based on Elliptic Functions"
*
* Lee, L. P. <EFBFBD>Some Conformal Projections Based on Elliptic Functions.<EFBFBD>
* Geographical Review, vol. 55, no. 4, 1965, pp. 563<EFBFBD>580. JSTOR,
* Lee, L. P. "Some Conformal Projections Based on Elliptic Functions."
* Geographical Review, vol. 55, no. 4, 1965, pp. 563-580. JSTOR,
* www.jstor.org/stable/212415.
*
* @author jkunimune

View File

@ -23,8 +23,6 @@
*/
package utils;
import java.awt.image.BufferedImage;
/**
* A collection of (currently just one) methods for dealing with images and coordinates
*
@ -32,22 +30,6 @@ import java.awt.image.BufferedImage;
*/
public class ImageUtils {
public static int getArgb(double[] coords, BufferedImage input) { // returns the color of any coordinate on earth
if (coords == null) return 0;
double x = 1/2.0 + coords[1]/(2*Math.PI);
x = (x - Math.floor(x)) * input.getWidth();
double y = input.getHeight()*(.5 - coords[0]/Math.PI);
if (y < 0)
y = 0;
else if (y >= input.getHeight())
y = input.getHeight() - 1;
return (0xFF000000) | input.getRGB((int) x, (int) y);
}
public static final int blend(int[] colors) {
return blend(colors, 2.2);
}

59
src/utils/PixelMap.java Normal file
View File

@ -0,0 +1,59 @@
/**
* 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;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
/**
* An input equirectangular map based on a raster image file
*
* @author jkunimune
*/
public class PixelMap {
private BufferedImage pixels;
public PixelMap(File f) throws IOException {
pixels = ImageIO.read(f);
}
public int getArgb(double lat, double lon) {
double x = 1/2.0 + lon/(2*Math.PI);
x = (x - Math.floor(x)) * pixels.getWidth();
double y = pixels.getHeight()*(.5 - lat/Math.PI);
if (y < 0)
y = 0;
else if (y >= pixels.getHeight())
y = pixels.getHeight() - 1;
return (0xFF000000) | pixels.getRGB((int) x, (int) y);
}
}

334
src/utils/SVGMap.java Normal file
View File

@ -0,0 +1,334 @@
/**
* 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;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.function.DoubleConsumer;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* An input equirectangular map based on an SVG file
*
* @author jkunimune
*/
public class SVGMap implements Iterable<SVGMap.Path> {
public double[] NULL_TRANSFORM = {1, 1, 0, 0};
private List<Path> paths; //the set of closed curves in this image
private List<String> format; //the stuff that goes between the curve descriptions, probably important for something.
private int minX, minY, width, height; //the SVG viewBox
private int size; //the total number of path commands, for optimization purposes
public SVGMap(File file) throws IOException, SAXException, ParserConfigurationException {
paths = new LinkedList<Path>();
format = new LinkedList<String>();
final SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
final DefaultHandler handler = new DefaultHandler() {
private Stack<double[]> transformStack = new Stack<double[]>();
private String currentFormatString = "";
@Override
public InputSource resolveEntity(String publicId, String systemId) {
return new InputSource(new StringReader("")); //ignore all external references - we don't need them
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
currentFormatString += "<"+qName;
if (qName.equals("path"))
parsePath(attributes.getValue("d"));
if (qName.equals("svg"))
parseViewBox(attributes.getValue("viewBox"));
for (int i = 0; i < attributes.getLength(); i ++)
if (!attributes.getQName(i).equals("d") && //d is already taken care of
!attributes.getQName(i).equals("transform")) //there shall be no transforms in the final output
currentFormatString +=
" "+attributes.getQName(i)+"="+attributes.getValue(i);
currentFormatString += ">\n";
if (attributes.getIndex("transform") >= 0)
parseTransform(attributes.getValue("transform"));
else
parseTransform();
}
@Override
public void endElement(String uri, String localName, String qName) {
currentFormatString += "</"+qName+">";
transformStack.pop();
}
@Override
public void characters(char[] ch, int start, int length) {
for (int i = 0; i < length; i ++)
currentFormatString += ch[start+i];
}
private void parseViewBox(String viewBox) {
String[] values = viewBox.split("\\s", 4);
minX = Integer.parseInt(values[0]);
minY = Integer.parseInt(values[1]);
width = Integer.parseInt(values[2]);
height = Integer.parseInt(values[3]);
}
private void parseTransform() {
if (transformStack.isEmpty())
transformStack.push(NULL_TRANSFORM);
else
transformStack.push(transformStack.peek());
}
private void parseTransform(String transString) {
double xScale = 1, yScale = 1, xTrans = 0, yTrans = 0;
int i;
while ((i = transString.indexOf('(')) >= 0) {
int j = transString.indexOf(')');
String type = transString.substring(0, i).trim();
String argString = transString.substring(i+1, j);
String[] args = argString.split("[,\\s]+");
if (type.equals("matrix")) {
xScale = Double.parseDouble(args[0]);
yScale = Double.parseDouble(args[3]);
xTrans = Double.parseDouble(args[4]);
yTrans = Double.parseDouble(args[5]);
}
else if (type.equals("translate")) {
if (args.length == 1) {
xTrans = yTrans = Double.parseDouble(args[0]);
}
else {
xTrans = Double.parseDouble(args[0]);
yTrans = Double.parseDouble(args[1]);
}
}
else if (type.equals("scale")) {
if (args.length == 1) {
xScale = yScale = Double.parseDouble(args[0]);
}
else {
xScale = Double.parseDouble(args[0]);
yScale = Double.parseDouble(args[1]);
}
} //I'm not worrying about shear and rotation because I don't want to
transString = transString.substring(j+1);
}
transformStack.push(new double[] {xScale, yScale, xTrans, yTrans});
}
private void parsePath(String d) {
currentFormatString += " d=\"";
format.add(currentFormatString);
paths.add(new Path(d, transformStack.peek(), minX, minY, width, height));
currentFormatString = "\"";
size += paths.get(paths.size()-1).length();
}
};
parser.parse(new BufferedInputStream(new FileInputStream(file)), handler);
}
public int size() {
return this.size;
}
public int numCurves() {
return paths.size();
}
@Override
public Iterator<Path> iterator() {
return paths.iterator();
}
public void save(Iterable<Path> paths, File file, DoubleConsumer tracker) throws IOException {
BufferedWriter out = new BufferedWriter(new FileWriter(file));
final Iterator<String> formatIterator = format.iterator();
final Iterator<Path> curveIterator = paths.iterator();
while (curveIterator.hasNext()) {
out.write(formatIterator.next());
out.write(curveIterator.next().toString(
minX, minY, Math.min(width, height), Math.min(width, height)));
// tracker.accept((double)i/curves.size());
}
out.write(formatIterator.next());
out.close();
}
public static boolean isLetter(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
/**
* An svg path String, stored in a modifiable form
* @author jkunimune
*/
public static class Path implements Iterable<Command> {
final private List<Command> commands;
public Path() {
commands = new ArrayList<Command>();
}
public Path(String d, double vbWidth, double vbHeight) {
this(d, new double[] {1,1,0,0}, 0, 0, vbWidth, vbHeight);
}
public Path(String d, double[] transform,
double vbMinX, double vbMinY, double vbWidth, double vbHeight) {
commands = new ArrayList<Command>();
int i = 0;
double[] last = new double[] {0,0}; //for relative coordinates
while (i < d.length()) {
char type = d.charAt(i);
String argString = "";
while (i+1 < d.length() && !isLetter(d.charAt(i+1))) {
i ++;
argString += d.charAt(i);
}
String[] argStrings;
if (argString.isEmpty()) argStrings = new String[0];
else argStrings = argString.trim().split("[\\s,]+");
double[] args = new double[argStrings.length];
for (int j = 0; j < args.length; j ++) {
args[j] = Double.parseDouble(argStrings[j]); //parse the coordinate
if (j%2 == 0) {
args[j] = args[j]*transform[0] + transform[2]; //apply the transformation
args[j] = Math2.linInterp(args[j], vbMinX, vbMinX+vbWidth,
-Math.PI, Math.PI); //scale to radians
}
else {
args[j] = args[j]*transform[1] + transform[3];
args[j] = Math2.linInterp(args[j], vbMinY+vbHeight, vbMinY, //keep in mind that these are paired longitude-latitude
-Math.PI/2, Math.PI/2); //not latitude-longitude, as they are elsewhere
}
}
if (type >= 'a') { //make all letters uppercase
type -= 32;
for (int j = 0; j < args.length; j ++)
args[j] += last[j%2];
}
if (args.length >= 2) {
last[0] = args[args.length-2];
last[1] = args[args.length-1];
}
commands.add(new Command(type, args));
i ++;
}
}
public Iterator<Command> iterator() {
return commands.iterator();
}
public int length() {
return commands.size();
}
public boolean add(Command c) {
return commands.add(c);
}
public String toString() {
return this.toString(-1, -1, 2, 2);
}
public String toString(double minX, double minY, double width, double height) {
String s = "";
for (Command c: commands)
s += c.toString(minX, minY, width, height)+" ";
return s;
}
}
/**
* An SVG command, like line or bezier curve or whatever
* @author jkunimune
*/
public static class Command {
final public char type; //M, L, C, etc. This will never be lowercase
final public double[] args; //the absolute coordinates that go with it
public Command(char type, double[] args) {
this.type = type;
this.args = args;
}
public String toString() {
return this.toString(-1, -1, 2, 2);
}
public String toString(double minX, double minY, double width, double height) {
String s = type+" ";
for (int i = 0; i < args.length; i ++) {
if (i%2 == 0)
s += Math2.linInterp(args[0], -1, 1, minX, minX+width)+",";
else
s += Math2.linInterp(args[1], -1, 1, minY+height, minY)+",";
}
return s.substring(0, s.length()-1);
}
}
}