package com.opendoorlogistics.core.distances.external;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.function.ObjIntConsumer;
import com.opendoorlogistics.api.geometry.LatLong;
import com.opendoorlogistics.core.geometry.GreateCircle;
import com.opendoorlogistics.core.gis.map.data.LatLongImpl;
import com.opendoorlogistics.core.utils.Numbers;
public class RoundingGrid {
public static class GridNeighboursResult implements Comparable<GridNeighboursResult> {
private final LatLong latLong;
private final double cartesianMetresFromRawLatLong;
private final int offsetI;
private final int offsetJ;
public GridNeighboursResult(LatLong latLong,int offsetI, int offsetJ, double diff) {
this.latLong = latLong;
this.offsetI = offsetI;
this.offsetJ = offsetJ;
this.cartesianMetresFromRawLatLong = diff;
}
@Override
public int compareTo(GridNeighboursResult o) {
return Double.compare(cartesianMetresFromRawLatLong, o.cartesianMetresFromRawLatLong);
}
@Override
public String toString() {
return "[latLong=" + latLong + ", metresDiff=" + cartesianMetresFromRawLatLong + ", offset=" + offsetI + "," + offsetJ + "]";
}
public LatLong getLatLong() {
return latLong;
}
}
private static final double EARTH_RADIUS_METRES = 6371000;
private static final double EARTH_CIRCUMFERENCE_METRES = EARTH_RADIUS_METRES * 2 * Math.PI;
/**
* See https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates. The axis alignment (i.e. is +z North?) doesn't matter as long as
* this transformation is used consistently.
*
* @param ll
* @return
*/
private static ThreeDPoint calculateXYZ(LatLong ll) {
ThreeDPoint ret = new ThreeDPoint();
double lat = Math.toRadians(ll.getLongitude());
double lng = Math.toRadians(ll.getLatitude());
ret.x = EARTH_RADIUS_METRES * Math.sin(lat) * Math.cos(lng);
ret.y = EARTH_RADIUS_METRES * Math.sin(lat) * Math.sin(lng);
ret.z = EARTH_RADIUS_METRES * Math.cos(lat);
return ret;
}
//
// public static LatLong roundToMetreGrid( LatLong ll){
// return roundToCustomGrid(ll, 1, 10);
// }
public static void main(String[] args) {
RoundingGrid grid = new RoundingGrid();
Random random = new Random(123);
for (int i = 0; i < 1000; i++) {
LatLongImpl ll = LatLongImpl.random(random);
LatLong rounded = grid.snapToGrid(ll);
double distance = GreateCircle.greatCircle(ll, rounded, true);
System.out.println("" + i + " - " + ll.toString() + " rounds to " + rounded.toString() + " with distance " + distance);
// if(Math.abs(ll.getLatitude() - rounded.getLatitude())>0.001){
// throw new RuntimeException();
// }
// if(Math.abs(ll.getLongitude() - rounded.getLongitude())>0.001){
// throw new RuntimeException();
// }
if (distance > 10) {
throw new RuntimeException();
}
}
for (int i = 0; i < 50; i++) {
LatLongImpl ll = LatLongImpl.random(random);
System.out.println(ll);
for(GridNeighboursResult ngb : grid.calculateNeighbouringGridCells(ll)){
System.out.println("\t" + ngb);
}
// System.out.println(grid.calculateNeighbouringGridCells(ll));
System.out.println(System.lineSeparator() + System.lineSeparator());
}
}
/**
* Round the lat-long to a standard grid of input resolution
*
* @param ll
* @param gridResolutionMetres
* @param latitudeSafetyFactor
* @return
*/
// public static LatLong roundToCustomGrid(LatLong ll, double gridResolutionMetres, double latitudeSafetyFactor){
// // Use a latitude limit where the a horizontal (constant latitude) section through the Earth is only X metres
// double minEarthSectionRadius = gridResolutionMetres * latitudeSafetyFactor;
// double latLimit = Math.acos(minEarthSectionRadius/EARTH_RADIUS_METRES);
// latLimit = 0.5* Math.PI - latLimit;
// latLimit = Math.toDegrees(latLimit);
// double minLat = -90 + latLimit;
// double maxLat = +90 - latLimit;
// double latRange = maxLat - minLat;
//
// double halfEarthCircumference = 0.5 * EARTH_CIRCUMFERENCE_METRES;
// double nbLatitudeCells = halfEarthCircumference/ gridResolutionMetres;
// double latitudeCellHeight = latRange / nbLatitudeCells;
//
// // get snapped-to-grid latitude first
// double latitude = Numbers.clamp(ll.getLatitude(), minLat,maxLat);
// long latitudeIndex = Math.round((latitude - minLat)/latitudeCellHeight);
// double snapped2GridLatitude = latitudeCellHeight * latitudeIndex+ minLat;
//
// // get circumference of a constant latitude circle at this latitude
// double circleRadius = EARTH_RADIUS_METRES * Math.cos(Math.toRadians(snapped2GridLatitude));
// double circleCircum = 2 * Math.PI * circleRadius;
//
// // get nb of longitude cells at this latitude
// double nbLongitudeCells = circleCircum / gridResolutionMetres;
// double longitudeCellWidth = 360 / nbLongitudeCells;
//
// // then get snapped-to-grid longitude
// double minLong = -180;
// double longitude = Numbers.clamp(ll.getLongitude(), minLong, 180);
// long longitudeIndx = Math.round((longitude - minLong)/longitudeCellWidth);
// double snapped2GridLongitude = longitudeCellWidth *longitudeIndx + minLong;
//
// return new LatLongImpl(snapped2GridLatitude, snapped2GridLongitude);
//
// }
// public static class CustomRoundingGrid{
private final double minLong = -180;
private final double gridResolutionMetres;
private final double minLat;
private final double maxLat;
private final double latitudeCellHeight;
public RoundingGrid() {
this(1, 10);
}
public RoundingGrid(double gridResolutionMetres, double latitudeSafetyFactor) {
this.gridResolutionMetres = gridResolutionMetres;
// Use a latitude limit where the a horizontal (constant latitude) section through the Earth is only X metres
double minEarthSectionRadius = gridResolutionMetres * latitudeSafetyFactor;
double latLimit = Math.acos(minEarthSectionRadius / EARTH_RADIUS_METRES);
latLimit = 0.5 * Math.PI - latLimit;
latLimit = Math.toDegrees(latLimit);
minLat = -90 + latLimit;
maxLat = +90 - latLimit;
double latRange = maxLat - minLat;
double halfEarthCircumference = 0.5 * EARTH_CIRCUMFERENCE_METRES;
double nbLatitudeCells = halfEarthCircumference / gridResolutionMetres;
latitudeCellHeight = latRange / nbLatitudeCells;
}
private double calcLongitudeCellWidth(double latitudeSnapped2Grid) {
// get circumference of a constant latitude circle at this latitude
double circleRadius = EARTH_RADIUS_METRES * Math.cos(Math.toRadians(latitudeSnapped2Grid));
double circleCircum = 2 * Math.PI * circleRadius;
// get nb of longitude cells at this latitude
double nbLongitudeCells = circleCircum / gridResolutionMetres;
double longitudeCellWidth = 360 / nbLongitudeCells;
return longitudeCellWidth;
}
private double calcSnappedLatitude(long latitudeIndx) {
return latitudeCellHeight * latitudeIndx + minLat;
}
private long calcSnappedLatitudeIndex(double latitude) {
latitude = Numbers.clamp(latitude, minLat, maxLat);
long latitudeIndex = Math.round((latitude - minLat) / latitudeCellHeight);
return latitudeIndex;
}
private double calcSnappedLongitude(double latitudeSnapped2Grid, long longitudeIndx) {
return calcLongitudeCellWidth(latitudeSnapped2Grid) * longitudeIndx + minLong;
}
private long calcSnappedLongitudeIndex(double latitudeSnapped2Grid, double longitude) {
double longitudeCellWidth = calcLongitudeCellWidth(latitudeSnapped2Grid);
// then get snapped-to-grid longitude
longitude = Numbers.clamp(longitude, minLong, 180);
long longitudeIndx = Math.round((longitude - minLong) / longitudeCellWidth);
return longitudeIndx;
}
public List<GridNeighboursResult> calculateNeighbouringGridCells(LatLong ll) {
return calculateNeighbouringGridCells(ll, 1);
}
/**
* Return neighbours (include the closest snap-to), sorted by closest first
* @param ll
* @param borderWidth
* @return
*/
public List<GridNeighboursResult> calculateNeighbouringGridCells(LatLong ll, int borderWidth) {
if(borderWidth<0){
throw new IllegalArgumentException();
}
ThreeDPoint rawCentre = calculateXYZ(ll);
long latIndx = calcSnappedLatitudeIndex(ll.getLatitude());
int length = 1 + 2 * borderWidth;
ArrayList<GridNeighboursResult> toSort = new ArrayList<>(length * length);
for (int i = -borderWidth; i <= borderWidth; i++) {
long latIndx2 = latIndx + i;
double snappedLat2 = calcSnappedLatitude(latIndx2);
if (snappedLat2 >= minLat && snappedLat2 <= maxLat) {
long longIndx = calcSnappedLongitudeIndex(snappedLat2, ll.getLongitude());
for (int j = -borderWidth; j <= borderWidth; j++) {
long longIndx2 = longIndx + j;
double snappedLng2 = calcSnappedLongitude(snappedLat2, longIndx2);
LatLong snappedLatLng = new LatLongImpl(snappedLat2, snappedLng2);
if (snappedLatLng.isValid()) {
ThreeDPoint pnt = calculateXYZ(snappedLatLng);
// use xyz Cartesian diff as Great Circle calculation is an approximation with inaccuracies at low separation
double diff = Math.sqrt(ThreeDPoint.absSqd(ThreeDPoint.subtract(rawCentre, pnt)));
toSort.add(new GridNeighboursResult(snappedLatLng,i,j, diff));
}
}
}
}
Collections.sort(toSort);
return toSort;
}
// }
private double snapLatitudeToGrid(double latitude) {
return calcSnappedLatitude(calcSnappedLatitudeIndex(latitude));
}
private double snapLongitudeToGrid(double latitudeSnapped2Grid, double longitude) {
return calcSnappedLongitude(latitudeSnapped2Grid, calcSnappedLongitudeIndex(latitudeSnapped2Grid, longitude));
}
public LatLong snapToGrid(LatLong ll) {
double snappedLat = snapLatitudeToGrid(ll.getLatitude());
double snappedLong = snapLongitudeToGrid(snappedLat, ll.getLongitude());
return new LatLongImpl(snappedLat, snappedLong);
}
}