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
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("noServiceAlert"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.opentripplanner.ext.vectortiles.layers.stops;

import static org.opentripplanner.ext.vectortiles.layers.stops.DigitransitStopPropertyMapper.getRoutes;
import static org.opentripplanner.ext.vectortiles.layers.stops.DigitransitStopPropertyMapper.getType;

import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import org.opentripplanner.apis.support.mapping.PropertyMapper;
import org.opentripplanner.framework.i18n.I18NStringMapper;
import org.opentripplanner.inspector.vector.KeyValue;
import org.opentripplanner.routing.alertpatch.AlertEffect;
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 = ZonedDateTime.now(transitService.getTimeZone()).toInstant();
Copy link
Member

@leonardehrenfried leonardehrenfried Mar 13, 2024

Choose a reason for hiding this comment

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

Suggested change
Instant currentTime = ZonedDateTime.now(transitService.getTimeZone()).toInstant();
Instant currentTime = Instant.now();

No need for the detour via the ZonedDateTime.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Really? I thought the time zone might be important in this context to ensure the alert is actually valid according to local time.

Copy link
Member

Choose a reason for hiding this comment

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

If you call toInstant all timezone information is lost again. An instant is just a number of seconds since 1970-01-01 UTC.

Copy link
Member

Choose a reason for hiding this comment

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

This instant can be converted to many time zones but it remains the same number of seconds.

Copy link
Member

Choose a reason for hiding this comment

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

And since we only want to give an answer at the time of someone asking and not the future/past the time zone doesn't matter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Haha, you're right. Somehow tricked myself into thinking the instant is different in different places. 😂 Thanks!

boolean noServiceAlert = transitService
.getTransitAlertService()
.getStopAlerts(stop.getId())
.stream()
.anyMatch(alert ->
alert.effect().equals(AlertEffect.NO_SERVICE) &&
(
alert.getEffectiveStartDate() != null &&
alert.getEffectiveStartDate().isBefore(currentTime)
) &&
(alert.getEffectiveEndDate() != null && alert.getEffectiveEndDate().isAfter(currentTime))
Copy link
Member

Choose a reason for hiding this comment

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

Can you move this into a method on TransitAlert? Maybe call it noServiceOn(instant).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also fixed the logic a little, as I realised that the end time doesn't need to be defined for the alert to be effective, it could be open ended.

);

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())),
Copy link
Member

Choose a reason for hiding this comment

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

Can you extract a method for these KeyValue instances that are shared between the two mappers?

You can define two lists and use ListUtils.combine to combine the two lists.

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.

I think this "null" is a mistake we made in the old layer, you can just return null

),
new KeyValue("type", getType(transitService, stop)),
new KeyValue("routes", getRoutes(transitService, stop)),
new KeyValue("noServiceAlert", noServiceAlert)
Copy link
Member

Choose a reason for hiding this comment

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

If you want to keep the boolean, you could use a property name like "closedByServiceAlert".

);
}
}
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,23 @@ protected static DigitransitStopPropertyMapper create(

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

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);
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 +61,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,
}
}
Loading