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
10 changes: 10 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": 60
},
// 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 Expand Up @@ -203,3 +212,4 @@ key, and a function to create the mapper, with a `Graph` object as a parameter,
* Added a new Digitransit vehicle parking mapper with no real-time information and less fields
- 2024-01-22: Make `basePath` configurable [#5627](https://github.com/opentripplanner/OpenTripPlanner/pull/5627)
- 2024-02-27: Add layer for flex zones [#5704](https://github.com/opentripplanner/OpenTripPlanner/pull/5704)
- 2024-03-25: Add layer for realtime stops [#5743](https://github.com/opentripplanner/OpenTripPlanner/pull/5743)
10 changes: 10 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": 60
},
// 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 Expand Up @@ -298,3 +307,4 @@ key, and a function to create the mapper, with a `Graph` object as a parameter,
* Added a new Digitransit vehicle parking mapper with no real-time information and less fields
- 2024-01-22: Make `basePath` configurable [#5627](https://github.com/opentripplanner/OpenTripPlanner/pull/5627)
- 2024-02-27: Add layer for flex zones [#5704](https://github.com/opentripplanner/OpenTripPlanner/pull/5704)
- 2024-03-25: Add layer for realtime stops [#5743](https://github.com/opentripplanner/OpenTripPlanner/pull/5743)
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 @@ -9,6 +9,7 @@
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
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.network.TripPattern;
Expand All @@ -20,7 +21,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 +35,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("type", getType(transitService, stop)),
new KeyValue("routes", getRoutes(transitService, stop)),
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.

)
);
}

String routes = JSONArray.toJSONString(
protected static String getRoutes(TransitService transitService, RegularStop stop) {
return JSONArray.toJSONString(
transitService
.getRoutesForStop(stop)
.stream()
Expand All @@ -58,18 +70,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,
}
}
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