app/src/main/java/ch/epfl/sdp/peakar/points/LineOfSight.java
package ch.epfl.sdp.peakar.points;
import android.content.Context;
import androidx.core.util.Pair;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* This class enables the computation of the visible POIPoints from the user's location.
* It uses the ElevationMap class to compute the elevation of the terrain at a given location.
*
* The ELEVATION_DIFFERENCE_THRESHOLD represents the maximum acceptable difference in meters
* between the line that connects the user to the POIPoint and the actual elevation of the
* terrain in a given point.
*/
public class LineOfSight {
static final int ELEVATION_DIFFERENCE_THRESHOLD = 100; // in meters
private final UserPoint userPoint;
private final ElevationMap elevationMap;
private double mapCellSize;
private double boundingBoxNorthLat;
private double boundingBoxWestLon;
private final Context context;
/**
* Constructor for the LineOfSight class.
*
* @param topography pair with topography map and cell size.
* @param userPoint userPoint from wich the visible POIPoints are computed.
* @param context context of the application.
*/
public LineOfSight(Pair<int[][], Double> topography, UserPoint userPoint, Context context) {
this.userPoint = userPoint;
this.mapCellSize = topography.second;
this.context = context;
this.elevationMap = new ElevationMap(topography, this.userPoint, context);
}
/**
* This method allows to filter the POIPoints that are not visible from the user's location.
*
* @param poiPoints a List of POIPoint to filter.
* @return a list of the visible POIPoints from the input list.
*/
public List<POIPoint> getVisiblePoints(List<POIPoint> poiPoints) {
elevationMap.updateElevationMatrix();
this.mapCellSize = elevationMap.getMapCellSize();
this.boundingBoxNorthLat = elevationMap.getBoundingBoxNorthLat();
this.boundingBoxWestLon = elevationMap.getBoundingBoxWestLong();
Pair<Integer, Integer> userIndexes = elevationMap
.getIndexesFromCoordinates(userPoint.getLatitude(), userPoint.getLongitude());
double userLatitude = userPoint.getLatitude();
double userLongitude = userPoint.getLongitude();
int userAltitude = (int) userPoint.getAltitude();
return poiPoints.parallelStream()
.filter(p -> isVisible(p, userIndexes, userLatitude, userLongitude, userAltitude))
.collect(Collectors.toList());
}
/**
* This method returns a map with the POPoints labeled with <code>true</code> if the
* POIPoints is visible, <code>false</code> otherwise
*
* @param poiPoints a List of POIPoint to filter.
* @return a map with the labeled POIPoints
*/
public Map<POIPoint, Boolean> getVisiblePointsLabeled(List<POIPoint> poiPoints) {
elevationMap.updateElevationMatrix();
this.mapCellSize = elevationMap.getMapCellSize();
this.boundingBoxNorthLat = elevationMap.getBoundingBoxNorthLat();
this.boundingBoxWestLon = elevationMap.getBoundingBoxWestLong();
Pair<Integer, Integer> userIndexes = elevationMap
.getIndexesFromCoordinates(userPoint.getLatitude(), userPoint.getLongitude());
double userLatitude = userPoint.getLatitude();
double userLongitude = userPoint.getLongitude();
int userAltitude = (int) userPoint.getAltitude();
Map<POIPoint, Boolean> labeledPOIPoints = Collections.synchronizedMap(new HashMap<>());
poiPoints.parallelStream()
.forEach(p -> labeledPOIPoints.put(p, isVisible(p, userIndexes, userLatitude, userLongitude, userAltitude)));
return labeledPOIPoints;
}
/**
* This method computes a boolean that indicates if a single POIPoint is visible from
* the user's location.
*
* @param poiPoint POIPoint to determine if it is visible.
* @param userIndexes indexes representing the user's location on the elevation map grid.
* @param userLatitude latitude of the user's location (in degrees).
* @param userLongitude longitude of the user's location (in degrees).
* @param userAltitude altitude of the user's location (in meters).
* @return <code>true</code> if the POIPoint is visible from the user's location.
* <code>false</code> otherwise.
*/
private boolean isVisible(POIPoint poiPoint, Pair<Integer, Integer> userIndexes,
double userLatitude, double userLongitude, int userAltitude) {
Pair<Integer, Integer> poiIndexes = elevationMap
.getIndexesFromCoordinates(poiPoint.getLatitude(), poiPoint.getLongitude());
double poiLatitude = poiPoint.getLatitude();
double poiLongitude = poiPoint.getLongitude();
int poiAltitude = (int) poiPoint.getAltitude();
boolean useRow = Math.abs(userIndexes.first - poiIndexes.first) >
Math.abs(userIndexes.second - poiIndexes.second);
double slope = (poiAltitude - userAltitude) / (useRow ? (poiLatitude - userLatitude) : (poiLongitude - userLongitude));
List<Pair<Integer, Integer>> line = drawLine( userIndexes.first, userIndexes.second,
poiIndexes.first, poiIndexes.second);
return line.stream()
.map(p -> computeMaxElevation(userLatitude, userLongitude, userAltitude, p.first, p.second, useRow, slope) -
elevationMap.getAltitudeAtLocation(p.first, p.second) >
- ELEVATION_DIFFERENCE_THRESHOLD)
.reduce(true, (vis, v) -> vis && v);
}
/**
* This method draws the line between the user's location and the POIPoint on the
* elevation map grid.
*
* @param x1 x index of the user's location on the elevation map grid.
* @param y1 y index of the user's location on the elevation map grid.
* @param x2 x index of the POIPoint on the elevation map grid.
* @param y2 y index of the POIPoint on the elevation map grid.
* @return a List of indexes representing the line in the elevation map grid.
*/
private List<Pair<Integer, Integer>> drawLine(int x1, int y1, int x2, int y2) {
int pixelX = x2 - x1;
int pixelY = y2 - y1;
int x,y;
List<Pair<Integer, Integer>> line = new ArrayList<>();
int ratio;
double error;
double cumulatedError = 0;
if (Math.abs(pixelY) >= Math.abs(pixelX)) {
ratio = pixelX != 0 ? Math.abs(pixelY / pixelX) : Math.abs(pixelY);
error = pixelX != 0 ? Math.abs((double) ratio - Math.abs(((double) pixelY / (double) pixelX))) : 0;
for (x = x1, y = y1; x1 < x2 ? x <= x2 : x >= x2; x = x1 < x2 ? x+1 : x-1) {
for (int i = 0; i < ratio && (y1 < y2 ? y <= y2 : y >= y2); i++) {
line.add(new Pair<>(x, y));
y = y1 < y2 ? y+1 : y-1;
cumulatedError += error;
if (cumulatedError >= 1) {
line.add(new Pair<>(x, y));
y = y1 < y2 ? y+1 : y-1;
cumulatedError -= 1;
}
}
}
} else {
ratio = pixelY != 0 ? Math.abs(pixelX / pixelY) : Math.abs(pixelX);
error = pixelY != 0 ? Math.abs((double) ratio - Math.abs(((double) pixelX / (double) pixelY))) : 0;
for (x = x1, y = y1; y1 < y2 ? y <= y2 : y >= y2; y = y1 < y2 ? y+1 : y-1) {
for (int i = 0; i < ratio && (x1 < x2 ? x <= x2 : x >= x2); i++) {
line.add(new Pair<>(x, y));
x = x1 < x2 ? x+1 : x-1;
cumulatedError += error;
if (cumulatedError >= 1) {
line.add(new Pair<>(x, y));
x = x1 < x2 ? x+1 : x-1;
cumulatedError -= 1;
}
}
}
}
if (x != x2 || y != y2) line.add(new Pair<>(x2, y2));
return line;
}
/**
* Method that computes the elevation of the line connecting the user's location and the
* POIPoint at a given index of the elevation map grid.
*
* @param userLatitude latitude of the user's location (in degrees).
* @param userLongitude longitude of the user's location (in degrees).
* @param userAltitude altitude of the user's location (in meters).
* @param rowIndex index of the row to compute the elevation.
* @param colIndex index of the column to compute the elevation.
* @param useRow boolean indicating the method to use row/column for computing the elevation.
* @param slope slope of the line connecting the user's location and the POIPoint.
* @return elevation (in meters).
*/
private int computeMaxElevation(double userLatitude, double userLongitude,
double userAltitude,
int rowIndex, int colIndex, boolean useRow,
double slope) {
double latitude = -(rowIndex * mapCellSize) + boundingBoxNorthLat;
double longitude = colIndex * mapCellSize + boundingBoxWestLon;
return (int) (useRow ? (slope*(latitude - userLatitude) + userAltitude) : (slope*(longitude - userLongitude) + userAltitude));
}
}