Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/nav 32 range search in kd tree #13

Merged
merged 55 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
0b603d7
start working on spatial dk tree implementation
Brunner246 Apr 28, 2024
422b54b
first implementation of DK Tree **untested** yet
Brunner246 Apr 28, 2024
f8fca09
implement haversine formula - it views the Earth as a perfect sphere.…
Brunner246 Apr 28, 2024
7308903
add `haversine` helper function
Brunner246 Apr 28, 2024
9d52e5e
Merge branch 'main' of https://github.com/naviqore/raptor into featur…
Brunner246 May 9, 2024
8b8216e
ENH: NAV-23- Clean code refactorings
Brunner246 May 9, 2024
aa80ccb
ENH: NAV-23- remove test code
Brunner246 May 9, 2024
30878ac
ENH: NAV-23- proposal to keep the design open for extension
Brunner246 May 9, 2024
a52af64
ENH: NAV-23- rename `CoordinatesType` from `X` `Y` to `LONGITUDE` `LA…
Brunner246 May 10, 2024
a227160
ENH: NAV-23
Brunner246 May 10, 2024
b97eab5
ENH: NAV-23 - make KDNode generic without any bounds
Brunner246 May 10, 2024
774138b
REFACTOR: NAV-23 - Add getCoordinateValue to CoordinatesType Enum.
clukas1 May 12, 2024
9add21e
ENH: NAV-23 - Add ability to add objects that have Coordinate (Locati…
clukas1 May 12, 2024
016fb35
REFACTOR: NAV-23 - Moved KDTree to utils.spatial and some renaming of…
clukas1 May 12, 2024
05cfe6c
STYLE: NAV-23 - Remove redundant type constraint.
clukas1 May 12, 2024
fcd9bce
TEST: NAV-30 - add simple unit test for `KDTree`
Brunner246 May 12, 2024
4a06b46
Merge branch 'feature/NAV-23-kd-tree-for-stops' of https://github.com…
Brunner246 May 12, 2024
3a4a20d
TEST: NAV-30 - make `Coordinate` comparable
Brunner246 May 12, 2024
ba43a6a
ENH: NAV-29 - Add KDTreeBuilder to builed balanced KDTrees from lists…
clukas1 May 12, 2024
07c72f4
TEST: NAV-23 - Add test class for KDTreeBuilder.
clukas1 May 12, 2024
5231c42
REFACTOR: NAV-29 - Remove unused methods from test class.
clukas1 May 12, 2024
881dd42
REFACTOR: NAV-29 - Remove unused import.
clukas1 May 12, 2024
c66d865
ENH: NAV-29 - Hide ArrayList logic from outside by adding add method …
clukas1 May 12, 2024
fe2a2d4
ENH: NAV-29 - Always throw exceptions when null arguments are passed …
clukas1 May 12, 2024
b7c26e4
TEST: NAV-29 - Update tests to work with new KDTreeBuilder structure.
clukas1 May 12, 2024
bb9ccd1
REFACTOR: NAV-30 - Move tests to utils.spatial
clukas1 May 12, 2024
a342fcb
TEST: NAV-30 - Add some new test cases.
clukas1 May 12, 2024
49449f3
FIX: NAV-30 - Fix failing tests.
clukas1 May 12, 2024
706ba9e
TEST: NAV-30 - Remove dependency to GTFS module in utils.spatial tests.
clukas1 May 12, 2024
eb950c3
Merge pull request #12 from naviqore/NAV-29-balanced-kd-tree
clukas1 May 12, 2024
88c68a7
Merge remote-tracking branch 'origin/feature/NAV-23-kd-tree-for-stops…
clukas1 May 12, 2024
76e8f01
TEST: NAV-30 - Reworked KDTree Builder tests to use mockFacility and …
clukas1 May 12, 2024
4957e75
ENH: NAV-32 - Make KDTree queries work without requiring a Location o…
clukas1 May 12, 2024
464e5fd
ENH: NAV-32 - Implement range search.
clukas1 May 12, 2024
0a13616
ENH: NAV-32 - Introduce KDCoordinate for better internal generic coor…
clukas1 May 12, 2024
53986a3
FIX: NAV-32 - Fix stupid error message.
clukas1 May 12, 2024
aef1483
ENH: NAV-34 - Integrate KDTree into GTFS Schedule.
clukas1 May 12, 2024
d74f9d5
TEST: NAV-30 - Make test names more consistent.
clukas1 May 12, 2024
fa35cd9
Merge pull request #10 from naviqore/feature/NAV-30-write-unit-tests-…
clukas1 May 12, 2024
7d9e900
Merge remote-tracking branch 'origin/feature/NAV-23-kd-tree-for-stops…
clukas1 May 12, 2024
fdd3949
TEST: NAV-32 - Add tests for range search.
clukas1 May 12, 2024
8bb6c46
ENH: NAV-32 - Add more input validation in range search to allow only…
clukas1 May 12, 2024
f5329a7
TEST: NAV-32 - Use @Nested to group tests by method.
clukas1 May 12, 2024
ab85532
REFACTOR: NAV-23 - Remove todos in gtfs coordinate.
clukas1 May 12, 2024
ef6cb31
TEST: NAV-32 - Add test for GTFS Coordinate implementation.
clukas1 May 12, 2024
085ea97
ENH: NAV-32 - Add coordinate validation when constructing coordinates.
clukas1 May 12, 2024
e1ffee9
FIX: NAV-32 - Fix harvesine distance calculation.
clukas1 May 12, 2024
45a4a7f
REFACTOR: NAV-32 - Solve out of scope visibility issues
munterfi May 15, 2024
c5bac72
ORG: NAV-32 - Remove default code style, use project specific
munterfi May 15, 2024
e3d953a
STYLE: NAV-32 - Extract constant for TOLERANCE in tests
munterfi May 15, 2024
2531356
TEST: NAV-32 - Add more coordinate tests.
clukas1 May 15, 2024
442bc87
ENH: NAV-32 - Add more argument validation for coordinate classes.
clukas1 May 15, 2024
f499cd7
Merge pull request #14 from naviqore/review/NAV-32-range-search-in-kd…
munterfi May 15, 2024
a5ee815
Merge remote-tracking branch 'refs/remotes/origin/main' into feature/…
munterfi May 16, 2024
1c8a1f8
ORG: NAV-32 - Solve merge conflicts
munterfi May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ch.naviqore.gtfs.schedule.model;

import ch.naviqore.gtfs.schedule.spatial.Coordinate;
import ch.naviqore.utils.spatial.index.KDTree;
import ch.naviqore.utils.spatial.index.KDTreeBuilder;
import lombok.Getter;

import java.time.LocalDate;
Expand All @@ -25,6 +26,7 @@ public class GtfsSchedule {
private final Map<String, Stop> stops;
private final Map<String, Route> routes;
private final Map<String, Trip> trips;
private final KDTree<Stop> spatialIndex;

/**
* Constructs an immutable GTFS schedule.
Expand All @@ -39,6 +41,7 @@ public class GtfsSchedule {
this.stops = Map.copyOf(stops);
this.routes = Map.copyOf(routes);
this.trips = Map.copyOf(trips);
this.spatialIndex = new KDTreeBuilder<Stop>().addLocations(stops.values()).build();
}

/**
Expand All @@ -59,12 +62,18 @@ public static GtfsScheduleBuilder builder() {
* @return A list of stops within the specified distance.
*/
public List<Stop> getNearestStops(double latitude, double longitude, int maxDistance) {
// TODO: Use a spatial index for efficient nearest neighbor search, e.g. KD-tree or R-tree
Coordinate origin = new Coordinate(latitude, longitude);
return stops.values()
.stream()
.filter(stop -> stop.getCoordinate().distanceTo(origin) <= maxDistance)
.collect(Collectors.toList());
return spatialIndex.rangeSearch(latitude, longitude, maxDistance);
}

/**
* Retrieves the nearest stop to a given location.
*
* @param latitude the latitude of the location.
* @param longitude the longitude of the location.
* @return The nearest stop to the specified location.
*/
public Stop getNearestStop(double latitude, double longitude) {
return spatialIndex.nearestNeighbour(latitude, longitude);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package ch.naviqore.gtfs.schedule.model;

import ch.naviqore.gtfs.schedule.spatial.Coordinate;
import ch.naviqore.gtfs.schedule.type.ExceptionType;
import ch.naviqore.gtfs.schedule.type.RouteType;
import ch.naviqore.gtfs.schedule.type.ServiceDayTime;
import ch.naviqore.gtfs.schedule.type.TransferType;
import ch.naviqore.utils.spatial.GeoCoordinate;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
Expand Down Expand Up @@ -55,7 +55,7 @@ public GtfsScheduleBuilder addStop(String id, String name, double lat, double lo
throw new IllegalArgumentException("Agency " + id + " already exists");
}
log.debug("Adding stop {}", id);
stops.put(id, new Stop(id, name, new Coordinate(lat, lon)));
stops.put(id, new Stop(id, name, new GeoCoordinate(lat, lon)));
return this;
}

Expand Down Expand Up @@ -171,7 +171,6 @@ public GtfsSchedule build() {
stops.values().parallelStream().forEach(Initializable::initialize);
routes.values().parallelStream().forEach(Initializable::initialize);
calendars.values().parallelStream().forEach(Initializable::initialize);
// TODO: Build k-d tree for spatial indexing
GtfsSchedule schedule = new GtfsSchedule(agencies, calendars, stops, routes, trips);
clear();
built = true;
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ch.naviqore.gtfs.schedule.model;

import ch.naviqore.gtfs.schedule.spatial.Coordinate;
import ch.naviqore.utils.spatial.GeoCoordinate;
import ch.naviqore.utils.spatial.Location;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand All @@ -12,11 +13,11 @@

@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
public final class Stop implements Initializable {
public final class Stop implements Initializable, Location<GeoCoordinate> {

private final String id;
private final String name;
private final Coordinate coordinate;
private final GeoCoordinate coordinate;
private List<StopTime> stopTimes = new ArrayList<>();
private List<Transfer> transfers = new ArrayList<>();

Expand Down
31 changes: 0 additions & 31 deletions src/main/java/ch/naviqore/gtfs/schedule/spatial/Coordinate.java

This file was deleted.

5 changes: 0 additions & 5 deletions src/main/java/ch/naviqore/gtfs/schedule/spatial/KdTree.java

This file was deleted.

67 changes: 67 additions & 0 deletions src/main/java/ch/naviqore/utils/spatial/CartesianCoordinate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package ch.naviqore.utils.spatial;

public class CartesianCoordinate implements Coordinate {
private final double x;
private final double y;

public CartesianCoordinate(double x, double y) {
validateCoordinate(x, y);
this.x = x;
this.y = y;
}

private static void validateCoordinate(double x, double y) {
if (Double.isNaN(x) || Double.isNaN(y)) {
throw new IllegalArgumentException("Coordinates cannot be NaN");
}
}

private void isOfSameType(Coordinate other) {
if (other == null) {
throw new IllegalArgumentException("Other coordinate must not be null");
}
if (other.getClass() != this.getClass()) {
throw new IllegalArgumentException("Other coordinate must be of type " + this.getClass().getSimpleName());
}
}

@Override
public double getFirstComponent() {
return x;
}

@Override
public double getSecondComponent() {
return y;
}

@Override
public double distanceTo(Coordinate other) {
isOfSameType(other);
return distanceTo(other.getFirstComponent(), other.getSecondComponent());
}

@Override
public double distanceTo(double x, double y) {
validateCoordinate(x, y);
return Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2));
}

@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
var that = (CartesianCoordinate) obj;
return this.x == that.x && this.y == that.y;
}

@Override
public String toString() {
return "[" + this.getClass().getSimpleName() + ": " + x + ", " + y + "]";
}

}
50 changes: 50 additions & 0 deletions src/main/java/ch/naviqore/utils/spatial/Coordinate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ch.naviqore.utils.spatial;

/**
* This interface represents a generic 2D coordinate. Implementations of this interface should provide methods to get
* the two components of the coordinate and calculate distances.
*/
public interface Coordinate {

/**
* Examples for the first component are latitude or X coordinate.
*
* @return the first component of the 2D-coordinate
*/
double getFirstComponent();

/**
* Examples for the first component are longitude or Y coordinate.
*
* @return the second component of the 2D-coordinate
*/
double getSecondComponent();

/**
* Calculates the distance to another {@code Coordinate} object.
* <p><i>Note: Implementations may raise an {@code IllegalArgumentException} if the other {@code Coordinate} object
* is not of the same type.</i></p>
*/
double distanceTo(Coordinate other);
// TODO: Should we somehow enforce that distances between geo and cartesian coordinates cannot be calculated?

/**
* Calculates the distance to another point specified by its components.
*/
double distanceTo(double firstComponent, double secondComponent);

/**
* Gets the coordinate component based on the specified {@code Axis}.
*/
default double getComponent(Axis axis) {
return axis == Axis.FIRST ? getFirstComponent() : getSecondComponent();
}

/**
* The axes of a 2D coordinate system.
*/
enum Axis {
FIRST,
SECOND
}
}
92 changes: 92 additions & 0 deletions src/main/java/ch/naviqore/utils/spatial/GeoCoordinate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package ch.naviqore.utils.spatial;

public record GeoCoordinate(double latitude, double longitude) implements Coordinate, Comparable<GeoCoordinate> {

private static final int EARTH_RADIUS = 6371000;

public GeoCoordinate {
validateCoordinate(latitude, longitude);
}

private static void validateCoordinate(double latitude, double longitude) {
if (latitude < -90 || latitude > 90) {
throw new IllegalArgumentException("Latitude must be between -90 and 90 degrees");
}
if (longitude < -180 || longitude > 180) {
throw new IllegalArgumentException("Longitude must be between -180 and 180 degrees");
}
if (Double.isNaN(latitude) || Double.isNaN(longitude)) {
throw new IllegalArgumentException("Coordinates cannot be NaN");
}
}

private void isOfSameType(Coordinate other) {
if (other == null) {
throw new IllegalArgumentException("Other coordinate must not be null");
}
if (other.getClass() != this.getClass()) {
throw new IllegalArgumentException("Other coordinate must be of type " + this.getClass().getSimpleName());
}
}

@Override
public double getFirstComponent() {
return latitude;
}

@Override
public double getSecondComponent() {
return longitude;
}

/**
* Calculates the distance to another Coordinates object using the Haversine formula.
*
* @param other The other Coordinates object to calculate the distance to.
* @return The distance in meters.
*/
@Override
public double distanceTo(Coordinate other) {
isOfSameType(other);
return distanceTo(other.getFirstComponent(), other.getSecondComponent());
}

@Override
public double distanceTo(double firstComponent, double secondComponent) {
validateCoordinate(firstComponent, secondComponent);

double lat1 = Math.toRadians(this.latitude);
double lat2 = Math.toRadians(firstComponent);
double lon1 = Math.toRadians(this.longitude);
double lon2 = Math.toRadians(secondComponent);

double dLat = lat2 - lat1;
double dLon = lon2 - lon1;

double a = Math.pow(Math.sin(dLat / 2), 2) + Math.pow(Math.sin(dLon / 2), 2) * Math.cos(lat1) * Math.cos(lat2);
double c = 2 * Math.asin(Math.sqrt(a));
return EARTH_RADIUS * c;
}

@Override
public int compareTo(GeoCoordinate other) {
double epsilon = 1e-5;

double diffLatitude = this.latitude - other.latitude();
if (Math.abs(diffLatitude) > epsilon) {
return diffLatitude > 0 ? 1 : -1;
}

double diffLongitude = this.longitude - other.longitude();
if (Math.abs(diffLongitude) > epsilon) {
return diffLongitude > 0 ? 1 : -1;
}

return 0;
}

@Override
public String toString() {
return "[" + this.getClass().getSimpleName() + ": " + latitude + "°, " + longitude + "°]";
}
}
15 changes: 15 additions & 0 deletions src/main/java/ch/naviqore/utils/spatial/Location.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ch.naviqore.utils.spatial;

/**
* A location in a generic spatial coordinate system.
*
* @param <T> The type of the coordinates that defines this location, must extend {@code Coordinate}.
*/
public interface Location<T extends Coordinate> {

/**
* Gets the coordinate that defines this location.
*/
T getCoordinate();

}
Loading
Loading