/// <summary> /// Get all of the Trips that go from a given station to another. /// </summary> public static IEnumerable <TFTrip> GetAllTripsFromTo(TFStop from, TFStop to, DateTimeOffset date) { var fromStops = AllLowestChildrenForStop(from); var toStops = AllLowestChildrenForStop(to); var fromTrips = fromStops.SelectMany(s => s.Trips); var toTrips = toStops.SelectMany(s => s.Trips); // Get all of the trips that stop at A and B var tripsBetween = fromTrips.Intersect(toTrips); // tripsBetween contains a Trips going in both directions between // the two stops. Create a filtered list with only the trips that // stop at 'from' before they stop at 'to'. var directionalTripsBetween = new HashSet <TFTrip>(); foreach (var trip in tripsBetween) { var fromStopTime = trip.StopTimes.FirstOrDefault(st => fromStops.Contains(st.Stop)); var toStopTime = trip.StopTimes.FirstOrDefault(st => toStops.Contains(st.Stop)); if (fromStopTime.Sequence < toStopTime.Sequence) { if (TripRunsOnDate(trip, date)) { directionalTripsBetween.Add(trip); } } } return(directionalTripsBetween); }
/// <summary> /// For a given stop, will follow the chain of parents up to the /// highest level parent. /// </summary> public static TFStop HighestParentStop(TFStop stop) { if (stop.Parent is null) { return(stop); } else { return(HighestParentStop(stop.Parent)); } }
/// <summary> /// Returns all bottom level children for a given stop. Will follow /// the full chain of children and return all leaf node child stops /// (child stops without any children). If the stop has no children, /// the stop will be returned in the list. /// </summay> public static IEnumerable <TFStop> AllLowestChildrenForStop(TFStop stop) { var result = new List <TFStop>(); if (stop.Children.Count > 0) { // If the stop has children, don't add it to the list, but get // all of it's lowest children and return them. foreach (var child in stop.Children) { result.AddRange(AllLowestChildrenForStop(child)); } } else { result.Add(stop); } return(result); }
/// <summary> /// Reads GTFS (General/Google Transit Feed Specification) data from /// a given location, loading it in to the TransitFeed object for /// storage in a database. /// </summary> /// <remarks> /// The GTFS data that will be loaded looks like the following. /// /// +--------+ /// | Routes | /// +--------+ /// ^ +------+ /// | | | /// +--------+ +------------+ +-------+ | /// | Trips |<--| Stop Times |-->| Stops |<-+ /// +--------+ +------------+ +-------+ /// | /// +-----------------+ /// | | /// v v /// +----------+ +----------------+ /// | Calendar | | Calendar Dates | /// +----------+ +----------------+ /// /// To most easily add references to the data as it is loaded, it will /// be read in the following order (class names in brackets): /// - Routes (TFRoute) /// - Calendar (TFServiceCalendar) /// - Calendar Dates (TFServiceCalendarException) /// - Trips (TFTrip) /// - Stops (TFStop) /// - Stop Times (TFStopTime) /// </remarks> public void Read(string gtfsFilePath) { // Routes var routesPath = Path.Combine(gtfsFilePath, Constants.Files.Routes); var routesTime = ReadFile(routesPath, (csv) => { var id = csv.GetField(Constants.Fields.RouteId); var route = new TFRoute { Id = id, ShortName = csv.GetField(Constants.Fields.RouteShortName).NullIfWhitespace(), LongName = csv.GetField(Constants.Fields.RouteLongName).NullIfWhitespace(), Type = csv.GetField <TFRouteType>(Constants.Fields.RouteType), Color = csv.GetField(Constants.Fields.RouteColor).NullIfWhitespace() }; _routes.Add(id, route); }); Console.WriteLine($"Done: Routes ({routesTime.TotalMilliseconds}ms)"); // Calendar var calendarPath = Path.Combine(gtfsFilePath, Constants.Files.Calendar); var calendarTime = ReadFile(calendarPath, (csv) => { var serviceId = csv.GetField(Constants.Fields.ServiceId); var startDate = DateTimeOffset.ParseExact(csv.GetField(Constants.Fields.StartDate), Constants.Formats.Date, CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.AssumeUniversal); var endDate = DateTimeOffset.ParseExact(csv.GetField(Constants.Fields.EndDate), Constants.Formats.Date, CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.AssumeUniversal); var calendar = new TFServiceCalendar { Monday = csv.GetField <bool>(Constants.Fields.Monday), Tuesday = csv.GetField <bool>(Constants.Fields.Tuesday), Wednesday = csv.GetField <bool>(Constants.Fields.Wednesday), Thursday = csv.GetField <bool>(Constants.Fields.Thursday), Friday = csv.GetField <bool>(Constants.Fields.Friday), Saturday = csv.GetField <bool>(Constants.Fields.Saturday), Sunday = csv.GetField <bool>(Constants.Fields.Sunday), StartDate = startDate, EndDate = endDate }; _calendar.Add(serviceId, calendar); }); Console.WriteLine($"Done: Calendar ({calendarTime.TotalMilliseconds}ms)"); // Calendar Date var calendarDatePath = Path.Combine(gtfsFilePath, Constants.Files.CalendarDates); var calendarDateTime = ReadFile(calendarDatePath, (csv) => { var id = csv.GetField(Constants.Fields.ServiceId); if (!_calendarExceptions.ContainsKey(id)) { _calendarExceptions[id] = new List <TFServiceCalendarException>(); } var date = DateTimeOffset.ParseExact(csv.GetField(Constants.Fields.Date), Constants.Formats.Date, CultureInfo.InvariantCulture.DateTimeFormat); var exceptionType = csv.GetField <TFCalendarExceptionType>(Constants.Fields.ExceptionType); var exception = new TFServiceCalendarException { Date = date, ExceptionType = exceptionType }; _calendarExceptions[id].Add(exception); }); Console.WriteLine($"Done: Calendar Dates ({calendarDateTime.TotalMilliseconds}ms)"); // Trips var tripsPath = Path.Combine(gtfsFilePath, Constants.Files.Trips); var tripsTime = ReadFile(tripsPath, (csv) => { var id = csv.GetField(Constants.Fields.TripId); var serviceId = csv.GetField(Constants.Fields.ServiceId); var trip = new TFTrip { Id = id, Route = _routes[csv.GetField(Constants.Fields.RouteId)], Calendar = _calendar.GetValueOrDefault(serviceId), CalendarExceptions = _calendarExceptions.GetValueOrDefault(serviceId), Headsign = csv.GetField(Constants.Fields.TripHeadsign).NullIfWhitespace(), Direction = csv.GetField(Constants.Fields.DirectionId).NullIfWhitespace() }; _trips.Add(id, trip); Trips.Add(trip); }); Console.WriteLine($"Done: Trips ({tripsTime.TotalMilliseconds}ms)"); // Stops var stopsPath = Path.Combine(gtfsFilePath, Constants.Files.Stops); var stopsTime = ReadFile(stopsPath, (csv) => { var id = csv.GetField(Constants.Fields.StopId); var stop = new TFStop { Id = id, Code = csv.GetField(Constants.Fields.StopCode).NullIfWhitespace(), Name = csv.GetField(Constants.Fields.StopName).NullIfWhitespace(), Latitude = csv.GetField <float>(Constants.Fields.StopLat), Longitude = csv.GetField <float>(Constants.Fields.StopLon), Type = csv.GetField(Constants.Fields.LocationType).NullIfWhitespace(), ParentId = csv.GetField(Constants.Fields.ParentStation).NullIfWhitespace(), Platform = csv.GetField(Constants.Fields.PlatformCode).NullIfWhitespace() }; _stops.Add(id, stop); }); // Put a reference to the parent stop in each stop, and build out a // list of all of the top level stops. foreach (var stop in _stops) { var parentId = stop.Value.ParentId; stop.Value.ParentId = null; if (parentId is object) { // If this has a parent, add references between the parent // and the child. var parent = _stops[parentId]; stop.Value.Parent = parent; parent.Children.Add(stop.Value); } else { // If this does not have a parent, add it to the output // public list of stops Stops.Add(stop.Value); } } Console.WriteLine($"Done: Stops ({stopsTime.TotalMilliseconds}ms)"); // Stop Times var stopTimesPath = Path.Combine(gtfsFilePath, Constants.Files.StopTimes); var stopTimesTime = ReadFile(stopTimesPath, (csv) => { var stopId = csv.GetField(Constants.Fields.StopId); var stop = _stops[stopId]; var trip = _trips[csv.GetField(Constants.Fields.TripId)]; var stopTopLevel = HighestParentStop(stop); var stopTopLevelId = stopTopLevel.Id; var stopTime = new TFStopTime { StopTopLevel = stopTopLevelId, Stop = stopId, ArrivalTime = TimeSpanParser.ParseStopTime(csv.GetField(Constants.Fields.ArrivalTime)), DepartureTime = TimeSpanParser.ParseStopTime(csv.GetField(Constants.Fields.DepartureTime)), Sequence = csv.GetField <int>(Constants.Fields.StopSequence), PickupType = csv.GetField <TFPickupDropOffType>(Constants.Fields.PickupType), DropOffType = csv.GetField <TFPickupDropOffType>(Constants.Fields.DropOffType) }; // StopTimes only relate to a stop and a time. Add a reference // for this StopTime to both of those. No list is maintained. trip.StopTimes.Add(stopTime); }); Console.WriteLine($"Done: StopTimes ({stopTimesTime.TotalMilliseconds}ms)"); }
/// <summary> /// Reads GTFS (General/Google Transit Feed Specification) data from /// a given location, loading it in to the TransitFeed object for /// querying and manipulation. /// </summary> /// <remarks> /// The feed has a lot of long string identifiers. Want to reduce /// the total number of string assignments and opt for references /// to the actual objects. The initial plan was to do this after /// reading all the data, but that's a whole pile of pointless /// string assignments. /// /// Instead, data will be read in the right order so that instead /// of storing string identifiers, the reference to the actual /// object will be stored. The following diagram shows the /// references in the files that need to be read: /// /// +--------+ /// | Routes | /// +--------+ /// ^ +------+ /// | | | /// +--------+ +------------+ +-------+ | /// | Trips |<--| Stop Times |-->| Stops |<-+ /// +--------+ +------------+ +-------+ /// | /// +-----------------+ /// | | /// v v /// +----------+ +----------------+ /// | Calendar | | Calendar Dates | /// +----------+ +----------------+ /// /// Based on this, the read order is: /// - Routes /// - Trips /// - Stops /// - Stop Times /// </remarks> public void Read(string gtfsFilePath) { // Routes var routesPath = Path.Combine(gtfsFilePath, Constants.Files.Routes); ReadFile(routesPath, (csv) => { var id = csv.GetField(Constants.Fields.RouteId); var route = new TFRoute { ShortName = csv.GetField(Constants.Fields.RouteShortName).NullIfWhitespace(), LongName = csv.GetField(Constants.Fields.RouteLongName).NullIfWhitespace(), Type = csv.GetField <TFRouteType>(Constants.Fields.RouteType), Color = csv.GetField(Constants.Fields.RouteColor).NullIfWhitespace() }; _routes.Add(id, route); }); Console.WriteLine("Done: Routes"); // Calendar var calendarPath = Path.Combine(gtfsFilePath, Constants.Files.Calendar); ReadFile(calendarPath, (csv) => { var serviceId = csv.GetField(Constants.Fields.ServiceId); var startDate = DateTimeOffset.ParseExact(csv.GetField(Constants.Fields.StartDate), Constants.Formats.Date, CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.AssumeUniversal); var endDate = DateTimeOffset.ParseExact(csv.GetField(Constants.Fields.EndDate), Constants.Formats.Date, CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.AssumeUniversal); var calendar = new TFServiceCalendar { Id = serviceId, Monday = csv.GetField <bool>(Constants.Fields.Monday), Tuesday = csv.GetField <bool>(Constants.Fields.Tuesday), Wednesday = csv.GetField <bool>(Constants.Fields.Wednesday), Thursday = csv.GetField <bool>(Constants.Fields.Thursday), Friday = csv.GetField <bool>(Constants.Fields.Friday), Saturday = csv.GetField <bool>(Constants.Fields.Saturday), Sunday = csv.GetField <bool>(Constants.Fields.Sunday), StartDate = startDate, EndDate = endDate }; _calendar.Add(serviceId, calendar); }); Console.WriteLine("Done: Calendar"); // Calendar Date var calendarDatePath = Path.Combine(gtfsFilePath, Constants.Files.CalendarDates); ReadFile(calendarDatePath, (csv) => { var id = csv.GetField(Constants.Fields.ServiceId); if (!_calendarExceptions.ContainsKey(id)) { _calendarExceptions[id] = new TFServiceCalendarExceptions(); } var date = DateTimeOffset.ParseExact(csv.GetField(Constants.Fields.Date), Constants.Formats.Date, CultureInfo.InvariantCulture.DateTimeFormat); var exceptionType = csv.GetField <TFCalendarExceptionType>(Constants.Fields.ExceptionType); _calendarExceptions[id].Exceptions.Add(date, exceptionType); }); Console.WriteLine("Done: Calendar Dates"); // Trips var tripsPath = Path.Combine(gtfsFilePath, Constants.Files.Trips); ReadFile(tripsPath, (csv) => { var id = csv.GetField(Constants.Fields.TripId); var route = _routes[csv.GetField(Constants.Fields.RouteId)]; var serviceId = csv.GetField(Constants.Fields.ServiceId); var trip = new TFTrip { Route = route, Calendar = _calendar.GetValueOrDefault(serviceId), CalendarExceptions = _calendarExceptions.GetValueOrDefault(serviceId), Headsign = csv.GetField(Constants.Fields.TripHeadsign).NullIfWhitespace(), Direction = csv.GetField(Constants.Fields.DirectionId).NullIfWhitespace() }; route.Trips.Add(trip); _trips.Add(id, trip); }); Console.WriteLine("Done: Trips"); // Stops var stopsPath = Path.Combine(gtfsFilePath, Constants.Files.Stops); ReadFile(stopsPath, (csv) => { var id = csv.GetField(Constants.Fields.StopId); var stop = new TFStop { Id = id, Code = csv.GetField(Constants.Fields.StopCode).NullIfWhitespace(), Name = csv.GetField(Constants.Fields.StopName).NullIfWhitespace(), Latitude = csv.GetField <float>(Constants.Fields.StopLat), Longitude = csv.GetField <float>(Constants.Fields.StopLon), Type = csv.GetField(Constants.Fields.LocationType).NullIfWhitespace(), ParentId = csv.GetField(Constants.Fields.ParentStation).NullIfWhitespace(), Platform = csv.GetField(Constants.Fields.PlatformCode).NullIfWhitespace() }; _stops.Add(id, stop); }); // Put a reference to the parent stop in each stop. foreach (var stop in _stops) { var parentId = stop.Value.ParentId; stop.Value.ParentId = null; // If this has a parent, add a reference to it. if (parentId is object) { var parent = _stops[parentId]; stop.Value.Parent = parent; parent.Children.Add(stop.Value); } } Console.WriteLine("Done: Stops"); // Stop Times var stopTimesPath = Path.Combine(gtfsFilePath, Constants.Files.StopTimes); ReadFile(stopTimesPath, (csv) => { var trip = _trips[csv.GetField(Constants.Fields.TripId)]; var stop = _stops[csv.GetField(Constants.Fields.StopId)]; var stopTime = new TFStopTime { Trip = trip, Stop = stop, DepartureTime = csv.GetField(Constants.Fields.DepartureTime).NullIfWhitespace(), Sequence = csv.GetField <int>(Constants.Fields.StopSequence), PickupType = csv.GetField <TFPickupDropOffType>(Constants.Fields.PickupType), DropOffType = csv.GetField <TFPickupDropOffType>(Constants.Fields.DropOffType) }; // StopTimes only relate to a stop and a time. Add a reference // for this StopTime to both of those. No list is maintained trip.StopTimes.Add(stopTime); stop.StopTimes.Add(stopTime); // Add two way references between trips and stops. This will // allow for easy lookup of the relationship between the two trip.Stops.Add(stop); stop.Trips.Add(trip); }); Console.WriteLine("Done: StopTimes"); }