diff --git a/GTFSimple.Core/Csv/Converters.cs b/GTFSimple.Core/Csv/Converters.cs index ed3e561..13a34c7 100644 --- a/GTFSimple.Core/Csv/Converters.cs +++ b/GTFSimple.Core/Csv/Converters.cs @@ -1,37 +1,113 @@ using System; +using System.Globalization; using CsvHelper.TypeConversion; namespace GTFSimple.Core.Csv { - internal class OneZeroConverter : DefaultTypeConverter + internal class DateConverter : GenericTypeConverter { - public override bool CanConvertFrom(Type type) + protected override string Format(CultureInfo culture, DateTime value) + { + return value.ToString("yyyyMMdd"); + } + + protected override DateTime Parse(CultureInfo culture, string text) + { + return DateTime.ParseExact(text, "yyyyMMdd", culture, DateTimeStyles.None); + } + } + + internal class BooleanOneZeroConverter : GenericTypeConverter + { + protected override string Format(CultureInfo culture, bool? value) + { + return value == null ? "" : value.Value ? "1" : "0"; + } + + protected override bool? Parse(CultureInfo culture, string text) + { + switch (text) + { + case "1": + return true; + case "0": + return false; + case null: + return null; + default: + throw new NotSupportedException("The conversion cannot be performed."); + } + } + } + + internal class TimeSpanSecondsConverter : GenericTypeConverter + { + protected override string Format(CultureInfo culture, TimeSpan? value) + { + return value == null ? "" : value.Value.TotalSeconds.ToString(culture); + } + + protected override TimeSpan? Parse(CultureInfo culture, string text) + { + return new TimeSpan(0, 0, int.Parse(text, culture)); + } + } + + internal class TimeSpanHourMinuteSecondConverter : GenericTypeConverter + { + protected override string Format(CultureInfo culture, TimeSpan? value) { - return type == typeof(bool); + return value == null ? "" : value.Value.ToString("c"); } - public override string ConvertToString(object value) + protected override TimeSpan? Parse(CultureInfo culture, string text) { - if (value is bool) - return (bool)value ? "1" : "0"; + return TimeSpan.Parse(text, culture); + } + } - return base.ConvertToString(value); + internal class UriConverter : GenericTypeConverter + { + protected override string Format(CultureInfo culture, Uri value) + { + return value == null ? "" : value.ToString(); + } + + protected override Uri Parse(CultureInfo culture, string text) + { + return new Uri(text); } } - internal class TimeSpanConverter : DefaultTypeConverter + internal abstract class GenericTypeConverter : DefaultTypeConverter { + protected abstract string Format(CultureInfo culture, T value); + + protected abstract T Parse(CultureInfo culture, string text); + public override bool CanConvertFrom(Type type) { - return type == typeof(TimeSpan); + return type == typeof(string); + } + + public override bool CanConvertTo(Type type) + { + return type == typeof(string); } - public override string ConvertToString(object value) + public override object ConvertFromString(CultureInfo culture, string text) { - if (value is TimeSpan) - return ((TimeSpan)value).TotalSeconds.ToString(); + return string.IsNullOrEmpty(text) ? default(T) : Parse(culture, text); + } - return base.ConvertToString(value); + public override object ConvertFromString(string text) + { + return ConvertFromString(CultureInfo.InvariantCulture, text); + } + + public override string ConvertToString(CultureInfo culture, object value) + { + return Format(culture, (T)value); } } } \ No newline at end of file diff --git a/GTFSimple.Core/Feed/Agency.cs b/GTFSimple.Core/Feed/Agency.cs index 5155ae0..5320c11 100644 --- a/GTFSimple.Core/Feed/Agency.cs +++ b/GTFSimple.Core/Feed/Agency.cs @@ -1,8 +1,11 @@ using System; +using CsvHelper.TypeConversion; using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("agency")] public class Agency { [FieldName("agency_id")] @@ -11,7 +14,7 @@ public class Agency [FieldName("agency_name")] public string Name { get; set; } - [FieldName("agency_url")] + [FieldName("agency_url"), TypeConverter(typeof(UriConverter))] public Uri Url { get; set; } [FieldName("agency_timezone")] @@ -23,7 +26,7 @@ public class Agency [FieldName("agency_phone")] public string Phone { get; set; } - [FieldName("agency_fare_url")] + [FieldName("agency_fare_url"), TypeConverter(typeof(UriConverter))] public Uri FareUrl { get; set; } } } \ No newline at end of file diff --git a/GTFSimple.Core/Feed/Calendar.cs b/GTFSimple.Core/Feed/Calendar.cs index e66a6f1..4554d38 100644 --- a/GTFSimple.Core/Feed/Calendar.cs +++ b/GTFSimple.Core/Feed/Calendar.cs @@ -1,39 +1,69 @@ using System; +using System.Collections.Generic; using CsvHelper.TypeConversion; using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("calendar")] public class Calendar { [FieldName("service_id")] public string ServiceId { get; set; } - [FieldName("monday"), TypeConverter(typeof(OneZeroConverter))] + [FieldName("monday"), TypeConverter(typeof(BooleanOneZeroConverter))] public bool Monday { get; set; } - [FieldName("tuesday"), TypeConverter(typeof(OneZeroConverter))] + [FieldName("tuesday"), TypeConverter(typeof(BooleanOneZeroConverter))] public bool Tuesday { get; set; } - [FieldName("wednesday"), TypeConverter(typeof(OneZeroConverter))] + [FieldName("wednesday"), TypeConverter(typeof(BooleanOneZeroConverter))] public bool Wednesday { get; set; } - [FieldName("thursday"), TypeConverter(typeof(OneZeroConverter))] + [FieldName("thursday"), TypeConverter(typeof(BooleanOneZeroConverter))] public bool Thursday { get; set; } - [FieldName("friday"), TypeConverter(typeof(OneZeroConverter))] + [FieldName("friday"), TypeConverter(typeof(BooleanOneZeroConverter))] public bool Friday { get; set; } - [FieldName("saturday"), TypeConverter(typeof(OneZeroConverter))] + [FieldName("saturday"), TypeConverter(typeof(BooleanOneZeroConverter))] public bool Saturday { get; set; } - [FieldName("sunday"), TypeConverter(typeof(OneZeroConverter))] + [FieldName("sunday"), TypeConverter(typeof(BooleanOneZeroConverter))] public bool Sunday { get; set; } - [FieldName("start_date", Format = "{0:yyyyMMdd}")] + [FieldName("start_date"), TypeConverter(typeof(DateConverter))] public DateTime StartDate { get; set; } - [FieldName("end_date", Format = "{0:yyyyMMdd}")] + [FieldName("end_date"), TypeConverter(typeof(DateConverter))] public DateTime EndDate { get; set; } + + public override string ToString() + { + return string.Format("{0} for {1:yyyy-MM-dd} to {2:yyyy-MM-dd}: {3}", + ServiceId, StartDate, EndDate, string.Join("", Days)); + } + + private IEnumerable Days + { + get + { + if (Monday) + yield return "M"; + if (Tuesday) + yield return "T"; + if (Wednesday) + yield return "W"; + if (Thursday) + yield return "H"; + if (Friday) + yield return "F"; + if (Saturday) + yield return "S"; + if (Sunday) + yield return "U"; + } + } } } \ No newline at end of file diff --git a/GTFSimple.Core/Feed/CalendarDate.cs b/GTFSimple.Core/Feed/CalendarDate.cs index c1f6e71..63e4c2a 100644 --- a/GTFSimple.Core/Feed/CalendarDate.cs +++ b/GTFSimple.Core/Feed/CalendarDate.cs @@ -1,18 +1,27 @@ using System; +using CsvHelper.TypeConversion; using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("calendar_dates")] public class CalendarDate { [FieldName("service_id")] public string ServiceId { get; set; } - [FieldName("date", Format = "{0:yyyyMMdd}")] + [FieldName("date"), TypeConverter(typeof(DateConverter))] public DateTime Date { get; set; } [FieldName("exception_type", Format = "{0:D}")] public ExceptionType ExceptionType { get; set; } + + public override string ToString() + { + return string.Format("{0} on {1:yyyy-MM-dd}: {2}", + ServiceId, Date, ExceptionType); + } } public enum ExceptionType diff --git a/GTFSimple.Core/Feed/FareAttributes.cs b/GTFSimple.Core/Feed/FareAttributes.cs index c6cfca4..95790a3 100644 --- a/GTFSimple.Core/Feed/FareAttributes.cs +++ b/GTFSimple.Core/Feed/FareAttributes.cs @@ -1,9 +1,11 @@ using System; using CsvHelper.TypeConversion; using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("fare_attributes")] public class FareAttributes { [FieldName("fare_id")] @@ -21,8 +23,14 @@ public class FareAttributes [FieldName("transfers", Format = "{0:D}")] public FareTransferType? Transfers { get; set; } - [FieldName("transfer_duration"), TypeConverter(typeof(TimeSpanConverter))] + [FieldName("transfer_duration"), TypeConverter(typeof(TimeSpanSecondsConverter))] public TimeSpan? TransferDuration { get; set; } + + public override string ToString() + { + return string.Format("{0}: {1:0.00} {2} ({3})", + FareId, Price, CurrencyType, PaymentMethod); + } } public enum PaymentMethod diff --git a/GTFSimple.Core/Feed/FareRules.cs b/GTFSimple.Core/Feed/FareRules.cs index 1670790..eb010f0 100644 --- a/GTFSimple.Core/Feed/FareRules.cs +++ b/GTFSimple.Core/Feed/FareRules.cs @@ -1,7 +1,9 @@ using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("fare_rules")] public class FareRules { [FieldName("fare_id")] diff --git a/GTFSimple.Core/Feed/FeedInfo.cs b/GTFSimple.Core/Feed/FeedInfo.cs index 6f471ec..35bf46f 100644 --- a/GTFSimple.Core/Feed/FeedInfo.cs +++ b/GTFSimple.Core/Feed/FeedInfo.cs @@ -1,5 +1,7 @@ using System; +using CsvHelper.TypeConversion; using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { @@ -9,6 +11,7 @@ namespace GTFSimple.Core.Feed /// However, the publisher of the feed is sometimes a different entity than any of the agencies (in the case of regional aggregators). /// In addition, there are some fields that are really feed-wide settings, rather than agency-wide. /// + [FeedFile("feed_info")] public class FeedInfo { /// @@ -59,7 +62,7 @@ public class FeedInfo /// the feed is making an explicit assertion that there is no service for dates within the feed_start_date or feed_end_date range but not included in the active calendar dates. /// /// The feed start date. - [FieldName("feed_start_date", Format = "{0:yyyyMMdd}")] + [FieldName("feed_start_date"), TypeConverter(typeof(DateConverter))] public DateTime? FeedStartDate { get; set; } /// @@ -74,7 +77,7 @@ public class FeedInfo /// the feed is making an explicit assertion that there is no service for dates within the feed_start_date or feed_end_date range but not included in the active calendar dates. /// /// The feed end date. - [FieldName("feed_end_date", Format = "{0:yyyyMMdd}")] + [FieldName("feed_end_date"), TypeConverter(typeof(DateConverter))] public DateTime? FeedEndDate { get; set; } /// diff --git a/GTFSimple.Core/Feed/Frequency.cs b/GTFSimple.Core/Feed/Frequency.cs index 141851e..c91ce7d 100644 --- a/GTFSimple.Core/Feed/Frequency.cs +++ b/GTFSimple.Core/Feed/Frequency.cs @@ -1,24 +1,26 @@ using System; using CsvHelper.TypeConversion; using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("frequencies")] public class Frequency { [FieldName("trip_id")] public string TripId { get; set; } - [FieldName("start_time")] + [FieldName("start_time"), TypeConverter(typeof(TimeSpanHourMinuteSecondConverter))] public TimeSpan StartTime { get; set; } - [FieldName("end_time")] + [FieldName("end_time"), TypeConverter(typeof(TimeSpanHourMinuteSecondConverter))] public TimeSpan EndTime { get; set; } - [FieldName("headway_secs"), TypeConverter(typeof(TimeSpanConverter))] + [FieldName("headway_secs"), TypeConverter(typeof(TimeSpanSecondsConverter))] public TimeSpan Headway { get; set; } - [FieldName("exact_times"), TypeConverter(typeof(OneZeroConverter))] + [FieldName("exact_times"), TypeConverter(typeof(BooleanOneZeroConverter))] public bool? ExactTimes { get; set; } } } \ No newline at end of file diff --git a/GTFSimple.Core/Feed/Route.cs b/GTFSimple.Core/Feed/Route.cs index 7442375..7496741 100644 --- a/GTFSimple.Core/Feed/Route.cs +++ b/GTFSimple.Core/Feed/Route.cs @@ -1,8 +1,11 @@ using System; +using CsvHelper.TypeConversion; using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("routes")] public class Route { [FieldName("route_id")] @@ -23,7 +26,7 @@ public class Route [FieldName("route_type", Format = "{0:D}")] public RouteType Type { get; set; } - [FieldName("route_url")] + [FieldName("route_url"), TypeConverter(typeof(UriConverter))] public Uri Url { get; set; } [FieldName("route_color")] @@ -31,6 +34,12 @@ public class Route [FieldName("route_text_color")] public string TextColor { get; set; } + + public override string ToString() + { + return string.Format("{0}: {1} {2}", + Id, ShortName, LongName); + } } public enum RouteType diff --git a/GTFSimple.Core/Feed/Shape.cs b/GTFSimple.Core/Feed/Shape.cs index 9f454c1..5c9bfd4 100644 --- a/GTFSimple.Core/Feed/Shape.cs +++ b/GTFSimple.Core/Feed/Shape.cs @@ -1,16 +1,18 @@ using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("shapes")] public class Shape { [FieldName("shape_id")] public string Id { get; set; } - [FieldName("shape_pt_lat", Format = "{0:0.00000}")] + [FieldName("shape_pt_lat", Format = "{0:0.000000}")] public double PointLatitude { get; set; } - [FieldName("shape_pt_lon", Format = "{0:0.00000}")] + [FieldName("shape_pt_lon", Format = "{0:0.000000}")] public double PointLongitude { get; set; } [FieldName("shape_pt_sequence")] @@ -18,5 +20,11 @@ public class Shape [FieldName("shape_dist_traveled", Format = "{0:0.0000}")] public double? DistanceTraveled { get; set; } + + public override string ToString() + { + return string.Format("{0} #{1} @ {2:0.000000}, {3:0.000000}", + Id, PointSequence, PointLatitude, PointLongitude); + } } } \ No newline at end of file diff --git a/GTFSimple.Core/Feed/Stop.cs b/GTFSimple.Core/Feed/Stop.cs index 7019eed..be0e983 100644 --- a/GTFSimple.Core/Feed/Stop.cs +++ b/GTFSimple.Core/Feed/Stop.cs @@ -1,9 +1,13 @@ using System; +using CsvHelper.TypeConversion; using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; +using GTFSimple.Core.Util; namespace GTFSimple.Core.Feed { - public class Stop + [FeedFile("stops")] + public class Stop : ILocation { [FieldName("stop_id")] public string Id { get; set; } @@ -26,7 +30,7 @@ public class Stop [FieldName("zone_id")] public string ZoneId { get; set; } - [FieldName("stop_url")] + [FieldName("stop_url"), TypeConverter(typeof(UriConverter))] public Uri Url { get; set; } [FieldName("location_type", Format = "{0:D}")] @@ -40,6 +44,12 @@ public class Stop [FieldName("wheelchair_boarding", Format = "{0:D}")] public WheelchairAccessibility? WheelchairBoarding { get; set; } + + public override string ToString() + { + return string.Format("{0}: '{1}' @ {2:0.000000}, {3:0.000000}", + Id, Name, Latitude, Longitude); + } } public enum LocationType diff --git a/GTFSimple.Core/Feed/StopTime.cs b/GTFSimple.Core/Feed/StopTime.cs index 3c4e24b..c695bcc 100644 --- a/GTFSimple.Core/Feed/StopTime.cs +++ b/GTFSimple.Core/Feed/StopTime.cs @@ -1,17 +1,20 @@ using System; +using CsvHelper.TypeConversion; using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("stop_times")] public class StopTime { [FieldName("trip_id")] public string TripId { get; set; } - [FieldName("arrival_time")] + [FieldName("arrival_time"), TypeConverter(typeof(TimeSpanHourMinuteSecondConverter))] public TimeSpan? ArrivalTime { get; set; } - [FieldName("departure_time")] + [FieldName("departure_time"), TypeConverter(typeof(TimeSpanHourMinuteSecondConverter))] public TimeSpan? DepartureTime { get; set; } [FieldName("stop_id")] @@ -31,6 +34,14 @@ public class StopTime [FieldName("shape_dist_traveled")] public double? ShapeDistanceTraveled { get; set; } + + public override string ToString() + { + return string.Format("{0} #{1}: {2}{3}{4}", + TripId, StopSequence, StopId, + ArrivalTime == null ? "" : " @ " + ArrivalTime, + ArrivalTime == DepartureTime ? "" : " \u2013 " + DepartureTime); + } } public enum StopPickupType diff --git a/GTFSimple.Core/Feed/Transfer.cs b/GTFSimple.Core/Feed/Transfer.cs index 921f724..1a61fe1 100644 --- a/GTFSimple.Core/Feed/Transfer.cs +++ b/GTFSimple.Core/Feed/Transfer.cs @@ -1,9 +1,11 @@ using System; using CsvHelper.TypeConversion; using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("transfers")] public class Transfer { [FieldName("from_stop_id")] @@ -15,7 +17,7 @@ public class Transfer [FieldName("transfer_type", Format = "{0:D}")] public TranferType TransferType { get; set; } - [FieldName("min_transfer_time"), TypeConverter(typeof(TimeSpanConverter))] + [FieldName("min_transfer_time"), TypeConverter(typeof(TimeSpanSecondsConverter))] public TimeSpan? MinimumTransferTime { get; set; } } diff --git a/GTFSimple.Core/Feed/Trip.cs b/GTFSimple.Core/Feed/Trip.cs index 10ff3c1..dfff793 100644 --- a/GTFSimple.Core/Feed/Trip.cs +++ b/GTFSimple.Core/Feed/Trip.cs @@ -1,7 +1,9 @@ using GTFSimple.Core.Csv; +using GTFSimple.Core.Files; namespace GTFSimple.Core.Feed { + [FeedFile("trips")] public class Trip { [FieldName("route_id")] @@ -33,6 +35,12 @@ public class Trip [FieldName("bikes_allowed", Format = "{0:D}")] public BikesAllowed? BikesAllowed { get; set; } + + public override string ToString() + { + return string.Format("{0}: {1}/{2} {3} {4}", + Id, RouteId, ShapeId, ServiceId, Headsign); + } } public enum TripDirection diff --git a/GTFSimple.Core/Files/FeedFileAttribute.cs b/GTFSimple.Core/Files/FeedFileAttribute.cs new file mode 100644 index 0000000..d121292 --- /dev/null +++ b/GTFSimple.Core/Files/FeedFileAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace GTFSimple.Core.Files +{ + internal class FeedFileAttribute : Attribute + { + private readonly string fileName; + + public FeedFileAttribute(string fileName) + { + this.fileName = fileName; + } + + public string FileName + { + get { return fileName; } + } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/Files/Registry.cs b/GTFSimple.Core/Files/Registry.cs new file mode 100644 index 0000000..fbbc81e --- /dev/null +++ b/GTFSimple.Core/Files/Registry.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace GTFSimple.Core.Files +{ + public class Registry : IEnumerable> + { + private readonly IDictionary lookup; + + public Registry() + { + var fileTypes = from type in GetType().Assembly.GetTypes() + let file = type.FeedFile() + where file != null + select new { type, file }; + + lookup = fileTypes.ToDictionary(x => x.file, x => x.type); + } + + public virtual Type this[string fileName] + { + get { return lookup[fileName]; } + } + + public IEnumerator> GetEnumerator() + { + return lookup.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public string FileName(Type type) + { + return type.FeedFile() + ".txt"; + } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/Files/TypeExtensions.cs b/GTFSimple.Core/Files/TypeExtensions.cs new file mode 100644 index 0000000..fcc5853 --- /dev/null +++ b/GTFSimple.Core/Files/TypeExtensions.cs @@ -0,0 +1,14 @@ +using System; +using System.Linq; + +namespace GTFSimple.Core.Files +{ + public static class TypeExtensions + { + public static string FeedFile(this Type type) + { + var attr = (FeedFileAttribute)type.GetCustomAttributes(typeof(FeedFileAttribute), false).FirstOrDefault(); + return attr == null ? null : attr.FileName; + } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/GTFSimple.Core.csproj b/GTFSimple.Core/GTFSimple.Core.csproj index fe5f87a..4fb9ba5 100644 --- a/GTFSimple.Core/GTFSimple.Core.csproj +++ b/GTFSimple.Core/GTFSimple.Core.csproj @@ -52,9 +52,19 @@ - - + + + + + + + + + + + + diff --git a/GTFSimple.Core/Generators/StopGenerator.cs b/GTFSimple.Core/Generators/StopGenerator.cs new file mode 100644 index 0000000..16781f0 --- /dev/null +++ b/GTFSimple.Core/Generators/StopGenerator.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using GTFSimple.Core.Feed; +using GTFSimple.Core.Input; +using GTFSimple.Core.Util; + +namespace GTFSimple.Core.Generators +{ + public class StopGenerator + { + private readonly double proximityThreshold; + private readonly IStopIdGenerator stopIdGenerator; + + public StopGenerator(double proximityThreshold, IStopIdGenerator stopIdGenerator) + { + this.proximityThreshold = proximityThreshold; + this.stopIdGenerator = stopIdGenerator; + } + + public IDictionary Generate(IEnumerable routeStops) + { + var stops = + from g in routeStops.GroupByCluster(Geo.Distance, proximityThreshold) + let rsKey = g.Key + let s = new Stop + { + Id = stopIdGenerator.Generate(rsKey), + Code = rsKey.Code, + Name = rsKey.Name, + Description = rsKey.Description, + Latitude = rsKey.Latitude, + Longitude = rsKey.Longitude, + ZoneId = rsKey.ZoneId, + Url = rsKey.Url, + LocationType = rsKey.LocationType, + ParentStation = rsKey.ParentStation, + TimeZone = rsKey.TimeZone, + WheelchairBoarding = rsKey.WheelchairBoarding, + } + from rs in g + select new + { + RouteStop = rs, + Stop = s, + }; + + return stops.ToDictionary(x => x.RouteStop, x => x.Stop); + } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/Generators/StopIdGenerator.cs b/GTFSimple.Core/Generators/StopIdGenerator.cs new file mode 100644 index 0000000..21ffac1 --- /dev/null +++ b/GTFSimple.Core/Generators/StopIdGenerator.cs @@ -0,0 +1,25 @@ +using GTFSimple.Core.Input; + +namespace GTFSimple.Core.Generators +{ + public interface IStopIdGenerator + { + string Generate(RouteStop rs); + } + + public class SequentialStopIdGenerator : IStopIdGenerator + { + private readonly string format; + private int nextId; + + public SequentialStopIdGenerator(int digits) + { + format = new string('0', digits); + } + + public string Generate(RouteStop rs) + { + return (nextId++).ToString(format); + } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/Generators/StopTimeGenerator.cs b/GTFSimple.Core/Generators/StopTimeGenerator.cs new file mode 100644 index 0000000..d1b5466 --- /dev/null +++ b/GTFSimple.Core/Generators/StopTimeGenerator.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using GTFSimple.Core.Feed; +using GTFSimple.Core.Input; +using GTFSimple.Core.Util; + +namespace GTFSimple.Core.Generators +{ + public class StopTimeGenerator + { + public IEnumerable Generate(IDictionary routeStops, + IDictionary tripTimes) + { + return from x in tripTimes + let tt = x.Key + let trip = x.Value + orderby tt.RouteId, tt.ServiceId, tt.ShapeId, tt.StartTime + join y in routeStops on + new { tt.RouteId, tt.ShapeId } equals + new { y.Key.RouteId, y.Key.ShapeId } + into tripStops + from st in + ( + from z in tripStops + let rs = z.Key + let stop = z.Value + orderby rs.StopSequence + from i in z.GetIndex() + select new StopTime + { + TripId = trip.Id, + ArrivalTime = tt.StartTime + rs.ArrivalOffset, + DepartureTime = tt.StartTime + rs.DepartureOffset, + StopId = stop.Id, + StopSequence = (uint)i, + StopHeadsign = rs.StopHeadsign, + PickupType = rs.PickupType, + DropOffType = rs.DropOffType, + ShapeDistanceTraveled = rs.ShapeDistanceTraveled, + } + ) + select st; + } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/Generators/TripGenerator.cs b/GTFSimple.Core/Generators/TripGenerator.cs new file mode 100644 index 0000000..e313e25 --- /dev/null +++ b/GTFSimple.Core/Generators/TripGenerator.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using GTFSimple.Core.Feed; +using GTFSimple.Core.Input; +using GTFSimple.Core.Util; + +namespace GTFSimple.Core.Generators +{ + public class TripGenerator + { + public IDictionary Generate(IEnumerable tripTimes, string idFormat) + { + var trips = + from t in tripTimes + group t by new { t.RouteId, t.ServiceId } + into g + from t in + ( + from tt in g + orderby tt.StartTime + from i in tt.GetIndex() + select new + { + TripTime = tt, + Trip = new Trip + { + RouteId = tt.RouteId, + ServiceId = tt.ServiceId, + Id = string.Format(idFormat, tt.RouteId, tt.ServiceId, i + 1), + Headsign = tt.Headsign, + ShortName = tt.ShortName, + DirectionId = tt.DirectionId, + BlockId = tt.BlockId, + ShapeId = tt.ShapeId, + WheelchairAccessible = tt.WheelchairAccessible, + BikesAllowed = tt.BikesAllowed, + }, + } + ) + select t; + + return trips.ToDictionary(x => x.TripTime, x => x.Trip); + } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/Input/RouteStop.cs b/GTFSimple.Core/Input/RouteStop.cs new file mode 100644 index 0000000..c9d60db --- /dev/null +++ b/GTFSimple.Core/Input/RouteStop.cs @@ -0,0 +1,82 @@ +using System; +using CsvHelper.TypeConversion; +using GTFSimple.Core.Csv; +using GTFSimple.Core.Feed; +using GTFSimple.Core.Files; +using GTFSimple.Core.Util; + +namespace GTFSimple.Core.Input +{ + [FeedFile("route_stops")] + public class RouteStop : ILocation + { + [FieldName("agency_id")] + public string AgencyId { get; set; } + + [FieldName("route_id")] + public string RouteId { get; set; } + + [FieldName("shape_id")] + public string ShapeId { get; set; } + + [FieldName("stop_sequence")] + public uint StopSequence { get; set; } + + [FieldName("stop_code")] + public string Code { get; set; } + + [FieldName("stop_name")] + public string Name { get; set; } + + [FieldName("stop_desc")] + public string Description { get; set; } + + [FieldName("stop_lat", Format = "{0:0.000000}")] + public double Latitude { get; set; } + + [FieldName("stop_lon", Format = "{0:0.000000}")] + public double Longitude { get; set; } + + [FieldName("zone_id")] + public string ZoneId { get; set; } + + [FieldName("stop_url"), TypeConverter(typeof(UriConverter))] + public Uri Url { get; set; } + + [FieldName("location_type", Format = "{0:D}")] + public LocationType? LocationType { get; set; } + + [FieldName("parent_station")] + public string ParentStation { get; set; } + + [FieldName("stop_timezone")] + public string TimeZone { get; set; } + + [FieldName("wheelchair_boarding", Format = "{0:D}")] + public WheelchairAccessibility? WheelchairBoarding { get; set; } + + [FieldName("arrival_offset"), TypeConverter(typeof(TimeSpanHourMinuteSecondConverter))] + public TimeSpan? ArrivalOffset { get; set; } + + [FieldName("departure_offset"), TypeConverter(typeof(TimeSpanHourMinuteSecondConverter))] + public TimeSpan? DepartureOffset { get; set; } + + [FieldName("stop_headsign")] + public string StopHeadsign { get; set; } + + [FieldName("pickup_type", Format = "{0:D}")] + public StopPickupType? PickupType { get; set; } + + [FieldName("drop_off_type", Format = "{0:D}")] + public StopDropOffType? DropOffType { get; set; } + + [FieldName("shape_dist_traveled")] + public double? ShapeDistanceTraveled { get; set; } + + public override string ToString() + { + return string.Format("{0}/{1} #{2} @ {3:0.000000}, {4:0.000000}", + RouteId, ShapeId, StopSequence, Latitude, Longitude); + } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/Input/TripTime.cs b/GTFSimple.Core/Input/TripTime.cs new file mode 100644 index 0000000..aac24b5 --- /dev/null +++ b/GTFSimple.Core/Input/TripTime.cs @@ -0,0 +1,48 @@ +using System; +using CsvHelper.TypeConversion; +using GTFSimple.Core.Csv; +using GTFSimple.Core.Feed; +using GTFSimple.Core.Files; + +namespace GTFSimple.Core.Input +{ + [FeedFile("trip_times")] + public class TripTime + { + [FieldName("route_id")] + public string RouteId { get; set; } + + [FieldName("shape_id")] + public string ShapeId { get; set; } + + [FieldName("service_id")] + public string ServiceId { get; set; } + + [FieldName("start_time"), TypeConverter(typeof(TimeSpanHourMinuteSecondConverter))] + public TimeSpan StartTime { get; set; } + + [FieldName("trip_headsign")] + public string Headsign { get; set; } + + [FieldName("trip_short_name")] + public string ShortName { get; set; } + + [FieldName("direction_id", Format = "{0:D}")] + public TripDirection? DirectionId { get; set; } + + [FieldName("block_id")] + public string BlockId { get; set; } + + [FieldName("wheelchair_accessible", Format = "{0:D}")] + public WheelchairAccessibility? WheelchairAccessible { get; set; } + + [FieldName("bikes_allowed", Format = "{0:D}")] + public BikesAllowed? BikesAllowed { get; set; } + + public override string ToString() + { + return string.Format("{0}/{1} {2} @ {3}", + RouteId, ShapeId, ServiceId, StartTime); + } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/Properties/AssemblyInfo.cs b/GTFSimple.Core/Properties/AssemblyInfo.cs index b01a1c9..45b309d 100644 --- a/GTFSimple.Core/Properties/AssemblyInfo.cs +++ b/GTFSimple.Core/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. @@ -23,5 +22,4 @@ // if desired. See the Mono documentation for more information about signing. //[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] - +//[assembly: AssemblyKeyFile("")] \ No newline at end of file diff --git a/GTFSimple.Core/Util/EnumerableExtensions.cs b/GTFSimple.Core/Util/EnumerableExtensions.cs new file mode 100644 index 0000000..35afd0e --- /dev/null +++ b/GTFSimple.Core/Util/EnumerableExtensions.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace GTFSimple.Core.Util +{ + public static class EnumerableExtensions + { + /// + /// Source: http://ideone.com/8l0LH + /// + public static IEnumerable> GroupByCluster(this IEnumerable source, + Func distance, double eps) + { + var result = new HashSet>(); + foreach (var t in source) + { + // need to materialize, as we are changing the result + var containingGroups = result.Where(g => g.Any(gt => distance(t, gt) < eps)).ToList(); + switch (containingGroups.Count) + { + case 0: + result.Add(new ClusterGrouping(t)); + break; + case 1: + containingGroups[0].Include(t); + break; + default: + result.Add(new ClusterGrouping(containingGroups)); + foreach (var g in containingGroups) + result.Remove(g); + break; + } + } + return result; + } + + private class ClusterGrouping : IGrouping + { + private readonly List items = new List(); + + internal ClusterGrouping(T t) + { + Key = t; + items.Add(t); + } + + internal ClusterGrouping(IList> containingGroups) + { + Key = containingGroups[0].Key; + items.AddRange(containingGroups.SelectMany(g => g)); + } + + public T Key { get; private set; } + + public IEnumerator GetEnumerator() + { + return items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + internal void Include(T t) + { + items.Add(t); + } + } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/Util/Geo.cs b/GTFSimple.Core/Util/Geo.cs new file mode 100644 index 0000000..115b18f --- /dev/null +++ b/GTFSimple.Core/Util/Geo.cs @@ -0,0 +1,39 @@ +using System; + +namespace GTFSimple.Core.Util +{ + public static class Geo + { + /// + /// Source: http://megocode3.wordpress.com/2008/02/05/haversine-formula-in-c/ + /// + public static double Distance(ILocation left, ILocation right) + { + if (left == null || right == null) + return 0f; + + var lLat = ToRadian(left.Latitude); + var rLat = ToRadian(right.Latitude); + + var dLat = rLat - lLat; + var dLon = ToRadian(right.Longitude - left.Longitude); + + var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + + Math.Cos(lLat) * Math.Cos(rLat) * + Math.Sin(dLon / 2) * Math.Sin(dLon / 2); + + return 2 * Math.Asin(Math.Min(1, Math.Sqrt(a))); + } + + private static double ToRadian(double val) + { + return (Math.PI / 180) * val; + } + } + + public interface ILocation + { + double Latitude { get; } + double Longitude { get; } + } +} \ No newline at end of file diff --git a/GTFSimple.Core/Util/SelectWithIndexExtension.cs b/GTFSimple.Core/Util/SelectWithIndexExtension.cs new file mode 100644 index 0000000..9c47a23 --- /dev/null +++ b/GTFSimple.Core/Util/SelectWithIndexExtension.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace GTFSimple.Core.Util +{ + internal static class SelectWithIndexExtension + { + internal static SelectIndexProvider GetIndex(this T element) + { + return default(SelectIndexProvider); + } + + internal struct SelectIndexProvider { } + + internal static IEnumerable SelectMany( + this IEnumerable source, + Func collectionSelector, + Func resultSelector) + { + return source.Select(resultSelector); + } + } +} \ No newline at end of file diff --git a/GTFSimple.Tests/Core/Csv/FeedEntityTestBase.cs b/GTFSimple.Tests/Core/Csv/FeedEntityTestBase.cs index bfe8e89..b80f968 100644 --- a/GTFSimple.Tests/Core/Csv/FeedEntityTestBase.cs +++ b/GTFSimple.Tests/Core/Csv/FeedEntityTestBase.cs @@ -18,9 +18,25 @@ protected static void AssertCsvRow(T entity, string expected) where T : class csvWriter.WriteRecord(entity); } - Assert.AreEqual(expected + Environment.NewLine, sb.ToString()); + var actual = sb.ToString(); + Assert.AreEqual(expected + Environment.NewLine, actual); + + //using (var csvReader = CsvReader(actual)) + //{ + // Assert.True(csvReader.Read()); + // var record = csvReader.GetRecord(); + // Assert.NotNull(record); + //} } + //private static CsvReader CsvReader(string actual) where T : class + //{ + // return new CsvReader(new StringReader(actual)) + // { + // Configuration = { HasHeaderRecord = false }, + // }; + //} + protected static void AssertCsvRows(IEnumerable entities, params string[] expected) where T : class { var pairs = entities.Zip(expected, (entity, csv) => new { entity, csv }); diff --git a/GTFSimple.Tests/Core/Feed/ShapeTests.cs b/GTFSimple.Tests/Core/Feed/ShapeTests.cs index ec7cfeb..dfe596a 100644 --- a/GTFSimple.Tests/Core/Feed/ShapeTests.cs +++ b/GTFSimple.Tests/Core/Feed/ShapeTests.cs @@ -44,9 +44,9 @@ public void PopulatedEntityHasExpectedValues() }; AssertCsvRows(entities, - "A_shp,37.61956,-122.48161,1,", - "A_shp,37.64430,-122.41070,2,6.8310", - "A_shp,37.65863,-122.30839,3,15.8765"); + "A_shp,37.619560,-122.481610,1,", + "A_shp,37.644300,-122.410700,2,6.8310", + "A_shp,37.658630,-122.308390,3,15.8765"); } } } \ No newline at end of file diff --git a/GTFSimple.Tests/Core/Feed/StopTimeTests.cs b/GTFSimple.Tests/Core/Feed/StopTimeTests.cs index 7361f48..f26540b 100644 --- a/GTFSimple.Tests/Core/Feed/StopTimeTests.cs +++ b/GTFSimple.Tests/Core/Feed/StopTimeTests.cs @@ -43,7 +43,7 @@ public void PopulatedEntityHasExpectedValues() { TripId = "101WD-1", ArrivalTime = new TimeSpan(5, 20, 0), - DepartureTime = new TimeSpan(5, 20, 0), + DepartureTime = new TimeSpan(5, 22, 30), StopId = "2", StopSequence = 2, DropOffType = StopDropOffType.NotAvailable, @@ -53,7 +53,7 @@ public void PopulatedEntityHasExpectedValues() AssertCsvRows(entities, "AWE1,06:10:00,06:10:00,S1,1,Round & Round,0,,", "AWE1,,,S2,2,,2,3,6.1123", - "101WD-1,05:20:00,05:20:00,2,2,,,1,"); + "101WD-1,05:20:00,05:22:30,2,2,,,1,"); } } } \ No newline at end of file diff --git a/GTFSimple.Tests/Core/Generators/StopTimeGeneratorTests.cs b/GTFSimple.Tests/Core/Generators/StopTimeGeneratorTests.cs new file mode 100644 index 0000000..93b03ad --- /dev/null +++ b/GTFSimple.Tests/Core/Generators/StopTimeGeneratorTests.cs @@ -0,0 +1,135 @@ +using System; +using System.Linq; +using GTFSimple.Core.Feed; +using GTFSimple.Core.Generators; +using GTFSimple.Core.Input; +using NUnit.Framework; + +namespace GTFSimple.Tests.Core.Generators +{ + [TestFixture] + public class StopTimeGeneratorTests + { + [Test] + public void Duplicate_stops_are_combined() + { + var routeStops = new[] + { + new RouteStop + { + AgencyId = "Bus", + RouteId = "R01", + StopSequence = 1, + Name = "GTC", + Latitude = 37.752240, + Longitude = -122.418450, + ArrivalOffset = TimeSpan.Zero, + DepartureOffset = TimeSpan.Zero, + }, + new RouteStop + { + AgencyId = "Bus", + RouteId = "R01", + Name = "A Street", + StopSequence = 2, + Latitude = 37.752340, + Longitude = -122.418350, + }, + new RouteStop + { + AgencyId = "Bus", + RouteId = "R01", + Name = "B Street", + StopSequence = 3, + Latitude = 37.752440, + Longitude = -122.418250, + ArrivalOffset = new TimeSpan(0, 4, 30), + DepartureOffset = new TimeSpan(0, 5, 00), + }, + new RouteStop + { + AgencyId = "Bus", + RouteId = "R02", + ShapeId = "S02", + StopSequence = 1, + Name = "Ground Transportation Center", + Latitude = 37.752245, + Longitude = -122.418445, + ArrivalOffset = TimeSpan.Zero, + DepartureOffset = TimeSpan.Zero, + }, + }; + + var tripTimes = new[] + { + new TripTime + { + RouteId = "R01", + ServiceId = "WE", + StartTime = new TimeSpan(8, 0, 0), + Headsign = "Downtown", + BlockId = "1", + }, + new TripTime + { + RouteId = "R01", + ServiceId = "WE", + StartTime = new TimeSpan(11, 20, 0), + Headsign = "Downtown", + BlockId = "2", + }, + new TripTime + { + RouteId = "R02", + ServiceId = "WD", + StartTime = new TimeSpan(14, 40, 0), + DirectionId = TripDirection.Outbound, + ShapeId = "S02", + WheelchairAccessible = WheelchairAccessibility.Accessible, + BikesAllowed = BikesAllowed.NotAllowed, + }, + new TripTime + { + RouteId = "R02", + ServiceId = "WD", + StartTime = new TimeSpan(08, 0, 0), + DirectionId = TripDirection.Inbound, + ShapeId = "S02", + WheelchairAccessible = WheelchairAccessibility.NotAccessible, + BikesAllowed = BikesAllowed.Allowed, + }, + }; + + + var stopGenerator = new StopGenerator(0.000001, new SequentialStopIdGenerator(2)); + + var stopLookup = stopGenerator.Generate(routeStops); + + Assert.AreSame(stopLookup[routeStops[0]], stopLookup[routeStops[3]]); + + var stops = stopLookup.Values.Distinct().ToList(); + Assert.AreEqual("00", stops[0].Id); + Assert.AreEqual("GTC", stops[0].Name); + Assert.AreEqual("01", stops[1].Id); + Assert.AreEqual("A Street", stops[1].Name); + + + var tripGenerator = new TripGenerator(); + + var tripLookup = tripGenerator.Generate(tripTimes, "{0}-{1}-{2:00}"); + + Assert.AreEqual("R01-WE-01", tripLookup[tripTimes[0]].Id); + Assert.AreEqual("R01-WE-02", tripLookup[tripTimes[1]].Id); + Assert.AreEqual("R02-WD-02", tripLookup[tripTimes[2]].Id); + Assert.AreEqual("R02-WD-01", tripLookup[tripTimes[3]].Id); + + var stopTimeGenerator = new StopTimeGenerator(); + + var stopTimes = stopTimeGenerator.Generate(stopLookup, tripLookup).ToList(); + + + foreach(var st in stopTimes) + Console.WriteLine(st); + } + } +} \ No newline at end of file diff --git a/GTFSimple.Tests/Core/Input/RouteStopTests.cs b/GTFSimple.Tests/Core/Input/RouteStopTests.cs new file mode 100644 index 0000000..00c88b4 --- /dev/null +++ b/GTFSimple.Tests/Core/Input/RouteStopTests.cs @@ -0,0 +1,83 @@ +using System; +using GTFSimple.Core.Feed; +using GTFSimple.Core.Input; +using GTFSimple.Tests.Core.Csv; +using NUnit.Framework; + +namespace GTFSimple.Tests.Core.Input +{ + [TestFixture] + public class RouteStopTests : FeedEntityTestBase + { + [Test] + public void HeaderHasExpectedFields() + { + AssertHeader( + "agency_id,route_id,shape_id,stop_sequence,stop_code,stop_name,stop_desc,stop_lat,stop_lon,zone_id,stop_url,location_type,parent_station,stop_timezone,wheelchair_boarding,arrival_offset,departure_offset,stop_headsign,pickup_type,drop_off_type,shape_dist_traveled"); + } + + [Test] + public void PopulatedEntityHasExpectedValues() + { + var entities = new[] + { + new RouteStop + { + AgencyId = "A1", + RouteId = "R1", + ShapeId = "S1", + StopSequence = 1, + Name = "Mission St. & 15th St.", + Description = "The stop is located 10 feet north of Mission St.", + Latitude = 37.766629, + Longitude = -122.419782, + ArrivalOffset = TimeSpan.Zero, + DepartureOffset = TimeSpan.Zero, + }, + new RouteStop + { + AgencyId = "A1", + RouteId = "R1", + ShapeId = "S1", + StopSequence = 2, + Code = "24th&Mission", + Name = "24th St. Mission Station", + Latitude = 37.752240, + Longitude = -122.418450, + ParentStation = "S8", + TimeZone = "UTC-06", + WheelchairBoarding = WheelchairAccessibility.Accessible, + }, + new RouteStop + { + AgencyId = "A1", + RouteId = "R1", + ShapeId = "S1", + StopSequence = 3, + Code = "24SMS", + Name = "24th St. Mission Station", + Description = "Awesome", + Latitude = 37.752240, + Longitude = -122.418450, + ZoneId = "F1", + Url = new Uri("http://www.bart.gov/stations/stationguide/stationoverview_24st.asp"), + LocationType = LocationType.Station, + ParentStation = "parent", + TimeZone = "CST", + WheelchairBoarding = WheelchairAccessibility.NotAccessible, + ArrivalOffset = new TimeSpan(0, 12, 0), + DepartureOffset = new TimeSpan(0, 15, 0), + StopHeadsign = "24 St Mission", + PickupType = StopPickupType.Scheduled, + DropOffType = StopDropOffType.Scheduled, + ShapeDistanceTraveled = 3.14, + }, + }; + + AssertCsvRows(entities, + "A1,R1,S1,1,,Mission St. & 15th St.,The stop is located 10 feet north of Mission St.,37.766629,-122.419782,,,,,,,00:00:00,00:00:00,,,,", + "A1,R1,S1,2,24th&Mission,24th St. Mission Station,,37.752240,-122.418450,,,,S8,UTC-06,1,,,,,,", + "A1,R1,S1,3,24SMS,24th St. Mission Station,Awesome,37.752240,-122.418450,F1,http://www.bart.gov/stations/stationguide/stationoverview_24st.asp,1,parent,CST,2,00:12:00,00:15:00,24 St Mission,0,0,3.14"); + } + } +} \ No newline at end of file diff --git a/GTFSimple.Tests/Core/Input/TripTimeTests.cs b/GTFSimple.Tests/Core/Input/TripTimeTests.cs new file mode 100644 index 0000000..e82dfb4 --- /dev/null +++ b/GTFSimple.Tests/Core/Input/TripTimeTests.cs @@ -0,0 +1,69 @@ +using System; +using GTFSimple.Core.Feed; +using GTFSimple.Core.Input; +using GTFSimple.Tests.Core.Csv; +using NUnit.Framework; + +namespace GTFSimple.Tests.Core.Input +{ + [TestFixture] + public class TripTimeTests : FeedEntityTestBase + { + [Test] + public void HeaderHasExpectedFields() + { + AssertHeader( + "route_id,shape_id,service_id,start_time,trip_headsign,trip_short_name,direction_id,block_id,wheelchair_accessible,bikes_allowed"); + } + + [Test] + public void PopulatedEntityHasExpectedValues() + { + var entities = new[] + { + new TripTime + { + RouteId = "A", + ServiceId = "WE", + StartTime = new TimeSpan(8, 0, 0), + Headsign = "Downtown", + BlockId = "1", + }, + new TripTime + { + RouteId = "A", + ServiceId = "WE", + StartTime = new TimeSpan(11, 20, 0), + Headsign = "Downtown", + BlockId = "2", + }, + new TripTime + { + RouteId = "B", + ShapeId = "S12", + ServiceId = "WD", + StartTime = new TimeSpan(14, 40, 0), + DirectionId = TripDirection.Outbound, + WheelchairAccessible = WheelchairAccessibility.Accessible, + BikesAllowed = BikesAllowed.NotAllowed, + }, + new TripTime + { + RouteId = "B", + ShapeId = "S13", + ServiceId = "WD", + StartTime = new TimeSpan(18, 0, 0), + DirectionId = TripDirection.Inbound, + WheelchairAccessible = WheelchairAccessibility.NotAccessible, + BikesAllowed = BikesAllowed.Allowed, + }, + }; + + AssertCsvRows(entities, + "A,,WE,08:00:00,Downtown,,,1,,", + "A,,WE,11:20:00,Downtown,,,2,,", + "B,S12,WD,14:40:00,,,0,,1,2", + "B,S13,WD,18:00:00,,,1,,2,1"); + } + } +} \ No newline at end of file diff --git a/GTFSimple.Tests/GTFSimple.Tests.csproj b/GTFSimple.Tests/GTFSimple.Tests.csproj index 19e3c94..d45f977 100644 --- a/GTFSimple.Tests/GTFSimple.Tests.csproj +++ b/GTFSimple.Tests/GTFSimple.Tests.csproj @@ -53,6 +53,9 @@ + + + diff --git a/GTFSimple.sln b/GTFSimple.sln index 9b90fdd..befe206 100644 --- a/GTFSimple.sln +++ b/GTFSimple.sln @@ -1,6 +1,8 @@  -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GTFSimple", "GTFSimple\GTFSimple.csproj", "{46D9B29D-D65F-4756-B8E2-EE000D8EEF52}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GTFSimple.Tests", "GTFSimple.Tests\GTFSimple.Tests.csproj", "{FDB95B90-0DFE-46D9-9988-F85BD8985568}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GTFSimple.Core", "GTFSimple.Core\GTFSimple.Core.csproj", "{EC32BC77-FF19-4088-B9FB-17085421B9A8}" @@ -11,14 +13,21 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EC32BC77-FF19-4088-B9FB-17085421B9A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC32BC77-FF19-4088-B9FB-17085421B9A8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC32BC77-FF19-4088-B9FB-17085421B9A8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC32BC77-FF19-4088-B9FB-17085421B9A8}.Release|Any CPU.Build.0 = Release|Any CPU {FDB95B90-0DFE-46D9-9988-F85BD8985568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FDB95B90-0DFE-46D9-9988-F85BD8985568}.Debug|Any CPU.Build.0 = Debug|Any CPU {FDB95B90-0DFE-46D9-9988-F85BD8985568}.Release|Any CPU.ActiveCfg = Release|Any CPU {FDB95B90-0DFE-46D9-9988-F85BD8985568}.Release|Any CPU.Build.0 = Release|Any CPU + {EC32BC77-FF19-4088-B9FB-17085421B9A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC32BC77-FF19-4088-B9FB-17085421B9A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC32BC77-FF19-4088-B9FB-17085421B9A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC32BC77-FF19-4088-B9FB-17085421B9A8}.Release|Any CPU.Build.0 = Release|Any CPU + {46D9B29D-D65F-4756-B8E2-EE000D8EEF52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46D9B29D-D65F-4756-B8E2-EE000D8EEF52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46D9B29D-D65F-4756-B8E2-EE000D8EEF52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46D9B29D-D65F-4756-B8E2-EE000D8EEF52}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution StartupItem = GTFSimple.Tests\GTFSimple.Tests.csproj diff --git a/GTFSimple/App.config b/GTFSimple/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/GTFSimple/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/GTFSimple/GTFSimple.csproj b/GTFSimple/GTFSimple.csproj new file mode 100644 index 0000000..ea4a923 --- /dev/null +++ b/GTFSimple/GTFSimple.csproj @@ -0,0 +1,62 @@ + + + + + Debug + AnyCPU + {46D9B29D-D65F-4756-B8E2-EE000D8EEF52} + Exe + Properties + GTFSimple + GTFSimple + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + {ec32bc77-ff19-4088-b9fb-17085421b9a8} + GTFSimple.Core + + + + + False + ..\packages\CsvHelper.1.17.0\lib\net40-client\CsvHelper.dll + + + + + \ No newline at end of file diff --git a/GTFSimple/Program.cs b/GTFSimple/Program.cs new file mode 100644 index 0000000..45e8867 --- /dev/null +++ b/GTFSimple/Program.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CsvHelper; +using GTFSimple.Core.Feed; +using GTFSimple.Core.Files; +using GTFSimple.Core.Generators; +using GTFSimple.Core.Input; + +namespace GTFSimple +{ + class Program + { + private static readonly Registry registry = new Registry(); + + private static readonly StopGenerator stopGenerator = + new StopGenerator(0.000001, new SequentialStopIdGenerator(3)); + + private static readonly StopTimeGenerator stopTimeGenerator = new StopTimeGenerator(); + + private static readonly TripGenerator tripGenerator = new TripGenerator(); + + static void Main(string[] args) + { + var srcDir = args.Length < 1 ? "." : args[0]; + var destDir = args.Length < 2 + ? Path.Combine(srcDir, DateTime.Now.ToString("'feed'_yyyy-MM-dd_HH-mm-ss")) + : args[1]; + + Console.WriteLine("Reading from {0}...", srcDir); + + var src = new DirectoryInfo(srcDir); + + var files = from feedFile in registry + let type = feedFile.Value + from file in src.GetFiles(feedFile.Key + ".*") + let values = ReadCsv(type, file) + select new + { + File = file, + Type = feedFile.Value, + Values = values, + }; + + var fileLookup = files.ToDictionary(f => f.Type, f => f.Values); + + foreach (var f in fileLookup) + { + Console.WriteLine(f.Key.Name); + foreach (var v in f.Value) + Console.WriteLine(" " + v); + } + + var routeStops = stopGenerator.Generate(fileLookup[typeof(RouteStop)].Cast()); + Console.WriteLine(typeof(Stop).Name); + var stops = routeStops.ToLookup(x => x.Value, x => x.Key); + foreach (var stop in stops.OrderByDescending(g => g.Count())) + { + Console.WriteLine(" " + stop.Key); + foreach (var rs in stop) + Console.WriteLine(" " + rs); + } + + var tripTimes = tripGenerator.Generate(fileLookup[typeof(TripTime)].Cast(), "{0}-{1}-{2:00}"); + Console.WriteLine(typeof(Trip).Name); + + foreach (var trip in tripTimes.Values) + { + Console.WriteLine(" " + trip); + } + + var stopTimes = stopTimeGenerator.Generate(routeStops, tripTimes).ToList(); + Console.WriteLine(typeof(StopTime).Name); + foreach (var stopTime in stopTimes) + { + Console.WriteLine(" " + stopTime); + } + + Console.WriteLine("Writing to {0}...", destDir); + if (!Directory.Exists(destDir)) + Directory.CreateDirectory(destDir); + + //foreach (var feedFile in fileLookup) + // if (feedFile.Key.Namespace == typeof(FeedInfo).Namespace) + // WriteCsv(destDir, feedFile.Key, feedFile.Value); + + WriteCsv(destDir, fileLookup); + WriteCsv(destDir, fileLookup); + WriteCsv(destDir, fileLookup); + WriteCsv(destDir, fileLookup); + WriteCsv(destDir, fileLookup); + WriteCsv(destDir, fileLookup); + WriteCsv(destDir, fileLookup); + WriteCsv(destDir, fileLookup); + WriteCsv(destDir, fileLookup); + WriteCsv(destDir, stops.Select(s => s.Key).OrderBy(s => s.Id)); + WriteCsv(destDir, stopTimes); + WriteCsv(destDir, fileLookup); + WriteCsv(destDir, tripTimes.Values.OrderBy(t => t.Id)); + } + + private static void WriteCsv(string destDir, Dictionary> fileLookup) where T : class + { + IEnumerable records; + if (fileLookup.TryGetValue(typeof(T), out records)) + WriteCsv(destDir, records.Cast()); + } + + private static IEnumerable ReadCsv(Type type, FileInfo file) + { + using (var fileReader = new StreamReader(file.OpenRead())) + using (var csvReader = new CsvReader(fileReader)) + return csvReader.GetRecords(type).ToList(); + } + + private static void WriteCsv(string destDir, IEnumerable records) where T : class + { + var destPath = Path.Combine(destDir, registry.FileName(typeof(T))); + using (var fileWriter = new StreamWriter(File.OpenWrite(destPath))) + using (var csvWriter = new CsvWriter(fileWriter)) + csvWriter.WriteRecords(records); + } + + private static void WriteCsv(string destDir, Type type, IEnumerable records) + { + var destPath = Path.Combine(destDir, registry.FileName(type)); + using (var fileWriter = new StreamWriter(File.OpenWrite(destPath))) + using (var csvWriter = new CsvWriter(fileWriter)) + csvWriter.WriteRecords(type, records); + } + } +} diff --git a/GTFSimple/Properties/AssemblyInfo.cs b/GTFSimple/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d0da4f8 --- /dev/null +++ b/GTFSimple/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GTFSimple")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GTFSimple")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("43d95172-65fb-40f4-a3d7-f73d406567e7")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/GTFSimple/packages.config b/GTFSimple/packages.config new file mode 100644 index 0000000..9d74c84 --- /dev/null +++ b/GTFSimple/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/repositories.config b/packages/repositories.config index 72b63a7..6d1ab06 100644 --- a/packages/repositories.config +++ b/packages/repositories.config @@ -2,4 +2,5 @@ + \ No newline at end of file