feat: Initial commit

This commit is contained in:
Thomas Schwery 2022-03-29 09:36:16 +02:00
commit 4f7687eaab
15 changed files with 117756 additions and 0 deletions

32
.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

85
pom.xml Normal file
View 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>

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}
}

View 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);
}
}

View 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);
}
}

View file

@ -0,0 +1,2 @@
org.slf4j.simpleLogger.showThreadName=false
org.slf4j.simpleLogger.showShortLogName=true

View 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);
}
}
}