Skip to content

Commit

Permalink
Add feed id to gtfs feeds, resolves #1990
Browse files Browse the repository at this point in the history
This adds improved support for multiple GTFS feeds and multi agency feeds

* Change to use a feed id instead of selecting a default agency id for feeds
* Change to re-index gtfs feeds using the feed id
* Change indexes that use agency id as key to group agencies with a feed id instead
* Change to use feedId instead of defaultAgency in gtfs-rt updaters, this makes entity selector for gtfs-rt alerts to work as expected
* Add support for multi agency feeds to the gtfs-rt updaters
* Change index api for agencies, these api now requires a feed id
* Add support for matching gtfs alert with a trip id
  • Loading branch information
johannilsson committed Jun 19, 2015
1 parent 1367661 commit b5ae935
Show file tree
Hide file tree
Showing 41 changed files with 868 additions and 413 deletions.
4 changes: 2 additions & 2 deletions docs/Configuration.md
Expand Up @@ -348,7 +348,7 @@ connect to a network resource is the `url` field.
type: "real-time-alerts", type: "real-time-alerts",
frequencySec: 30, frequencySec: 30,
url: "http://developer.trimet.org/ws/V1/FeedSpecAlerts/appID/0123456789ABCDEF", url: "http://developer.trimet.org/ws/V1/FeedSpecAlerts/appID/0123456789ABCDEF",
defaultAgencyId: "TriMet" feedId: "TriMet"
}, },


// Polling bike rental updater. // Polling bike rental updater.
Expand Down Expand Up @@ -381,7 +381,7 @@ connect to a network resource is the `url` field.
// this is either http or file... shouldn't it default to http or guess from the presence of a URL? // this is either http or file... shouldn't it default to http or guess from the presence of a URL?
sourceType: "gtfs-http", sourceType: "gtfs-http",
url: "http://developer.trimet.org/ws/V1/TripUpdate/appID/0123456789ABCDEF", url: "http://developer.trimet.org/ws/V1/TripUpdate/appID/0123456789ABCDEF",
defaultAgencyId: "TriMet" feedId: "TriMet"
}, },


// Streaming differential GTFS-RT TripUpdates over websockets // Streaming differential GTFS-RT TripUpdates over websockets
Expand Down
Expand Up @@ -16,10 +16,7 @@ the License, or (props, at your option) any later version.
import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LineString;
import org.onebusaway.gtfs.model.Agency; import org.onebusaway.gtfs.model.*;
import org.onebusaway.gtfs.model.Route;
import org.onebusaway.gtfs.model.Stop;
import org.onebusaway.gtfs.model.Trip;
import org.opentripplanner.api.model.*; import org.opentripplanner.api.model.*;
import org.opentripplanner.common.geometry.DirectionUtils; import org.opentripplanner.common.geometry.DirectionUtils;
import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.common.geometry.GeometryUtils;
Expand Down Expand Up @@ -293,8 +290,6 @@ private static Leg generateLeg(Graph graph, State[] states, boolean showIntermed
leg.distance += edges[i].getDistance(); leg.distance += edges[i].getDistance();
} }


addModeAndAlerts(graph, leg, states, requestedLocale);

TimeZone timeZone = leg.startTime.getTimeZone(); TimeZone timeZone = leg.startTime.getTimeZone();
leg.agencyTimeZoneOffset = timeZone.getOffset(leg.startTime.getTimeInMillis()); leg.agencyTimeZoneOffset = timeZone.getOffset(leg.startTime.getTimeInMillis());


Expand All @@ -315,6 +310,8 @@ private static Leg generateLeg(Graph graph, State[] states, boolean showIntermed


leg.rentedBike = states[0].isBikeRenting() && states[states.length - 1].isBikeRenting(); leg.rentedBike = states[0].isBikeRenting() && states[states.length - 1].isBikeRenting();


addModeAndAlerts(graph, leg, states, requestedLocale);

return leg; return leg;
} }


