refactor: Refactor Features processing
This commit is contained in:
parent
578f8bdf6e
commit
6ae6d2e69f
8 changed files with 472 additions and 208 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
87
src/main/java/ch/inf3/horizoncut/data/Feature.java
Normal file
87
src/main/java/ch/inf3/horizoncut/data/Feature.java
Normal 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 + ")";
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
34
src/main/java/ch/inf3/horizoncut/data/ObservedFeature.java
Normal file
34
src/main/java/ch/inf3/horizoncut/data/ObservedFeature.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue