feat: Initial commit
This commit is contained in:
commit
4f7687eaab
15 changed files with 117756 additions and 0 deletions
.gitignore
data
pom.xmlsrc
main
java/ch/inf3/horizoncut
cli
data
resources
test/java/ch/inf3/horizoncut/data
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
target
|
||||
|
||||
.idea
|
||||
/nbproject/
|
||||
nb-configuration.xml
|
109306
data/osm-overpass-peaks.geojson
Normal file
109306
data/osm-overpass-peaks.geojson
Normal file
File diff suppressed because it is too large
Load diff
7358
data/osm-overpass-villages.geojson
Normal file
7358
data/osm-overpass-villages.geojson
Normal file
File diff suppressed because it is too large
Load diff
85
pom.xml
Normal file
85
pom.xml
Normal file
|
@ -0,0 +1,85 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>ch.inf3</groupId>
|
||||
<artifactId>horizoncut</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.6.1</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-dependencies</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/${project.build.finalName}.lib</outputDirectory>
|
||||
<includeScope>compile</includeScope>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.1.2</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addClasspath>true</addClasspath>
|
||||
<classpathPrefix>${project.build.finalName}.lib/</classpathPrefix>
|
||||
<mainClass>ch.inf3.horizoncut.cli.Cli</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>info.picocli</groupId>
|
||||
<artifactId>picocli</artifactId>
|
||||
<version>4.6.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>1.7.28</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.8.7</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
266
src/main/java/ch/inf3/horizoncut/cli/Cli.java
Normal file
266
src/main/java/ch/inf3/horizoncut/cli/Cli.java
Normal file
|
@ -0,0 +1,266 @@
|
|||
package ch.inf3.horizoncut.cli;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import ch.inf3.horizoncut.data.DisplayCalculator;
|
||||
import ch.inf3.horizoncut.data.DistanceLayer;
|
||||
import ch.inf3.horizoncut.data.GeoJsonExport;
|
||||
import ch.inf3.horizoncut.data.Peak;
|
||||
import ch.inf3.horizoncut.data.Position;
|
||||
import ch.inf3.horizoncut.data.Tile;
|
||||
import ch.inf3.horizoncut.data.TileMap;
|
||||
import ch.inf3.horizoncut.data.TileReader;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.imageio.ImageIO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Cli {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Cli.class);
|
||||
|
||||
private static final int SLICE_DISTANCE = 100;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
Position c = new Position(46.326570, 7.466880);
|
||||
TileMap tileMap = new TileReader("/home/valdor/Downloads/SRTM1/").readHGTFiles(c, 2);
|
||||
|
||||
final int width = 8000;
|
||||
final int height = 2000;
|
||||
|
||||
Tile atOrigin = tileMap.getByLatLon(c.gridLat, c.gridLon);
|
||||
double eyeHeight = atOrigin.elevation(c.row, c.col);
|
||||
LOG.info("Eye at {} is at {}", c.getLatLon(), eyeHeight);
|
||||
|
||||
double minHorizAngle = 135; //107; // 135 ~Brunegghorn
|
||||
double maxHorizAngle = 226; //226; // 197 ~Combin de Valsorey
|
||||
|
||||
DisplayCalculator dc = new DisplayCalculator(width, height, minHorizAngle, maxHorizAngle, eyeHeight);
|
||||
|
||||
int maxDistance = 100_000;
|
||||
int minDistance = 500;
|
||||
|
||||
LOG.info("Distances of {} to {}", minDistance, maxDistance);
|
||||
|
||||
BufferedImage img = new BufferedImage(width, height + 1, BufferedImage.TYPE_INT_RGB);
|
||||
|
||||
Collection<DistanceLayer> layers = new ArrayList<>(maxDistance);
|
||||
for (int d = maxDistance; d > minDistance; d -= SLICE_DISTANCE) {
|
||||
final int cDist = d;
|
||||
double[] data = computeAtDistance(c, tileMap, cDist, dc);
|
||||
layers.add(new DistanceLayer(d - SLICE_DISTANCE, d, data));
|
||||
}
|
||||
|
||||
// We compute the horizon and delete any layer that is behind the horizon
|
||||
// and is not used at all for the landscape.
|
||||
// For this, we are using the fact that we computed the layers starting
|
||||
// with the farthest ones in the build loop.
|
||||
DistanceLayer horizonLayer = layers.stream()
|
||||
.reduce((a, b) -> a.mergeWith(b))
|
||||
.get();
|
||||
|
||||
layers = layers.stream()
|
||||
.dropWhile(l -> horizonLayer.isLower(l))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
LOG.info("There are {} layers remaining after horizon cleanup", layers.size());
|
||||
|
||||
// We recompute the min and max distances after the cleanup of layers that
|
||||
// are not useful for the display.
|
||||
maxDistance = layers.stream()
|
||||
.map(l -> l.farDistance)
|
||||
.max(Integer::compareTo)
|
||||
.get();
|
||||
|
||||
minDistance = layers.stream()
|
||||
.map(l -> l.nearDistance)
|
||||
.min(Integer::compareTo)
|
||||
.get();
|
||||
|
||||
LOG.info("Distances of {} to {}", minDistance, maxDistance);
|
||||
|
||||
Collection<DistanceLayer> nLayers = layers.stream()
|
||||
.collect(Collectors.groupingBy(l -> l.nearDistance / 1, Collectors.reducing((a, b) -> a.mergeWith(b))))
|
||||
.values().stream()
|
||||
.flatMap(o -> o.stream())
|
||||
.sorted((a, b) -> b.compareTo(a))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
nLayers = DistanceLayer.filterInvisible(nLayers);
|
||||
|
||||
LOG.info("There are {} layers remaining after layer merges", nLayers.size());
|
||||
|
||||
// We dont want to compte the min angle to avoid looking at the ground
|
||||
// just before the position ...
|
||||
double minVertAngle = 80;
|
||||
double maxVertAngle = nLayers.stream()
|
||||
.map(l -> l.getMax())
|
||||
.max(Double::compareTo)
|
||||
.get();
|
||||
|
||||
maxVertAngle += 2;
|
||||
|
||||
LOG.info("Displaying angles are {} - {}", minVertAngle, maxVertAngle);
|
||||
|
||||
// We loop in descending order to avoid writing further points on top
|
||||
// of nearer points.
|
||||
for (DistanceLayer layer : nLayers) {
|
||||
float dPos = (float) ((maxDistance - layer.nearDistance) / (maxDistance * 1.0f));
|
||||
int color = Color.HSBtoRGB(dPos * 0.5f, 1.0f, 1.0f);
|
||||
|
||||
for (int x = 0; x < width; x++) {
|
||||
double visibleAngle = layer.data[x];
|
||||
double pos = (visibleAngle - minVertAngle) * (height / (maxVertAngle - minVertAngle));
|
||||
|
||||
int y = (int) (height - pos - 1);
|
||||
|
||||
if (y <= 0 || y > height) {
|
||||
y = height;
|
||||
}
|
||||
|
||||
img.setRGB(x, y, color);
|
||||
}
|
||||
}
|
||||
|
||||
Graphics2D g = (Graphics2D) img.getGraphics();
|
||||
Font font = new Font("Arial", Font.BOLD, 12);
|
||||
g.setColor(Color.white);
|
||||
g.setFont(font);
|
||||
|
||||
Collection<Peak> peaks = readFeatures("data/osm-overpass-peaks.geojson");
|
||||
Collection<Peak> villages = readFeatures("data/osm-overpass-villages.geojson");
|
||||
for (int i = 0; i < 2; i++) {
|
||||
Collection<Peak> feature;
|
||||
int txtPosition;
|
||||
boolean displayInvisible;
|
||||
|
||||
if (i == 0) {
|
||||
feature = peaks;
|
||||
txtPosition = 150;
|
||||
displayInvisible = false;
|
||||
} else {
|
||||
feature = villages;
|
||||
txtPosition = height - 50;
|
||||
displayInvisible = true;
|
||||
}
|
||||
|
||||
for (Peak peak : feature) {
|
||||
double peakBearing = DisplayCalculator.radToDeg(Position.bearing(c, peak.position));
|
||||
if (!dc.isVisible(peakBearing)) {
|
||||
LOG.info("Peak {} is not visible at bearing {}", peak.name, peakBearing);
|
||||
continue;
|
||||
}
|
||||
|
||||
int peakDistance = c.distanceToMeters(peak.position);
|
||||
|
||||
Tile atPos = tileMap.getByLatLon(peak.position.gridLat, peak.position.gridLon);
|
||||
double elevation = atPos.elevation(peak.position.row, peak.position.col);
|
||||
|
||||
double visibleAngle = dc.calculateVisibilityAngle(elevation, peakDistance);
|
||||
|
||||
int x = dc.getXAtBearing(peakBearing);
|
||||
|
||||
boolean isVisible = DistanceLayer.isVisible(layers, peakDistance, x, visibleAngle);
|
||||
if (!isVisible && !displayInvisible) {
|
||||
LOG.info("Peak {} is not visible, hidden behind terrain.", peak.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (visibleAngle < minVertAngle) {
|
||||
LOG.info("Peak {} is not visible, too low.", peak.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
double pos = (visibleAngle - minVertAngle) * (height / (maxVertAngle - minVertAngle));
|
||||
int y = (int) (height - pos - 1);
|
||||
|
||||
LOG.info("Peak {} is at bearing {} ({}x{})", peak.name, peakBearing, x, y);
|
||||
|
||||
String title = String.format("%s", peak.name);
|
||||
|
||||
AffineTransform old = g.getTransform();
|
||||
g.rotate(Math.toRadians(-45), x, txtPosition);
|
||||
g.drawString(title, x, txtPosition);
|
||||
g.setTransform(old);
|
||||
|
||||
g.drawLine(x, y, x, txtPosition);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < 360; i += 5) {
|
||||
int x = dc.getXAtBearing(i);
|
||||
|
||||
g.drawLine(x, 0, x, height);
|
||||
g.drawString(i + "", x, 20);
|
||||
}
|
||||
|
||||
g.dispose();
|
||||
|
||||
try {
|
||||
String imageName = String.format("image-%05d.png", 1);
|
||||
File outputfile = new File("/home/valdor/", imageName);
|
||||
outputfile.createNewFile();
|
||||
|
||||
ImageIO.write(img, "png", outputfile);
|
||||
} catch (IOException ex) {
|
||||
System.out.println("Exception while writing image: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
static final double[] computeAtDistance(Position eyeCoordinates, TileMap tileMap,
|
||||
int d, DisplayCalculator dc) {
|
||||
|
||||
double[] output = new double[dc.getWidth()];
|
||||
|
||||
for (int x = 0; x < dc.getWidth(); x += 1) {
|
||||
double bearing = dc.getBearingAtX(x);
|
||||
Position target = Position.get(eyeCoordinates, Math.toRadians(bearing), d);
|
||||
|
||||
Tile atPos = tileMap.getByLatLon(target.gridLat, target.gridLon);
|
||||
double elevation = atPos.elevation(target.row, target.col);
|
||||
|
||||
int dMeters = eyeCoordinates.distanceToMeters(target);
|
||||
double visibleAngle = dc.calculateVisibilityAngle(elevation, dMeters);
|
||||
|
||||
output[x] = visibleAngle;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public static Collection<Peak> readFeatures(String fileName) throws IOException {
|
||||
Collection<Peak> peaks = new ArrayList<>();
|
||||
|
||||
try ( FileReader fr = new FileReader(fileName)) {
|
||||
Gson gson = new GsonBuilder().create();
|
||||
GeoJsonExport export = gson.fromJson(fr, GeoJsonExport.class);
|
||||
|
||||
for (GeoJsonExport.Feature feature : export.features) {
|
||||
double[] coordinates = feature.geometry.coordinates;
|
||||
String name = feature.properties.name;
|
||||
Integer elevation = 0; // Integer.parseInt(feature.properties.ele);
|
||||
|
||||
if (name == null || name.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Peak peak = new Peak(coordinates[1], coordinates[0], name, elevation);
|
||||
peaks.add(peak);
|
||||
}
|
||||
}
|
||||
|
||||
return peaks;
|
||||
}
|
||||
|
||||
}
|
85
src/main/java/ch/inf3/horizoncut/data/DisplayCalculator.java
Normal file
85
src/main/java/ch/inf3/horizoncut/data/DisplayCalculator.java
Normal file
|
@ -0,0 +1,85 @@
|
|||
package ch.inf3.horizoncut.data;
|
||||
|
||||
public class DisplayCalculator {
|
||||
|
||||
final int width;
|
||||
final int height;
|
||||
final double minHorizAngle;
|
||||
final double maxHorizAngle;
|
||||
final double eyeHeight;
|
||||
|
||||
public DisplayCalculator(int width, int height, double minHorizAngle, double maxHorizAngle, double eyeHeight) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.minHorizAngle = minHorizAngle;
|
||||
this.maxHorizAngle = maxHorizAngle;
|
||||
this.eyeHeight = eyeHeight;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public double getMinHorizAngle() {
|
||||
return minHorizAngle;
|
||||
}
|
||||
|
||||
public double getMaxHorizAngle() {
|
||||
return maxHorizAngle;
|
||||
}
|
||||
|
||||
public double getEyeHeight() {
|
||||
return eyeHeight;
|
||||
}
|
||||
|
||||
public double getAngleStep() {
|
||||
return (maxHorizAngle - minHorizAngle) / width;
|
||||
}
|
||||
|
||||
public boolean isVisible(double bearing) {
|
||||
return (bearing <= maxHorizAngle && bearing >= minHorizAngle);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param x
|
||||
* @return Bearing in degrees
|
||||
*/
|
||||
public double getBearingAtX(int x) {
|
||||
return (x * getAngleStep()) + minHorizAngle;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param bearing Bearing in degrees
|
||||
* @return
|
||||
*/
|
||||
public int getXAtBearing(double bearing) {
|
||||
return (int) ((bearing - minHorizAngle) / getAngleStep());
|
||||
}
|
||||
|
||||
public double calculateVisibilityAngle(double targetVisibleHeight, int targetDistance) {
|
||||
if (eyeHeight == targetVisibleHeight) {
|
||||
return 90.0;
|
||||
} else if (eyeHeight < targetVisibleHeight) {
|
||||
double op = targetVisibleHeight - eyeHeight;
|
||||
double ad = targetDistance;
|
||||
return radToDeg(Math.atan(op / ad)) + 90;
|
||||
} else {
|
||||
return radToDeg(Math.atan(targetDistance / (eyeHeight - targetVisibleHeight)));
|
||||
}
|
||||
}
|
||||
|
||||
public static double degToRad(double angleDeg) {
|
||||
return Math.PI * angleDeg / 180.0;
|
||||
}
|
||||
|
||||
public static double radToDeg(double angleDeg) {
|
||||
return angleDeg * 180.0 / Math.PI;
|
||||
}
|
||||
|
||||
}
|
133
src/main/java/ch/inf3/horizoncut/data/DistanceLayer.java
Normal file
133
src/main/java/ch/inf3/horizoncut/data/DistanceLayer.java
Normal file
|
@ -0,0 +1,133 @@
|
|||
package ch.inf3.horizoncut.data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DistanceLayer implements Comparable<DistanceLayer>, Cloneable {
|
||||
|
||||
public final int nearDistance;
|
||||
public final int farDistance;
|
||||
public final double[] data;
|
||||
|
||||
public DistanceLayer(int nearDistance, int farDistance, double[] data) {
|
||||
this.nearDistance = nearDistance;
|
||||
this.farDistance = farDistance;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(DistanceLayer other) {
|
||||
return Integer.compare(this.nearDistance, other.nearDistance);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DistanceLayer clone() {
|
||||
double[] cloneData = Arrays.copyOf(data, data.length);
|
||||
return new DistanceLayer(nearDistance, farDistance, cloneData);
|
||||
}
|
||||
|
||||
public DistanceLayer mergeWith(DistanceLayer other) {
|
||||
DistanceLayer near = this.compareTo(other) <= 0 ? this : other;
|
||||
DistanceLayer far = this.compareTo(other) > 0 ? this : other;
|
||||
|
||||
double[] output = new double[near.data.length];
|
||||
|
||||
boolean anyNear = false;
|
||||
|
||||
for (int d = 0; d < near.data.length; d += 1) {
|
||||
double yNear = near.data[d];
|
||||
double yFar = far.data[d];
|
||||
|
||||
if (yNear > yFar) {
|
||||
output[d] = yNear;
|
||||
anyNear = true;
|
||||
} else {
|
||||
output[d] = yFar;
|
||||
}
|
||||
}
|
||||
|
||||
int outputNearDistance = anyNear ? near.nearDistance : far.nearDistance;
|
||||
|
||||
return new DistanceLayer(outputNearDistance, far.farDistance, output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given layer is always lower than the current layer.
|
||||
*
|
||||
* @param other
|
||||
* @return
|
||||
*/
|
||||
public boolean isLower(DistanceLayer other) {
|
||||
for (int d = 0; d < this.data.length; d += 1) {
|
||||
double yThis = this.data[d];
|
||||
double yOther = other.data[d];
|
||||
|
||||
if (yThis == 0 || yOther == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (yOther >= yThis) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public double getMax() {
|
||||
return Arrays.stream(this.data)
|
||||
.boxed()
|
||||
.filter(e -> !e.isNaN())
|
||||
.max(Double::compareTo)
|
||||
.orElse(0.0);
|
||||
}
|
||||
|
||||
public static Collection<DistanceLayer> filterInvisible(Collection<DistanceLayer> input) {
|
||||
|
||||
List<DistanceLayer> sorted = input.stream()
|
||||
.sorted()
|
||||
.map(d -> d.clone())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
int layerSize = sorted.get(0).data.length;
|
||||
|
||||
for (int x = 0; x < layerSize; x++) {
|
||||
double maxAngle = Double.NEGATIVE_INFINITY;
|
||||
for (DistanceLayer l : sorted) {
|
||||
double c = l.data[x];
|
||||
|
||||
if (c >= maxAngle) {
|
||||
maxAngle = c;
|
||||
} else {
|
||||
l.data[x] = Double.NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
public static boolean isVisible(Collection<DistanceLayer> layers, int distance, int x, double visibleAngle) {
|
||||
List<DistanceLayer> sorted = layers.stream()
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (DistanceLayer l : sorted) {
|
||||
if (l.farDistance > (distance - 200)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
double c = l.data[x];
|
||||
|
||||
if (c >= visibleAngle) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
24
src/main/java/ch/inf3/horizoncut/data/GeoJsonExport.java
Normal file
24
src/main/java/ch/inf3/horizoncut/data/GeoJsonExport.java
Normal file
|
@ -0,0 +1,24 @@
|
|||
package ch.inf3.horizoncut.data;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class GeoJsonExport {
|
||||
|
||||
public Collection<Feature> features;
|
||||
|
||||
public static class Feature {
|
||||
public String type;
|
||||
public Properties properties;
|
||||
public Geometry geometry;
|
||||
}
|
||||
|
||||
public static class Properties {
|
||||
public String ele;
|
||||
public String name;
|
||||
}
|
||||
|
||||
public static class Geometry {
|
||||
public double[] coordinates;
|
||||
}
|
||||
|
||||
}
|
23
src/main/java/ch/inf3/horizoncut/data/Peak.java
Normal file
23
src/main/java/ch/inf3/horizoncut/data/Peak.java
Normal file
|
@ -0,0 +1,23 @@
|
|||
package ch.inf3.horizoncut.data;
|
||||
|
||||
public class Peak {
|
||||
|
||||
public final Position position;
|
||||
public final String name;
|
||||
public final int elevation;
|
||||
|
||||
public Peak(Position position, String name, int elevation) {
|
||||
this.position = position;
|
||||
this.name = name;
|
||||
this.elevation = elevation;
|
||||
}
|
||||
|
||||
public Peak(double lat, double lon, String name, int elevation) {
|
||||
this.position = new Position(lat, lon);
|
||||
this.name = name;
|
||||
this.elevation = elevation;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
175
src/main/java/ch/inf3/horizoncut/data/Position.java
Normal file
175
src/main/java/ch/inf3/horizoncut/data/Position.java
Normal file
|
@ -0,0 +1,175 @@
|
|||
package ch.inf3.horizoncut.data;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Position {
|
||||
|
||||
public final int gridLat;
|
||||
public final int gridLon;
|
||||
|
||||
public final int row;
|
||||
public final int col;
|
||||
|
||||
private final double lat;
|
||||
private final double lon;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Position.class);
|
||||
|
||||
public Position(double lat, double lon) {
|
||||
this.gridLat = integerPart(lat);
|
||||
this.gridLon = integerPart(lon);
|
||||
this.row = (int) Math.floor(fractionalPart(lat) * Tile.RESOLUTION_SRTM1);
|
||||
this.col = (int) Math.floor(fractionalPart(lon) * Tile.RESOLUTION_SRTM1);
|
||||
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
}
|
||||
|
||||
public static Position get(Position coordinates, double angleRad, int distance) {
|
||||
final double rEarth = 6371 * 1000;
|
||||
|
||||
double rDistance = (distance * 1.0) / rEarth; // normalize linear distance to radian angle
|
||||
double φ1 = coordinates.getLatitudeRad();
|
||||
double λ1 = coordinates.getLongitudeRad();
|
||||
|
||||
double φ2 = Math.asin(Math.sin(φ1) * Math.cos(rDistance)
|
||||
+ Math.cos(φ1) * Math.sin(rDistance) * Math.cos(angleRad));
|
||||
double λ2 = λ1 + Math.atan2(Math.sin(angleRad) * Math.sin(rDistance) * Math.cos(φ1),
|
||||
Math.cos(rDistance) - Math.sin(φ1) * Math.sin(φ2));
|
||||
|
||||
return new Position(radToDeg(φ2), radToDeg(λ2));
|
||||
}
|
||||
|
||||
public int distanceToUnits(Position other) {
|
||||
int dxGrid = this.gridLat - other.gridLat;
|
||||
int dxCol = this.col - other.col;
|
||||
|
||||
int dyGrid = this.gridLon - other.gridLon;
|
||||
int dyRow = this.row - other.row;
|
||||
|
||||
int dx = dxGrid * Tile.RESOLUTION_SRTM1 + dxCol;
|
||||
int dy = dyGrid * Tile.RESOLUTION_SRTM1 + dyRow;
|
||||
|
||||
return (int) Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
public int distanceToMeters(Position other) {
|
||||
final double R = 6371 * 1000;
|
||||
|
||||
double dLat = degToRad(other.getLatitude() - this.getLatitude());
|
||||
double dLon = degToRad(other.getLongitude()- this.getLongitude());
|
||||
|
||||
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
|
||||
+ Math.cos(this.getLatitude() * (Math.PI / 180.0)) * Math.cos(other.getLatitude() * (Math.PI / 180.0))
|
||||
* Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
int d = (int) (R * c);
|
||||
return d;
|
||||
}
|
||||
|
||||
public static double bearing(Position a, Position b) {
|
||||
|
||||
double φ1 = a.getLatitudeRad();
|
||||
double φ2 = b.getLatitudeRad();
|
||||
|
||||
double λ1 = a.getLongitudeRad();
|
||||
double λ2 = b.getLongitudeRad();
|
||||
|
||||
double y = Math.sin(λ2 - λ1) * Math.cos(φ2);
|
||||
double x = Math.cos(φ1) * Math.sin(φ2)
|
||||
- Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1);
|
||||
double θ = Math.atan2(y, x);
|
||||
double brng = (θ + 2 * Math.PI) % (2 * Math.PI);
|
||||
|
||||
return brng;
|
||||
}
|
||||
|
||||
static int mod(int a, int b) {
|
||||
int mod = a % b;
|
||||
// We still need to move negative values into the range
|
||||
return (mod + b) % b;
|
||||
}
|
||||
|
||||
static int integerPart(double d) {
|
||||
return (int) d;
|
||||
}
|
||||
|
||||
static double fractionalPart(double d) {
|
||||
return d - integerPart(d);
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return this.lat;
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return this.lon;
|
||||
}
|
||||
|
||||
public double getLatitudeRad() {
|
||||
return degToRad(getLatitude());
|
||||
}
|
||||
|
||||
public double getLongitudeRad() {
|
||||
return degToRad(getLongitude());
|
||||
}
|
||||
|
||||
public String getLatLon() {
|
||||
return String.format("(%.4f, %.4f)", getLatitude(), getLongitude());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + gridLat;
|
||||
result = prime * result + gridLon;
|
||||
result = prime * result + col;
|
||||
result = prime * result + row;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Position other = (Position) obj;
|
||||
if (gridLat != other.gridLat) {
|
||||
return false;
|
||||
}
|
||||
if (gridLon != other.gridLon) {
|
||||
return false;
|
||||
}
|
||||
if (col != other.col) {
|
||||
return false;
|
||||
}
|
||||
if (row != other.row) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Position[gridLat=" + gridLat + ", gridLon=" + gridLon + ", col=" + col + ", row=" + row + "]";
|
||||
}
|
||||
|
||||
static double degToRad(double angleDeg) {
|
||||
return Math.PI * angleDeg / 180.0;
|
||||
}
|
||||
|
||||
static double radToDeg(double angleDeg) {
|
||||
return angleDeg * 180.0 / Math.PI;
|
||||
}
|
||||
|
||||
|
||||
}
|
121
src/main/java/ch/inf3/horizoncut/data/Tile.java
Normal file
121
src/main/java/ch/inf3/horizoncut/data/Tile.java
Normal file
|
@ -0,0 +1,121 @@
|
|||
package ch.inf3.horizoncut.data;
|
||||
|
||||
import java.nio.ShortBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Height data for a one by one degree latitude and longitude tile in high
|
||||
* resolution (E.g. N44E016), read from a HGT file.
|
||||
*
|
||||
* The tile is divided to 3600 rows and 3600 columns. The real-world size of
|
||||
* such pixel depends on its latitude and longitude. - Degrees of latitude are
|
||||
* parallel, so the distance between each degree is mostly constant, 1 degree ~=
|
||||
* 111km, so the height of each pixel is roughly 30.8 m. - Longitude varies
|
||||
* based on the actual location. It is also ~= 111km at the equator, but shrinks
|
||||
* to 0 as it reaches the poles.
|
||||
*
|
||||
* @see https://lpdaac.usgs.gov/documents/179/SRTM_User_Guide_V3.pdf
|
||||
*/
|
||||
public class Tile {
|
||||
public static final int RESOLUTION_SRTM1 = 3601;
|
||||
public static final int RESOLUTION_SRTM3 = 1201;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Tile.class);
|
||||
|
||||
private final ShortBuffer data;
|
||||
|
||||
private final int lon;
|
||||
|
||||
private final int lat;
|
||||
|
||||
private boolean isMaxElevationCalculated = false;
|
||||
private double maxElevation = 0;
|
||||
|
||||
Tile(int lat, int lon, ShortBuffer data) {
|
||||
this.lat = lat;
|
||||
this.lon = lon;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public double elevation(final int row, final int col) {
|
||||
if (row < 0 || (RESOLUTION_SRTM1 - 1) < row || col < 0 || (RESOLUTION_SRTM1 - 1) < col) {
|
||||
LOG.error("{}x{} is not within the file for {},{}", row, col, lat, lon);
|
||||
throw new IllegalArgumentException(createExceptionMessage(row, col));
|
||||
}
|
||||
|
||||
int cell = (RESOLUTION_SRTM1 * (RESOLUTION_SRTM1 - row - 1)) + col;
|
||||
|
||||
if (data.limit() <= cell) {
|
||||
throw new IllegalArgumentException(createExceptionMessage(row, col));
|
||||
}
|
||||
|
||||
short elevation;
|
||||
try {
|
||||
elevation = data.get(cell);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
throw new IllegalArgumentException(createExceptionMessage(row, col), e);
|
||||
}
|
||||
if (elevation == Short.MIN_VALUE) {
|
||||
return Double.NaN;
|
||||
} else {
|
||||
return elevation;
|
||||
}
|
||||
}
|
||||
|
||||
public double getMaxElevation() {
|
||||
if (!isMaxElevationCalculated) {
|
||||
isMaxElevationCalculated = true;
|
||||
for (int x = 0; x < RESOLUTION_SRTM1; x++) {
|
||||
for (int y = 0; y < RESOLUTION_SRTM1; y++) {
|
||||
final double elevation = elevation(x, y);
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxElevation;
|
||||
}
|
||||
|
||||
public double elevationByExactCoordinates(final double lat, final double lon) {
|
||||
if (lat < this.lat || lat >= this.lat + 1 || lon < this.lon || lon >= this.lon + 1) {
|
||||
throw new IllegalArgumentException("{lat: " + lat + " lon: " + lon + "} is not in tile of {lat: "
|
||||
+ this.lat + " lon: " + this.lon + "}");
|
||||
}
|
||||
double latDelta = lat - this.lat;
|
||||
double lonDelta = lon - this.lon;
|
||||
|
||||
int row = (int) (RESOLUTION_SRTM1 * latDelta);
|
||||
int col = (int) (RESOLUTION_SRTM1 * lonDelta);
|
||||
|
||||
return elevation(row, col);
|
||||
}
|
||||
|
||||
private static String createExceptionMessage(int row, int col) {
|
||||
return "Query: " + row + "/" + col;
|
||||
}
|
||||
|
||||
public int lat() {
|
||||
return lat;
|
||||
}
|
||||
|
||||
public int lon() {
|
||||
return lon;
|
||||
}
|
||||
|
||||
static class EmptyTile extends Tile {
|
||||
|
||||
EmptyTile(int lat, int lon, ShortBuffer data) {
|
||||
super(lat, lon, data);
|
||||
|
||||
System.out.println("Failed to access tile at lat/lon " + lat + "/" + lon + ". Using zero elevations in this region.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public double elevation(int row, int col) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
33
src/main/java/ch/inf3/horizoncut/data/TileMap.java
Normal file
33
src/main/java/ch/inf3/horizoncut/data/TileMap.java
Normal file
|
@ -0,0 +1,33 @@
|
|||
package ch.inf3.horizoncut.data;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class TileMap {
|
||||
|
||||
private final Map<Integer, Map<Integer, Tile>> tileByLatLon = new HashMap<>();
|
||||
|
||||
public TileMap(Collection<Tile> tiles) {
|
||||
for (Tile tile : tiles) {
|
||||
add(tile);
|
||||
}
|
||||
}
|
||||
|
||||
private void add(Tile tile) {
|
||||
insertIntoCache(tile);
|
||||
}
|
||||
|
||||
private void insertIntoCache(Tile tile) {
|
||||
tileByLatLon.putIfAbsent(tile.lat(), new HashMap<>());
|
||||
tileByLatLon.get(tile.lat()).put(tile.lon(), tile);
|
||||
}
|
||||
|
||||
public Tile getByLatLon(int lat, int lon) {
|
||||
if (!tileByLatLon.containsKey(lat) || !tileByLatLon.get(lat).containsKey(lon)) {
|
||||
add(new Tile.EmptyTile(lat, lon, null));
|
||||
}
|
||||
return tileByLatLon.get(lat).get(lon);
|
||||
}
|
||||
|
||||
}
|
73
src/main/java/ch/inf3/horizoncut/data/TileReader.java
Normal file
73
src/main/java/ch/inf3/horizoncut/data/TileReader.java
Normal file
|
@ -0,0 +1,73 @@
|
|||
package ch.inf3.horizoncut.data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Read HGT files into {@link Tile} objects to form a {@link TileMap}.
|
||||
*/
|
||||
public class TileReader {
|
||||
|
||||
private final String baseDir;
|
||||
|
||||
public TileReader(String baseDir) {
|
||||
this.baseDir = baseDir;
|
||||
}
|
||||
|
||||
public TileMap readHGTFiles(Position c, int radius) {
|
||||
List<Tile> tiles = new ArrayList<>();
|
||||
|
||||
for (int lat = c.gridLat - radius; lat <= c.gridLat + radius; lat++) {
|
||||
for (int lon = c.gridLon - radius; lon <= c.gridLon + radius; lon++) {
|
||||
tiles.add(readHgtFile(lat, lon));
|
||||
}
|
||||
}
|
||||
|
||||
return new TileMap(tiles);
|
||||
}
|
||||
|
||||
private Tile readHgtFile(final int lat, final int lon) {
|
||||
File hgtFile = getHGTFile(lat, lon);
|
||||
|
||||
try(FileInputStream fis = new FileInputStream(hgtFile);
|
||||
FileChannel fc = fis.getChannel()) {
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocateDirect((int) fc.size());
|
||||
while (bb.remaining() > 0) {
|
||||
fc.read(bb);
|
||||
}
|
||||
|
||||
bb.flip();
|
||||
return new Tile(lat, lon, bb.order(ByteOrder.BIG_ENDIAN).asShortBuffer());
|
||||
} catch (IOException e) {
|
||||
return new Tile.EmptyTile(lat, lon, null);
|
||||
}
|
||||
}
|
||||
|
||||
private File getHGTFile(int lat, int lon) {
|
||||
char hWe;
|
||||
char hNs;
|
||||
|
||||
if (lat < 0) {
|
||||
hNs = 'S';
|
||||
} else {
|
||||
hNs = 'N';
|
||||
}
|
||||
|
||||
if (lon < 0) {
|
||||
hWe = 'W';
|
||||
} else {
|
||||
hWe = 'E';
|
||||
}
|
||||
|
||||
String fileName = String.format("%c%02d%c%03d.hgt", hNs, lat, hWe, lon);
|
||||
|
||||
return new File(baseDir, fileName);
|
||||
}
|
||||
}
|
2
src/main/resources/simplelogger.properties
Normal file
2
src/main/resources/simplelogger.properties
Normal file
|
@ -0,0 +1,2 @@
|
|||
org.slf4j.simpleLogger.showThreadName=false
|
||||
org.slf4j.simpleLogger.showShortLogName=true
|
40
src/test/java/ch/inf3/horizoncut/data/PositionTest.java
Normal file
40
src/test/java/ch/inf3/horizoncut/data/PositionTest.java
Normal file
|
@ -0,0 +1,40 @@
|
|||
package ch.inf3.horizoncut.data;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class PositionTest {
|
||||
private static final double TOLERANCE = 0.000833;
|
||||
|
||||
@Test
|
||||
public void testBearingEast01() {
|
||||
Position a = new Position(46.0341763, 7.6119305);
|
||||
Position b = Position.get(a, Math.PI/2, 5_000);
|
||||
|
||||
Assert.assertEquals(46.034158, b.getLatitude(), TOLERANCE);
|
||||
Assert.assertEquals(7.676710, b.getLongitude(), TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBearingEast02() {
|
||||
Position a = new Position(46.0341763, 7.6119305);
|
||||
Position b = Position.get(a, Math.PI/2, 50_000);
|
||||
|
||||
Assert.assertEquals(46.032346, b.getLatitude(), TOLERANCE);
|
||||
Assert.assertEquals(8.259716, b.getLongitude(), TOLERANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBearing() {
|
||||
Position a = new Position(46.0341763, 7.6119305);
|
||||
|
||||
for(int i = 0; i < 360; i++) {
|
||||
|
||||
Position b = Position.get(a, Math.toRadians(i), 10000);
|
||||
double bearing = Position.bearing(a, b);
|
||||
|
||||
Assert.assertEquals(i, Math.toDegrees(bearing), 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue