vehicleParkingToRemove = new ArrayList<>();
for (VehicleParkingEntranceVertex vehicleParkingEntranceVertex : graph.getVerticesOfType(
VehicleParkingEntranceVertex.class
)) {
- if (vehicleParkingEntranceHasLinks(vehicleParkingEntranceVertex)) {
+ if (vehicleParkingEntranceVertex.isLinkedToGraph()) {
continue;
}
@@ -296,22 +292,6 @@ private void linkVehicleParks(Graph graph, DataImportIssueStore issueStore) {
var vehicleParkingService = graph.getVehicleParkingService();
vehicleParkingService.updateVehicleParking(List.of(), vehicleParkingToRemove);
}
- graph.hasLinkedBikeParks = true;
- }
-
- private boolean vehicleParkingEntranceHasLinks(
- VehicleParkingEntranceVertex vehicleParkingEntranceVertex
- ) {
- return !(
- vehicleParkingEntranceVertex
- .getIncoming()
- .stream()
- .allMatch(VehicleParkingEdge.class::isInstance) &&
- vehicleParkingEntranceVertex
- .getOutgoing()
- .stream()
- .allMatch(VehicleParkingEdge.class::isInstance)
- );
}
/**
diff --git a/src/main/java/org/opentripplanner/inspector/vector/KeyValue.java b/src/main/java/org/opentripplanner/inspector/vector/KeyValue.java
index 6c8b0f3aa4e..edad5e1b295 100644
--- a/src/main/java/org/opentripplanner/inspector/vector/KeyValue.java
+++ b/src/main/java/org/opentripplanner/inspector/vector/KeyValue.java
@@ -1,7 +1,43 @@
package org.opentripplanner.inspector.vector;
+import jakarta.annotation.Nullable;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import org.opentripplanner.transit.model.framework.FeedScopedId;
+
+/**
+ * A key value pair that represents data being sent to the vector tile library for visualisation
+ * in a map (including popups).
+ *
+ * The underlying format (and library) supports only a limited number of Java types and silently
+ * drops those that aren't supported: https://github.com/CI-CMG/mapbox-vector-tile/blob/master/src/main/java/edu/colorado/cires/cmg/mvt/encoding/MvtValue.java#L18-L40
+ *
+ * For this reason this class also has static initializer that automatically converts common
+ * OTP classes into vector tile-compatible strings.
+ */
public record KeyValue(String key, Object value) {
public static KeyValue kv(String key, Object value) {
return new KeyValue(key, value);
}
+
+ /**
+ * A {@link FeedScopedId} is not a type that can be converted to a vector tile feature property
+ * value. Therefore, we convert it to a string after performing a null check.
+ */
+ public static KeyValue kv(String key, @Nullable FeedScopedId value) {
+ if (value != null) {
+ return new KeyValue(key, value.toString());
+ } else {
+ return new KeyValue(key, null);
+ }
+ }
+
+ /**
+ * Takes a key and a collection of values, calls toString on the values and joins them using
+ * comma as the separator.
+ */
+ public static KeyValue kColl(String key, Collection> value) {
+ var values = value.stream().map(Object::toString).collect(Collectors.joining(","));
+ return new KeyValue(key, values);
+ }
}
diff --git a/src/main/java/org/opentripplanner/inspector/vector/vertex/VertexPropertyMapper.java b/src/main/java/org/opentripplanner/inspector/vector/vertex/VertexPropertyMapper.java
index a493269cc3b..01f5263b11a 100644
--- a/src/main/java/org/opentripplanner/inspector/vector/vertex/VertexPropertyMapper.java
+++ b/src/main/java/org/opentripplanner/inspector/vector/vertex/VertexPropertyMapper.java
@@ -1,15 +1,22 @@
package org.opentripplanner.inspector.vector.vertex;
+import static org.opentripplanner.inspector.vector.KeyValue.kColl;
import static org.opentripplanner.inspector.vector.KeyValue.kv;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import org.opentripplanner.apis.support.mapping.PropertyMapper;
import org.opentripplanner.framework.collection.ListUtils;
import org.opentripplanner.inspector.vector.KeyValue;
+import org.opentripplanner.routing.vehicle_parking.VehicleParking;
+import org.opentripplanner.routing.vehicle_parking.VehicleParkingEntrance;
import org.opentripplanner.service.vehiclerental.street.VehicleRentalPlaceVertex;
import org.opentripplanner.street.model.vertex.BarrierVertex;
+import org.opentripplanner.street.model.vertex.VehicleParkingEntranceVertex;
import org.opentripplanner.street.model.vertex.Vertex;
+import org.opentripplanner.street.search.TraverseMode;
public class VertexPropertyMapper extends PropertyMapper {
@@ -22,9 +29,36 @@ protected Collection map(Vertex input) {
List properties =
switch (input) {
case BarrierVertex v -> List.of(kv("permission", v.getBarrierPermissions().toString()));
- case VehicleRentalPlaceVertex v -> List.of(kv("rentalId", v.getStation().getId()));
+ case VehicleRentalPlaceVertex v -> List.of(kv("rentalId", v.getStation()));
+ case VehicleParkingEntranceVertex v -> List.of(
+ kv("parkingId", v.getVehicleParking().getId()),
+ kColl("spacesFor", spacesFor(v.getVehicleParking())),
+ kColl("traversalPermission", traversalPermissions(v.getParkingEntrance()))
+ );
default -> List.of();
};
return ListUtils.combine(baseProps, properties);
}
+
+ private Set spacesFor(VehicleParking vehicleParking) {
+ var ret = new HashSet();
+ if (vehicleParking.hasAnyCarPlaces()) {
+ ret.add(TraverseMode.CAR);
+ }
+ if (vehicleParking.hasBicyclePlaces()) {
+ ret.add(TraverseMode.BICYCLE);
+ }
+ return ret;
+ }
+
+ private Set traversalPermissions(VehicleParkingEntrance entrance) {
+ var ret = new HashSet();
+ if (entrance.isCarAccessible()) {
+ ret.add(TraverseMode.CAR);
+ }
+ if (entrance.isWalkAccessible()) {
+ ret.add(TraverseMode.WALK);
+ }
+ return ret;
+ }
}
diff --git a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java
index 43c18cec59d..373b99f0bc6 100644
--- a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java
+++ b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java
@@ -24,6 +24,7 @@
import org.opentripplanner.model.transfer.ConstrainedTransfer;
import org.opentripplanner.model.transfer.TransferPoint;
import org.opentripplanner.routing.api.request.framework.TimePenalty;
+import org.opentripplanner.routing.vehicle_parking.VehicleParking;
import org.opentripplanner.transit.model.basic.Notice;
import org.opentripplanner.transit.model.framework.AbstractTransitEntity;
import org.opentripplanner.transit.model.framework.DefaultEntityById;
@@ -116,6 +117,8 @@ public class OtpTransitServiceBuilder {
private final EntityById groupOfRouteById = new DefaultEntityById<>();
+ private final List vehicleParkings = new ArrayList<>();
+
private final DataImportIssueStore issueStore;
public OtpTransitServiceBuilder(StopModel stopModel, DataImportIssueStore issueStore) {
@@ -264,6 +267,14 @@ public CalendarServiceData buildCalendarServiceData() {
);
}
+ /**
+ * The list of parking lots contained in the transit data (so far only NeTEx).
+ * Note that parking lots can also be sourced from OSM data as well as realtime updaters.
+ */
+ public List vehicleParkings() {
+ return vehicleParkings;
+ }
+
public OtpTransitService build() {
return new OtpTransitServiceImpl(this);
}
diff --git a/src/main/java/org/opentripplanner/netex/NetexBundle.java b/src/main/java/org/opentripplanner/netex/NetexBundle.java
index 8d6f098de89..3cd52cd246e 100644
--- a/src/main/java/org/opentripplanner/netex/NetexBundle.java
+++ b/src/main/java/org/opentripplanner/netex/NetexBundle.java
@@ -9,6 +9,7 @@
import org.opentripplanner.datastore.api.DataSource;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.model.impl.OtpTransitServiceBuilder;
+import org.opentripplanner.netex.config.IgnorableFeature;
import org.opentripplanner.netex.config.NetexFeedParameters;
import org.opentripplanner.netex.index.NetexEntityIndex;
import org.opentripplanner.netex.loader.GroupEntries;
@@ -45,7 +46,7 @@ public class NetexBundle implements Closeable {
private final Set ferryIdsNotAllowedForBicycle;
private final double maxStopToShapeSnapDistance;
private final boolean noTransfersOnIsolatedStops;
- private final boolean ignoreFareFrame;
+ private final Set ignoredFeatures;
/** The NeTEx entities loaded from the input files and passed on to the mapper. */
private NetexEntityIndex index = new NetexEntityIndex();
/** Report errors to issue store */
@@ -62,7 +63,7 @@ public NetexBundle(
Set ferryIdsNotAllowedForBicycle,
double maxStopToShapeSnapDistance,
boolean noTransfersOnIsolatedStops,
- boolean ignoreFareFrame
+ Set ignorableFeatures
) {
this.feedId = feedId;
this.source = source;
@@ -71,7 +72,7 @@ public NetexBundle(
this.ferryIdsNotAllowedForBicycle = ferryIdsNotAllowedForBicycle;
this.maxStopToShapeSnapDistance = maxStopToShapeSnapDistance;
this.noTransfersOnIsolatedStops = noTransfersOnIsolatedStops;
- this.ignoreFareFrame = ignoreFareFrame;
+ this.ignoredFeatures = Set.copyOf(ignorableFeatures);
}
/** load the bundle, map it to the OTP transit model and return */
@@ -136,7 +137,7 @@ private void loadFileEntries() {
});
}
mapper.finishUp();
- NetexDocumentParser.finnishUp();
+ NetexDocumentParser.finishUp();
}
/**
@@ -179,7 +180,7 @@ private void loadSingeFileEntry(String fileDescription, DataSource entry) {
LOG.info("reading entity {}: {}", fileDescription, entry.name());
issueStore.startProcessingSource(entry.name());
PublicationDeliveryStructure doc = xmlParser.parseXmlDoc(entry.asInputStream());
- NetexDocumentParser.parseAndPopulateIndex(index, doc, ignoreFareFrame);
+ NetexDocumentParser.parseAndPopulateIndex(index, doc, ignoredFeatures);
} catch (JAXBException e) {
throw new RuntimeException(e.getMessage(), e);
} finally {
diff --git a/src/main/java/org/opentripplanner/netex/NetexModule.java b/src/main/java/org/opentripplanner/netex/NetexModule.java
index b9a05d25b10..2bf3403395c 100644
--- a/src/main/java/org/opentripplanner/netex/NetexModule.java
+++ b/src/main/java/org/opentripplanner/netex/NetexModule.java
@@ -13,6 +13,7 @@
import org.opentripplanner.model.calendar.ServiceDateInterval;
import org.opentripplanner.model.impl.OtpTransitServiceBuilder;
import org.opentripplanner.routing.graph.Graph;
+import org.opentripplanner.routing.vehicle_parking.VehicleParkingHelper;
import org.opentripplanner.standalone.config.BuildConfig;
import org.opentripplanner.transit.service.TransitModel;
@@ -100,6 +101,11 @@ public void buildGraph() {
);
transitModel.validateTimeZones();
+
+ var lots = transitBuilder.vehicleParkings();
+ graph.getVehicleParkingService().updateVehicleParking(lots, List.of());
+ var linker = new VehicleParkingHelper(graph);
+ lots.forEach(linker::linkVehicleParkingToGraph);
}
transitModel.updateCalendarServiceData(hasActiveTransit, calendarServiceData, issueStore);
diff --git a/src/main/java/org/opentripplanner/netex/config/IgnorableFeature.java b/src/main/java/org/opentripplanner/netex/config/IgnorableFeature.java
new file mode 100644
index 00000000000..53fe7f87f48
--- /dev/null
+++ b/src/main/java/org/opentripplanner/netex/config/IgnorableFeature.java
@@ -0,0 +1,9 @@
+package org.opentripplanner.netex.config;
+
+/**
+ * Optional data that can be ignored during the NeTEx parsing process.
+ */
+public enum IgnorableFeature {
+ FARE_FRAME,
+ PARKING,
+}
diff --git a/src/main/java/org/opentripplanner/netex/config/NetexFeedParameters.java b/src/main/java/org/opentripplanner/netex/config/NetexFeedParameters.java
index cffecea0d48..0c6c75c4db3 100644
--- a/src/main/java/org/opentripplanner/netex/config/NetexFeedParameters.java
+++ b/src/main/java/org/opentripplanner/netex/config/NetexFeedParameters.java
@@ -1,6 +1,8 @@
package org.opentripplanner.netex.config;
import static java.util.Objects.requireNonNull;
+import static org.opentripplanner.netex.config.IgnorableFeature.FARE_FRAME;
+import static org.opentripplanner.netex.config.IgnorableFeature.PARKING;
import java.net.URI;
import java.util.Collection;
@@ -29,7 +31,7 @@ public class NetexFeedParameters implements DataSourceConfig {
private static final String SHARED_GROUP_FILE_PATTERN = "(\\w{3})-.*-shared\\.xml";
private static final String GROUP_FILE_PATTERN = "(\\w{3})-.*\\.xml";
private static final boolean NO_TRANSFERS_ON_ISOLATED_STOPS = false;
- private static final boolean IGNORE_FARE_FRAME = false;
+ private static final Set IGNORED_FEATURES = Set.of(PARKING);
private static final Set FERRY_IDS_NOT_ALLOWED_FOR_BICYCLE = Collections.emptySet();
@@ -48,7 +50,7 @@ public class NetexFeedParameters implements DataSourceConfig {
private final String ignoreFilePattern;
private final Set ferryIdsNotAllowedForBicycle;
private final boolean noTransfersOnIsolatedStops;
- private final boolean ignoreFareFrame;
+ private final Set ignoredFeatures;
private NetexFeedParameters() {
this.source = null;
@@ -63,7 +65,7 @@ private NetexFeedParameters() {
}
this.ferryIdsNotAllowedForBicycle = FERRY_IDS_NOT_ALLOWED_FOR_BICYCLE;
this.noTransfersOnIsolatedStops = NO_TRANSFERS_ON_ISOLATED_STOPS;
- this.ignoreFareFrame = IGNORE_FARE_FRAME;
+ this.ignoredFeatures = IGNORED_FEATURES;
}
private NetexFeedParameters(Builder builder) {
@@ -75,7 +77,7 @@ private NetexFeedParameters(Builder builder) {
this.ignoreFilePattern = requireNonNull(builder.ignoreFilePattern);
this.ferryIdsNotAllowedForBicycle = Set.copyOf(builder.ferryIdsNotAllowedForBicycle);
this.noTransfersOnIsolatedStops = builder.noTransfersOnIsolatedStops;
- this.ignoreFareFrame = builder.ignoreFareFrame;
+ this.ignoredFeatures = Set.copyOf(builder.ignoredFeatures);
}
public static Builder of() {
@@ -127,7 +129,11 @@ public boolean noTransfersOnIsolatedStops() {
/** See {@link org.opentripplanner.standalone.config.buildconfig.NetexConfig}. */
public boolean ignoreFareFrame() {
- return ignoreFareFrame;
+ return ignoredFeatures.contains(FARE_FRAME);
+ }
+
+ public boolean ignoreParking() {
+ return ignoredFeatures.contains(PARKING);
}
@Override
@@ -142,7 +148,7 @@ public boolean equals(Object o) {
sharedFilePattern.equals(that.sharedFilePattern) &&
sharedGroupFilePattern.equals(that.sharedGroupFilePattern) &&
groupFilePattern.equals(that.groupFilePattern) &&
- ignoreFareFrame == that.ignoreFareFrame &&
+ ignoredFeatures.equals(that.ignoredFeatures) &&
ferryIdsNotAllowedForBicycle.equals(that.ferryIdsNotAllowedForBicycle)
);
}
@@ -156,7 +162,7 @@ public int hashCode() {
sharedFilePattern,
sharedGroupFilePattern,
groupFilePattern,
- ignoreFareFrame,
+ ignoredFeatures,
ferryIdsNotAllowedForBicycle
);
}
@@ -171,11 +177,15 @@ public String toString() {
.addStr("sharedGroupFilePattern", sharedGroupFilePattern, DEFAULT.sharedGroupFilePattern)
.addStr("groupFilePattern", groupFilePattern, DEFAULT.groupFilePattern)
.addStr("ignoreFilePattern", ignoreFilePattern, DEFAULT.ignoreFilePattern)
- .addBoolIfTrue("ignoreFareFrame", ignoreFareFrame)
+ .addCol("ignoredFeatures", ignoredFeatures)
.addCol("ferryIdsNotAllowedForBicycle", ferryIdsNotAllowedForBicycle, Set.of())
.toString();
}
+ public Set ignoredFeatures() {
+ return ignoredFeatures;
+ }
+
public static class Builder {
private final NetexFeedParameters original;
@@ -187,7 +197,7 @@ public static class Builder {
private String ignoreFilePattern;
private final Set ferryIdsNotAllowedForBicycle = new HashSet<>();
private boolean noTransfersOnIsolatedStops;
- private boolean ignoreFareFrame;
+ private final Set ignoredFeatures;
private Builder(NetexFeedParameters original) {
this.original = original;
@@ -199,7 +209,7 @@ private Builder(NetexFeedParameters original) {
this.ignoreFilePattern = original.ignoreFilePattern;
this.ferryIdsNotAllowedForBicycle.addAll(original.ferryIdsNotAllowedForBicycle);
this.noTransfersOnIsolatedStops = original.noTransfersOnIsolatedStops;
- this.ignoreFareFrame = original.ignoreFareFrame;
+ this.ignoredFeatures = new HashSet<>(original.ignoredFeatures);
}
public URI source() {
@@ -247,7 +257,19 @@ public Builder withNoTransfersOnIsolatedStops(boolean noTransfersOnIsolatedStops
}
public Builder withIgnoreFareFrame(boolean ignoreFareFrame) {
- this.ignoreFareFrame = ignoreFareFrame;
+ return applyIgnore(ignoreFareFrame, FARE_FRAME);
+ }
+
+ public Builder withIgnoreParking(boolean ignoreParking) {
+ return applyIgnore(ignoreParking, PARKING);
+ }
+
+ private Builder applyIgnore(boolean ignore, IgnorableFeature feature) {
+ if (ignore) {
+ ignoredFeatures.add(feature);
+ } else {
+ ignoredFeatures.remove(feature);
+ }
return this;
}
diff --git a/src/main/java/org/opentripplanner/netex/configure/NetexConfigure.java b/src/main/java/org/opentripplanner/netex/configure/NetexConfigure.java
index 464c03f28e1..50c49836246 100644
--- a/src/main/java/org/opentripplanner/netex/configure/NetexConfigure.java
+++ b/src/main/java/org/opentripplanner/netex/configure/NetexConfigure.java
@@ -75,7 +75,7 @@ public NetexBundle netexBundle(
config.ferryIdsNotAllowedForBicycle(),
buildParams.maxStopToShapeSnapDistance,
config.noTransfersOnIsolatedStops(),
- config.ignoreFareFrame()
+ config.ignoredFeatures()
);
}
diff --git a/src/main/java/org/opentripplanner/netex/index/NetexEntityIndex.java b/src/main/java/org/opentripplanner/netex/index/NetexEntityIndex.java
index 0ca734d5b6a..0cb94f66a77 100644
--- a/src/main/java/org/opentripplanner/netex/index/NetexEntityIndex.java
+++ b/src/main/java/org/opentripplanner/netex/index/NetexEntityIndex.java
@@ -28,6 +28,7 @@
import org.rutebanken.netex.model.OperatingDay;
import org.rutebanken.netex.model.OperatingPeriod_VersionStructure;
import org.rutebanken.netex.model.Operator;
+import org.rutebanken.netex.model.Parking;
import org.rutebanken.netex.model.Quay;
import org.rutebanken.netex.model.Route;
import org.rutebanken.netex.model.ServiceJourney;
@@ -97,8 +98,9 @@ public class NetexEntityIndex {
public final HierarchicalVersionMapById stopPlaceById;
public final HierarchicalVersionMapById tariffZonesById;
public final HierarchicalMapById brandingById;
+ public final HierarchicalMapById parkings;
- // Relations between entities - The Netex XML sometimes rely on the the
+ // Relations between entities - The Netex XML sometimes relies on the
// nested structure of the XML document, rater than explicit references.
// Since we throw away the document we need to keep track of these.
@@ -142,6 +144,7 @@ public NetexEntityIndex() {
this.tariffZonesById = new HierarchicalVersionMapById<>();
this.brandingById = new HierarchicalMapById<>();
this.timeZone = new HierarchicalElement<>();
+ this.parkings = new HierarchicalMapById<>();
}
/**
@@ -184,6 +187,7 @@ public NetexEntityIndex(NetexEntityIndex parent) {
this.tariffZonesById = new HierarchicalVersionMapById<>(parent.tariffZonesById);
this.brandingById = new HierarchicalMapById<>(parent.brandingById);
this.timeZone = new HierarchicalElement<>(parent.timeZone);
+ this.parkings = new HierarchicalMapById<>(parent.parkings);
}
/**
@@ -353,6 +357,11 @@ public ReadOnlyHierarchicalVersionMapById getStopPlaceById() {
return stopPlaceById;
}
+ @Override
+ public ReadOnlyHierarchicalMapById getParkingsById() {
+ return parkings;
+ }
+
@Override
public ReadOnlyHierarchicalVersionMapById getTariffZonesById() {
return tariffZonesById;
diff --git a/src/main/java/org/opentripplanner/netex/index/api/NetexEntityIndexReadOnlyView.java b/src/main/java/org/opentripplanner/netex/index/api/NetexEntityIndexReadOnlyView.java
index 3c7bc98b36a..37b8e9790b9 100644
--- a/src/main/java/org/opentripplanner/netex/index/api/NetexEntityIndexReadOnlyView.java
+++ b/src/main/java/org/opentripplanner/netex/index/api/NetexEntityIndexReadOnlyView.java
@@ -19,6 +19,7 @@
import org.rutebanken.netex.model.OperatingDay;
import org.rutebanken.netex.model.OperatingPeriod_VersionStructure;
import org.rutebanken.netex.model.Operator;
+import org.rutebanken.netex.model.Parking;
import org.rutebanken.netex.model.Quay;
import org.rutebanken.netex.model.Route;
import org.rutebanken.netex.model.ServiceJourney;
@@ -80,6 +81,8 @@ public interface NetexEntityIndexReadOnlyView {
ReadOnlyHierarchicalVersionMapById getStopPlaceById();
+ ReadOnlyHierarchicalMapById getParkingsById();
+
ReadOnlyHierarchicalVersionMapById getTariffZonesById();
ReadOnlyHierarchicalMapById getBrandingById();
diff --git a/src/main/java/org/opentripplanner/netex/loader/parser/NetexDocumentParser.java b/src/main/java/org/opentripplanner/netex/loader/parser/NetexDocumentParser.java
index 925b6dfd019..f8058f2df8e 100644
--- a/src/main/java/org/opentripplanner/netex/loader/parser/NetexDocumentParser.java
+++ b/src/main/java/org/opentripplanner/netex/loader/parser/NetexDocumentParser.java
@@ -1,8 +1,12 @@
package org.opentripplanner.netex.loader.parser;
+import static org.opentripplanner.netex.config.IgnorableFeature.FARE_FRAME;
+
import jakarta.xml.bind.JAXBElement;
import java.util.Collection;
import java.util.List;
+import java.util.Set;
+import org.opentripplanner.netex.config.IgnorableFeature;
import org.opentripplanner.netex.index.NetexEntityIndex;
import org.rutebanken.netex.model.Common_VersionFrameStructure;
import org.rutebanken.netex.model.CompositeFrame;
@@ -30,11 +34,11 @@ public class NetexDocumentParser {
private static final Logger LOG = LoggerFactory.getLogger(NetexDocumentParser.class);
private final NetexEntityIndex netexIndex;
- private final boolean ignoreFareFrame;
+ private final Set ignoredFeatures;
- private NetexDocumentParser(NetexEntityIndex netexIndex, boolean ignoreFareFrame) {
+ private NetexDocumentParser(NetexEntityIndex netexIndex, Set ignoredFeatures) {
this.netexIndex = netexIndex;
- this.ignoreFareFrame = ignoreFareFrame;
+ this.ignoredFeatures = ignoredFeatures;
}
/**
@@ -44,12 +48,12 @@ private NetexDocumentParser(NetexEntityIndex netexIndex, boolean ignoreFareFrame
public static void parseAndPopulateIndex(
NetexEntityIndex index,
PublicationDeliveryStructure doc,
- boolean ignoreFareFrame
+ Set ignoredFeatures
) {
- new NetexDocumentParser(index, ignoreFareFrame).parse(doc);
+ new NetexDocumentParser(index, ignoredFeatures).parse(doc);
}
- public static void finnishUp() {
+ public static void finishUp() {
ServiceFrameParser.logSummary();
}
@@ -74,8 +78,8 @@ private void parseCommonFrame(Common_VersionFrameStructure value) {
} else if (value instanceof ServiceFrame) {
parse((ServiceFrame) value, new ServiceFrameParser(netexIndex.flexibleStopPlaceById));
} else if (value instanceof SiteFrame) {
- parse((SiteFrame) value, new SiteFrameParser());
- } else if (!ignoreFareFrame && value instanceof FareFrame) {
+ parse((SiteFrame) value, new SiteFrameParser(ignoredFeatures));
+ } else if (!ignoredFeatures.contains(FARE_FRAME) && value instanceof FareFrame) {
parse((FareFrame) value, new FareFrameParser());
} else if (value instanceof CompositeFrame) {
// We recursively parse composite frames and content until there
diff --git a/src/main/java/org/opentripplanner/netex/loader/parser/NetexParser.java b/src/main/java/org/opentripplanner/netex/loader/parser/NetexParser.java
index 54b0043e072..3c24562ef6f 100644
--- a/src/main/java/org/opentripplanner/netex/loader/parser/NetexParser.java
+++ b/src/main/java/org/opentripplanner/netex/loader/parser/NetexParser.java
@@ -16,7 +16,7 @@
abstract class NetexParser {
/**
- * Currently a lot of elements on a frame is skipped. If any of these elements are pressent we
+ * Currently a lot of elements on a frame is skipped. If any of these elements are present we
* print a warning for elements that might be relevant for OTP and an info message for none
* relevant elements.
*/
@@ -39,10 +39,10 @@ static void verifyCommonUnusedPropertiesIsNotSet(Logger log, VersionFrame_Versio
/**
* Log a warning for Netex elements which is not mapped. There might be something wrong with the
* data or there might be something wrong with the Netex data import(ignoring these elements). The
- * element should be relevant to OTP. OTP do not support Netex 100%, but elements in Nordic
+ * element should be relevant to OTP. OTP does not support NeTEx 100%, but elements in the Nordic
* profile, see https://enturas.atlassian.net/wiki/spaces/PUBLIC/overview should be supported.
*
- * If you get this warning and think the element should be mapped, please feel free to report an
+ * If you see this warning and think the element should be mapped, please feel free to report an
* issue on GitHub.
*/
static void warnOnMissingMapping(Logger log, Object rel) {
diff --git a/src/main/java/org/opentripplanner/netex/loader/parser/SiteFrameParser.java b/src/main/java/org/opentripplanner/netex/loader/parser/SiteFrameParser.java
index 8cbe0c8aee6..38cd91ded98 100644
--- a/src/main/java/org/opentripplanner/netex/loader/parser/SiteFrameParser.java
+++ b/src/main/java/org/opentripplanner/netex/loader/parser/SiteFrameParser.java
@@ -1,14 +1,19 @@
package org.opentripplanner.netex.loader.parser;
+import static org.opentripplanner.netex.config.IgnorableFeature.PARKING;
+
import jakarta.xml.bind.JAXBElement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Set;
import org.opentripplanner.framework.application.OTPFeature;
+import org.opentripplanner.netex.config.IgnorableFeature;
import org.opentripplanner.netex.index.NetexEntityIndex;
import org.opentripplanner.netex.support.JAXBUtils;
import org.rutebanken.netex.model.FlexibleStopPlace;
import org.rutebanken.netex.model.GroupOfStopPlaces;
+import org.rutebanken.netex.model.Parking;
import org.rutebanken.netex.model.Quay;
import org.rutebanken.netex.model.Quays_RelStructure;
import org.rutebanken.netex.model.Site_VersionFrameStructure;
@@ -36,6 +41,14 @@ class SiteFrameParser extends NetexParser {
private final Collection quays = new ArrayList<>();
+ private final Collection parkings = new ArrayList<>(0);
+
+ private final Set ignoredFeatures;
+
+ SiteFrameParser(Set ignoredFeatures) {
+ this.ignoredFeatures = ignoredFeatures;
+ }
+
@Override
public void parse(Site_VersionFrameStructure frame) {
if (frame.getStopPlaces() != null) {
@@ -51,6 +64,10 @@ public void parse(Site_VersionFrameStructure frame) {
if (frame.getTariffZones() != null) {
parseTariffZones(frame.getTariffZones().getTariffZone());
}
+
+ if (!ignoredFeatures.contains(PARKING) && frame.getParkings() != null) {
+ parseParkings(frame.getParkings().getParking());
+ }
// Keep list sorted alphabetically
warnOnMissingMapping(LOG, frame.getAccesses());
warnOnMissingMapping(LOG, frame.getAddresses());
@@ -59,7 +76,6 @@ public void parse(Site_VersionFrameStructure frame) {
warnOnMissingMapping(LOG, frame.getCheckConstraintDelays());
warnOnMissingMapping(LOG, frame.getCheckConstraintThroughputs());
warnOnMissingMapping(LOG, frame.getNavigationPaths());
- warnOnMissingMapping(LOG, frame.getParkings());
warnOnMissingMapping(LOG, frame.getPathJunctions());
warnOnMissingMapping(LOG, frame.getPathLinks());
warnOnMissingMapping(LOG, frame.getPointsOfInterest());
@@ -79,6 +95,7 @@ void setResultOnIndex(NetexEntityIndex netexIndex) {
netexIndex.stopPlaceById.addAll(stopPlaces);
netexIndex.tariffZonesById.addAll(tariffZones);
netexIndex.quayById.addAll(quays);
+ netexIndex.parkings.addAll(parkings);
}
private void parseFlexibleStopPlaces(Collection flexibleStopPlacesList) {
@@ -89,6 +106,10 @@ private void parseGroupsOfStopPlaces(Collection groupsOfStopP
groupsOfStopPlaces.addAll(groupsOfStopPlacesList);
}
+ private void parseParkings(List parking) {
+ parkings.addAll(parking);
+ }
+
private void parseStopPlaces(List> stopPlaceList) {
for (JAXBElement extends Site_VersionStructure> jaxBStopPlace : stopPlaceList) {
StopPlace stopPlace = (StopPlace) jaxBStopPlace.getValue();
diff --git a/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java b/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java
index 991c0477266..025a2349874 100644
--- a/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java
+++ b/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java
@@ -77,9 +77,9 @@ public class NetexMapper {
/**
* Shared/cached entity index, used by more than one mapper. This index provides alternative
- * indexes to netex entites, as well as global indexes to OTP domain objects needed in the mapping
+ * indexes to netex entities, as well as global indexes to OTP domain objects needed in the mapping
* process. Some of these indexes are feed scoped, and some are file group level scoped. As a rule
- * of tomb the indexes for OTP Model entities are global(small memory overhead), while the indexes
+ * of thumb the indexes for OTP Model entities are global(small memory overhead), while the indexes
* for the Netex entities follow the main index {@link #currentNetexIndex}, hence sopped by file
* group.
*/
@@ -158,7 +158,7 @@ public void finishUp() {
/**
*
- * This method mapes the last Netex file imported using the *local* entities in the hierarchical
+ * This method maps the last Netex file imported using the *local* entities in the hierarchical
* {@link NetexEntityIndexReadOnlyView}.
*
*
@@ -199,6 +199,8 @@ public void mapNetexToOtp(NetexEntityIndexReadOnlyView netexIndex) {
mapTripPatterns(serviceIds);
mapNoticeAssignments();
+ mapVehicleParkings();
+
addEntriesToGroupMapperForPostProcessingLater();
}
@@ -520,6 +522,19 @@ private void addEntriesToGroupMapperForPostProcessingLater() {
}
}
+ private void mapVehicleParkings() {
+ var mapper = new VehicleParkingMapper(idFactory, issueStore);
+ currentNetexIndex
+ .getParkingsById()
+ .localKeys()
+ .forEach(id -> {
+ var parking = mapper.map(currentNetexIndex.getParkingsById().lookup(id));
+ if (parking != null) {
+ transitBuilder.vehicleParkings().add(parking);
+ }
+ });
+ }
+
/**
* The start of period is used to find the valid entities based on the current time. This should
* probably be configurable in the future, or even better incorporate the version number into the
diff --git a/src/main/java/org/opentripplanner/netex/mapping/VehicleParkingMapper.java b/src/main/java/org/opentripplanner/netex/mapping/VehicleParkingMapper.java
new file mode 100644
index 00000000000..862c5f0c648
--- /dev/null
+++ b/src/main/java/org/opentripplanner/netex/mapping/VehicleParkingMapper.java
@@ -0,0 +1,103 @@
+package org.opentripplanner.netex.mapping;
+
+import static org.rutebanken.netex.model.ParkingVehicleEnumeration.CYCLE;
+import static org.rutebanken.netex.model.ParkingVehicleEnumeration.E_CYCLE;
+import static org.rutebanken.netex.model.ParkingVehicleEnumeration.PEDAL_CYCLE;
+
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.opentripplanner.framework.i18n.NonLocalizedString;
+import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
+import org.opentripplanner.netex.mapping.support.FeedScopedIdFactory;
+import org.opentripplanner.routing.vehicle_parking.VehicleParking;
+import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces;
+import org.rutebanken.netex.model.Parking;
+import org.rutebanken.netex.model.ParkingVehicleEnumeration;
+
+/**
+ * Maps from NeTEx Parking to an internal {@link VehicleParking}.
+ */
+class VehicleParkingMapper {
+
+ private final FeedScopedIdFactory idFactory;
+
+ private static final Set BICYCLE_TYPES = Set.of(
+ PEDAL_CYCLE,
+ E_CYCLE,
+ CYCLE
+ );
+ private final DataImportIssueStore issueStore;
+
+ VehicleParkingMapper(FeedScopedIdFactory idFactory, DataImportIssueStore issueStore) {
+ this.idFactory = idFactory;
+ this.issueStore = issueStore;
+ }
+
+ @Nullable
+ VehicleParking map(Parking parking) {
+ if (parking.getTotalCapacity() == null) {
+ issueStore.add(
+ "MissingParkingCapacity",
+ "NeTEx Parking '%s' does not contain totalCapacity",
+ parkingDebugId(parking)
+ );
+ return null;
+ }
+ return VehicleParking
+ .builder()
+ .id(idFactory.createId(parking.getId()))
+ .name(NonLocalizedString.ofNullable(parking.getName().getValue()))
+ .coordinate(WgsCoordinateMapper.mapToDomain(parking.getCentroid()))
+ .capacity(mapCapacity(parking))
+ .bicyclePlaces(hasBikes(parking))
+ .carPlaces(!hasBikes(parking))
+ .entrance(mapEntrance(parking))
+ .build();
+ }
+
+ /**
+ * In the Nordic profile many fields of {@link Parking} are optional so even adding the ID to the
+ * issue store can lead to NPEs. For this reason we have a lot of fallbacks.
+ */
+ private static String parkingDebugId(Parking parking) {
+ if (parking.getId() != null) {
+ return parking.getId();
+ } else if (parking.getName() != null) {
+ return parking.getName().getValue();
+ } else if (parking.getCentroid() != null) {
+ return parking.getCentroid().toString();
+ } else {
+ return parking.toString();
+ }
+ }
+
+ private VehicleParking.VehicleParkingEntranceCreator mapEntrance(Parking parking) {
+ return builder ->
+ builder
+ .entranceId(idFactory.createId(parking.getId() + "/entrance"))
+ .coordinate(WgsCoordinateMapper.mapToDomain(parking.getCentroid()))
+ .walkAccessible(true)
+ .carAccessible(true);
+ }
+
+ private static VehicleParkingSpaces mapCapacity(Parking parking) {
+ var builder = VehicleParkingSpaces.builder();
+ int capacity = parking.getTotalCapacity().intValue();
+
+ // we assume that if we have something bicycle-like in the vehicle types it's a bicycle parking
+ // lot
+ // it's not possible in NeTEx to split the spaces between the types, so if you want that
+ // you have to define two parking lots with the same coordinates
+ if (hasBikes(parking)) {
+ builder.bicycleSpaces(capacity);
+ } else {
+ builder.carSpaces(capacity);
+ }
+
+ return builder.build();
+ }
+
+ private static boolean hasBikes(Parking parking) {
+ return parking.getParkingVehicleTypes().stream().anyMatch(BICYCLE_TYPES::contains);
+ }
+}
diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java
index c92bbd750ad..e5f0544f584 100644
--- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java
+++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java
@@ -1,7 +1,6 @@
package org.opentripplanner.routing.algorithm.raptoradapter.transit;
import java.time.LocalDate;
-import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -51,8 +50,6 @@ public class TransitLayer {
private final StopModel stopModel;
- private final ZoneId transitDataZoneId;
-
private final RaptorRequestTransferCache transferCache;
private ConstrainedTransfersForPatterns constrainedTransfers;
@@ -73,7 +70,6 @@ public TransitLayer(TransitLayer transitLayer) {
transitLayer.transfersByStopIndex,
transitLayer.transferService,
transitLayer.stopModel,
- transitLayer.transitDataZoneId,
transitLayer.transferCache,
transitLayer.constrainedTransfers,
transitLayer.transferIndexGenerator,
@@ -86,7 +82,6 @@ public TransitLayer(
List> transfersByStopIndex,
TransferService transferService,
StopModel stopModel,
- ZoneId transitDataZoneId,
RaptorRequestTransferCache transferCache,
ConstrainedTransfersForPatterns constrainedTransfers,
TransferIndexGenerator transferIndexGenerator,
@@ -96,7 +91,6 @@ public TransitLayer(
this.transfersByStopIndex = transfersByStopIndex;
this.transferService = transferService;
this.stopModel = stopModel;
- this.transitDataZoneId = transitDataZoneId;
this.transferCache = transferCache;
this.constrainedTransfers = constrainedTransfers;
this.transferIndexGenerator = transferIndexGenerator;
@@ -117,16 +111,6 @@ public Collection getTripPatternsForRunningDate(LocalDate da
return tripPatternsRunningOnDate.getOrDefault(date, List.of());
}
- /**
- * This is the time zone which is used for interpreting all local "service" times (in transfers,
- * trip schedules and so on). This is the time zone of the internal OTP time - which is used in
- * logging and debugging. This is independent of the time zone of imported data and of the time
- * zone used on any API - it can be the same, but it does not need to.
- */
- public ZoneId getTransitDataZoneId() {
- return transitDataZoneId;
- }
-
public int getStopCount() {
return stopModel.stopIndexSize();
}
diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java
index 8ce328fe1b6..d375b4c546c 100644
--- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java
+++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java
@@ -103,7 +103,6 @@ private TransitLayer map(TransitTuningParameters tuningParameters) {
transferByStopIndex,
transitService.getTransferService(),
stopModel,
- transitService.getTimeZone(),
transferCache,
constrainedTransfers,
transferIndexGenerator,
diff --git a/src/main/java/org/opentripplanner/routing/graph/Graph.java b/src/main/java/org/opentripplanner/routing/graph/Graph.java
index 4b6c9938317..fa3d12b92f7 100644
--- a/src/main/java/org/opentripplanner/routing/graph/Graph.java
+++ b/src/main/java/org/opentripplanner/routing/graph/Graph.java
@@ -16,7 +16,6 @@
import javax.annotation.Nullable;
import org.locationtech.jts.geom.Geometry;
import org.opentripplanner.ext.dataoverlay.configuration.DataOverlayParameterBindings;
-import org.opentripplanner.ext.geocoder.LuceneIndex;
import org.opentripplanner.framework.geometry.CompactElevationProfile;
import org.opentripplanner.framework.geometry.GeometryUtils;
import org.opentripplanner.model.calendar.openinghours.OpeningHoursCalendarService;
@@ -90,12 +89,6 @@ public class Graph implements Serializable {
/** True if OSM data was loaded into this Graph. */
public boolean hasStreets = false;
- /**
- * Have bike parks already been linked to the graph. As the linking happens twice if a base graph
- * is used, we store information on whether bike park linking should be skipped.
- */
-
- public boolean hasLinkedBikeParks = false;
/**
* The difference in meters between the WGS84 ellipsoid height and geoid height at the graph's
* center
@@ -136,7 +129,6 @@ public class Graph implements Serializable {
* creating the data overlay context when routing.
*/
public DataOverlayParameterBindings dataOverlayParameterBindings;
- private LuceneIndex luceneIndex;
@Inject
public Graph(
@@ -384,14 +376,6 @@ public void setFareService(FareService fareService) {
this.fareService = fareService;
}
- public LuceneIndex getLuceneIndex() {
- return luceneIndex;
- }
-
- public void setLuceneIndex(LuceneIndex luceneIndex) {
- this.luceneIndex = luceneIndex;
- }
-
private void indexIfNotIndexed(StopModel stopModel) {
if (streetIndex == null) {
index(stopModel);
diff --git a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParking.java b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParking.java
index 6ebd34e1287..098af909296 100644
--- a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParking.java
+++ b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParking.java
@@ -96,8 +96,10 @@ public class VehicleParking implements Serializable {
private final List entrances = new ArrayList<>();
/**
* The currently available spaces at this vehicle parking.
+ *
+ * The volatile keyword is used to ensure safe publication by clearing CPU caches.
*/
- private VehicleParkingSpaces availability;
+ private volatile VehicleParkingSpaces availability;
/**
* The vehicle parking group this parking belongs to.
*/
@@ -239,19 +241,24 @@ public boolean hasRealTimeDataForMode(
return false;
}
- switch (traverseMode) {
- case BICYCLE:
- return availability.getBicycleSpaces() != null;
- case CAR:
+ return switch (traverseMode) {
+ case BICYCLE -> availability.getBicycleSpaces() != null;
+ case CAR -> {
var places = wheelchairAccessibleCarPlaces
? availability.getWheelchairAccessibleCarSpaces()
: availability.getCarSpaces();
- return places != null;
- default:
- return false;
- }
+ yield places != null;
+ }
+ default -> false;
+ };
}
+ /**
+ * The only mutable method in this class: it allows to update the available parking spaces during
+ * real-time updates.
+ * Since the entity is used both by writer threads (real-time updates) and reader threads
+ * (A* routing), the variable holding the information is marked as volatile.
+ */
public void updateAvailability(VehicleParkingSpaces vehicleParkingSpaces) {
this.availability = vehicleParkingSpaces;
}
@@ -308,6 +315,7 @@ public boolean equals(Object o) {
public String toString() {
return ToStringBuilder
.of(VehicleParking.class)
+ .addStr("id", id.toString())
.addStr("name", name.toString())
.addObj("coordinate", coordinate)
.toString();
diff --git a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingHelper.java b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingHelper.java
index 507ce9329ed..257f2805ca2 100644
--- a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingHelper.java
+++ b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingHelper.java
@@ -2,7 +2,6 @@
import java.util.List;
import java.util.Objects;
-import java.util.stream.Collectors;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.street.model.edge.StreetVehicleParkingLink;
import org.opentripplanner.street.model.edge.VehicleParkingEdge;
@@ -30,7 +29,7 @@ public List createVehicleParkingVertices(
.getEntrances()
.stream()
.map(vertexFactory::vehicleParkingEntrance)
- .collect(Collectors.toList());
+ .toList();
}
public static void linkVehicleParkingEntrances(
diff --git a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingService.java b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingService.java
index 639066871be..b0c08a2309b 100644
--- a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingService.java
+++ b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingService.java
@@ -21,13 +21,17 @@ public class VehicleParkingService implements Serializable {
/**
* To ensure that his is thread-safe, the set stored here should always be immutable.
+ *
+ * The volatile keyword is used to ensure safe publication by clearing CPU caches.
*/
- private Set vehicleParkings = Set.of();
+ private volatile Set vehicleParkings = Set.of();
/**
* To ensure that his is thread-safe, {@link ImmutableListMultimap} is used.
+ *
+ * The volatile keyword is used to ensure safe publication by clearing CPU caches.
*/
- private ImmutableListMultimap vehicleParkingGroups = ImmutableListMultimap.of();
+ private volatile ImmutableListMultimap vehicleParkingGroups = ImmutableListMultimap.of();
/**
* Does atomic update of {@link VehicleParking} and index of {@link VehicleParkingGroup} in this
diff --git a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingSpaces.java b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingSpaces.java
index de25fc75521..8b21cc21f2c 100644
--- a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingSpaces.java
+++ b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingSpaces.java
@@ -2,6 +2,7 @@
import java.io.Serializable;
import java.util.Objects;
+import org.opentripplanner.framework.tostring.ToStringBuilder;
/**
* The number of spaces by type. {@code null} if unknown.
@@ -70,6 +71,16 @@ public boolean equals(Object o) {
);
}
+ @Override
+ public String toString() {
+ return ToStringBuilder
+ .of(VehicleParkingSpaces.class)
+ .addNum("carSpaces", carSpaces)
+ .addNum("wheelchairAccessibleCarSpaces", wheelchairAccessibleCarSpaces)
+ .addNum("bicycleSpaces", bicycleSpaces)
+ .toString();
+ }
+
public static class VehicleParkingSpacesBuilder {
private Integer bicycleSpaces;
diff --git a/src/main/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java b/src/main/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java
index 07fa48fa84f..0058cdd9e15 100644
--- a/src/main/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java
+++ b/src/main/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java
@@ -8,7 +8,6 @@
import java.util.Comparator;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;
import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository;
@@ -33,8 +32,16 @@ public DefaultRealtimeVehicleService(TransitService transitService) {
this.transitService = transitService;
}
+ /**
+ * Stores the relationship between a list of realtime vehicles with a pattern. If the pattern is
+ * a realtime-added one, then the original (scheduled) one is used as the key for the map storing
+ * the information.
+ */
@Override
public void setRealtimeVehicles(TripPattern pattern, List updates) {
+ if (pattern.getOriginalTripPattern() != null) {
+ pattern = pattern.getOriginalTripPattern();
+ }
vehicles.put(pattern, List.copyOf(updates));
}
@@ -43,8 +50,18 @@ public void clearRealtimeVehicles(TripPattern pattern) {
vehicles.remove(pattern);
}
+ /**
+ * Gets the realtime vehicles for a given pattern. If the pattern is a realtime-added one
+ * then the original (scheduled) one is used for the lookup instead, so you receive the correct
+ * result no matter if you use the realtime or static information.
+ *
+ * @see DefaultRealtimeVehicleService#setRealtimeVehicles(TripPattern, List)
+ */
@Override
public List getRealtimeVehicles(@Nonnull TripPattern pattern) {
+ if (pattern.getOriginalTripPattern() != null) {
+ pattern = pattern.getOriginalTripPattern();
+ }
// the list is made immutable during insertion, so we can safely return them
return vehicles.getOrDefault(pattern, List.of());
}
diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java
index 6552d82770f..3a577611617 100644
--- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java
+++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java
@@ -8,6 +8,7 @@
import org.opentripplanner.ext.dataoverlay.routing.DataOverlayContext;
import org.opentripplanner.ext.emissions.EmissionsService;
import org.opentripplanner.ext.flex.FlexParameters;
+import org.opentripplanner.ext.geocoder.LuceneIndex;
import org.opentripplanner.ext.ridehailing.RideHailingService;
import org.opentripplanner.ext.stopconsolidation.StopConsolidationService;
import org.opentripplanner.framework.application.OTPFeature;
@@ -136,4 +137,7 @@ default DataOverlayContext dataOverlayContext(RouteRequest request) {
)
);
}
+
+ @Nullable
+ LuceneIndex lucenceIndex();
}
diff --git a/src/main/java/org/opentripplanner/standalone/config/buildconfig/NetexConfig.java b/src/main/java/org/opentripplanner/standalone/config/buildconfig/NetexConfig.java
index d4cd4521e54..52bccf605d8 100644
--- a/src/main/java/org/opentripplanner/standalone/config/buildconfig/NetexConfig.java
+++ b/src/main/java/org/opentripplanner/standalone/config/buildconfig/NetexConfig.java
@@ -3,6 +3,7 @@
import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_0;
import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_2;
import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_3;
+import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_6;
import org.opentripplanner.netex.config.NetexFeedParameters;
import org.opentripplanner.standalone.config.framework.json.NodeAdapter;
@@ -164,6 +165,14 @@ private static NetexFeedParameters.Builder mapFilePatternParameters(
.summary("Ignore contents of the FareFrame")
.docDefaultValue(base.ignoreFareFrame())
.asBoolean(base.ignoreFareFrame())
+ )
+ .withIgnoreParking(
+ config
+ .of("ignoreParking")
+ .since(V2_6)
+ .summary("Ignore Parking elements.")
+ .docDefaultValue(base.ignoreParking())
+ .asBoolean(base.ignoreParking())
);
}
diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehicleParkingUpdaterConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehicleParkingUpdaterConfig.java
index c687899009f..88b47aac4eb 100644
--- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehicleParkingUpdaterConfig.java
+++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehicleParkingUpdaterConfig.java
@@ -13,6 +13,7 @@
import org.opentripplanner.ext.vehicleparking.hslpark.HslParkUpdaterParameters;
import org.opentripplanner.ext.vehicleparking.noi.NoiUpdaterParameters;
import org.opentripplanner.ext.vehicleparking.parkapi.ParkAPIUpdaterParameters;
+import org.opentripplanner.ext.vehicleparking.sirifm.SiriFmUpdaterParameters;
import org.opentripplanner.standalone.config.framework.json.NodeAdapter;
import org.opentripplanner.updater.vehicle_parking.VehicleParkingSourceType;
import org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdaterParameters;
@@ -29,7 +30,7 @@ public static VehicleParkingUpdaterParameters create(String updaterRef, NodeAdap
.of("feedId")
.since(V2_2)
.summary("The id of the data source, which will be the prefix of the parking lot's id.")
- .description("This will end up in the API responses as the feed id of of the parking lot.")
+ .description("This will end up in the API responses as the feed id of the parking lot.")
.asString();
return switch (sourceType) {
case HSL_PARK -> new HslParkUpdaterParameters(
@@ -100,6 +101,30 @@ public static VehicleParkingUpdaterParameters create(String updaterRef, NodeAdap
.asDuration(Duration.ofMinutes(1)),
HttpHeadersConfig.headers(c, V2_6)
);
+ case SIRI_FM -> new SiriFmUpdaterParameters(
+ updaterRef,
+ c
+ .of("url")
+ .since(V2_6)
+ .summary("URL of the SIRI-FM Light endpoint.")
+ .description(
+ """
+ SIRI Light means that it must be available as a HTTP GET request rather than the usual
+ SIRI request mechanism of HTTP POST.
+
+ The contents must also conform to the [Italian SIRI profile](https://github.com/5Tsrl/siri-italian-profile)
+ which requires SIRI 2.1.
+ """
+ )
+ .asUri(),
+ feedId,
+ c
+ .of("frequency")
+ .since(V2_6)
+ .summary("How often to update the source.")
+ .asDuration(Duration.ofMinutes(1)),
+ HttpHeadersConfig.headers(c, V2_6)
+ );
};
}
diff --git a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java
index 6fa33aac267..dc91388458a 100644
--- a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java
+++ b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java
@@ -514,7 +514,7 @@ the access legs used. In other cases where the access(CAR) is faster than transi
guaranteed to be optimal. Use itinerary-filters to limit what is presented to the client. The
duration can be set per mode(`maxDurationForMode`), because some street modes searches
are much more resource intensive than others. A default value is applied if the mode specific value
-do not exist.
+does not exist.
"""
)
.asDuration(dftAccessEgress.maxDuration().defaultValue()),
@@ -554,7 +554,7 @@ duration can be set per mode(`maxDurationForMode`), because some street modes se
guaranteed to be optimal. Use itinerary-filters to limit what is presented to the client. The
duration can be set per mode(`maxDirectStreetDurationForMode`), because some street modes searches
are much more resource intensive than others. A default value is applied if the mode specific value
-do not exist."
+does not exist."
"""
)
.asDuration(dft.maxDirectDuration().defaultValue()),
diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java
index 7d07ca29f0e..52f7970ccd7 100644
--- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java
+++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java
@@ -5,7 +5,6 @@
import org.opentripplanner.apis.transmodel.TransmodelAPI;
import org.opentripplanner.datastore.api.DataSource;
import org.opentripplanner.ext.emissions.EmissionsDataModel;
-import org.opentripplanner.ext.geocoder.LuceneIndex;
import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository;
import org.opentripplanner.framework.application.LogMDCSupport;
import org.opentripplanner.framework.application.OTPFeature;
@@ -181,8 +180,9 @@ private void setupTransitRoutingServer() {
}
if (OTPFeature.SandboxAPIGeocoder.isOn()) {
- LOG.info("Creating debug client geocoder lucene index");
- LuceneIndex.forServer(createServerContext());
+ LOG.info("Initializing geocoder");
+ // eagerly initialize the geocoder
+ this.factory.luceneIndex();
}
}
diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java
index fe95fe0447d..b307776ef52 100644
--- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java
+++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java
@@ -6,6 +6,8 @@
import javax.annotation.Nullable;
import org.opentripplanner.ext.emissions.EmissionsDataModel;
import org.opentripplanner.ext.emissions.EmissionsServiceModule;
+import org.opentripplanner.ext.geocoder.LuceneIndex;
+import org.opentripplanner.ext.geocoder.configure.GeocoderModule;
import org.opentripplanner.ext.interactivelauncher.configuration.InteractiveLauncherModule;
import org.opentripplanner.ext.ridehailing.configure.RideHailingServicesModule;
import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository;
@@ -56,6 +58,7 @@
StopConsolidationServiceModule.class,
InteractiveLauncherModule.class,
StreetLimitationParametersServiceModule.class,
+ GeocoderModule.class,
}
)
public interface ConstructApplicationFactory {
@@ -87,6 +90,9 @@ public interface ConstructApplicationFactory {
StreetLimitationParameters streetLimitationParameters();
+ @Nullable
+ LuceneIndex luceneIndex();
+
@Component.Builder
interface Builder {
@BindsInstance
diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java
index eb244ce726c..6c830054c49 100644
--- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java
+++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java
@@ -7,6 +7,7 @@
import javax.annotation.Nullable;
import org.opentripplanner.astar.spi.TraverseVisitor;
import org.opentripplanner.ext.emissions.EmissionsService;
+import org.opentripplanner.ext.geocoder.LuceneIndex;
import org.opentripplanner.ext.interactivelauncher.api.LauncherRequestDecorator;
import org.opentripplanner.ext.ridehailing.RideHailingService;
import org.opentripplanner.ext.stopconsolidation.StopConsolidationService;
@@ -40,7 +41,8 @@ OtpServerRequestContext providesServerContext(
StreetLimitationParametersService streetLimitationParametersService,
@Nullable TraverseVisitor, ?> traverseVisitor,
EmissionsService emissionsService,
- LauncherRequestDecorator launcherRequestDecorator
+ LauncherRequestDecorator launcherRequestDecorator,
+ @Nullable LuceneIndex luceneIndex
) {
var defaultRequest = launcherRequestDecorator.intercept(routerConfig.routingRequestDefaults());
@@ -60,7 +62,8 @@ OtpServerRequestContext providesServerContext(
rideHailingServices,
stopConsolidationService,
streetLimitationParametersService,
- traverseVisitor
+ traverseVisitor,
+ luceneIndex
);
}
diff --git a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java
index 7a4ccea9247..0e81193d787 100644
--- a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java
+++ b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java
@@ -7,6 +7,7 @@
import org.opentripplanner.astar.spi.TraverseVisitor;
import org.opentripplanner.ext.emissions.EmissionsService;
import org.opentripplanner.ext.flex.FlexParameters;
+import org.opentripplanner.ext.geocoder.LuceneIndex;
import org.opentripplanner.ext.ridehailing.RideHailingService;
import org.opentripplanner.ext.stopconsolidation.StopConsolidationService;
import org.opentripplanner.inspector.raster.TileRendererManager;
@@ -49,6 +50,7 @@ public class DefaultServerRequestContext implements OtpServerRequestContext {
private final EmissionsService emissionsService;
private final StopConsolidationService stopConsolidationService;
private final StreetLimitationParametersService streetLimitationParametersService;
+ private final LuceneIndex luceneIndex;
/**
* Make sure all mutable components are copied/cloned before calling this constructor.
@@ -70,7 +72,8 @@ private DefaultServerRequestContext(
StopConsolidationService stopConsolidationService,
StreetLimitationParametersService streetLimitationParametersService,
FlexParameters flexParameters,
- TraverseVisitor traverseVisitor
+ TraverseVisitor traverseVisitor,
+ @Nullable LuceneIndex luceneIndex
) {
this.graph = graph;
this.transitService = transitService;
@@ -89,6 +92,7 @@ private DefaultServerRequestContext(
this.emissionsService = emissionsService;
this.stopConsolidationService = stopConsolidationService;
this.streetLimitationParametersService = streetLimitationParametersService;
+ this.luceneIndex = luceneIndex;
}
/**
@@ -110,7 +114,8 @@ public static DefaultServerRequestContext create(
List rideHailingServices,
@Nullable StopConsolidationService stopConsolidationService,
StreetLimitationParametersService streetLimitationParametersService,
- @Nullable TraverseVisitor traverseVisitor
+ @Nullable TraverseVisitor traverseVisitor,
+ @Nullable LuceneIndex luceneIndex
) {
return new DefaultServerRequestContext(
graph,
@@ -129,7 +134,8 @@ public static DefaultServerRequestContext create(
stopConsolidationService,
streetLimitationParametersService,
flexParameters,
- traverseVisitor
+ traverseVisitor,
+ luceneIndex
);
}
@@ -235,6 +241,12 @@ public VectorTileConfig vectorTileConfig() {
return vectorTileConfig;
}
+ @Nullable
+ @Override
+ public LuceneIndex lucenceIndex() {
+ return luceneIndex;
+ }
+
@Override
public EmissionsService emissionsService() {
return emissionsService;
diff --git a/src/main/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLink.java b/src/main/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLink.java
index e682a7bfac1..2ad9d0f39c4 100644
--- a/src/main/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLink.java
+++ b/src/main/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLink.java
@@ -2,6 +2,7 @@
import javax.annotation.Nonnull;
import org.locationtech.jts.geom.LineString;
+import org.opentripplanner.framework.geometry.GeometryUtils;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.framework.tostring.ToStringBuilder;
import org.opentripplanner.routing.api.request.preference.VehicleParkingPreferences;
@@ -95,11 +96,8 @@ public I18NString getName() {
return vehicleParkingEntranceVertex.getName();
}
+ @Override
public LineString getGeometry() {
- return null;
- }
-
- public double getDistanceMeters() {
- return 0;
+ return GeometryUtils.makeLineString(fromv.getCoordinate(), tov.getCoordinate());
}
}
diff --git a/src/main/java/org/opentripplanner/street/model/vertex/VehicleParkingEntranceVertex.java b/src/main/java/org/opentripplanner/street/model/vertex/VehicleParkingEntranceVertex.java
index a3ab0aa17b0..ba7a1540715 100644
--- a/src/main/java/org/opentripplanner/street/model/vertex/VehicleParkingEntranceVertex.java
+++ b/src/main/java/org/opentripplanner/street/model/vertex/VehicleParkingEntranceVertex.java
@@ -1,10 +1,12 @@
package org.opentripplanner.street.model.vertex;
+import java.util.Collection;
import java.util.Objects;
import javax.annotation.Nonnull;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.routing.vehicle_parking.VehicleParking;
import org.opentripplanner.routing.vehicle_parking.VehicleParkingEntrance;
+import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.edge.StreetVehicleParkingLink;
import org.opentripplanner.street.model.edge.VehicleParkingEdge;
@@ -49,4 +51,15 @@ public boolean isCarAccessible() {
public boolean isWalkAccessible() {
return parkingEntrance.isWalkAccessible();
}
+
+ /**
+ * Is this vertex already linked to the graph with a {@link StreetVehicleParkingLink}?
+ */
+ public boolean isLinkedToGraph() {
+ return hasLink(getIncoming()) || hasLink(getOutgoing());
+ }
+
+ private boolean hasLink(Collection edges) {
+ return edges.stream().anyMatch(StreetVehicleParkingLink.class::isInstance);
+ }
}
diff --git a/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java b/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java
index 103120b7ecb..83e0bd0fe85 100644
--- a/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java
+++ b/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java
@@ -25,13 +25,13 @@
import org.opentripplanner.updater.trip.MqttGtfsRealtimeUpdater;
import org.opentripplanner.updater.trip.PollingTripUpdater;
import org.opentripplanner.updater.trip.TimetableSnapshotSource;
+import org.opentripplanner.updater.vehicle_parking.AvailabilityDatasourceFactory;
+import org.opentripplanner.updater.vehicle_parking.VehicleParkingAvailabilityUpdater;
import org.opentripplanner.updater.vehicle_parking.VehicleParkingDataSourceFactory;
import org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdater;
import org.opentripplanner.updater.vehicle_position.PollingVehiclePositionUpdater;
import org.opentripplanner.updater.vehicle_rental.VehicleRentalUpdater;
import org.opentripplanner.updater.vehicle_rental.datasources.VehicleRentalDataSourceFactory;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Sets up and starts all the graph updaters.
@@ -42,8 +42,6 @@
*/
public class UpdaterConfigurator {
- private static final Logger LOG = LoggerFactory.getLogger(UpdaterConfigurator.class);
-
private final Graph graph;
private final TransitModel transitModel;
private final UpdatersParameters updatersParameters;
@@ -187,15 +185,32 @@ private List createUpdatersFromConfig() {
);
}
for (var configItem : updatersParameters.getVehicleParkingUpdaterParameters()) {
- var source = VehicleParkingDataSourceFactory.create(configItem, openingHoursCalendarService);
- updaters.add(
- new VehicleParkingUpdater(
- configItem,
- source,
- graph.getLinker(),
- graph.getVehicleParkingService()
- )
- );
+ switch (configItem.updateType()) {
+ case FULL -> {
+ var source = VehicleParkingDataSourceFactory.create(
+ configItem,
+ openingHoursCalendarService
+ );
+ updaters.add(
+ new VehicleParkingUpdater(
+ configItem,
+ source,
+ graph.getLinker(),
+ graph.getVehicleParkingService()
+ )
+ );
+ }
+ case AVAILABILITY_ONLY -> {
+ var source = AvailabilityDatasourceFactory.create(configItem);
+ updaters.add(
+ new VehicleParkingAvailabilityUpdater(
+ configItem,
+ source,
+ graph.getVehicleParkingService()
+ )
+ );
+ }
+ }
}
for (var configItem : updatersParameters.getSiriAzureETUpdaterParameters()) {
updaters.add(
diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabilityDatasourceFactory.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabilityDatasourceFactory.java
new file mode 100644
index 00000000000..876cd303c78
--- /dev/null
+++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabilityDatasourceFactory.java
@@ -0,0 +1,23 @@
+package org.opentripplanner.updater.vehicle_parking;
+
+import org.opentripplanner.ext.vehicleparking.sirifm.SiriFmDatasource;
+import org.opentripplanner.ext.vehicleparking.sirifm.SiriFmUpdaterParameters;
+import org.opentripplanner.updater.spi.DataSource;
+
+/**
+ * Class that can be used to return a custom vehicle parking {@link DataSource}.
+ */
+public class AvailabilityDatasourceFactory {
+
+ public static DataSource create(VehicleParkingUpdaterParameters parameters) {
+ return switch (parameters.sourceType()) {
+ case SIRI_FM -> new SiriFmDatasource((SiriFmUpdaterParameters) parameters);
+ case PARK_API,
+ BICYCLE_PARK_API,
+ HSL_PARK,
+ BIKEEP,
+ NOI_OPEN_DATA_HUB,
+ BIKELY -> throw new IllegalArgumentException("Cannot instantiate SIRI-FM data source");
+ };
+ }
+}
diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabiltyUpdate.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabiltyUpdate.java
new file mode 100644
index 00000000000..a9d352cbaf2
--- /dev/null
+++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabiltyUpdate.java
@@ -0,0 +1,12 @@
+package org.opentripplanner.updater.vehicle_parking;
+
+import java.util.Objects;
+import org.opentripplanner.framework.lang.IntUtils;
+import org.opentripplanner.transit.model.framework.FeedScopedId;
+
+public record AvailabiltyUpdate(FeedScopedId vehicleParkingId, int spacesAvailable) {
+ public AvailabiltyUpdate {
+ Objects.requireNonNull(vehicleParkingId);
+ IntUtils.requireNotNegative(spacesAvailable);
+ }
+}
diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java
new file mode 100644
index 00000000000..31074aafe38
--- /dev/null
+++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java
@@ -0,0 +1,104 @@
+package org.opentripplanner.updater.vehicle_parking;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.opentripplanner.framework.tostring.ToStringBuilder;
+import org.opentripplanner.routing.graph.Graph;
+import org.opentripplanner.routing.vehicle_parking.VehicleParking;
+import org.opentripplanner.routing.vehicle_parking.VehicleParkingService;
+import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces;
+import org.opentripplanner.transit.model.framework.FeedScopedId;
+import org.opentripplanner.transit.service.TransitModel;
+import org.opentripplanner.updater.GraphWriterRunnable;
+import org.opentripplanner.updater.spi.DataSource;
+import org.opentripplanner.updater.spi.PollingGraphUpdater;
+import org.opentripplanner.updater.spi.WriteToGraphCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Graph updater that dynamically sets availability information on vehicle parking lots. This
+ * updater fetches data from a single {@link DataSource}.
+ */
+public class VehicleParkingAvailabilityUpdater extends PollingGraphUpdater {
+
+ private static final Logger LOG = LoggerFactory.getLogger(
+ VehicleParkingAvailabilityUpdater.class
+ );
+ private final DataSource source;
+ private WriteToGraphCallback saveResultOnGraph;
+
+ private final VehicleParkingService vehicleParkingService;
+
+ public VehicleParkingAvailabilityUpdater(
+ VehicleParkingUpdaterParameters parameters,
+ DataSource source,
+ VehicleParkingService vehicleParkingService
+ ) {
+ super(parameters);
+ this.source = source;
+ this.vehicleParkingService = vehicleParkingService;
+
+ LOG.info("Creating vehicle-parking updater running every {}: {}", pollingPeriod(), source);
+ }
+
+ @Override
+ public void setup(WriteToGraphCallback writeToGraphCallback) {
+ this.saveResultOnGraph = writeToGraphCallback;
+ }
+
+ @Override
+ protected void runPolling() {
+ if (source.update()) {
+ var updates = source.getUpdates();
+
+ var graphWriterRunnable = new AvailabilityUpdater(updates);
+ saveResultOnGraph.execute(graphWriterRunnable);
+ }
+ }
+
+ private class AvailabilityUpdater implements GraphWriterRunnable {
+
+ private final List updates;
+ private final Map parkingById;
+
+ private AvailabilityUpdater(List updates) {
+ this.updates = List.copyOf(updates);
+ this.parkingById =
+ vehicleParkingService
+ .getVehicleParkings()
+ .collect(Collectors.toUnmodifiableMap(VehicleParking::getId, Function.identity()));
+ }
+
+ @Override
+ public void run(Graph graph, TransitModel ignored) {
+ updates.forEach(this::handleUpdate);
+ }
+
+ private void handleUpdate(AvailabiltyUpdate update) {
+ if (!parkingById.containsKey(update.vehicleParkingId())) {
+ LOG.warn(
+ "Parking with id {} does not exist. Skipping availability update.",
+ update.vehicleParkingId()
+ );
+ } else {
+ var parking = parkingById.get(update.vehicleParkingId());
+ var builder = VehicleParkingSpaces.builder();
+ if (parking.hasCarPlaces()) {
+ builder.carSpaces(update.spacesAvailable());
+ }
+ if (parking.hasBicyclePlaces()) {
+ builder.bicycleSpaces(update.spacesAvailable());
+ }
+ parking.updateAvailability(builder.build());
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.of(this.getClass()).addObj("source", source).toString();
+ }
+}
diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingDataSourceFactory.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingDataSourceFactory.java
index a956fda0d87..09ecb67a54d 100644
--- a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingDataSourceFactory.java
+++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingDataSourceFactory.java
@@ -42,6 +42,7 @@ public static DataSource create(
case BIKELY -> new BikelyUpdater((BikelyUpdaterParameters) parameters);
case NOI_OPEN_DATA_HUB -> new NoiUpdater((NoiUpdaterParameters) parameters);
case BIKEEP -> new BikeepUpdater((BikeepUpdaterParameters) parameters);
+ case SIRI_FM -> throw new IllegalArgumentException("Cannot instantiate SIRI-FM data source");
};
}
}
diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingSourceType.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingSourceType.java
index f6a28177d8e..1601245b16c 100644
--- a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingSourceType.java
+++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingSourceType.java
@@ -7,4 +7,5 @@ public enum VehicleParkingSourceType {
BIKELY,
NOI_OPEN_DATA_HUB,
BIKEEP,
+ SIRI_FM,
}
diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterParameters.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterParameters.java
index 3422ec3e300..bff0022383f 100644
--- a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterParameters.java
+++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterParameters.java
@@ -8,4 +8,10 @@
*/
public interface VehicleParkingUpdaterParameters extends PollingGraphUpdaterParameters {
VehicleParkingSourceType sourceType();
+ UpdateType updateType();
+
+ enum UpdateType {
+ FULL,
+ AVAILABILITY_ONLY,
+ }
}
diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls
index 6e1195f5901..927af19f8b1 100644
--- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls
+++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls
@@ -1620,9 +1620,17 @@ type QueryType {
"Only return routes with these feedIds"
feeds: [String],
"Only return routes with these ids"
- ids: [String],
+ ids: [String] @deprecated(reason : "Since it is hard to reason about the ID filter being combined with others in this resolver, it will be moved to a separate one."),
"Query routes by this name"
name: String,
+ """
+ Only include routes whose pattern operates on at least one service date specified by this filter.
+
+ **Note**: A service date is a technical term useful for transit planning purposes and might not
+ correspond to a how a passenger thinks of a calendar date. For example, a night bus running
+ on Sunday morning at 1am to 3am, might have the previous Saturday's service date.
+ """
+ serviceDates: LocalDateRangeInput,
"Only include routes, which use one of these modes"
transportModes: [Mode]
): [Route]
@@ -1847,7 +1855,16 @@ type Route implements Node {
"Transport mode of this route, e.g. `BUS`"
mode: TransitMode
"List of patterns which operate on this route"
- patterns: [Pattern]
+ patterns(
+ """
+ Filter patterns by the service dates they operate on.
+
+ **Note**: A service date is a technical term useful for transit planning purposes and might not
+ correspond to a how a passenger thinks of a calendar date. For example, a night bus running
+ on Sunday morning at 1am to 3am, might have the previous Saturday's service date.
+ """
+ serviceDates: LocalDateRangeInput
+ ): [Pattern]
"Short name of the route, usually a line number, e.g. 550"
shortName: String
"""
@@ -3517,6 +3534,13 @@ scalar GeoJson @specifiedBy(url : "https://www.rfcreader.com/#rfc7946")
scalar Grams
+"""
+An ISO-8601-formatted local date, i.e. `2024-05-24` for the 24th of May, 2024.
+
+ISO-8601 allows many different date formats, however only the most common one - `yyyy-MM-dd` - is accepted.
+"""
+scalar LocalDate @specifiedBy(url : "https://www.iso.org/standard/70907.html")
+
"A IETF BCP 47 language tag"
scalar Locale @specifiedBy(url : "https://www.rfcreader.com/#rfc5646")
@@ -3857,6 +3881,23 @@ input InputUnpreferred {
useUnpreferredRoutesPenalty: Int @deprecated(reason : "Use unpreferredCost instead")
}
+"Filters an entity by a date range."
+input LocalDateRangeInput {
+ """
+ **Exclusive** end date of the filter. This means that if you want a time window from Sunday to
+ Sunday, `end` must be on Monday.
+
+ If `null` this means that no end filter is applied and all entities that are after or on `start`
+ are selected.
+ """
+ end: LocalDate
+ """
+ **Inclusive** start date of the filter. If `null` this means that no `start` filter is applied and all
+ dates that are before `end` are selected.
+ """
+ start: LocalDate
+}
+
"""
The filter definition to include or exclude parking facilities used during routing.
diff --git a/src/test/java/org/opentripplanner/TestServerContext.java b/src/test/java/org/opentripplanner/TestServerContext.java
index 2f4ded1121d..90dca6ff840 100644
--- a/src/test/java/org/opentripplanner/TestServerContext.java
+++ b/src/test/java/org/opentripplanner/TestServerContext.java
@@ -55,6 +55,7 @@ public static OtpServerRequestContext createServerContext(
List.of(),
null,
createStreetLimitationParametersService(),
+ null,
null
);
creatTransitLayerForRaptor(transitModel, routerConfig.transitTuningConfig());
diff --git a/src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java b/src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java
new file mode 100644
index 00000000000..f01bac12006
--- /dev/null
+++ b/src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java
@@ -0,0 +1,159 @@
+package org.opentripplanner.apis.gtfs;
+
+import static java.time.LocalDate.parse;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.opentripplanner.apis.gtfs.PatternByServiceDatesFilterTest.FilterExpectation.NOT_REMOVED;
+import static org.opentripplanner.apis.gtfs.PatternByServiceDatesFilterTest.FilterExpectation.REMOVED;
+import static org.opentripplanner.transit.model._data.TransitModelForTest.id;
+
+import java.time.LocalDate;
+import java.util.List;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.opentripplanner.apis.gtfs.model.LocalDateRange;
+import org.opentripplanner.transit.model._data.TransitModelForTest;
+import org.opentripplanner.transit.model.framework.FeedScopedId;
+import org.opentripplanner.transit.model.network.Route;
+import org.opentripplanner.transit.model.network.StopPattern;
+import org.opentripplanner.transit.model.network.TripPattern;
+import org.opentripplanner.transit.model.site.RegularStop;
+import org.opentripplanner.transit.model.timetable.ScheduledTripTimes;
+import org.opentripplanner.transit.model.timetable.Trip;
+import org.opentripplanner.transit.service.StopModel;
+
+class PatternByServiceDatesFilterTest {
+
+ private static final Route ROUTE_1 = TransitModelForTest.route("1").build();
+ private static final FeedScopedId SERVICE_ID = id("service");
+ private static final Trip TRIP = TransitModelForTest
+ .trip("t1")
+ .withRoute(ROUTE_1)
+ .withServiceId(SERVICE_ID)
+ .build();
+ private static final TransitModelForTest MODEL = new TransitModelForTest(StopModel.of());
+ private static final RegularStop STOP_1 = MODEL.stop("1").build();
+ private static final StopPattern STOP_PATTERN = TransitModelForTest.stopPattern(STOP_1, STOP_1);
+ private static final TripPattern PATTERN_1 = pattern();
+
+ enum FilterExpectation {
+ REMOVED,
+ NOT_REMOVED,
+ }
+
+ private static TripPattern pattern() {
+ var pattern = TransitModelForTest
+ .tripPattern("1", ROUTE_1)
+ .withStopPattern(STOP_PATTERN)
+ .build();
+
+ var tt = ScheduledTripTimes
+ .of()
+ .withTrip(TRIP)
+ .withArrivalTimes("10:00 10:05")
+ .withDepartureTimes("10:00 10:05")
+ .build();
+ pattern.add(tt);
+ return pattern;
+ }
+
+ static List invalidRangeCases() {
+ return List.of(
+ Arguments.of(null, null),
+ Arguments.of(parse("2024-05-02"), parse("2024-05-01")),
+ Arguments.of(parse("2024-05-03"), parse("2024-05-01"))
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidRangeCases")
+ void invalidRange(LocalDate start, LocalDate end) {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new PatternByServiceDatesFilter(
+ new LocalDateRange(start, end),
+ r -> List.of(),
+ d -> List.of()
+ )
+ );
+ }
+
+ static List validRangeCases() {
+ return List.of(
+ Arguments.of(parse("2024-05-02"), parse("2024-05-02")),
+ Arguments.of(parse("2024-05-02"), parse("2024-05-03")),
+ Arguments.of(null, parse("2024-05-03")),
+ Arguments.of(parse("2024-05-03"), null)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("validRangeCases")
+ void validRange(LocalDate start, LocalDate end) {
+ assertDoesNotThrow(() ->
+ new PatternByServiceDatesFilter(
+ new LocalDateRange(start, end),
+ r -> List.of(),
+ d -> List.of()
+ )
+ );
+ }
+
+ static List ranges() {
+ return List.of(
+ Arguments.of(null, parse("2024-05-03"), NOT_REMOVED),
+ Arguments.of(parse("2024-05-03"), null, NOT_REMOVED),
+ Arguments.of(parse("2024-05-01"), null, NOT_REMOVED),
+ Arguments.of(null, parse("2024-04-30"), REMOVED),
+ Arguments.of(null, parse("2024-05-01"), REMOVED),
+ Arguments.of(parse("2024-05-02"), parse("2024-05-02"), REMOVED),
+ Arguments.of(parse("2024-05-02"), parse("2024-05-03"), REMOVED),
+ Arguments.of(parse("2024-05-02"), parse("2024-06-01"), REMOVED),
+ Arguments.of(parse("2025-01-01"), null, REMOVED),
+ Arguments.of(parse("2025-01-01"), parse("2025-01-02"), REMOVED),
+ Arguments.of(null, parse("2023-12-31"), REMOVED),
+ Arguments.of(parse("2023-12-31"), parse("2024-04-30"), REMOVED)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("ranges")
+ void filterPatterns(LocalDate start, LocalDate end, FilterExpectation expectation) {
+ var filter = defaultFilter(start, end);
+
+ var filterInput = List.of(PATTERN_1);
+ var filterOutput = filter.filterPatterns(filterInput);
+
+ if (expectation == NOT_REMOVED) {
+ assertEquals(filterOutput, filterInput);
+ } else {
+ assertEquals(List.of(), filterOutput);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("ranges")
+ void filterRoutes(LocalDate start, LocalDate end, FilterExpectation expectation) {
+ var filter = defaultFilter(start, end);
+
+ var filterInput = List.of(ROUTE_1);
+ var filterOutput = filter.filterRoutes(filterInput.stream());
+
+ if (expectation == NOT_REMOVED) {
+ assertEquals(filterOutput, filterInput);
+ } else {
+ assertEquals(List.of(), filterOutput);
+ }
+ }
+
+ private static PatternByServiceDatesFilter defaultFilter(LocalDate start, LocalDate end) {
+ return new PatternByServiceDatesFilter(
+ new LocalDateRange(start, end),
+ route -> List.of(PATTERN_1),
+ trip -> List.of(parse("2024-05-01"), parse("2024-06-01"))
+ );
+ }
+}
diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/LocalDateRangeMapperTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/LocalDateRangeMapperTest.java
new file mode 100644
index 00000000000..cbd771df933
--- /dev/null
+++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/LocalDateRangeMapperTest.java
@@ -0,0 +1,42 @@
+package org.opentripplanner.apis.gtfs.mapping;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.opentripplanner.apis.gtfs.generated.GraphQLTypes;
+import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil;
+
+class LocalDateRangeMapperTest {
+
+ private static final LocalDate DATE = LocalDate.parse("2024-05-27");
+
+ private static List noFilterCases() {
+ var list = new ArrayList();
+ list.add(null);
+ list.add(new GraphQLTypes.GraphQLLocalDateRangeInput(Map.of()));
+ return list;
+ }
+
+ @ParameterizedTest
+ @MethodSource("noFilterCases")
+ void hasNoServiceDateFilter(GraphQLTypes.GraphQLLocalDateRangeInput input) {
+ assertFalse(LocalDateRangeUtil.hasServiceDateFilter(input));
+ }
+
+ private static List