Skip to content

Commit

Permalink
Add functionality to schedule
Browse files Browse the repository at this point in the history
- Nearest stations, prepare spatial indexing.
- Next departures.
- Active trips.
  • Loading branch information
munterfi committed Apr 26, 2024
1 parent f99beed commit ee0a7ba
Show file tree
Hide file tree
Showing 14 changed files with 302 additions and 71 deletions.
25 changes: 19 additions & 6 deletions src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ private void parseCalendar(CSVRecord record) {
}

private void parseCalendarDate(CSVRecord record) {
builder.addCalendarDate(record.get("service_id"), LocalDate.parse(record.get("date"), DATE_FORMATTER),
ExceptionType.parse(record.get("exception_type")));

try {
builder.addCalendarDate(record.get("service_id"), LocalDate.parse(record.get("date"), DATE_FORMATTER),
ExceptionType.parse(record.get("exception_type")));
} catch (IllegalArgumentException e) {
log.warn("Skipping invalid calendar date {}: {}", record.get("date"), e.getMessage());
}
}

private void parseStop(CSVRecord record) {
Expand All @@ -95,11 +98,21 @@ private void parseRoute(CSVRecord record) {
}

private void parseTrips(CSVRecord record) {
builder.addTrip(record.get("trip_id"), record.get("route_id"), record.get("service_id"));
try {
builder.addTrip(record.get("trip_id"), record.get("route_id"), record.get("service_id"));
} catch (IllegalArgumentException e) {
log.warn("Skipping invalid trip {}: {}", record.get("trip_id"), e.getMessage());
}
}

private void parseStopTimes(CSVRecord record) {
builder.addStopTime(record.get("trip_id"), record.get("stop_id"),
ServiceDayTime.parse(record.get("arrival_time")), ServiceDayTime.parse(record.get("departure_time")));
try {
builder.addStopTime(record.get("trip_id"), record.get("stop_id"),
ServiceDayTime.parse(record.get("arrival_time")),
ServiceDayTime.parse(record.get("departure_time")));
} catch (IllegalArgumentException e) {
log.warn("Skipping invalid stop time {}-{}: {}", record.get("trip_id"), record.get("stop_id"),
e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private static void readCsvRecords(InputStreamReader reader, GtfsScheduleParser

public GtfsSchedule read(String path) throws IOException {
File file = new File(path);
GtfsScheduleBuilder builder = GtfsScheduleBuilder.builder();
GtfsScheduleBuilder builder = GtfsSchedule.builder();
GtfsScheduleParser parser = new GtfsScheduleParser(builder);

if (file.isDirectory()) {
Expand Down
74 changes: 64 additions & 10 deletions src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package ch.naviqore.gtfs.schedule.model;

import ch.naviqore.gtfs.schedule.spatial.Coordinate;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* General Transit Feed Specification (GTFS) schedule
* <p>
* Use the {@link GtfsScheduleBuilder} to construct a GTFS schedule instance.
*
* @author munterfi
*/
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class GtfsSchedule {

Expand All @@ -18,20 +28,64 @@ public class GtfsSchedule {
private final Map<String, Trip> trips;

/**
* Retrieves a snapshot of the GTFS schedule active on a specific date.
* Creates a new GTFS schedule builder.
*
* @param date the date for which the active schedule is requested.
* @return GtfsScheduleDay containing only the active routes, stops, and trips for the specified date.
* @return A new GTFS schedule builder.
*/
public static GtfsScheduleBuilder builder() {
return new GtfsScheduleBuilder();
}

/**
* Retrieves a list of stops within a specified distance from a given location.
*
* @param latitude the latitude of the origin location.
* @param longitude the longitude of the origin location.
* @param maxDistance the maximum distance from the origin location.
* @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());
}

/**
* Retrieves a list of the next departures from a specific stop.
*
* @param stopId the identifier of the stop.
* @param dateTime the date and time for which the next departures are requested.
* @param limit the maximum number of departures to return.
* @return A list of the next departures from the specified stop.
*/
public GtfsScheduleDay getScheduleForDay(LocalDate date) {
Map<String, Trip> activeTrips = trips.entrySet()
public List<StopTime> getNextDepartures(String stopId, LocalDateTime dateTime, int limit) {
Stop stop = stops.get(stopId);
if (stop == null) {
throw new IllegalArgumentException("Stop " + stopId + " not found");
}
return stop.getStopTimes()
.stream()
.filter(entry -> entry.getValue().getCalendar().isServiceAvailable(date))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
.filter(stopTime -> stopTime.departure().getTotalSeconds() >= dateTime.toLocalTime().toSecondOfDay())
.filter(stopTime -> stopTime.trip().getCalendar().isServiceAvailable(dateTime.toLocalDate()))
.limit(limit)
.toList();
}

// TODO: Implement efficiently without copying.
// return new GtfsScheduleDay(date, activeStops, activeRoutes, activeTrips);
return null;
/**
* Retrieves a snapshot of the trips active on a specific date.
*
* @param date the date for which the active schedule is requested.
* @return A list containing only the active trips.
*/
public List<Trip> getActiveTrips(LocalDate date) {
return calendars.values()
.stream()
.filter(calendar -> calendar.isServiceAvailable(date))
.flatMap(calendar -> calendar.getTrips().stream())
.collect(Collectors.toList());
}

public Map<String, Agency> getAgencies() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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;
Expand Down Expand Up @@ -27,7 +28,7 @@
*
* @author munterfi
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@Log4j2
public class GtfsScheduleBuilder {

Expand All @@ -38,10 +39,6 @@ public class GtfsScheduleBuilder {
private final Map<String, Route> routes = new HashMap<>();
private final Map<String, Trip> trips = new HashMap<>();

public static GtfsScheduleBuilder builder() {
return new GtfsScheduleBuilder();
}

public GtfsScheduleBuilder addAgency(String id, String name, String url, String timezone) {
if (agencies.containsKey(id)) {
throw new IllegalArgumentException("Agency " + id + " already exists");
Expand All @@ -56,7 +53,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, lat, lon));
stops.put(id, new Stop(id, name, new Coordinate(lat, lon)));
return this;
}

Expand Down Expand Up @@ -136,6 +133,7 @@ public GtfsSchedule build() {
trips.values().parallelStream().forEach(Trip::initialize);
stops.values().parallelStream().forEach(Stop::initialize);
routes.values().parallelStream().forEach(Route::initialize);
// TODO: Build k-d tree for spatial indexing
return new GtfsSchedule(agencies, calendars, stops, routes, trips);
}

Expand Down
36 changes: 0 additions & 36 deletions src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleDay.java

This file was deleted.

6 changes: 3 additions & 3 deletions src/main/java/ch/naviqore/gtfs/schedule/model/Stop.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ch.naviqore.gtfs.schedule.model;

import ch.naviqore.gtfs.schedule.spatial.Coordinate;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand All @@ -14,8 +15,7 @@
public final class Stop implements Initializable {
private final String id;
private final String name;
private final double lat;
private final double lon;
private final Coordinate coordinate;
private final List<StopTime> stopTimes = new ArrayList<>();

void addStopTime(StopTime stopTime) {
Expand All @@ -42,6 +42,6 @@ public int hashCode() {

@Override
public String toString() {
return "Stop[" + "id=" + id + ", " + "name=" + name + ", " + "lat=" + lat + ", " + "lon=" + lon + ']';
return "Stop[" + "id=" + id + ", " + "name=" + name + ']';
}
}
31 changes: 31 additions & 0 deletions src/main/java/ch/naviqore/gtfs/schedule/spatial/Coordinate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ch.naviqore.gtfs.schedule.spatial;

import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Coordinate {

private static final int EARTH_RADIUS = 6371000;
private final double latitude;
private final double 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.
*/
public double distanceTo(Coordinate other) {
double latDistance = Math.toRadians(other.latitude - this.latitude);
double lonDistance = Math.toRadians(other.longitude - this.longitude);
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) + Math.cos(
Math.toRadians(this.latitude)) * Math.cos(Math.toRadians(other.latitude)) * Math.sin(
lonDistance / 2) * Math.sin(lonDistance / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS * c;
}
}
5 changes: 5 additions & 0 deletions src/main/java/ch/naviqore/gtfs/schedule/spatial/KdTree.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ch.naviqore.gtfs.schedule.spatial;

public class KdTree {
// TODO: Implement KdTree
}
2 changes: 1 addition & 1 deletion src/main/java/ch/naviqore/raptor/model/RouteTraversal.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
public class RouteTraversal {
private final StopTime[] stopTimes;
private final Route[] routes;
private final Stop[] routeStops;
// private final Stop[] routeStops;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@

import ch.naviqore.gtfs.schedule.GtfsScheduleBenchmarkData.Dataset;
import ch.naviqore.gtfs.schedule.model.GtfsSchedule;
import ch.naviqore.gtfs.schedule.model.GtfsScheduleDay;
import ch.naviqore.gtfs.schedule.model.Trip;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.time.LocalDate;
import java.util.List;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class GtfsScheduleBenchmark {
final class GtfsScheduleBenchmark {

private static final Dataset DATASET = Dataset.GERMANY;
private static final Dataset DATASET = Dataset.SWITZERLAND;

public static void main(String[] args) throws IOException, InterruptedException {
String path = GtfsScheduleBenchmarkData.get(DATASET);
GtfsSchedule schedule = new GtfsScheduleReader().read(path);
GtfsScheduleDay scheduleDay = schedule.getScheduleForDay(LocalDate.now());
List<Trip> activeTrips = schedule.getActiveTrips(LocalDate.now());
// clean heap from reading artifacts
System.gc();
// monitor effect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Log4j2
public final class GtfsScheduleBenchmarkData {
final class GtfsScheduleBenchmarkData {
private static final Path DATA_DIRECTORY = Path.of("benchmark/input");
private static final HttpClient httpClient = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
Expand Down
20 changes: 18 additions & 2 deletions src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleReaderIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
import java.io.IOException;
import java.nio.file.Path;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Integration test for {@link GtfsScheduleReader}
* <p>
* Read GTFS schedule data from a ZIP file and a directory.
*
* @author munterfi
*/
class GtfsScheduleReaderIT {

private GtfsScheduleReader gtfsScheduleReader;
Expand All @@ -22,13 +31,20 @@ void setUp() {
void readFromZipFile(@TempDir Path tempDir) throws IOException {
File zipFile = GtfsScheduleTestData.prepareZipDataset(tempDir);
GtfsSchedule schedule = gtfsScheduleReader.read(zipFile.getAbsolutePath());
// assertFalse(records.isEmpty(), "The records map should not be empty");
assertScheduleSizes(schedule);
}

@Test
void readFromDirectory(@TempDir Path tempDir) throws IOException {
File unzippedDir = GtfsScheduleTestData.prepareUnzippedDataset(tempDir);
GtfsSchedule schedule = gtfsScheduleReader.read(unzippedDir.getAbsolutePath());
// assertFalse(records.isEmpty(), "The records map should not be empty");
assertScheduleSizes(schedule);
}

private void assertScheduleSizes(GtfsSchedule schedule) {
assertThat(schedule.getAgencies()).as("Agencies").hasSize(1);
assertThat(schedule.getRoutes()).as("Routes").hasSize(5);
assertThat(schedule.getStops()).as("Stops").hasSize(9);
assertThat(schedule.getTrips()).as("Trips").hasSize(11);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
* @author munterfi
*/
public final class GtfsScheduleTestData {
final class GtfsScheduleTestData {

public static final String SAMPLE_FEED = "sample-feed-1";
public static final String SAMPLE_FEED_ZIP = SAMPLE_FEED + ".zip";
Expand Down
Loading

0 comments on commit ee0a7ba

Please sign in to comment.