Author: Adam Evyčędo <git@apiote.xyz>
convert berlin
api/api.go | 297 +++++++++++++++++++++++++++++++++++++++++++++ api/structs_gen.go | 141 ++++++++++++++++++++ server/router.go | 12 traffic/berlin_vbb.go | 224 +++++++++++++++++++++++++++++++++ traffic/convert.go | 12 + traffic/feeds.go | 1 transformers/de.go | 29 ++++ transformers/fr.go | 21 +++
diff --git a/api/api.go b/api/api.go index 3abdd769edfd177d94bc6bde1087d9ee3a742d44..2931d8d64c2c244d598c2f70f734d2a66d486439 100644 --- a/api/api.go +++ b/api/api.go @@ -109,6 +109,39 @@ return line, nil } +func convertTrafficLineGraphsV1forLineV2(trafficLine traffic.Line, line LineV2, context traffic.Context, t *traffic.Traffic) (LineV2, error) { + if len(trafficLine.GraphThere.StopCodes) != 0 { + graph := LineGraphV1{ + Stops: make([]StopStubV1, len(trafficLine.GraphThere.StopCodes)), + NextNodes: trafficLine.GraphThere.NextNodes, + } + for i, code := range trafficLine.GraphThere.StopCodes { + stopStub, err := traffic.GetStopStub(code, line.Name, context, t) + if err != nil { + return line, fmt.Errorf("while getting stopStub for %s: %w", code, err) + } + graph.Stops[i] = convertTrafficStopStub(stopStub) + } + line.Graphs = append(line.Graphs, graph) + } + if len(trafficLine.GraphBack.StopCodes) != 0 { + graph := LineGraphV1{ + Stops: make([]StopStubV1, len(trafficLine.GraphBack.StopCodes)), + NextNodes: trafficLine.GraphBack.NextNodes, + } + for i, code := range trafficLine.GraphBack.StopCodes { + stopStub, err := traffic.GetStopStub(code, line.Name, context, t) + if err != nil { + return line, fmt.Errorf("while getting stopStub for %s: %w", code, err) + } + graph.Stops[i] = convertTrafficStopStub(stopStub) + } + line.Graphs = append(line.Graphs, graph) + } + + return line, nil +} + func convertTrafficLine(line traffic.Line, feedID string) LineV1 { l := LineV1{ Name: line.Name, @@ -127,6 +160,24 @@ } return l } +func convertTrafficLineV2(line traffic.Line, feedID string) LineV2 { + l := LineV2{ + Name: line.Name, + Colour: fromColor(line.Colour), + Kind: makeLineTypeV3(line), + FeedID: feedID, + Headsigns: [][]string{}, + } + + if len(line.HeadsignsThere) != 0 { + l.Headsigns = append(l.Headsigns, line.HeadsignsThere) + } + if len(line.HeadsignsBack) != 0 { + l.Headsigns = append(l.Headsigns, line.HeadsignsBack) + } + return l +} + func convertTrafficVehicle(vehicle traffic.VehicleStatus, context traffic.Context, t *traffic.Traffic) (VehicleV1, error) { line, err := traffic.GetLine(vehicle.LineName, context, t) if err != nil { @@ -159,6 +210,26 @@ Position: PositionV1{vehicle.Position.Lat, vehicle.Position.Lon}, Capabilities: vehicle.Capabilities, Speed: vehicle.Speed, Line: LineStubV2{Name: line.Name, Kind: makeLineTypeV2(line), Colour: fromColor(line.Colour)}, + Headsign: vehicle.Headsign, + // todo CongestionLevel + // todo OccupancyStatus + // todo Status + // todo Delay + }, nil +} + +func convertTrafficVehicleV3(vehicle traffic.VehicleStatus, context traffic.Context, t *traffic.Traffic) (VehicleV3, error) { + line, err := traffic.GetLine(vehicle.LineName, context, t) + if err != nil { + return VehicleV3{}, fmt.Errorf("while getting line %s: %w", vehicle.LineName, err) + } + log.Printf("convertTrafficVehicle:: trafficVehicle: %+v, line: %+v\n", vehicle, line) + return VehicleV3{ + Id: string(vehicle.Id), + Position: PositionV1{vehicle.Position.Lat, vehicle.Position.Lon}, + Capabilities: vehicle.Capabilities, + Speed: vehicle.Speed, + Line: LineStubV3{Name: line.Name, Kind: makeLineTypeV3(line), Colour: fromColor(line.Colour)}, Headsign: vehicle.Headsign, // todo CongestionLevel // todo OccupancyStatus @@ -265,6 +336,30 @@ success.Queryables = sortQueryables(query, success.Queryables) return success, nil } +func CreateSuccessQueryablesV3(query string, items []traffic.Item, context traffic.Context, t *traffic.Traffic, other QueryablesResponse) (QueryablesResponse, error) { + success := QueryablesResponseDev{ + Queryables: []QueryableV3{}, + } + for _, item := range items { + if stop, ok := item.(traffic.Stop); ok { + s := convertTrafficStopV2(stop, context.FeedName) + success.Queryables = append(success.Queryables, QueryableV3(s)) + } else if line, ok := item.(traffic.Line); ok { + l := convertTrafficLineV2(line, context.FeedName) + success.Queryables = append(success.Queryables, QueryableV3(l)) + } else { + // todo error + } + } + if otherV3, ok := other.(QueryablesResponseDev); ok { + success.Queryables = append(success.Queryables, otherV3.Queryables...) + } else { + return success, errors.New("wrong version of other") + } + success.Queryables = sortQueryablesV3(query, success.Queryables) + return success, nil +} + func LimitQueryables(r QueryablesResponse, offset, limit uint64) QueryablesResponse { var result QueryablesResponse @@ -289,6 +384,16 @@ } else { queryables = r.(QueryablesResponseV2).Queryables[offset : offset+limit] } result = QueryablesResponseV2{queryables} + case QueryablesResponseDev: + var queryables []QueryableV3 + if int(offset) > len(r.(QueryablesResponseDev).Queryables) { + queryables = []QueryableV3{} + } else if len(r.(QueryablesResponseDev).Queryables) < int(offset+limit) { + queryables = r.(QueryablesResponseDev).Queryables[offset:] + } else { + queryables = r.(QueryablesResponseDev).Queryables[offset : offset+limit] + } + result = QueryablesResponseDev{queryables} } @@ -325,6 +430,36 @@ }) return queryables } +func sortQueryablesV3(query string, queryables []QueryableV3) []QueryableV3 { + // fixme query and names should be cleaned + sort.Slice(queryables, func(i, j int) bool { + var nameI, nameJ string + + switch queryables[i].(type) { + case StopV2: + nameI = queryables[i].(StopV2).Name + case LineV2: + nameI = queryables[i].(LineV2).Name + } + switch queryables[j].(type) { + case StopV2: + nameJ = queryables[j].(StopV2).Name + case LineV2: + nameJ = queryables[j].(LineV2).Name + } + levenshtein := &metrics.Levenshtein{ + CaseSensitive: false, + InsertCost: 1, + DeleteCost: 1, + ReplaceCost: 1, + } + distance1 := strutil.Similarity(query, nameI, levenshtein) + distance2 := strutil.Similarity(query, nameJ, levenshtein) + return distance1 > distance2 + }) + return queryables +} + func CreateSuccessLocatables(locatables []traffic.Locatable, context traffic.Context, t *traffic.Traffic, other LocatablesResponse) (LocatablesResponse, error) { success := LocatablesResponseV1{ Locatables: []LocatableV1{}, @@ -373,6 +508,30 @@ } return success, nil } +func CreateSuccessLocatablesV3(locatables []traffic.Locatable, context traffic.Context, t *traffic.Traffic, other LocatablesResponse) (LocatablesResponse, error) { + success := LocatablesResponseDev{ + Locatables: []LocatableV3{}, + } + for _, locatable := range locatables { + if stop, ok := locatable.(traffic.Stop); ok { + s := convertTrafficStopV2(stop, context.FeedName) + success.Locatables = append(success.Locatables, LocatableV3(s)) + } else if vehicle, ok := locatable.(traffic.VehicleStatus); ok { + v, err := convertTrafficVehicleV3(vehicle, context, t) + if err != nil { + return success, fmt.Errorf("while converting Traffic Vehicle %s: %w", vehicle.Id, err) + } + success.Locatables = append(success.Locatables, LocatableV3(v)) + } + } + if otherV3, ok := other.(LocatablesResponseDev); ok { + success.Locatables = append(success.Locatables, otherV3.Locatables...) + } else { + return success, errors.New("wrong version of other") + } + return success, nil +} + func CreateSuccessLine(line traffic.Line, context traffic.Context, t *traffic.Traffic) (LineResponse, error) { l := convertTrafficLine(line, context.FeedName) l, err := convertTrafficLineGraphs(line, l, context, t) @@ -386,6 +545,19 @@ return success, nil } +func CreateSuccessLineV2(line traffic.Line, context traffic.Context, t *traffic.Traffic) (LineResponse, error) { + l := convertTrafficLineV2(line, context.FeedName) + l, err := convertTrafficLineGraphsV1forLineV2(line, l, context, t) + if err != nil { + return LineResponseDev{}, fmt.Errorf("while converting graph: %w", err) + } + success := LineResponseDev{ + Line: l, + } + + return success, nil +} + func convertVehicle(update gtfs_rt.Update, vehicles map[string]traffic.Vehicle, line traffic.Line, headsign string) VehicleV1 { return VehicleV1{ Id: update.VehicleID, @@ -406,6 +578,19 @@ Position: PositionV1{Lat: float64(update.Latitude), Lon: float64(update.Longitude)}, Capabilities: vehicles[update.VehicleID].Capabilities, Speed: update.Speed, Line: LineStubV2{Name: line.Name, Kind: makeLineTypeV2(line), Colour: fromColor(line.Colour)}, + Headsign: headsign, + // todo CongestionLevel + // todo OccupancyStatus + } +} + +func convertVehicleV3(update gtfs_rt.Update, vehicles map[string]traffic.Vehicle, line traffic.Line, headsign string) VehicleV3 { + return VehicleV3{ + Id: update.VehicleID, + Position: PositionV1{Lat: float64(update.Latitude), Lon: float64(update.Longitude)}, + Capabilities: vehicles[update.VehicleID].Capabilities, + Speed: update.Speed, + Line: LineStubV3{Name: line.Name, Kind: makeLineTypeV3(line), Colour: fromColor(line.Colour)}, Headsign: headsign, // todo CongestionLevel // todo OccupancyStatus @@ -582,6 +767,91 @@ } return success, nil } +func CreateSuccessDeparturesV3(stop traffic.Stop, departures []traffic.DepartureRealtime, + date time.Time, vehicles map[string]traffic.Vehicle, alerts []traffic.Alert, ctx traffic.Context, t *traffic.Traffic, accept uint) (DeparturesResponse, error) { + d := []DepartureV3{} + var success DeparturesResponse + now := time.Now() + timezone, err := traffic.GetTimezone(stop, t.Feeds[ctx.FeedName]) + if err != nil { + return success, err + } + datetime := time.Date(date.Year(), date.Month(), + date.Day(), now.Hour(), now.Minute(), now.Second(), 0, timezone) + for _, trafficDeparture := range departures { + zoneAbbr := trafficDeparture.Update.Time.Location().String() + stopOrder, err := marshalStopOrder(trafficDeparture.Order.TripOffset, trafficDeparture.Order.Sequence) + if err != nil { + return success, err + } + line, err := traffic.GetLine(trafficDeparture.LineName, ctx, t) + if err != nil { + return success, fmt.Errorf("while getting line %s: %w", trafficDeparture.LineName, err) + } + departure := DepartureV3{ + Id: stopOrder, + Time: TimeV1{ + DayOffset: getDayOffset(datetime, trafficDeparture.Update.Time), + Hour: uint8(trafficDeparture.Update.Time.Hour()), + Minute: uint8(trafficDeparture.Update.Time.Minute()), + Second: uint8(trafficDeparture.Update.Time.Second()), + Zone: zoneAbbr, + }, + Status: STATUS_IN_TRANSIT, + IsRealtime: trafficDeparture.Update.TripUpdate != nil, + Vehicle: convertVehicleV3(trafficDeparture.Update, vehicles, line, trafficDeparture.Headsign), + Boarding: makeBoardingV1(trafficDeparture.Departure.Pickup, trafficDeparture.Departure.Dropoff), + } + timeToArrival := trafficDeparture.Update.Time.Sub(datetime).Minutes() + if departure.IsRealtime { + if trafficDeparture.Update.Status == pb.VehiclePosition_STOPPED_AT { + departure.Status = STATUS_AT_STOP + } else if timeToArrival < 0 { + departure.Status = STATUS_DEPARTED + } else if timeToArrival < 1 { + departure.Status = STATUS_INCOMING + } + + if trafficDeparture.Update.CongestionLevel == nil { + departure.Vehicle.CongestionLevel = CONGESTION_UNKNOWN + } else if *trafficDeparture.Update.CongestionLevel == pb.VehiclePosition_RUNNING_SMOOTHLY { + departure.Vehicle.CongestionLevel = CONGESTION_SMOOTH + } else if *trafficDeparture.Update.CongestionLevel == pb.VehiclePosition_STOP_AND_GO { + departure.Vehicle.CongestionLevel = CONGESTION_STOP_AND_GO + } else if *trafficDeparture.Update.CongestionLevel == pb.VehiclePosition_CONGESTION { + departure.Vehicle.CongestionLevel = CONGESTION_SIGNIFICANT + } else if *trafficDeparture.Update.CongestionLevel == pb.VehiclePosition_SEVERE_CONGESTION { + departure.Vehicle.CongestionLevel = CONGESTION_SEVERE + } + + if trafficDeparture.Update.OccupancyStatus == nil { + departure.Vehicle.OccupancyStatus = OCCUPANCY_UNKNOWN + } else if *trafficDeparture.Update.OccupancyStatus == pb.VehiclePosition_EMPTY { + departure.Vehicle.OccupancyStatus = OCCUPANCY_EMPTY + } else if *trafficDeparture.Update.OccupancyStatus == pb.VehiclePosition_MANY_SEATS_AVAILABLE { + departure.Vehicle.OccupancyStatus = OCCUPANCY_MANY_AVAILABLE + } else if *trafficDeparture.Update.OccupancyStatus == pb.VehiclePosition_FEW_SEATS_AVAILABLE { + departure.Vehicle.OccupancyStatus = OCCUPANCY_FEW_AVAILABLE + } else if *trafficDeparture.Update.OccupancyStatus == pb.VehiclePosition_STANDING_ROOM_ONLY { + departure.Vehicle.OccupancyStatus = OCCUPANCY_STANDING_ONLY + } else if *trafficDeparture.Update.OccupancyStatus == pb.VehiclePosition_CRUSHED_STANDING_ROOM_ONLY { + departure.Vehicle.OccupancyStatus = OCCUPANCY_CRUSHED + } else if *trafficDeparture.Update.OccupancyStatus == pb.VehiclePosition_FULL { + departure.Vehicle.OccupancyStatus = OCCUPANCY_FULL + } else if *trafficDeparture.Update.OccupancyStatus == pb.VehiclePosition_NOT_ACCEPTING_PASSENGERS { + departure.Vehicle.OccupancyStatus = OCCUPANCY_NOT_ACCEPTING + } + } + d = append(d, departure) + } + success = DeparturesResponseDev{ + Stop: convertTrafficStopV2(stop, ctx.FeedName), + Departures: d, + Alerts: convertTrafficAlerts(alerts), + } + return success, nil +} + func fromColor(c traffic.Colour) ColourV1 { return ColourV1{ R: c.R, @@ -609,6 +879,33 @@ } else if line.Kind == traffic.TROLLEYBUS { return LINE_V2_TROLLEYBUS } else { return LINE_V2_UNKNOWN + } +} + +func makeLineTypeV3(line traffic.Line) LineTypeV3 { + switch line.Kind { + case traffic.TRAM: + return LINE_V3_TRAM + case traffic.BUS: + return LINE_V3_BUS + case traffic.TROLLEYBUS: + return LINE_V3_TROLLEYBUS + case traffic.METRO: + return LINE_V3_METRO + case traffic.RAIL: + return LINE_V3_RAIL + case traffic.FERRY: + return LINE_V3_FERRY + case traffic.CABLE_TRAM: + return LINE_V3_CABLE_TRAM + case traffic.CABLE_CAR: + return LINE_V3_CABLE_CAR + case traffic.FUNICULAR: + return LINE_V3_FUNICULAR + case traffic.MONORAIL: + return LINE_V3_MONORAIL + default: + return LINE_V3_UNKNOWN } } diff --git a/api/structs_gen.go b/api/structs_gen.go index 66e546b0bf55c30718aeaad52aa08abc145f03d6..ed9bb898e15e05874302e98b911d8afea5674d03 100644 --- a/api/structs_gen.go +++ b/api/structs_gen.go @@ -8,7 +8,7 @@ "git.sr.ht/~sircmpwn/go-bare" ) type LineResponseDev struct { - Line LineV1 `bare:"line"` + Line LineV2 `bare:"line"` } func (t *LineResponseDev) Decode(data []byte) error { @@ -72,7 +72,7 @@ return bare.Marshal(t) } type QueryablesResponseDev struct { - Queryables []QueryableV2 `bare:"queryables"` + Queryables []QueryableV3 `bare:"queryables"` } func (t *QueryablesResponseDev) Decode(data []byte) error { @@ -158,6 +158,23 @@ func (t *LineV1) Encode() ([]byte, error) { return bare.Marshal(t) } +type LineV2 struct { + Name string `bare:"name"` + Colour ColourV1 `bare:"colour"` + Kind LineTypeV3 `bare:"kind"` + FeedID string `bare:"feedID"` + Headsigns [][]string `bare:"headsigns"` + Graphs []LineGraphV1 `bare:"graphs"` +} + +func (t *LineV2) Decode(data []byte) error { + return bare.Unmarshal(data, t) +} + +func (t *LineV2) Encode() ([]byte, error) { + return bare.Marshal(t) +} + type LineGraphV1 struct { Stops []StopStubV1 `bare:"stops"` NextNodes map[int][]int `bare:"nextNodes"` @@ -214,7 +231,7 @@ return bare.Marshal(t) } type LocatablesResponseDev struct { - Locatables []LocatableV2 `bare:"locatables"` + Locatables []LocatableV3 `bare:"locatables"` } func (t *LocatablesResponseDev) Decode(data []byte) error { @@ -287,6 +304,25 @@ func (t *VehicleV2) Encode() ([]byte, error) { return bare.Marshal(t) } +type VehicleV3 struct { + Id string `bare:"id"` + Position PositionV1 `bare:"position"` + Capabilities uint16 `bare:"capabilities"` + Speed float32 `bare:"speed"` + Line LineStubV3 `bare:"line"` + Headsign string `bare:"headsign"` + CongestionLevel CongestionLevelV1 `bare:"congestionLevel"` + OccupancyStatus OccupancyStatusV1 `bare:"occupancyStatus"` +} + +func (t *VehicleV3) Decode(data []byte) error { + return bare.Unmarshal(data, t) +} + +func (t *VehicleV3) Encode() ([]byte, error) { + return bare.Marshal(t) +} + type LineStubV1 struct { Name string `bare:"name"` Kind LineTypeV1 `bare:"kind"` @@ -315,6 +351,20 @@ func (t *LineStubV2) Encode() ([]byte, error) { return bare.Marshal(t) } +type LineStubV3 struct { + Name string `bare:"name"` + Kind LineTypeV3 `bare:"kind"` + Colour ColourV1 `bare:"colour"` +} + +func (t *LineStubV3) Decode(data []byte) error { + return bare.Unmarshal(data, t) +} + +func (t *LineStubV3) Encode() ([]byte, error) { + return bare.Marshal(t) +} + type ColourV1 struct { R uint8 `bare:"r"` G uint8 `bare:"g"` @@ -331,7 +381,7 @@ } type DeparturesResponseDev struct { Alerts []AlertV1 `bare:"alerts"` - Departures []DepartureV2 `bare:"departures"` + Departures []DepartureV3 `bare:"departures"` Stop StopV2 `bare:"stop"` } @@ -421,6 +471,23 @@ func (t *DepartureV2) Encode() ([]byte, error) { return bare.Marshal(t) } +type DepartureV3 struct { + Id string `bare:"id"` + Time TimeV1 `bare:"time"` + Status VehicleStatusV1 `bare:"status"` + IsRealtime bool `bare:"isRealtime"` + Vehicle VehicleV3 `bare:"vehicle"` + Boarding uint8 `bare:"boarding"` +} + +func (t *DepartureV3) Decode(data []byte) error { + return bare.Unmarshal(data, t) +} + +func (t *DepartureV3) Encode() ([]byte, error) { + return bare.Marshal(t) +} + type TimeV1 struct { Hour uint8 `bare:"hour"` Minute uint8 `bare:"minute"` @@ -554,6 +621,50 @@ } panic(errors.New("Invalid LineTypeV2 value")) } +type LineTypeV3 uint + +const ( + LINE_V3_UNKNOWN LineTypeV3 = 0 + LINE_V3_TRAM LineTypeV3 = 1 + LINE_V3_BUS LineTypeV3 = 2 + LINE_V3_TROLLEYBUS LineTypeV3 = 3 + LINE_V3_METRO LineTypeV3 = 4 + LINE_V3_RAIL LineTypeV3 = 5 + LINE_V3_FERRY LineTypeV3 = 6 + LINE_V3_CABLE_TRAM LineTypeV3 = 7 + LINE_V3_CABLE_CAR LineTypeV3 = 8 + LINE_V3_FUNICULAR LineTypeV3 = 9 + LINE_V3_MONORAIL LineTypeV3 = 10 +) + +func (t LineTypeV3) String() string { + switch t { + case LINE_V3_UNKNOWN: + return "LINE_V3_UNKNOWN" + case LINE_V3_TRAM: + return "LINE_V3_TRAM" + case LINE_V3_BUS: + return "LINE_V3_BUS" + case LINE_V3_TROLLEYBUS: + return "LINE_V3_TROLLEYBUS" + case LINE_V3_METRO: + return "LINE_V3_METRO" + case LINE_V3_RAIL: + return "LINE_V3_RAIL" + case LINE_V3_FERRY: + return "LINE_V3_FERRY" + case LINE_V3_CABLE_TRAM: + return "LINE_V3_CABLE_TRAM" + case LINE_V3_CABLE_CAR: + return "LINE_V3_CABLE_CAR" + case LINE_V3_FUNICULAR: + return "LINE_V3_FUNICULAR" + case LINE_V3_MONORAIL: + return "LINE_V3_MONORAIL" + } + panic(errors.New("Invalid LineTypeV3 value")) +} + type AlertCauseV1 uint const ( @@ -740,6 +851,13 @@ func (_ StopV2) IsUnion() {} func (_ LineV1) IsUnion() {} +type QueryableV3 interface { + bare.Union +} + + +func (_ LineV2) IsUnion() {} + type LocatablesResponse interface { bare.Union } @@ -764,6 +882,13 @@ func (_ VehicleV2) IsUnion() {} +type LocatableV3 interface { + bare.Union +} + + +func (_ VehicleV3) IsUnion() {} + type DeparturesResponse interface { bare.Union } @@ -795,6 +920,10 @@ bare.RegisterUnion((*QueryableV2)(nil)). Member(*new(StopV2), 0). Member(*new(LineV1), 1) + bare.RegisterUnion((*QueryableV3)(nil)). + Member(*new(StopV2), 0). + Member(*new(LineV2), 1) + bare.RegisterUnion((*LocatablesResponse)(nil)). Member(*new(LocatablesResponseDev), 0). Member(*new(LocatablesResponseV1), 1). @@ -807,6 +936,10 @@ bare.RegisterUnion((*LocatableV2)(nil)). Member(*new(StopV2), 0). Member(*new(VehicleV2), 1) + + bare.RegisterUnion((*LocatableV3)(nil)). + Member(*new(StopV2), 0). + Member(*new(VehicleV3), 1) bare.RegisterUnion((*DeparturesResponse)(nil)). Member(*new(DeparturesResponseDev), 0). diff --git a/server/router.go b/server/router.go index 5873f48d16c1dff0e8a5306356e60d9bbf84ff8b..69fb052922a1d6c99b0d2f7d8b38d413774f9379 100644 --- a/server/router.go +++ b/server/router.go @@ -253,7 +253,7 @@ locatables = append(locatables, vehicle) } switch accept { case 0: - locatablesSuccess, err = api.CreateSuccessLocatablesV2(locatables, context, t, locatablesSuccess) + locatablesSuccess, err = api.CreateSuccessLocatablesV3(locatables, context, t, locatablesSuccess) case 1: locatablesSuccess, err = api.CreateSuccessLocatables(locatables, context, t, locatablesSuccess) case 2: @@ -353,7 +353,7 @@ items = append(items, stop) } switch accept { case 0: - queryablesSuccess, err = api.CreateSuccessQueryablesV2(query, items, context, t, queryablesSuccess) + queryablesSuccess, err = api.CreateSuccessQueryablesV3(query, items, context, t, queryablesSuccess) case 1: queryablesSuccess, err = api.CreateSuccessQueryables(items, context, t, queryablesSuccess) case 2: @@ -373,7 +373,7 @@ return fmt.Errorf("while getting stop: %w", err) } switch accept { case 0: - queryablesSuccess, err = api.CreateSuccessQueryablesV2(query, []traffic.Item{stop}, context, t, queryablesSuccess) + queryablesSuccess, err = api.CreateSuccessQueryablesV3(query, []traffic.Item{stop}, context, t, queryablesSuccess) case 1: queryablesSuccess, err = api.CreateSuccessQueryables([]traffic.Item{stop}, context, t, queryablesSuccess) case 2: @@ -402,7 +402,7 @@ } switch accept { case 0: - queryablesSuccess, err = api.CreateSuccessQueryablesV2(query, items, context, t, queryablesSuccess) + queryablesSuccess, err = api.CreateSuccessQueryablesV3(query, items, context, t, queryablesSuccess) case 1: queryablesSuccess, err = api.CreateSuccessQueryables(items, context, t, queryablesSuccess) case 2: @@ -550,7 +550,7 @@ var success api.DeparturesResponse switch accept { case 0: - success, err = api.CreateSuccessDeparturesV2(stop, departures, date, t.Vehicles[feedName][versionCode], alerts, context, t, accept) + success, err = api.CreateSuccessDeparturesV3(stop, departures, date, t.Vehicles[feedName][versionCode], alerts, context, t, accept) case 1: success, err = api.CreateSuccessDeparturesV1(stop, departures, date, t.Vehicles[feedName][versionCode], alerts, context, t, accept) case 2: @@ -653,7 +653,7 @@ } var success api.LineResponse switch accept { case 0: - success, err = api.CreateSuccessLine(line, context, t) + success, err = api.CreateSuccessLineV2(line, context, t) case 1: success, err = api.CreateSuccessLine(line, context, t) } diff --git a/traffic/berlin_vbb.go b/traffic/berlin_vbb.go new file mode 100644 index 0000000000000000000000000000000000000000..24090b69fc818cf5aac3a6eef86392bff72fd2df --- /dev/null +++ b/traffic/berlin_vbb.go @@ -0,0 +1,224 @@ +package traffic + +import ( + "bufio" + "encoding/csv" + "io" + "path/filepath" + "strings" + + "apiote.xyz/p/szczanieckiej/transformers" + + "fmt" + "net/http" + "os" + "time" + + "golang.org/x/text/language" + "golang.org/x/text/transform" +) + +type VbbBerlin struct { + client http.Client +} + +func (z VbbBerlin) ConvertVehicles(path string) error { + result, err := os.Create(filepath.Join(path, "vehicles.bare")) + if err != nil { + return fmt.Errorf("ConvertVehicles: cannot create bare file: %w", err) + } + defer result.Close() + return nil +} + +func (z VbbBerlin) GetVersions(date time.Time) ([]Version, error) { + versions := []Version{} + version, err := MakeVersion("00010101_99991231", z.GetLocation()) + if err != nil { + return nil, err + } + version.Link = "https://www.vbb.de/fileadmin/user_upload/VBB/Dokumente/API-Datensaetze/gtfs-mastscharf/GTFS.zip" + versions = append(versions, version) + return versions, nil +} + +func (z VbbBerlin) GetLocation() *time.Location { + l, _ := time.LoadLocation("Europe/Berlin") + return l +} + +func (z VbbBerlin) String() string { + return "berlin_vbb" +} + +func (z VbbBerlin) RealtimeFeeds() []string { + return []string{} +} + +func (z VbbBerlin) Transformer() transform.Transformer { + return transform.Chain(transformers.TransformerDE, transformers.TransformerPL, transformers.TransformerFR) +} + +func (z VbbBerlin) Name() string { + return "VBB Berlin" +} + +func (z VbbBerlin) Attribution() map[language.Tag]string { + // TODO + return map[language.Tag]string{ + language.Und: "GTFS files downloaded from https://www.vbb.de/vbbgtfs and converted to TRAFFIC", + } +} + +func (z VbbBerlin) Description() map[language.Tag]string { + return map[language.Tag]string{ + language.Und: "Timetable for public transport in German metropolitan region including the city state of Berlin and the surrounding state of Brandenburg organised by Verkehrsverbund Berlin-Brandenburg (VBB). Includes trams, busses, U-Bahn (underground), S-Bahn, ferries, and intercity trains", + language.BritishEnglish: "Timetable for public transport in German metropolitan region including the city state of Berlin and the surrounding state of Brandenburg organised by Verkehrsverbund Berlin-Brandenburg (VBB). Includes trams, busses, U-Bahn (underground), S-Bahn, ferries, and intercity trains", + language.AmericanEnglish: "Timetable for public transport in German metropolitan region including the city state of Berlin and the surrounding state of Brandenburg organized by Verkehrsverbund Berlin-Brandenburg (VBB). Includes trams, busses, U-Bahn (subway), S-Bahn, ferries, and intercity trains", + language.Polish: "Rozkład jazdy transportu publicznego w niemieckim rejonie metropolitalnym (Berlin i Brandenburgia) organizowanego przez Verkehrsverbund Berlin-Brandenburg (VBB). Zawiera tramwaje, autobusy, U-Bahn (metro), S-Bahn, promy i koleje międzymiastowe", + language.German: "Fahrplan für den ÖPNV in Berlin und Brandenburg (VBB). Umfasst Straßenbahnen, Busse, U-Bahn, S-Bahn, Fähren und Fernverkehr", + } +} + +func (z VbbBerlin) Flags() FeedFlags { + return FeedFlags{ + Headsign: HeadsignTripHeadsing, + StopIdFormat: "{{stop_id}}", + StopName: "{{stop_name}} | {{platform_code}}", // TODO platform_code might not be present, but currently templating doesn’t support this + LineName: "{{route_short_name}}", + } +} + +func (z VbbBerlin) FeedPrepareZip(path string) error { + // NOTE add platform codes from stop_id + stopsFile, err := os.Open(filepath.Join(path, "stops.txt")) + if err != nil { + return fmt.Errorf("while opening stops file: %w", err) + } + defer stopsFile.Close() + stops2File, err := os.OpenFile(filepath.Join(path, "stops2.txt"), os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return fmt.Errorf("while opening stops2 file: %w", err) + } + defer stops2File.Close() + r := csv.NewReader(bufio.NewReader(stopsFile)) + w := csv.NewWriter(stops2File) + header, err := r.Read() + if err != nil { + return fmt.Errorf("while reading stops header: %w", err) + } + fields := map[string]int{} + for i, headerField := range header { + fields[headerField] = i + } + err = w.Write(header) + if err != nil { + return fmt.Errorf("while writing stops header: %w", err) + } + fmt.Println("header", fields) + for { + record, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("while reading a stop record: %w", err) + } + f, ok := fields["location_type"] + fmt.Println("FIX stop:", f, ok, "Platform:", record[fields["platform_code"]]) + if ok { + fmt.Println(record[f]) + } + if (!ok || record[f] == "" || record[f] == "0") && record[fields["platform_code"]] == "" { + fmt.Println(fields["stop_id"], record[fields["stop_id"]]) + stopID := strings.Split(record[fields["stop_id"]], ":") + fmt.Println("So fixing", len(stopID), stopID) + if len(stopID) >= 5 { + record[fields["platform_code"]] = stopID[4] + } + } + fmt.Println("write:", record) + err = w.Write(record) + fmt.Println("NEXT") + if err != nil { + return fmt.Errorf("while writing a stop record: %w", err) + } + } + w.Flush() + err = os.Remove(filepath.Join(path, "stops.txt")) + if err != nil { + return fmt.Errorf("while removing stops: %w", err) + } + err = os.Rename(filepath.Join(path, "stops2.txt"), filepath.Join(path, "stops.txt")) + if err != nil { + return fmt.Errorf("while renaming stops: %w", err) + } + + // NOTE fix route types + routesFile, err := os.Open(filepath.Join(path, "routes.txt")) + if err != nil { + return fmt.Errorf("while opening routes file: %w", err) + } + defer routesFile.Close() + routes2File, err := os.OpenFile(filepath.Join(path, "routes2.txt"), os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return fmt.Errorf("while opening routes2 file: %w", err) + } + defer routes2File.Close() + r = csv.NewReader(bufio.NewReader(routesFile)) + w = csv.NewWriter(routes2File) + header, err = r.Read() + if err != nil { + return fmt.Errorf("while reading routes header: %w", err) + } + fields = map[string]int{} + for i, headerField := range header { + fields[headerField] = i + } + err = w.Write(header) + if err != nil { + return fmt.Errorf("while writing routes header: %w", err) + } + + for { + record, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("while reading a route record: %w", err) + } + if record[fields["route_type"]] == "100" { + record[fields["route_type"]] = "2" + } + if record[fields["route_type"]] == "109" { + record[fields["route_type"]] = "0" + } + if record[fields["route_type"]] == "400" { + record[fields["route_type"]] = "1" + } + if record[fields["route_type"]] == "700" { + record[fields["route_type"]] = "3" + } + if record[fields["route_type"]] == "900" { + record[fields["route_type"]] = "0" + } + if record[fields["route_type"]] == "1000" { + record[fields["route_type"]] = "4" + } + err = w.Write(record) + if err != nil { + return fmt.Errorf("while writing a route record: %w", err) + } + } + w.Flush() + err = os.Remove(filepath.Join(path, "routes.txt")) + if err != nil { + return fmt.Errorf("while removing routes: %w", err) + } + err = os.Rename(filepath.Join(path, "routes2.txt"), filepath.Join(path, "routes.txt")) + if err != nil { + return fmt.Errorf("while renaming routes: %w", err) + } + return nil +} diff --git a/traffic/convert.go b/traffic/convert.go index a2d2e55a70394e2b2aec6eb0200d104c70b23829..9d037ca66f577b5d0875fc32ea8f5fedaed521ec 100644 --- a/traffic/convert.go +++ b/traffic/convert.go @@ -7,6 +7,8 @@ // todo Agency.phoneNumber -> E.123 format // todo remove StopOrder.tripID // todo validity to FeedInfo, not filename +// FIXME LineName is not unique + import ( "apiote.xyz/p/szczanieckiej/config" "apiote.xyz/p/szczanieckiej/file" @@ -716,6 +718,13 @@ if err != nil { return c, fmt.Errorf("while reading a record: %w", err) } + if f, ok := fields["location_type"]; ok && record[f] != "" && record[f] != "0" { + // NOTE for now ignore everything that’s not a stop/platform + // TODO use Portals (location_type == 2) to show on map if platform has a parent (location_type == 1) that has a Portal + // TODO use location_type in {3,4} for routing inside stations (with pathways, transfers, and levels) + continue + } + stopID := record[fields["stop_id"]] stopTrips := tripsThroughStop[stopID] @@ -726,13 +735,14 @@ } stop.Id = stopID - templates := []string{"stop_code", "stop_id", "stop_name"} + templates := []string{"stop_code", "stop_id", "stop_name", "platform_code"} stop.Code = c.Feed.Flags().StopIdFormat for _, template := range templates { stop.Code = strings.Replace(stop.Code, "{{"+template+"}}", record[fields[template]], -1) } stop.Name = c.Feed.Flags().StopName for _, template := range templates { + // TODO if '{{template}}' is empty stop.Name = strings.Replace(stop.Name, "{{"+template+"}}", record[fields[template]], -1) } if field, ok := fields["zone_id"]; ok { diff --git a/traffic/feeds.go b/traffic/feeds.go index 141547e237448e2c9aac822a738e379345f6388f..0c2ba66c8c8ba7616414af236adf2b317cc44556 100644 --- a/traffic/feeds.go +++ b/traffic/feeds.go @@ -78,6 +78,7 @@ }, }, "krakow_ztp": ZtpKrakow{}, "gzm_ztm": GzmZtm{}, + "berlin_vbb": VbbBerlin{}, } } diff --git a/transformers/de.go b/transformers/de.go new file mode 100644 index 0000000000000000000000000000000000000000..92cdff2436be901d1c8bdda1010b3c6f41b11f15 --- /dev/null +++ b/transformers/de.go @@ -0,0 +1,29 @@ +package transformers + +import ( + "golang.org/x/text/transform" +) + +//nolint:gochecknoglobals +var TransformerDE transform.Transformer = Replace(func(r rune) []rune { + switch r { + case 'ö': + return []rune{'o', 'e'} + case 'Ö': + return []rune{'O', 'e'} + case 'ä': + return []rune{'a', 'e'} + case 'Ä': + return []rune{'A', 'e'} + case 'ß': + return []rune{'s', 's'} + case 'ẞ': + return []rune{'S', 'S'} + case 'ü': + return []rune{'u', 'e'} + case 'Ü': + return []rune{'U', 'e'} + default: + return []rune{r} + } +}) diff --git a/transformers/fr.go b/transformers/fr.go new file mode 100644 index 0000000000000000000000000000000000000000..abfad31a9abd6c132b9d6dbbfb421c16732d2b05 --- /dev/null +++ b/transformers/fr.go @@ -0,0 +1,21 @@ +package transformers + +import ( + "golang.org/x/text/transform" +) + +//nolint:gochecknoglobals +var TransformerFR transform.Transformer = Replace(func(r rune) []rune { + switch r { + case 'è': + return []rune{'e'} + case 'È': + return []rune{'E'} + case 'é': + return []rune{'e'} + case 'É': + return []rune{'E'} + default: + return []rune{r} + } +})