Expand Down Expand Up @@ -527,7 +524,16 @@ private static void addModeAndAlerts(Graph graph, Leg leg, State[] states, Local


for (AlertPatch alertPatch : graph.getAlertPatches(edge)) { for (AlertPatch alertPatch : graph.getAlertPatches(edge)) {
if (alertPatch.displayDuring(state)) { if (alertPatch.displayDuring(state)) {
leg.addAlert(alertPatch.getAlert(), requestedLocale); if (alertPatch.hasTrip()) {
// If the alert patch contains a trip and that trip match this leg only add the alert for
// this leg.
if (alertPatch.getTrip().equals(new AgencyAndId(alertPatch.getFeedId(), leg.tripId))) {
leg.addAlert(alertPatch.getAlert(), requestedLocale);
}
} else {
// If we are not matching a particular trip add all known alerts for this trip pattern.
leg.addAlert(alertPatch.getAlert(), requestedLocale);
}
} }
} }
} }
Expand Down
@@ -0,0 +1,257 @@
/**
* Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org>
* Copyright (C) 2012 Google, Inc.
* Copyright (C) 2013 Codemass, Inc. <aaron@codemass.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.opentripplanner.calendar.impl;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;

import org.onebusaway.gtfs.impl.calendar.CalendarServiceImpl;
import org.onebusaway.gtfs.impl.calendar.UnknownAgencyTimezoneException;
import org.onebusaway.gtfs.model.Agency;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.ServiceCalendar;
import org.onebusaway.gtfs.model.ServiceCalendarDate;
import org.onebusaway.gtfs.model.calendar.CalendarServiceData;
import org.onebusaway.gtfs.model.calendar.LocalizedServiceId;
import org.onebusaway.gtfs.model.calendar.ServiceDate;
import org.onebusaway.gtfs.services.GtfsRelationalDao;
import org.onebusaway.gtfs.services.calendar.CalendarService;
import org.onebusaway.gtfs.services.calendar.CalendarServiceDataFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* We perform initial date calculations in the timezone of the host jvm, which
* may be different than the timezone of an agency with the specified service
* id. To my knowledge, the calculation should work the same, which is to say I
* can't immediately think of any cases where the service dates would be
* computed incorrectly.
*
* @author bdferris
*/
public class CalendarServiceDataFactoryImpl implements
CalendarServiceDataFactory {

private final Logger _log = LoggerFactory.getLogger(CalendarServiceDataFactoryImpl.class);

private GtfsRelationalDao _dao;

private int _excludeFutureServiceDatesInDays;

public static CalendarService createService(GtfsRelationalDao dao) {
CalendarServiceDataFactoryImpl factory = new CalendarServiceDataFactoryImpl(
dao);
return new CalendarServiceImpl(factory.createData());
}

public CalendarServiceDataFactoryImpl() {

}

public CalendarServiceDataFactoryImpl(GtfsRelationalDao dao) {
_dao = dao;
}

public void setGtfsDao(GtfsRelationalDao dao) {
_dao = dao;
}

public void setExcludeFutureServiceDatesInDays(
int excludeFutureServiceDatesInDays) {
_excludeFutureServiceDatesInDays = excludeFutureServiceDatesInDays;
}

@Override
public CalendarServiceData createData() {

CalendarServiceData data = new CalendarServiceData();

setTimeZonesForAgencies(data);

List<AgencyAndId> serviceIds = _dao.getAllServiceIds();

int index = 0;

for (AgencyAndId serviceId : serviceIds) {

index++;

_log.debug("serviceId=" + serviceId + " (" + index + "/"
+ serviceIds.size() + ")");

TimeZone serviceIdTimeZone = data.getTimeZoneForAgencyId(serviceId.getAgencyId());
if (serviceIdTimeZone == null) {
serviceIdTimeZone = TimeZone.getDefault();
}

Set<ServiceDate> activeDates = getServiceDatesForServiceId(serviceId,
serviceIdTimeZone);

List<ServiceDate> serviceDates = new ArrayList<ServiceDate>(activeDates);
Collections.sort(serviceDates);

data.putServiceDatesForServiceId(serviceId, serviceDates);

// List<String> tripAgencyIds = _dao.getTripAgencyIdsReferencingServiceId(serviceId);

// Set<TimeZone> timeZones = new HashSet<TimeZone>();
// for (String tripAgencyId : tripAgencyIds) {
// TimeZone timeZone = data.getTimeZoneForAgencyId(tripAgencyId);
// timeZones.add(timeZone);
// }

// for (TimeZone timeZone : timeZones) {
//
// List<Date> dates = new ArrayList<Date>(serviceDates.size());
// for (ServiceDate serviceDate : serviceDates)
// dates.add(serviceDate.getAsDate(timeZone));
//
// LocalizedServiceId id = new LocalizedServiceId(serviceId, timeZone);
// data.putDatesForLocalizedServiceId(id, dates);
// }
}

return data;
}

public Set<ServiceDate> getServiceDatesForServiceId(AgencyAndId serviceId,
TimeZone serviceIdTimeZone) {
Set<ServiceDate> activeDates = new HashSet<ServiceDate>();
ServiceCalendar c = _dao.getCalendarForServiceId(serviceId);

if (c != null) {
addDatesFromCalendar(c, serviceIdTimeZone, activeDates);
}
for (ServiceCalendarDate cd : _dao.getCalendarDatesForServiceId(serviceId)) {
addAndRemoveDatesFromCalendarDate(cd, serviceIdTimeZone, activeDates);
}
return activeDates;
}

private void setTimeZonesForAgencies(CalendarServiceData data) {
for (Agency agency : _dao.getAllAgencies()) {
TimeZone timeZone = TimeZone.getTimeZone(agency.getTimezone());
if (timeZone.getID().equals("GMT")
&& !agency.getTimezone().toUpperCase().equals("GMT")) {
throw new UnknownAgencyTimezoneException(agency.getName(),
agency.getTimezone());
}
data.putTimeZoneForAgencyId(agency.getId(), timeZone);
}
}

private void addDatesFromCalendar(ServiceCalendar calendar,
TimeZone timeZone, Set<ServiceDate> activeDates) {

/**
* We calculate service dates relative to noon so as to avoid any weirdness
* relative to DST.
*/
Date startDate = getServiceDateAsNoon(calendar.getStartDate(), timeZone);
Date endDate = getServiceDateAsNoon(calendar.getEndDate(), timeZone);

java.util.Calendar c = java.util.Calendar.getInstance(timeZone);
c.setTime(startDate);

while (true) {
Date date = c.getTime();
if (date.after(endDate))
break;

int day = c.get(java.util.Calendar.DAY_OF_WEEK);
boolean active = false;

switch (day) {
case java.util.Calendar.MONDAY:
active = calendar.getMonday() == 1;
break;
case java.util.Calendar.TUESDAY:
active = calendar.getTuesday() == 1;
break;
case java.util.Calendar.WEDNESDAY:
active = calendar.getWednesday() == 1;
break;
case java.util.Calendar.THURSDAY:
active = calendar.getThursday() == 1;
break;
case java.util.Calendar.FRIDAY:
active = calendar.getFriday() == 1;
break;
case java.util.Calendar.SATURDAY:
active = calendar.getSaturday() == 1;
break;
case java.util.Calendar.SUNDAY:
active = calendar.getSunday() == 1;
break;
}

if (active) {
addServiceDate(activeDates, new ServiceDate(c), timeZone);
}

c.add(java.util.Calendar.DAY_OF_YEAR, 1);
}
}

private void addAndRemoveDatesFromCalendarDate(
ServiceCalendarDate calendarDate, TimeZone serviceIdTimeZone,
Set<ServiceDate> activeDates) {

ServiceDate serviceDate = calendarDate.getDate();
Date targetDate = calendarDate.getDate().getAsDate();
Calendar c = Calendar.getInstance();
c.setTime(targetDate);

switch (calendarDate.getExceptionType()) {
case ServiceCalendarDate.EXCEPTION_TYPE_ADD:
addServiceDate(activeDates, serviceDate, serviceIdTimeZone);
break;
case ServiceCalendarDate.EXCEPTION_TYPE_REMOVE:
activeDates.remove(serviceDate);
break;
default:
_log.warn("unknown CalendarDate exception type: "
+ calendarDate.getExceptionType());
break;
}
}

private void addServiceDate(Set<ServiceDate> activeDates,
ServiceDate serviceDate, TimeZone timeZone) {
if (_excludeFutureServiceDatesInDays > 0) {
int days = (int) ((serviceDate.getAsDate().getTime() - System.currentTimeMillis()) / (24 * 60 * 60 * 1000));
if (days > _excludeFutureServiceDatesInDays)
return;
}

activeDates.add(new ServiceDate(serviceDate));
}

private static Date getServiceDateAsNoon(ServiceDate serviceDate,
TimeZone timeZone) {
Calendar c = serviceDate.getAsCalendar(timeZone);
c.add(Calendar.HOUR_OF_DAY, 12);
return c.getTime();
}
}
Expand Up @@ -25,6 +25,7 @@ the License, or (at your option) any later version.
import org.onebusaway.csv_entities.FileCsvInputSource; import org.onebusaway.csv_entities.FileCsvInputSource;
import org.onebusaway.csv_entities.ZipFileCsvInputSource; import org.onebusaway.csv_entities.ZipFileCsvInputSource;
import org.opentripplanner.graph_builder.module.DownloadableGtfsInputSource; import org.opentripplanner.graph_builder.module.DownloadableGtfsInputSource;
import org.opentripplanner.graph_builder.module.GtfsFeedId;
import org.opentripplanner.util.HttpUtils; import org.opentripplanner.util.HttpUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
Expand All @@ -37,7 +38,7 @@ public class GtfsBundle {


private URL url; private URL url;


private String defaultAgencyId; private GtfsFeedId feedId;


private CsvInputSource csvInputSource; private CsvInputSource csvInputSource;


Expand Down Expand Up @@ -72,6 +73,7 @@ public GtfsBundle() {


public GtfsBundle(File gtfsFile) { public GtfsBundle(File gtfsFile) {
this.setPath(gtfsFile); this.setPath(gtfsFile);
this.setFeedId(GtfsFeedId.createFromFile(gtfsFile));
} }


public void setPath(File path) { public void setPath(File path) {
Expand Down Expand Up @@ -120,26 +122,21 @@ public String toString () {
} else { } else {
src = "(no source)"; src = "(no source)";
} }
if (feedId != null) {
src += " (" + feedId.getId() + ")";
}
return "GTFS bundle at " + src; return "GTFS bundle at " + src;
} }


/** /**
* So that you can load multiple gtfs feeds into the same database / system without entity id * So that we can load multiple gtfs feeds into the same database.
* collisions, everything has an agency id, including entities like stops, shapes, and service
* ids that don't explicitly have an agency id (as opposed to routes + trips + stop times).
* However, the spec doesn't currently have a method to specify which agency a stop
* should be assigned to in the case of multiple agencies being specified in the same feed.
* Routes (and thus everything belonging to them) do have an agency id, but stops don't.
* The defaultAgencyId allows you to define which agency will be used as the default
* when figuring out which agency a stop should be assigned to (also applies to shapes + service
* ids as well). If not specified, the first agency in the agency list will be used.
*/ */
public String getDefaultAgencyId() { public GtfsFeedId getFeedId() {
return defaultAgencyId; return feedId;
} }


public void setDefaultAgencyId(String defaultAgencyId) { public void setFeedId(GtfsFeedId feedId) {
this.defaultAgencyId = defaultAgencyId; this.feedId = feedId;
} }


public Map<String, String> getAgencyIdMappings() { public Map<String, String> getAgencyIdMappings() {
Expand Down

0 comments on commit b5ae935

Please sign in to comment.