Skip to content

Commit

Permalink
Merge pull request #7 from naviqore/feature/NAV-18-read-transfers-not…
Browse files Browse the repository at this point in the history
…-required-by-gtfs
  • Loading branch information
munterfi authored May 15, 2024
2 parents efb2890 + 2129c23 commit a37759d
Show file tree
Hide file tree
Showing 23 changed files with 640 additions and 274 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.1.0</version>
<scope>compile</scope>
</dependency>
<!-- logging -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/ch/naviqore/gtfs/schedule/GtfsScheduleFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ enum GtfsScheduleFile {
ROUTES("routes.txt", Presence.REQUIRED),
// SHAPES("shapes.txt", Presence.OPTIONAL),
TRIPS("trips.txt", Presence.REQUIRED),
STOP_TIMES("stop_times.txt", Presence.REQUIRED);
// TRANSFERS("transfers.txt", Presence.OPTIONAL);
STOP_TIMES("stop_times.txt", Presence.REQUIRED),
TRANSFERS("transfers.txt", Presence.OPTIONAL);

private final String fileName;
private final Presence presence;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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 lombok.extern.log4j.Log4j2;
import org.apache.commons.csv.CSVRecord;

Expand Down Expand Up @@ -50,6 +51,7 @@ private void initializeParsers() {
parsers.put(GtfsScheduleFile.ROUTES, this::parseRoute);
parsers.put(GtfsScheduleFile.TRIPS, this::parseTrips);
parsers.put(GtfsScheduleFile.STOP_TIMES, this::parseStopTimes);
parsers.put(GtfsScheduleFile.TRANSFERS, this::parseTransfers);
}

private void parseAgency(CSVRecord record) {
Expand Down Expand Up @@ -107,4 +109,11 @@ private void parseStopTimes(CSVRecord record) {
e.getMessage());
}
}

private void parseTransfers(CSVRecord record) {
String minTransferTime = record.get("min_transfer_time");
builder.addTransfer(record.get("from_stop_id"), record.get("to_stop_id"),
TransferType.parse(record.get("transfer_type")),
minTransferTime.isEmpty() ? null : Integer.parseInt(record.get("min_transfer_time")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class GtfsScheduleReader {
private static void readFromDirectory(File directory, GtfsScheduleParser parser) throws IOException {
for (GtfsScheduleFile fileType : GtfsScheduleFile.values()) {
File csvFile = new File(directory, fileType.getFileName());

if (csvFile.exists()) {
log.info("Reading GTFS CSV file: {}", csvFile.getAbsolutePath());
readCsvFile(csvFile, parser, fileType);
Expand All @@ -51,6 +52,7 @@ private static void readFromZip(File zipFile, GtfsScheduleParser parser) throws
try (ZipFile zf = new ZipFile(zipFile, StandardCharsets.UTF_8)) {
for (GtfsScheduleFile fileType : GtfsScheduleFile.values()) {
ZipEntry entry = zf.getEntry(fileType.getFileName());

if (entry != null) {
log.info("Reading GTFS file from ZIP: {}", entry.getName());
try (InputStreamReader reader = new InputStreamReader(BOMInputStream.builder()
Expand Down
13 changes: 10 additions & 3 deletions src/main/java/ch/naviqore/gtfs/schedule/model/Calendar.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@

@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
public final class Calendar {
public final class Calendar implements Initializable {

private final String id;
private final EnumSet<DayOfWeek> serviceDays;
private final LocalDate startDate;
private final LocalDate endDate;
private final Map<LocalDate, CalendarDate> calendarDates = new HashMap<>();
private final List<Trip> trips = new ArrayList<>();
private Map<LocalDate, CalendarDate> calendarDates = new HashMap<>();
private List<Trip> trips = new ArrayList<>();

/**
* Determines if the service is operational on a specific day, considering both regular service days and
Expand All @@ -38,6 +38,13 @@ public boolean isServiceAvailable(LocalDate date) {
return serviceDays.contains(date.getDayOfWeek());
}

@Override
public void initialize() {
Collections.sort(trips);
trips = List.copyOf(trips);
calendarDates = Map.copyOf(calendarDates);
}

void addCalendarDate(CalendarDate calendarDate) {
calendarDates.put(calendarDate.date(), calendarDate);
}
Expand Down
43 changes: 19 additions & 24 deletions src/main/java/ch/naviqore/gtfs/schedule/model/GtfsSchedule.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
package ch.naviqore.gtfs.schedule.model;

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

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.
* This is an immutable class, meaning that once an instance is created, it cannot be modified. Use the
* {@link GtfsScheduleBuilder} to construct a GTFS schedule instance.
*
* @author munterfi
*/
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
public class GtfsSchedule {

private final Map<String, Agency> agencies;
Expand All @@ -27,6 +26,21 @@ public class GtfsSchedule {
private final Map<String, Route> routes;
private final Map<String, Trip> trips;

/**
* Constructs an immutable GTFS schedule.
* <p>
* Each map passed to this constructor is copied into an immutable map to prevent further modification and to
* enhance memory efficiency and thread-safety in a concurrent environment.
*/
GtfsSchedule(Map<String, Agency> agencies, Map<String, Calendar> calendars, Map<String, Stop> stops,
Map<String, Route> routes, Map<String, Trip> trips) {
this.agencies = Map.copyOf(agencies);
this.calendars = Map.copyOf(calendars);
this.stops = Map.copyOf(stops);
this.routes = Map.copyOf(routes);
this.trips = Map.copyOf(trips);
}

/**
* Creates a new GTFS schedule builder.
*
Expand Down Expand Up @@ -88,23 +102,4 @@ public List<Trip> getActiveTrips(LocalDate date) {
.collect(Collectors.toList());
}

public Map<String, Agency> getAgencies() {
return Collections.unmodifiableMap(agencies);
}

public Map<String, Calendar> getCalendars() {
return Collections.unmodifiableMap(calendars);
}

public Map<String, Stop> getStops() {
return Collections.unmodifiableMap(stops);
}

public Map<String, Route> getRoutes() {
return Collections.unmodifiableMap(routes);
}

public Map<String, Trip> getTrips() {
return Collections.unmodifiableMap(trips);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
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 lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.Nullable;

import java.time.DayOfWeek;
import java.time.LocalDate;
Expand All @@ -16,15 +18,11 @@
import java.util.concurrent.ConcurrentHashMap;

/**
* General Transit Feed Specification (GTFS) schedule builder
* Implements a builder pattern for constructing instances of {@link GtfsSchedule}. This builder helps assemble a GTFS
* schedule by adding components like agencies, stops, routes, trips, and calendars in a controlled and consistent
* manner.
* <p>
* Provides a builder pattern implementation for constructing a {@link GtfsSchedule} instance. This class encapsulates
* the complexity of assembling a GTFS schedule by incrementally adding components such as agencies, stops, routes,
* trips, and calendars. The builder ensures that all components are added in a controlled manner and that the resulting
* schedule is consistent and ready for use.
*
* <p>Instances of this class should be obtained through the static {@code builder()} method. This class uses
* a private constructor to enforce the use of the builder pattern.</p>
* Use {@link GtfsSchedule#builder()} to obtain an instance.
*
* @author munterfi
*/
Expand All @@ -39,7 +37,10 @@ public class GtfsScheduleBuilder {
private final Map<String, Route> routes = new HashMap<>();
private final Map<String, Trip> trips = new HashMap<>();

private boolean built = false;

public GtfsScheduleBuilder addAgency(String id, String name, String url, String timezone) {
checkNotBuilt();
if (agencies.containsKey(id)) {
throw new IllegalArgumentException("Agency " + id + " already exists");
}
Expand All @@ -49,6 +50,7 @@ public GtfsScheduleBuilder addAgency(String id, String name, String url, String
}

public GtfsScheduleBuilder addStop(String id, String name, double lat, double lon) {
checkNotBuilt();
if (stops.containsKey(id)) {
throw new IllegalArgumentException("Agency " + id + " already exists");
}
Expand All @@ -58,6 +60,7 @@ public GtfsScheduleBuilder addStop(String id, String name, double lat, double lo
}

public GtfsScheduleBuilder addRoute(String id, String agencyId, String shortName, String longName, RouteType type) {
checkNotBuilt();
if (routes.containsKey(id)) {
throw new IllegalArgumentException("Route " + id + " already exists");
}
Expand All @@ -72,6 +75,7 @@ public GtfsScheduleBuilder addRoute(String id, String agencyId, String shortName

public GtfsScheduleBuilder addCalendar(String id, EnumSet<DayOfWeek> serviceDays, LocalDate startDate,
LocalDate endDate) {
checkNotBuilt();
if (calendars.containsKey(id)) {
throw new IllegalArgumentException("Calendar " + id + " already exists");
}
Expand All @@ -81,6 +85,7 @@ public GtfsScheduleBuilder addCalendar(String id, EnumSet<DayOfWeek> serviceDays
}

public GtfsScheduleBuilder addCalendarDate(String calendarId, LocalDate date, ExceptionType type) {
checkNotBuilt();
Calendar calendar = calendars.get(calendarId);
if (calendar == null) {
throw new IllegalArgumentException("Calendar " + calendarId + " does not exist");
Expand All @@ -92,6 +97,7 @@ public GtfsScheduleBuilder addCalendarDate(String calendarId, LocalDate date, Ex
}

public GtfsScheduleBuilder addTrip(String id, String routeId, String serviceId) {
checkNotBuilt();
if (trips.containsKey(id)) {
throw new IllegalArgumentException("Trip " + id + " already exists");
}
Expand All @@ -113,6 +119,7 @@ public GtfsScheduleBuilder addTrip(String id, String routeId, String serviceId)

public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayTime arrival,
ServiceDayTime departure) {
checkNotBuilt();
Trip trip = trips.get(tripId);
if (trip == null) {
throw new IllegalArgumentException("Trip " + tripId + " does not exist");
Expand All @@ -121,20 +128,79 @@ public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayT
if (stop == null) {
throw new IllegalArgumentException("Stop " + stopId + " does not exist");
}
log.debug("Adding stop {} to trip {} ({}-{})", stopId, tripId, arrival, departure);
log.debug("Adding stop time at {} to trip {} ({}-{})", stopId, tripId, arrival, departure);
StopTime stopTime = new StopTime(stop, trip, cache.getOrAdd(arrival), cache.getOrAdd(departure));
stop.addStopTime(stopTime);
trip.addStopTime(stopTime);
return this;
}

public GtfsScheduleBuilder addTransfer(String fromStopId, String toStopId, TransferType transferType,
@Nullable Integer minTransferTime) {
checkNotBuilt();
Stop fromStop = stops.get(fromStopId);
if (fromStop == null) {
throw new IllegalArgumentException("Stop " + fromStopId + " does not exist");
}
Stop toStop = stops.get(toStopId);
if (toStop == null) {
throw new IllegalArgumentException("Stop " + toStopId + " does not exist");
}
if (transferType == TransferType.MINIMUM_TIME && minTransferTime == null) {
throw new IllegalArgumentException(
"Minimal transfer time is not present for transfer of type " + transferType.name() + " from stop " + fromStopId + " to stop " + toStopId);
}
log.debug("Adding transfer {}-{} of type {} {}", fromStopId, toStopId, transferType, minTransferTime);
fromStop.addTransfer(new Transfer(fromStop, toStop, transferType, minTransferTime));
return this;
}

/**
* Constructs and returns a {@link GtfsSchedule} using the current builder state.
* <p>
* This method finalizes the schedule and initializes all components. After this method is called, the builder is
* cleared and cannot be used to build another schedule without being reset.
*
* @return The constructed {@link GtfsSchedule}.
* @throws IllegalStateException if the builder has already built a schedule.
*/
public GtfsSchedule build() {
checkNotBuilt();
log.info("Building schedule with {} stops, {} routes and {} trips", stops.size(), routes.size(), trips.size());
trips.values().parallelStream().forEach(Trip::initialize);
stops.values().parallelStream().forEach(Stop::initialize);
routes.values().parallelStream().forEach(Route::initialize);
trips.values().parallelStream().forEach(Initializable::initialize);
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
return new GtfsSchedule(agencies, calendars, stops, routes, trips);
GtfsSchedule schedule = new GtfsSchedule(agencies, calendars, stops, routes, trips);
clear();
built = true;
return schedule;
}

/**
* Resets the builder to its initial state, allowing it to be reused.
*/
public void reset() {
log.debug("Resetting builder");
clear();
built = false;
}

private void clear() {
log.debug("Clearing maps and cache of the builder");
agencies.clear();
calendars.clear();
stops.clear();
routes.clear();
trips.clear();
cache.clear();
}

private void checkNotBuilt() {
if (built) {
throw new IllegalStateException("Cannot modify builder after build() has been called.");
}
}

/**
Expand All @@ -151,5 +217,10 @@ public LocalDate getOrAdd(LocalDate value) {
public ServiceDayTime getOrAdd(ServiceDayTime value) {
return serviceDayTimes.computeIfAbsent(value, k -> value);
}

public void clear() {
localDates.clear();
serviceDayTimes.clear();
}
}
}
4 changes: 3 additions & 1 deletion src/main/java/ch/naviqore/gtfs/schedule/model/Route.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
public final class Route implements Initializable {

private final String id;
private final Agency agency;
private final String shortName;
private final String longName;
private final RouteType type;
private final List<Trip> trips = new ArrayList<>();
private List<Trip> trips = new ArrayList<>();

void addTrip(Trip trip) {
trips.add(trip);
Expand All @@ -27,6 +28,7 @@ void addTrip(Trip trip) {
@Override
public void initialize() {
Collections.sort(trips);
trips = List.copyOf(trips);
}

@Override
Expand Down
Loading

0 comments on commit a37759d

Please sign in to comment.