Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

New vectortile layer for Digitransit realtime stops #5743

Merged
Merged
9 changes: 9 additions & 0 deletions doc-templates/sandbox/MapboxVectorTilesApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ The feature must be configured in `router-config.json` as follows
"minZoom": 14,
"cacheMaxSeconds": 60
},
// Contains just stops and real-time information for them
{
"name": "realtimeStops",
"type": "Stop",
"mapper": "DigitransitRealtime",
"maxZoom": 20,
"minZoom": 14,
"cacheMaxSeconds": 600
Copy link
Member

@optionsome optionsome Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the other realtime layers, we have suggested 60 second cache time. Not sure what is actually optimal here but we could use the same.

},
// This exists for backwards compatibility. At some point, we might want
// to add a new real-time parking mapper with better translation support
// and less unnecessary fields.
Expand Down
9 changes: 9 additions & 0 deletions docs/sandbox/MapboxVectorTilesApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ The feature must be configured in `router-config.json` as follows
"minZoom": 14,
"cacheMaxSeconds": 60
},
// Contains just stops and real-time information for them
{
"name": "realtimeStops",
"type": "Stop",
"mapper": "DigitransitRealtime",
"maxZoom": 20,
"minZoom": 14,
"cacheMaxSeconds": 600
},
// This exists for backwards compatibility. At some point, we might want
// to add a new real-time parking mapper with better translation support
// and less unnecessary fields.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
package org.opentripplanner.ext.vectortiles.layers.stops;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.opentripplanner.framework.time.TimeUtils.time;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opentripplanner._support.time.ZoneIds;
import org.opentripplanner.ext.realtimeresolver.RealtimeResolver;
import org.opentripplanner.framework.i18n.TranslatedString;
import org.opentripplanner.model.plan.Place;
import org.opentripplanner.routing.alertpatch.AlertEffect;
import org.opentripplanner.routing.alertpatch.EntitySelector;
import org.opentripplanner.routing.alertpatch.TimePeriod;
import org.opentripplanner.routing.alertpatch.TransitAlert;
import org.opentripplanner.routing.impl.TransitAlertServiceImpl;
import org.opentripplanner.routing.services.TransitAlertService;
import org.opentripplanner.transit.model._data.TransitModelForTest;
import org.opentripplanner.transit.model.framework.Deduplicator;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.network.Route;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.service.DefaultTransitService;
import org.opentripplanner.transit.service.StopModel;
Expand All @@ -18,6 +35,7 @@
public class StopsLayerTest {

private RegularStop stop;
private RegularStop stop2;

@BeforeEach
public void setUp() {
Expand Down Expand Up @@ -49,6 +67,14 @@ public void setUp() {
.withDescription(descTranslations)
.withCoordinate(50, 10)
.build();
stop2 =
StopModel
.of()
.regularStop(new FeedScopedId("F", "name"))
.withName(nameTranslations)
.withDescription(descTranslations)
.withCoordinate(51, 10)
.build();
}

@Test
Expand Down Expand Up @@ -89,4 +115,49 @@ public void digitransitStopPropertyMapperTranslationTest() {
assertEquals("nameDE", map.get("name"));
assertEquals("descDE", map.get("desc"));
}

@Test
public void digitransitRealtimeStopPropertyMapperTest() {
var deduplicator = new Deduplicator();
var transitModel = new TransitModel(new StopModel(), deduplicator);
transitModel.initTimeZone(ZoneIds.HELSINKI);
transitModel.index();
var alertService = new TransitAlertServiceImpl(transitModel);
var transitService = new DefaultTransitService(transitModel) {
@Override
public TransitAlertService getTransitAlertService() {
return alertService;
}
};

Route route = TransitModelForTest.route("route").build();
var itinerary = newItinerary(Place.forStop(stop), time("11:00"))
.bus(route, 1, time("11:05"), time("11:20"), Place.forStop(stop2))
.build();
var startDate = ZonedDateTime.now(ZoneIds.HELSINKI).minusDays(1).toEpochSecond();
var endDate = ZonedDateTime.now(ZoneIds.HELSINKI).plusDays(1).toEpochSecond();
var alert = TransitAlert
.of(stop.getId())
.addEntity(new EntitySelector.Stop(stop.getId()))
.addTimePeriod(new TimePeriod(startDate, endDate))
.withEffect(AlertEffect.NO_SERVICE)
.build();
transitService.getTransitAlertService().setAlerts(List.of(alert));

var itineraries = List.of(itinerary);
RealtimeResolver.populateLegsWithRealtime(itineraries, transitService);

DigitransitRealtimeStopPropertyMapper mapper = new DigitransitRealtimeStopPropertyMapper(
transitService,
new Locale("en-US")
);

Map<String, Object> map = new HashMap<>();
mapper.map(stop).forEach(o -> map.put(o.key(), o.value()));

assertEquals("F:name", map.get("gtfsId"));
assertEquals("name", map.get("name"));
assertEquals("desc", map.get("desc"));
assertEquals(true, map.get("closedByServiceAlert"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.opentripplanner.ext.vectortiles.layers.stops;

import static org.opentripplanner.ext.vectortiles.layers.stops.DigitransitStopPropertyMapper.getBaseKeyValues;

import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import org.opentripplanner.apis.support.mapping.PropertyMapper;
import org.opentripplanner.framework.collection.ListUtils;
import org.opentripplanner.framework.i18n.I18NStringMapper;
import org.opentripplanner.inspector.vector.KeyValue;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.service.TransitService;

public class DigitransitRealtimeStopPropertyMapper extends PropertyMapper<RegularStop> {

private final TransitService transitService;
private final I18NStringMapper i18NStringMapper;

public DigitransitRealtimeStopPropertyMapper(TransitService transitService, Locale locale) {
this.transitService = transitService;
this.i18NStringMapper = new I18NStringMapper(locale);
}

@Override
protected Collection<KeyValue> map(RegularStop stop) {
Instant currentTime = Instant.now();
boolean noServiceAlert = transitService
.getTransitAlertService()
.getStopAlerts(stop.getId())
.stream()
.anyMatch(alert -> alert.noServiceAt(currentTime));

Collection<KeyValue> sharedKeyValues = getBaseKeyValues(stop, i18NStringMapper, transitService);
return ListUtils.combine(
sharedKeyValues,
List.of(new KeyValue("closedByServiceAlert", noServiceAlert))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class DigitransitStopPropertyMapper extends PropertyMapper<RegularStop> {
private final TransitService transitService;
private final I18NStringMapper i18NStringMapper;

private DigitransitStopPropertyMapper(TransitService transitService, Locale locale) {
DigitransitStopPropertyMapper(TransitService transitService, Locale locale) {
this.transitService = transitService;
this.i18NStringMapper = new I18NStringMapper(locale);
}
Expand All @@ -34,20 +34,31 @@ protected static DigitransitStopPropertyMapper create(

@Override
protected Collection<KeyValue> map(RegularStop stop) {
Collection<TripPattern> patternsForStop = transitService.getPatternsForStop(stop);
return getBaseKeyValues(stop, i18NStringMapper, transitService);
}

String type = patternsForStop
.stream()
.map(TripPattern::getMode)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.map(Enum::name)
.orElse(null);
protected static Collection<KeyValue> getBaseKeyValues(
RegularStop stop,
I18NStringMapper i18NStringMapper,
TransitService transitService
) {
return List.of(
new KeyValue("gtfsId", stop.getId().toString()),
new KeyValue("name", i18NStringMapper.mapNonnullToApi(stop.getName())),
new KeyValue("code", stop.getCode()),
new KeyValue("platform", stop.getPlatformCode()),
new KeyValue("desc", i18NStringMapper.mapToApi(stop.getDescription())),
new KeyValue(
"parentStation",
stop.getParentStation() != null ? stop.getParentStation().getId() : null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change. This shouldn't be shared between the mappers as the old one always returned a string.

),
new KeyValue("type", getType(transitService, stop)),
new KeyValue("routes", getRoutes(transitService, stop))
);
}

String routes = JSONArray.toJSONString(
protected static String getRoutes(TransitService transitService, RegularStop stop) {
return JSONArray.toJSONString(
transitService
.getRoutesForStop(stop)
.stream()
Expand All @@ -58,18 +69,20 @@ protected Collection<KeyValue> map(RegularStop stop) {
})
.toList()
);
return List.of(
new KeyValue("gtfsId", stop.getId().toString()),
new KeyValue("name", i18NStringMapper.mapNonnullToApi(stop.getName())),
new KeyValue("code", stop.getCode()),
new KeyValue("platform", stop.getPlatformCode()),
new KeyValue("desc", i18NStringMapper.mapToApi(stop.getDescription())),
new KeyValue(
"parentStation",
stop.getParentStation() != null ? stop.getParentStation().getId() : "null"
),
new KeyValue("type", type),
new KeyValue("routes", routes)
);
}

protected static String getType(TransitService transitService, RegularStop stop) {
Collection<TripPattern> patternsForStop = transitService.getPatternsForStop(stop);

return patternsForStop
.stream()
.map(TripPattern::getMode)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.map(Enum::name)
.orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.opentripplanner.ext.vectortiles.layers.stops;

import static java.util.Map.entry;

import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand All @@ -14,7 +16,7 @@
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.service.TransitService;

public class StopsLayerBuilder extends LayerBuilder<RegularStop> {
public class StopsLayerBuilder<T> extends LayerBuilder<T> {

static Map<MapperType, BiFunction<TransitService, Locale, PropertyMapper<RegularStop>>> mappers = Map.of(
MapperType.Digitransit,
Expand All @@ -28,7 +30,15 @@ public StopsLayerBuilder(
Locale locale
) {
super(
mappers.get(MapperType.valueOf(layerParameters.mapper())).apply(transitService, locale),
(PropertyMapper<T>) Map
.ofEntries(
entry(MapperType.Digitransit, new DigitransitStopPropertyMapper(transitService, locale)),
entry(
MapperType.DigitransitRealtime,
new DigitransitRealtimeStopPropertyMapper(transitService, locale)
)
)
.get(MapperType.valueOf(layerParameters.mapper())),
layerParameters.name(),
layerParameters.expansionFactor()
);
Expand All @@ -51,5 +61,6 @@ protected List<Geometry> getGeometries(Envelope query) {

enum MapperType {
Digitransit,
DigitransitRealtime,
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been regenerated (although there wasn't really a need since the graphql schema has not been updated. There are just some duplicate imports here.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
package org.opentripplanner.apis.gtfs.generated;

import graphql.relay.Connection;
import graphql.relay.Connection;
import graphql.relay.Edge;
import graphql.relay.Edge;
import graphql.schema.DataFetcher;
import graphql.schema.TypeResolver;
Expand All @@ -21,8 +23,12 @@
import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRoutingErrorCode;
import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode;
import org.opentripplanner.apis.gtfs.model.RideHailingProvider;
import org.opentripplanner.apis.gtfs.model.RouteTypeModel;
import org.opentripplanner.apis.gtfs.model.StopOnRouteModel;
import org.opentripplanner.apis.gtfs.model.StopOnTripModel;
import org.opentripplanner.apis.gtfs.model.StopPosition;
import org.opentripplanner.apis.gtfs.model.TripOccupancy;
import org.opentripplanner.apis.gtfs.model.UnknownModel;
import org.opentripplanner.ext.fares.model.FareRuleSet;
import org.opentripplanner.ext.ridehailing.model.RideEstimate;
import org.opentripplanner.model.StopTimesInPattern;
Expand All @@ -44,6 +50,8 @@
import org.opentripplanner.routing.graphfinder.PatternAtStop;
import org.opentripplanner.routing.graphfinder.PlaceAtDistance;
import org.opentripplanner.routing.vehicle_parking.VehicleParking;
import org.opentripplanner.routing.vehicle_parking.VehicleParking;
import org.opentripplanner.routing.vehicle_parking.VehicleParking;
import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces;
import org.opentripplanner.routing.vehicle_parking.VehicleParkingState;
import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle;
Expand All @@ -54,6 +62,7 @@
import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris;
import org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle;
import org.opentripplanner.transit.model.basic.Money;
import org.opentripplanner.transit.model.network.Route;
Expand Down Expand Up @@ -1235,6 +1244,10 @@ public interface GraphQLElevationProfileComponent {
public DataFetcher<Double> elevation();
}

/**
* This type is only here for backwards-compatibility and this API will never return it anymore.
* Please use the leg's `fareProducts` instead.
*/
public interface GraphQLFare {
public DataFetcher<Integer> cents();

Expand All @@ -1245,7 +1258,10 @@ public interface GraphQLFare {
public DataFetcher<String> type();
}

/** Component of the fare (i.e. ticket) for a part of the itinerary */
/**
* This type is only here for backwards-compatibility and this API will never return it anymore.
* Please use the leg's `fareProducts` instead.
*/
public interface GraphQLFareComponent {
public DataFetcher<Integer> cents();

Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
package org.opentripplanner.apis.gtfs.generated;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,19 @@ public Instant getEffectiveEndDate() {
.orElse(null);
}

/**
* Checks if the alert has a NO_SERVICE alert active at the requested time.
* @param instant
* @return
*/
public boolean noServiceAt(Instant instant) {
return (
effect.equals(AlertEffect.NO_SERVICE) &&
(getEffectiveStartDate() != null && getEffectiveStartDate().isBefore(instant)) &&
(getEffectiveEndDate() == null || getEffectiveEndDate().isAfter(instant))
);
}

@Override
public boolean sameAs(@Nonnull TransitAlert other) {
return (
Expand Down
Loading