diff --git a/src/main/java/ch/inf3/horizoncut/cli/Cli.java b/src/main/java/ch/inf3/horizoncut/cli/Cli.java index 4bf2a2d..50bac51 100644 --- a/src/main/java/ch/inf3/horizoncut/cli/Cli.java +++ b/src/main/java/ch/inf3/horizoncut/cli/Cli.java @@ -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 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 allFeatures = rawFeatures.stream() + .map(f -> f.observe(dc)) + .sorted() + .collect(Collectors.toList()); - Collection 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 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 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 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 isVisibleFilter = (feature) -> { + int x = dc.getXAtBearing(feature.bearing); + return DistanceLayer.isVisible(dLayers.values(), feature.distance, x, feature.visibleAngle); + }; - maxVertAngle += 2; + TreeMap 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 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 peaks = readFeatures("data/osm-overpass-peaks.geojson"); - Collection villages = readFeatures("data/osm-overpass-villages.geojson"); - for (int i = 0; i < 2; i++) { - Collection 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 readFeatures(String fileName) throws IOException { - Collection 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; - } - } diff --git a/src/main/java/ch/inf3/horizoncut/data/DisplayCalculator.java b/src/main/java/ch/inf3/horizoncut/data/DisplayCalculator.java index 59a4812..d3fe320 100644 --- a/src/main/java/ch/inf3/horizoncut/data/DisplayCalculator.java +++ b/src/main/java/ch/inf3/horizoncut/data/DisplayCalculator.java @@ -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 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; diff --git a/src/main/java/ch/inf3/horizoncut/data/DistanceLayer.java b/src/main/java/ch/inf3/horizoncut/data/DistanceLayer.java index 9ee6819..96e891d 100644 --- a/src/main/java/ch/inf3/horizoncut/data/DistanceLayer.java +++ b/src/main/java/ch/inf3/horizoncut/data/DistanceLayer.java @@ -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, 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 horizonFeatures; + public final Collection 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 horizonFeatures, Collection 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, Cloneable { @Override protected DistanceLayer clone() { double[] cloneData = Arrays.copyOf(data, data.length); - return new DistanceLayer(nearDistance, farDistance, cloneData); + + Collection horizonFeatures = new ArrayList<>(this.horizonFeatures); + Collection 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, Cloneable { int outputNearDistance = anyNear ? near.nearDistance : far.nearDistance; - return new DistanceLayer(outputNearDistance, far.farDistance, output); + List outHorizonFeatures = Stream.concat(this.horizonFeatures.parallelStream(), other.horizonFeatures.parallelStream()) + .filter(f -> isAtHorizon(f, output)) + .collect(Collectors.toList()); + + List 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, Cloneable { return true; } + public void addFeature(ObservedFeature feature) { + if (feature.requireHorizon) { + this.horizonFeatures.add(feature); + } else { + this.nonHorizonFeatures.add(feature); + } + } + + public Collection getHorizonFeatures() { + return Collections.unmodifiableCollection(this.horizonFeatures); + } + + public Collection 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, Cloneable { .max(Double::compareTo) .orElse(0.0); } - - public static Collection filterInvisible(Collection input) { - - List 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 layers, int distance, int x, double visibleAngle) { - List sorted = layers.stream() - .sorted() - .collect(Collectors.toList()); - - for (DistanceLayer l : sorted) { + public static boolean isVisible(Collection 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; } diff --git a/src/main/java/ch/inf3/horizoncut/data/Feature.java b/src/main/java/ch/inf3/horizoncut/data/Feature.java new file mode 100644 index 0000000..b97d9d9 --- /dev/null +++ b/src/main/java/ch/inf3/horizoncut/data/Feature.java @@ -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 readFeatures(String fileName, TileMap tileMap, int textPosition, boolean requireHorizon) throws IOException { + Collection 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 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 + ")"; + } + +} diff --git a/src/main/java/ch/inf3/horizoncut/data/GeoJsonExport.java b/src/main/java/ch/inf3/horizoncut/data/GeoJsonExport.java index fcccbbf..198d907 100644 --- a/src/main/java/ch/inf3/horizoncut/data/GeoJsonExport.java +++ b/src/main/java/ch/inf3/horizoncut/data/GeoJsonExport.java @@ -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 { diff --git a/src/main/java/ch/inf3/horizoncut/data/ObservedFeature.java b/src/main/java/ch/inf3/horizoncut/data/ObservedFeature.java new file mode 100644 index 0000000..2996db5 --- /dev/null +++ b/src/main/java/ch/inf3/horizoncut/data/ObservedFeature.java @@ -0,0 +1,34 @@ +package ch.inf3.horizoncut.data; + +public class ObservedFeature extends Feature implements Comparable { + + 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); + } +} diff --git a/src/main/java/ch/inf3/horizoncut/data/Peak.java b/src/main/java/ch/inf3/horizoncut/data/Peak.java deleted file mode 100644 index 692ce3d..0000000 --- a/src/main/java/ch/inf3/horizoncut/data/Peak.java +++ /dev/null @@ -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; - } - - - -} diff --git a/src/main/java/ch/inf3/horizoncut/data/Tile.java b/src/main/java/ch/inf3/horizoncut/data/Tile.java index db527e3..3d6a6ca 100644 --- a/src/main/java/ch/inf3/horizoncut/data/Tile.java +++ b/src/main/java/ch/inf3/horizoncut/data/Tile.java @@ -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