refactor: Refactor Features processing

This commit is contained in:
Thomas Schwery 2022-04-03 21:58:24 +02:00
parent 578f8bdf6e
commit 6ae6d2e69f
8 changed files with 472 additions and 208 deletions

View file

@ -2,25 +2,29 @@ package ch.inf3.horizoncut.cli;
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.Feature;
import ch.inf3.horizoncut.data.ObservedFeature;
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 com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
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.List;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.imageio.ImageIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,178 +37,286 @@ public class Cli {
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);
long initStart = System.currentTimeMillis();
Position eyePosition = new Position(46.326570, 7.466880);
TileMap tileMap = new TileReader("/home/valdor/Downloads/SRTM1/").readHGTFiles(eyePosition, 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);
Tile atOrigin = tileMap.getByLatLon(eyePosition.gridLat, eyePosition.gridLon);
double eyeHeight = atOrigin.elevation(eyePosition.row, eyePosition.col);
double minHorizAngle = 135; //107; // 135 ~Brunegghorn
double minHorizAngle = 107; //107; // 135 ~Brunegghorn
double maxHorizAngle = 226; //226; // 197 ~Combin de Valsorey
DisplayCalculator dc = new DisplayCalculator(width, height, minHorizAngle, maxHorizAngle, eyeHeight);
DisplayCalculator dc = new DisplayCalculator(width, height, minHorizAngle, maxHorizAngle, eyePosition, eyeHeight);
int maxDistance = 100_000;
int minDistance = 500;
LOG.info("Distances of {} to {}", minDistance, maxDistance);
Collection<Feature> rawFeatures = new ArrayList<>();
rawFeatures.addAll(Feature.readFeatures("data/osm-overpass-peaks.geojson", tileMap, 150, true));
rawFeatures.addAll(Feature.readFeatures("data/osm-overpass-villages.geojson", tileMap, height - 50, false));
BufferedImage img = new BufferedImage(width, height + 1, BufferedImage.TYPE_INT_RGB);
Collection<ObservedFeature> allFeatures = rawFeatures.stream()
.map(f -> f.observe(dc))
.sorted()
.collect(Collectors.toList());
Collection<DistanceLayer> layers = new ArrayList<>(maxDistance);
BufferedImage bufferedImg = new BufferedImage(width, height + 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) bufferedImg.getGraphics();
long initStop = System.currentTimeMillis();
LOG.info("Initialized system for computations from {}@{}m with distances of {} to {} ({} ms)",
eyePosition.getLatLon(), eyeHeight, minDistance, maxDistance, initStop - initStart);
long layersStart = System.currentTimeMillis();
Collection<DistanceLayer> reversedLayers = 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));
double[] data = computeAtDistance(tileMap, cDist, dc);
reversedLayers.add(new DistanceLayer(d - SLICE_DISTANCE, d, data));
}
long layersEnd = System.currentTimeMillis();
LOG.info("Computed {} layers ({} ms)", reversedLayers.size(), layersEnd - layersStart);
long horizonStart = System.currentTimeMillis();
// 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()
DistanceLayer horizonLayer = reversedLayers.stream()
.reduce((a, b) -> a.mergeWith(b))
.get();
layers = layers.stream()
reversedLayers = reversedLayers.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()
maxDistance = reversedLayers.stream()
.map(l -> l.farDistance)
.max(Integer::compareTo)
.get();
minDistance = layers.stream()
minDistance = reversedLayers.stream()
.map(l -> l.nearDistance)
.min(Integer::compareTo)
.get();
LOG.info("Distances of {} to {}", minDistance, maxDistance);
dc.updateMinMaxVertAngle(reversedLayers);
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());
long horizonEnd = System.currentTimeMillis();
LOG.info("There are {} layers remaining after horizon cleanup at distances {} to {} ({} ms)",
reversedLayers.size(), minDistance, maxDistance, horizonEnd - horizonStart);
nLayers = DistanceLayer.filterInvisible(nLayers);
long labelsStart = System.currentTimeMillis();
TreeMap<Integer, DistanceLayer> dLayers = reversedLayers.stream()
.collect(Collectors.toMap(l -> l.nearDistance, l -> l, (a, b) -> a.mergeWith(b), TreeMap::new));
LOG.info("There are {} layers remaining after layer merges", nLayers.size());
// We start processing the features to avoid overlapping features in the
// labels as well as displaying features that are hidden from the observation
// point.
// 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();
Predicate<ObservedFeature> isVisibleFilter = (feature) -> {
int x = dc.getXAtBearing(feature.bearing);
return DistanceLayer.isVisible(dLayers.values(), feature.distance, x, feature.visibleAngle);
};
maxVertAngle += 2;
TreeMap<Integer, ObservedFeature> labeledFeatures = allFeatures.stream()
.filter(f -> dc.isDisplayed(f.bearing, f.visibleAngle))
.filter(isVisibleFilter)
.collect(Collectors.toMap(f -> f.getObservedX(), f -> f, (a, b) -> Feature.highest(a, b), TreeMap::new));
LOG.info("Displaying angles are {} - {}", minVertAngle, maxVertAngle);
Integer cCol = labeledFeatures.firstKey();
while (cCol < labeledFeatures.lastKey()) {
Integer nCol = labeledFeatures.higherKey(cCol);
// 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);
if (nCol - cCol <= 25) {
double nScore = labeledFeatures.get(nCol).getScore();
double cScore = labeledFeatures.get(cCol).getScore();
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;
if (nScore > cScore) {
LOG.debug("Removing {} ({}) to keep {} ({}) at X {}", labeledFeatures.get(cCol), cScore, labeledFeatures.get(nCol), nScore, cCol);
labeledFeatures.remove(cCol);
cCol = nCol;
} else {
labeledFeatures.remove(nCol);
}
img.setRGB(x, y, color);
} else {
cCol = nCol;
}
}
Graphics2D g = (Graphics2D) img.getGraphics();
Font font = new Font("Arial", Font.BOLD, 12);
for (ObservedFeature feature : labeledFeatures.values()) {
var featureLayer = dLayers.floorEntry(feature.distance);
if (featureLayer == null) {
LOG.info("Unable to find layer for feature {}", feature);
continue;
}
featureLayer.getValue().addFeature(feature);
}
long labelsEnd = System.currentTimeMillis();
LOG.info("Computed {} visible labels ({} ms)", labeledFeatures.size(), labelsEnd - labelsStart);
long mergeStart = System.currentTimeMillis();
List<DistanceLayer> sortedLayers = new ArrayList<>();
Integer cDist = dLayers.firstKey();
DistanceLayer cLayer = dLayers.get(cDist);
while (cDist < dLayers.lastKey()) {
Integer nDist = dLayers.higherKey(cDist);
DistanceLayer nLayer = dLayers.get(nDist);
var bothFeatures = Stream.concat(cLayer.getHorizonFeatures().stream(),nLayer.getHorizonFeatures().stream())
.filter(isVisibleFilter)
.collect(Collectors.toList());
DistanceLayer mLayer = cLayer.mergeWith(nLayer);
var mergeFeatures = nLayer.getHorizonFeatures().stream()
.filter(isVisibleFilter)
.collect(Collectors.toList());
boolean continueMerge = false;
if (mergeFeatures.isEmpty()) {
// If no features are present on the merge result, we always continue;
continueMerge = true;
} else {
double bothScore = bothFeatures.stream()
.map(Feature::getScore)
.reduce(Double::sum)
.orElse(0.0);
double mergeScore = mergeFeatures.stream()
.map(Feature::getScore)
.reduce(Double::sum)
.orElse(0.0);
var missingFeatures = bothFeatures.stream()
.filter(f -> !mergeFeatures.contains(f))
.collect(Collectors.toList());
LOG.info("Merging layers {} and {} ({}) would generate {} feature score: {}",
cLayer.nearDistance, nLayer.nearDistance, bothScore,
mergeScore, continueMerge);
if (!missingFeatures.isEmpty()) {
LOG.info(" Removed {}", missingFeatures);
}
continueMerge = mergeScore >= bothScore * 0.1;
}
if (!continueMerge) {
sortedLayers.add(cLayer);
cLayer = nLayer;
} else {
cLayer = mLayer;
}
cDist = nDist;
}
sortedLayers.add(cLayer);
long mergeEnd = System.currentTimeMillis();
LOG.info("There are {} layers remaining after layer merges for {} features ({} ms)",
sortedLayers.size(), labeledFeatures.size(), mergeEnd - mergeStart);
long renderStart = System.currentTimeMillis();
// We loop in descending order to avoid writing further points on top
// of nearer points.
for (DistanceLayer layer : sortedLayers) {
float dPos = (float) ((maxDistance - layer.nearDistance) / (maxDistance * 1.0f));
Color layerColor = Color.getHSBColor(dPos * 1f, 1.0f, 1.0f);
g.setColor(layerColor);
Polygon p = new Polygon();
p.addPoint(0, height);
for (int x = 0; x < width; x++) {
double visibleAngle = layer.data[x];
int y = dc.getYAtAngle(visibleAngle);
p.addPoint(x, y);
}
p.addPoint(width, height);
g.drawPolygon(p);
for (ObservedFeature feature : layer.getFeatures()) {
if (!dc.isDisplayed(feature.bearing, feature.visibleAngle)) {
LOG.debug("Feature {} is not visible at bearing {} and angle {}", feature.name, feature.bearing, feature.visibleAngle);
continue;
}
int x = dc.getXAtBearing(feature.bearing);
int y = dc.getYAtAngle(feature.visibleAngle);
boolean isVisible = DistanceLayer.isVisible(dLayers.values(), feature.distance, x, feature.visibleAngle);
if (!isVisible) {
LOG.debug("Feature {} is hidden behind terrain", feature.name);
continue;
}
Shape s = new Rectangle(x -5 , y - 5, 10, 10);
if (layer.isAtHorizon(feature)) {
g.fill(s);
} else {
g.draw(s);
}
}
}
Font font = new Font("Helvetica", 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 (ObservedFeature feature : labeledFeatures.values()) {
if (!dc.isDisplayed(feature.bearing, feature.visibleAngle)) {
LOG.debug("Feature {} is not visible at bearing {} and angle {}", feature.name, feature.bearing, feature.visibleAngle);
continue;
}
for (Peak peak : feature) {
double peakBearing = Math.toDegrees(Position.bearing(c, peak.position));
if (!dc.isVisible(peakBearing)) {
LOG.info("Peak {} is not visible at bearing {}", peak.name, peakBearing);
continue;
}
int x = dc.getXAtBearing(feature.bearing);
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);
boolean isVisible = DistanceLayer.isVisible(dLayers.values(), feature.distance, x, feature.visibleAngle);
if (!isVisible) {
LOG.debug("Feature {} is not visible, hidden behind terrain.", feature.name);
continue;
}
int y = dc.getYAtAngle(feature.visibleAngle);
LOG.debug("Peak {} is at bearing {} ({}x{})", feature.name, feature.bearing, x, y);
String title = String.format("%s (%.0f)", feature.name, feature.getScore());
AffineTransform old = g.getTransform();
g.rotate(Math.toRadians(-45), x, feature.textPosition);
g.drawString(title, x, feature.textPosition);
g.setTransform(old);
g.drawLine(x, y, x, feature.textPosition);
}
for (int i = 0; i < 360; i += 5) {
g.setColor(Color.gray);
for (int i = 0; i < 360; i += 1) {
int x = dc.getXAtBearing(i);
g.drawLine(x, 0, x, height);
g.drawString(i + "", x, 20);
g.drawString(i + "", x + 5, 20);
}
long renderEnd = System.currentTimeMillis();
LOG.info("Rendered panorama ({} ms)", renderEnd - renderStart);
g.dispose();
try {
@ -212,25 +324,24 @@ public class Cli {
File outputfile = new File("/home/valdor/", imageName);
outputfile.createNewFile();
ImageIO.write(img, "png", outputfile);
ImageIO.write(bufferedImg, "png", outputfile);
} catch (IOException ex) {
System.out.println("Exception while writing image: " + ex);
LOG.error("Exception while writing image", ex);
}
}
static final double[] computeAtDistance(Position eyeCoordinates, TileMap tileMap,
int d, DisplayCalculator dc) {
static final double[] computeAtDistance(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);
Position target = Position.get(dc.getEyePosition(), 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);
int dMeters = dc.getEyePosition().distanceToMeters(target);
double visibleAngle = dc.calculateVisibilityAngle(elevation, dMeters);
output[x] = visibleAngle;
@ -239,28 +350,4 @@ public class Cli {
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

@ -1,18 +1,29 @@
package ch.inf3.horizoncut.data;
import java.util.Collection;
public class DisplayCalculator {
final int width;
final int height;
final double minHorizAngle;
final double maxHorizAngle;
final Position eyePosition;
final double eyeHeight;
public DisplayCalculator(int width, int height, double minHorizAngle, double maxHorizAngle, double eyeHeight) {
double minVertAngle;
double maxVertAngle;
public DisplayCalculator(int width, int height, double minHorizAngle, double maxHorizAngle, Position eyePosition, double eyeHeight) {
this.width = width;
this.height = height;
this.minHorizAngle = minHorizAngle;
this.maxHorizAngle = maxHorizAngle;
this.eyePosition = eyePosition;
this.eyeHeight = eyeHeight;
}
@ -36,12 +47,17 @@ public class DisplayCalculator {
return eyeHeight;
}
public Position getEyePosition() {
return eyePosition;
}
public double getAngleStep() {
return (maxHorizAngle - minHorizAngle) / width;
}
public boolean isVisible(double bearing) {
return (bearing <= maxHorizAngle && bearing >= minHorizAngle);
public boolean isDisplayed(double bearing, double visibleAngle) {
return (bearing <= maxHorizAngle && bearing >= minHorizAngle)
&& (visibleAngle <= maxVertAngle && visibleAngle >= minVertAngle);
}
/**
@ -62,6 +78,28 @@ public class DisplayCalculator {
return (int) ((bearing - minHorizAngle) / getAngleStep());
}
public int getYAtAngle(double visibleAngle) {
double pos = (visibleAngle - minVertAngle) * (height / (maxVertAngle - minVertAngle));
int y = (int) (height - pos - 1);
if (y <= 0 || y > height) {
y = height;
}
return y;
}
public void updateMinMaxVertAngle(Collection<DistanceLayer> nLayers) {
// We dont want to compte the min angle to avoid looking at the ground
// just before the position ...
this.minVertAngle = 80;
this.maxVertAngle = nLayers.stream()
.map(l -> l.getMax())
.max(Double::compareTo)
.orElse(0.0) + 3;
}
public double calculateVisibilityAngle(double targetVisibleHeight, int targetDistance) {
if (eyeHeight == targetVisibleHeight) {
return 90.0;

View file

@ -3,19 +3,36 @@ package ch.inf3.horizoncut.data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DistanceLayer implements Comparable<DistanceLayer>, Cloneable {
private static final Logger LOG = LoggerFactory.getLogger(DistanceLayer.class);
public final int nearDistance;
public final int farDistance;
public final double[] data;
public final Collection<ObservedFeature> horizonFeatures;
public final Collection<ObservedFeature> nonHorizonFeatures;
public DistanceLayer(int nearDistance, int farDistance, double[] data) {
this(nearDistance, farDistance, data, new ArrayList<>(), new ArrayList<>());
}
private DistanceLayer(int nearDistance, int farDistance, double[] data,
Collection<ObservedFeature> horizonFeatures, Collection<ObservedFeature> nonHorizonFeatures) {
this.nearDistance = nearDistance;
this.farDistance = farDistance;
this.data = data;
this.horizonFeatures = horizonFeatures;
this.nonHorizonFeatures = nonHorizonFeatures;
}
@Override
@ -26,7 +43,11 @@ public class DistanceLayer implements Comparable<DistanceLayer>, Cloneable {
@Override
protected DistanceLayer clone() {
double[] cloneData = Arrays.copyOf(data, data.length);
return new DistanceLayer(nearDistance, farDistance, cloneData);
Collection<ObservedFeature> horizonFeatures = new ArrayList<>(this.horizonFeatures);
Collection<ObservedFeature> nonHorizonFeatures = new ArrayList<>(this.nonHorizonFeatures);
return new DistanceLayer(nearDistance, farDistance, cloneData, horizonFeatures, nonHorizonFeatures);
}
public DistanceLayer mergeWith(DistanceLayer other) {
@ -51,9 +72,17 @@ public class DistanceLayer implements Comparable<DistanceLayer>, Cloneable {
int outputNearDistance = anyNear ? near.nearDistance : far.nearDistance;
return new DistanceLayer(outputNearDistance, far.farDistance, output);
List<ObservedFeature> outHorizonFeatures = Stream.concat(this.horizonFeatures.parallelStream(), other.horizonFeatures.parallelStream())
.filter(f -> isAtHorizon(f, output))
.collect(Collectors.toList());
List<ObservedFeature> outNonHorizonFeatures = new ArrayList<>();
outNonHorizonFeatures.addAll(this.nonHorizonFeatures);
outNonHorizonFeatures.addAll(other.nonHorizonFeatures);
return new DistanceLayer(outputNearDistance, far.farDistance, output, outHorizonFeatures, outNonHorizonFeatures);
}
/**
* Returns true if the given layer is always lower than the current layer.
*
@ -77,6 +106,45 @@ public class DistanceLayer implements Comparable<DistanceLayer>, Cloneable {
return true;
}
public void addFeature(ObservedFeature feature) {
if (feature.requireHorizon) {
this.horizonFeatures.add(feature);
} else {
this.nonHorizonFeatures.add(feature);
}
}
public Collection<ObservedFeature> getHorizonFeatures() {
return Collections.unmodifiableCollection(this.horizonFeatures);
}
public Collection<ObservedFeature> getFeatures() {
return Stream.concat(this.horizonFeatures.stream(), this.nonHorizonFeatures.stream())
.sorted()
.collect(Collectors.toList());
}
public boolean isAtHorizon(ObservedFeature feature) {
return isAtHorizon(feature, data);
}
private static boolean isAtHorizon(ObservedFeature feature, double[] data) {
int xFeature = feature.getObservedX();
double angleFeature = feature.visibleAngle;
double angleThis = data[xFeature];
boolean isAtHorizon = Math.abs(angleThis - angleFeature) < 0.2;
LOG.debug("Feature {}, {} vs {} -> Horizon {}", feature.name, angleFeature, angleThis, isAtHorizon);
return isAtHorizon;
}
public int countHorizonFeatures() {
return this.horizonFeatures.size();
}
public double getMax() {
return Arrays.stream(this.data)
.boxed()
@ -84,49 +152,20 @@ public class DistanceLayer implements Comparable<DistanceLayer>, Cloneable {
.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) {
public static boolean isVisible(Collection<DistanceLayer> sortedLayers, int distance, int x, double visibleAngle) {
for (DistanceLayer l : sortedLayers) {
if (l.farDistance > (distance - 200)) {
return true;
}
double c = l.data[x];
if (c >= visibleAngle) {
return false;
}
}
return true;
}

View file

@ -0,0 +1,87 @@
package ch.inf3.horizoncut.data;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class Feature {
private final Position position;
private final double elevation;
private final double score;
public final String name;
public final int textPosition;
public final boolean requireHorizon;
protected Feature(Position position, double elevation, String name, int textPosition, boolean requireHorizon, double score) {
this.position = position;
this.elevation = elevation;
this.name = name;
this.textPosition = textPosition;
this.requireHorizon = requireHorizon;
this.score = score;
}
public ObservedFeature observe(DisplayCalculator observation) {
return new ObservedFeature(this.position, this.elevation, observation, this.name, this.textPosition, this.requireHorizon, this.score);
}
public static Collection<Feature> readFeatures(String fileName, TileMap tileMap, int textPosition, boolean requireHorizon) throws IOException {
Collection<Feature> 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;
if (name == null || name.isBlank()) {
continue;
}
Position featurePosition = new Position(coordinates[1], coordinates[0]);
Tile atPos = tileMap.getByLatLon(featurePosition.gridLat, featurePosition.gridLon);
double elevation = atPos.elevation(featurePosition.row, featurePosition.col);
double score = elevation;
if (feature.properties.prominence != null) {
score += 10 * Integer.parseInt(feature.properties.prominence);
}
if (feature.properties.wikidata != null) {
score += 9_000;
}
Feature peak = new Feature(featurePosition, elevation, name, textPosition, requireHorizon, score);
peaks.add(peak);
}
}
return peaks;
}
public static <T extends Feature> T highest(T... features) {
return Arrays.stream(features)
.max((a, b) -> Double.compare(a.getScore(), b.getScore()))
.get();
}
public double getScore() {
return this.score;
}
@Override
public String toString() {
return name + "(score=" + score + ")";
}
}

View file

@ -15,6 +15,9 @@ public class GeoJsonExport {
public static class Properties {
public String ele;
public String name;
public String prominence;
public String wikidata;
}
public static class Geometry {

View file

@ -0,0 +1,34 @@
package ch.inf3.horizoncut.data;
public class ObservedFeature extends Feature implements Comparable<ObservedFeature> {
public final int distance;
public final double visibleAngle;
public final double bearing;
private final DisplayCalculator observator;
public ObservedFeature(Position position, double elevation, DisplayCalculator observator, String name, int textPosition, boolean requireHorizon, double score) {
super(position, elevation, name, textPosition, requireHorizon, score);
this.observator = observator;
this.distance = observator.eyePosition.distanceToMeters(position);
this.visibleAngle = observator.calculateVisibilityAngle(elevation, distance);
this.bearing = Math.toDegrees(Position.bearing(observator.eyePosition, position));
}
@Override
public int compareTo(ObservedFeature other) {
return Double.compare(this.bearing, other.bearing);
}
public int getObservedX() {
return observator.getXAtBearing(bearing);
}
public int getObservedY() {
return observator.getYAtAngle(visibleAngle);
}
}

View file

@ -1,23 +0,0 @@
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

@ -108,8 +108,7 @@ public class 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.");
LOG.debug("Failed to access tile at {},{}. Using zero elevations in this region.", lat, lon);
}
@Override