Skip to content

Commit

Permalink
Merge pull request #5743 from HSLdevcom/new-vectortile-realtime-stop-…
Browse files Browse the repository at this point in the history
…layer

New vectortile layer for Digitransit realtime stops
  • Loading branch information
optionsome authored Mar 25, 2024
2 parents a88bef7 + e6c31e6 commit 4d7f4dd
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 28 deletions.
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
)
);
}

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

0 comments on commit 4d7f4dd

Please sign in to comment.