
org.opentripplanner.graph_builder.module.GtfsModule Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otp Show documentation
Show all versions of otp Show documentation
The OpenTripPlanner multimodal journey planning system
package org.opentripplanner.graph_builder.module;
import com.google.common.collect.Sets;
import org.onebusaway.csv_entities.EntityHandler;
import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl;
import org.onebusaway.gtfs.model.Agency;
import org.onebusaway.gtfs.model.FareAttribute;
import org.onebusaway.gtfs.model.IdentityBean;
import org.onebusaway.gtfs.model.Pathway;
import org.onebusaway.gtfs.model.Route;
import org.onebusaway.gtfs.model.ServiceCalendar;
import org.onebusaway.gtfs.model.ServiceCalendarDate;
import org.onebusaway.gtfs.model.ShapePoint;
import org.onebusaway.gtfs.model.Stop;
import org.onebusaway.gtfs.model.Trip;
import org.onebusaway.gtfs.serialization.GtfsReader;
import org.onebusaway.gtfs.services.GenericMutableDao;
import org.onebusaway.gtfs.services.GtfsMutableRelationalDao;
import org.opentripplanner.ext.flex.FlexTripsMapper;
import org.opentripplanner.graph_builder.DataImportIssueStore;
import org.opentripplanner.graph_builder.model.GtfsBundle;
import org.opentripplanner.graph_builder.module.geometry.GeometryAndBlockProcessor;
import org.opentripplanner.graph_builder.services.GraphBuilderModule;
import org.opentripplanner.gtfs.GenerateTripPatternsOperation;
import org.opentripplanner.gtfs.RepairStopTimesForEachTripOperation;
import org.opentripplanner.model.BikeAccess;
import org.opentripplanner.model.FeedScopedId;
import org.opentripplanner.model.OtpTransitService;
import org.opentripplanner.model.TripStopTimes;
import org.opentripplanner.model.calendar.CalendarServiceData;
import org.opentripplanner.model.calendar.ServiceDateInterval;
import org.opentripplanner.model.impl.OtpTransitServiceBuilder;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.services.FareServiceFactory;
import org.opentripplanner.standalone.config.BuildConfig;
import org.opentripplanner.util.OTPFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.opentripplanner.gtfs.mapping.GTFSToOtpTransitServiceMapper.mapGtfsDaoToInternalTransitServiceBuilder;
public class GtfsModule implements GraphBuilderModule {
private static final Logger LOG = LoggerFactory.getLogger(GtfsModule.class);
private DataImportIssueStore issueStore;
private EntityHandler counter = new EntityCounter();
private FareServiceFactory fareServiceFactory;
/** will be applied to all bundles which do not have the cacheDirectory property set */
private File cacheDirectory;
/** will be applied to all bundles which do not have the useCached property set */
private Boolean useCached;
private Set agencyIdsSeen = Sets.newHashSet();
private int nextAgencyId = 1; // used for generating agency IDs to resolve ID conflicts
/**
* @see BuildConfig#transitServiceStart
* @see BuildConfig#transitServiceEnd
*/
private final ServiceDateInterval transitPeriodLimit;
private List gtfsBundles;
public GtfsModule(List bundles, ServiceDateInterval transitPeriodLimit) {
this.gtfsBundles = bundles;
this.transitPeriodLimit = transitPeriodLimit;
}
public List provides() {
List result = new ArrayList();
result.add("transit");
return result;
}
public List getPrerequisites() {
return Collections.emptyList();
}
public void setFareServiceFactory(FareServiceFactory factory) {
fareServiceFactory = factory;
}
@Override
public void buildGraph(
Graph graph,
HashMap, Object> extra,
DataImportIssueStore issueStore
) {
this.issueStore = issueStore;
// we're about to add another agency to the graph, so clear the cached timezone
// in case it should change
// OTP doesn't currently support multiple time zones in a single graph;
// at least this way we catch the error and log it instead of silently ignoring
// because the time zone from the first agency is cached
graph.clearTimeZone();
CalendarServiceData calendarServiceData = new CalendarServiceData();
try {
for (GtfsBundle gtfsBundle : gtfsBundles) {
// apply global defaults to individual GTFSBundles (if globals have been set)
if (cacheDirectory != null && gtfsBundle.cacheDirectory == null) {
gtfsBundle.cacheDirectory = cacheDirectory;
}
if (useCached != null && gtfsBundle.useCached == null) {
gtfsBundle.useCached = useCached;
}
OtpTransitServiceBuilder builder = mapGtfsDaoToInternalTransitServiceBuilder(
loadBundle(gtfsBundle),
gtfsBundle.getFeedId().getId(),
issueStore
);
builder.limitServiceDays(transitPeriodLimit);
calendarServiceData.add(builder.buildCalendarServiceData());
// NB! The calls below have side effects - the builder state is updated!
if (OTPFeature.FlexRouting.isOn()) {
FlexTripsMapper.createFlexTrips(builder);
}
repairStopTimesForEachTrip(builder.getStopTimesSortedByTrip());
// NB! The calls below have side effects - the builder state is updated!
createTripPatterns(graph, builder, calendarServiceData.getServiceIds());
OtpTransitService transitModel = builder.build();
addTransitModelToGraph(graph, gtfsBundle, transitModel);
createGeometryAndBlockProcessor(gtfsBundle, transitModel).run(graph, issueStore);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
finally {
// Note the close method of each bundle should NOT throw an exception, so this
// code should be safe without the try/catch block.
gtfsBundles.forEach(GtfsBundle::close);
}
// We need to save the calendar service data so we can use it later
graph.putService(
org.opentripplanner.model.calendar.CalendarServiceData.class,
calendarServiceData
);
graph.updateTransitFeedValidity(calendarServiceData, issueStore);
graph.hasTransit = true;
graph.calculateTransitCenter();
}
/* Private Methods */
/**
* This method have side-effects, the {@code stopTimesByTrip} is updated.
*/
private void repairStopTimesForEachTrip(TripStopTimes stopTimesByTrip) {
new RepairStopTimesForEachTripOperation(stopTimesByTrip, issueStore).run();
}
/**
* This method have side-effects, the {@code builder} is updated with new TripPatterns.
*/
private void createTripPatterns(Graph graph, OtpTransitServiceBuilder builder, Set calServiceIds) {
GenerateTripPatternsOperation buildTPOp = new GenerateTripPatternsOperation(
builder, this.issueStore, graph.deduplicator, calServiceIds
);
buildTPOp.run();
graph.hasFrequencyService = graph.hasFrequencyService || buildTPOp.hasFrequencyBasedTrips();
graph.hasScheduledService = graph.hasScheduledService || buildTPOp.hasScheduledTrips();
}
private void addTransitModelToGraph(Graph graph, GtfsBundle gtfsBundle, OtpTransitService transitModel) {
AddTransitModelEntitiesToGraph.addToGraph(
gtfsBundle.getFeedId(),
transitModel,
gtfsBundle.subwayAccessTime,
graph
);
}
private GeometryAndBlockProcessor createGeometryAndBlockProcessor (GtfsBundle gtfsBundle, OtpTransitService transitService) {
return new GeometryAndBlockProcessor(
transitService,
fareServiceFactory,
gtfsBundle.getMaxStopToShapeSnapDistance(),
gtfsBundle.maxInterlineDistance
);
}
private GtfsMutableRelationalDao loadBundle(GtfsBundle gtfsBundle)
throws IOException {
StoreImpl store = new StoreImpl(new GtfsRelationalDaoImpl());
store.open();
LOG.info("reading {}", gtfsBundle.toString());
GtfsFeedId gtfsFeedId = gtfsBundle.getFeedId();
GtfsReader reader = new GtfsReader();
reader.setInputSource(gtfsBundle.getCsvInputSource());
reader.setEntityStore(store);
reader.setInternStrings(true);
reader.setDefaultAgencyId(gtfsFeedId.getId());
if (LOG.isDebugEnabled())
reader.addEntityHandler(counter);
if (gtfsBundle.getDefaultBikesAllowed())
reader.addEntityHandler(new EntityBikeability(true));
for (Class> entityClass : reader.getEntityClasses()) {
LOG.info("reading entities: " + entityClass.getName());
reader.readEntities(entityClass);
store.flush();
// NOTE that agencies are first in the list and read before all other entity types, so it is effective to
// set the agencyId here. Each feed ("bundle") is loaded by a separate reader, so there is no risk of
// agency mappings accumulating.
if (entityClass == Agency.class) {
for (Agency agency : reader.getAgencies()) {
String agencyId = agency.getId();
LOG.info("This Agency has the ID {}", agencyId);
// Somehow, when the agency's id field is missing, OBA replaces it with the agency's name.
// TODO Figure out how and why this is happening.
if (agencyId == null || agencyIdsSeen.contains(gtfsFeedId.getId() + agencyId)) {
// Loop in case generated name is already in use.
String generatedAgencyId = null;
while (generatedAgencyId == null || agencyIdsSeen.contains(generatedAgencyId)) {
generatedAgencyId = "F" + nextAgencyId;
nextAgencyId++;
}
LOG.warn("The agency ID '{}' was already seen, or I think it's bad. Replacing with '{}'.", agencyId, generatedAgencyId);
reader.addAgencyIdMapping(agencyId, generatedAgencyId); // NULL key should work
agency.setId(generatedAgencyId);
agencyId = generatedAgencyId;
}
if (agencyId != null) agencyIdsSeen.add(gtfsFeedId.getId() + agencyId);
}
}
}
for (ShapePoint shapePoint : store.getAllEntitiesForType(ShapePoint.class)) {
shapePoint.getShapeId().setAgencyId(reader.getDefaultAgencyId());
}
for (Route route : store.getAllEntitiesForType(Route.class)) {
route.getId().setAgencyId(reader.getDefaultAgencyId());
generateRouteColor(route);
}
for (Stop stop : store.getAllEntitiesForType(Stop.class)) {
stop.getId().setAgencyId(reader.getDefaultAgencyId());
}
for (Trip trip : store.getAllEntitiesForType(Trip.class)) {
trip.getId().setAgencyId(reader.getDefaultAgencyId());
}
for (ServiceCalendar serviceCalendar : store.getAllEntitiesForType(ServiceCalendar.class)) {
serviceCalendar.getServiceId().setAgencyId(reader.getDefaultAgencyId());
}
for (ServiceCalendarDate serviceCalendarDate : store.getAllEntitiesForType(ServiceCalendarDate.class)) {
serviceCalendarDate.getServiceId().setAgencyId(reader.getDefaultAgencyId());
}
for (FareAttribute fareAttribute : store.getAllEntitiesForType(FareAttribute.class)) {
fareAttribute.getId().setAgencyId(reader.getDefaultAgencyId());
}
for (Pathway pathway : store.getAllEntitiesForType(Pathway.class)) {
pathway.getId().setAgencyId(reader.getDefaultAgencyId());
}
store.close();
return store.dao;
}
/**
* Generates routeText colors for routes with routeColor and without routeTextColor
*
* If route doesn't have color or already has routeColor and routeTextColor nothing is done.
*
* textColor can be black or white. White for dark colors and black for light colors of routeColor.
* If color is light or dark is calculated based on luminance formula:
* sqrt( 0.299*Red^2 + 0.587*Green^2 + 0.114*Blue^2 )
*
* @param route
*/
private void generateRouteColor(Route route) {
String routeColor = route.getColor();
//No route color - skipping
if (routeColor == null) {
return;
}
String textColor = route.getTextColor();
//Route already has text color skipping
if (textColor != null) {
return;
}
Color routeColorColor = Color.decode("#"+routeColor);
//gets float of RED, GREEN, BLUE in range 0...1
float[] colorComponents = routeColorColor.getRGBColorComponents(null);
//Calculates luminance based on https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
double newRed = 0.299*Math.pow(colorComponents[0],2.0);
double newGreen = 0.587*Math.pow(colorComponents[1],2.0);
double newBlue = 0.114*Math.pow(colorComponents[2],2.0);
double luminance = Math.sqrt(newRed+newGreen+newBlue);
//For brighter colors use black text color and reverse for darker
if (luminance > 0.5) {
textColor = "000000";
} else {
textColor = "FFFFFF";
}
route.setTextColor(textColor);
}
private class StoreImpl implements GenericMutableDao {
private GtfsMutableRelationalDao dao;
StoreImpl(GtfsMutableRelationalDao dao) {
this.dao = dao;
}
@Override
public void open() {
dao.open();
}
@Override
public T getEntityForId(Class type, Serializable id) {
return dao.getEntityForId(type, id);
}
@Override
public void saveEntity(Object entity) {
dao.saveEntity(entity);
}
@Override
public void flush() {
dao.flush();
}
@Override
public void close() {
dao.close();
}
@Override
public void clearAllEntitiesForType(Class type) {
throw new UnsupportedOperationException();
}
@Override
public > void removeEntity(T entity) {
throw new UnsupportedOperationException();
}
@Override
public Collection getAllEntitiesForType(Class type) {
return dao.getAllEntitiesForType(type);
}
@Override
public void saveOrUpdateEntity(Object entity) {
throw new UnsupportedOperationException();
}
@Override
public void updateEntity(Object entity) {
throw new UnsupportedOperationException();
}
}
private static class EntityCounter implements EntityHandler {
private Map, Integer> _count = new HashMap, Integer>();
@Override
public void handleEntity(Object bean) {
int count = incrementCount(bean.getClass());
if (count % 1000000 == 0)
if (LOG.isDebugEnabled()) {
String name = bean.getClass().getName();
int index = name.lastIndexOf('.');
if (index != -1)
name = name.substring(index + 1);
LOG.debug("loading " + name + ": " + count);
}
}
private int incrementCount(Class> entityType) {
Integer value = _count.get(entityType);
if (value == null)
value = 0;
value++;
_count.put(entityType, value);
return value;
}
}
private static class EntityBikeability implements EntityHandler {
private Boolean defaultBikesAllowed;
public EntityBikeability(Boolean defaultBikesAllowed) {
this.defaultBikesAllowed = defaultBikesAllowed;
}
@Override
public void handleEntity(Object bean) {
if (!(bean instanceof Trip)) {
return;
}
Trip trip = (Trip) bean;
if (defaultBikesAllowed && BikeAccess.fromTrip(trip) == BikeAccess.UNKNOWN) {
BikeAccess.setForTrip(trip, BikeAccess.ALLOWED);
}
}
}
@Override
public void checkInputs() {
for (GtfsBundle bundle : gtfsBundles) {
bundle.checkInputs();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy