1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891 |
- // SPDX-FileCopyrightText: Adam Evyčędo
- //
- // SPDX-License-Identifier: AGPL-3.0-or-later
- package traffic
- // TODO(BAF10) direction (0|1) to const (TO|BACK)
- // TODO Agency.language, FeedInfo.language -> IETF language tag
- // TODO Agency.phoneNumber -> E.123 format
- import (
- "text/template"
- "apiote.xyz/p/szczanieckiej/config"
- "apiote.xyz/p/szczanieckiej/file"
- "bufio"
- "embed"
- "encoding/csv"
- "errors"
- "fmt"
- "io"
- "io/fs"
- "log"
- "net/http"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "syscall"
- "time"
- "gopkg.in/yaml.v3"
- gott2 "apiote.xyz/p/gott/v2"
- "git.sr.ht/~sircmpwn/go-bare"
- "notabug.org/apiote/gott"
- )
- //go:embed realtime_lua
- var luaScripts embed.FS
- type _LineGraph struct {
- StopCodesArray []string
- StopCodes map[string]int
- NextNodes map[int]map[int]struct{}
- }
- type ErrEmpty struct{}
- func (ErrEmpty) Error() string {
- return ""
- }
- type result struct {
- config config.Config
- pid int
- tmpPath string
- feed Feed
- feedName string
- location *time.Location
- tmpFeedPath string
- homeFeedPath string
- downloadedVersions []Version
- allVersions []Version
- gtfsFilenames []string
- missingVersions []Version
- updatesFile *os.File
- updates map[string]string
- etags map[string]string
- newEtags map[string]string
- feedTranslations embed.FS
- }
- type feedConverter struct {
- TmpFeedPath string
- GtfsFilename string
- Feed Feed
- HomeFeedPath string
- feedTranslations embed.FS
- config config.Config
- Timezone *time.Location
- TrafficCalendarFile *os.File
- Departures map[string][]Departure
- TripsThroughStop map[string]map[string]StopOrder
- LineNames map[string]string
- TripsOffsets map[string]uint
- TripChangeOpts map[string]ChangeOption
- StopsCodeIndex CodeIndex
- StopsNameIndex map[string][]uint
- Stops map[string]string
- LineGraphs map[string]map[uint]LineGraph
- lineHeadsigns map[string]map[uint][]string
- LineIndex map[string][]uint
- LineIdIndex CodeIndex
- ValidFrom time.Time
- ValidFromError []error
- ValidTill time.Time
- ValidTillError []error
- tripHeadsigns map[string]string
- stopNames map[string]string
- feedInfo FeedInfo
- defaultLanguage string
- translations map[string]map[string]string
- schedules map[string]Schedule
- trips map[string]Trip
- }
- // helper functions
- func translateFieldDefault(key, feedLanguage, defaultLanguage string, translations map[string]map[string]string) string {
- if feedLanguage == "mul" {
- if value, ok := translations[key][defaultLanguage]; !ok {
- return key
- } else {
- return value
- }
- }
- return key
- }
- func translateField(key, feedLanguage, defaultLanguage string, translations map[string]map[string]string) []Translation {
- var result []Translation
- if feedLanguage == "mul" {
- if value, ok := translations[key][defaultLanguage]; !ok {
- result = []Translation{{Language: defaultLanguage, Value: key}}
- } else {
- result = []Translation{{Language: defaultLanguage, Value: value}}
- }
- }
- for language, value := range translations[key] {
- if language == defaultLanguage {
- continue
- }
- result = append(result, Translation{Language: language, Value: value})
- }
- return result
- }
- func hex2colour(hex string) Colour {
- if hex[0] == '#' {
- hex = hex[1:]
- }
- colour := Colour{
- A: 0xff,
- }
- hexToByte := func(b byte) byte {
- switch {
- case b >= '0' && b <= '9':
- return b - '0'
- case b >= 'a' && b <= 'f':
- return b - 'a' + 10
- case b >= 'A' && b <= 'F':
- return b - 'A' + 10
- default:
- return 0
- }
- }
- switch len(hex) {
- case 6:
- colour.R = hexToByte(hex[0])<<4 + hexToByte(hex[1])
- colour.G = hexToByte(hex[2])<<4 + hexToByte(hex[3])
- colour.B = hexToByte(hex[4])<<4 + hexToByte(hex[5])
- case 3:
- colour.R = hexToByte(hex[0])<<4 + hexToByte(hex[0])
- colour.G = hexToByte(hex[1])<<4 + hexToByte(hex[1])
- colour.B = hexToByte(hex[2])<<4 + hexToByte(hex[2])
- }
- return colour
- }
- func readEtags(cfg config.Config) (map[string]string, error) {
- etagsFilename := filepath.Join(cfg.FeedsPath, "etags.bare")
- etagsFile, err := os.Open(etagsFilename)
- if err != nil {
- var pathError *os.PathError
- if errors.As(err, &pathError) && errors.Is(pathError, fs.ErrNotExist) {
- return map[string]string{}, nil
- }
- return nil, fmt.Errorf("while opening file: %w", err)
- }
- defer etagsFile.Close()
- var etags map[string]string
- err = bare.UnmarshalReader(etagsFile, &etags)
- if err != nil {
- return nil, fmt.Errorf("while unmarshalling: %w", err)
- }
- return etags, nil
- }
- func saveEtags(cfg config.Config, etags map[string]string) error {
- etagsFilename := filepath.Join(cfg.FeedsPath, "etags.bare")
- etagsFile, err := os.OpenFile(etagsFilename, os.O_RDWR|os.O_CREATE, 0644)
- if err != nil {
- return fmt.Errorf("while opening: %w", err)
- }
- defer etagsFile.Close()
- bytes, err := bare.Marshal(&etags)
- if err != nil {
- return fmt.Errorf("while marshalling: %w", err)
- }
- _, err = etagsFile.Write(bytes)
- if err != nil {
- return fmt.Errorf("while writing: %w", err)
- }
- return nil
- }
- // converting functions
- func createTmpPath(input ...interface{}) (interface{}, error) {
- args := input[0].(result)
- p := filepath.Join(args.tmpPath, args.feedName)
- err := os.MkdirAll(p, 0755)
- args.tmpFeedPath = p
- return gott.Tuple{args}, err
- }
- func createFeedHome(input ...interface{}) (interface{}, error) {
- args := input[0].(result)
- p := filepath.Join(args.config.FeedsPath, args.feedName)
- err := os.MkdirAll(p, 0755)
- args.homeFeedPath = p
- return gott.Tuple{args}, err
- }
- func listDownloadedVersions(input ...interface{}) (interface{}, error) {
- args := input[0].(result)
- v, err := ListVersionsTimezone(args.config, args.feed, args.feed.getTimezone())
- args.downloadedVersions = v
- return gott.Tuple{args}, err
- }
- func getAllVersions(input ...interface{}) (interface{}, error) {
- args := input[0].(result)
- v, err := args.feed.GetVersions(time.Now().In(args.location), args.location)
- args.allVersions = v
- return gott.Tuple{args}, err
- }
- func findValidVersions(input ...interface{}) interface{} {
- args := input[0].(result)
- now := time.Now().In(args.location)
- validVersions := FindValidVersions(args.allVersions, now)
- downloadedVersions := map[string]struct{}{}
- for _, downloadedVersion := range args.downloadedVersions {
- downloadedVersions[downloadedVersion.String()] = struct{}{}
- }
- missingVersions := []Version{}
- for _, version := range validVersions {
- if _, ok := downloadedVersions[version.String()]; !ok {
- missingVersions = append(missingVersions, version)
- }
- }
- // log.Println("all", args.allVersions)
- // log.Println("valid", validVersions)
- // log.Println("downloaded", downloadedVersions)
- // log.Println("missing", missingVersions)
- args.missingVersions = missingVersions
- return gott.Tuple{args}
- }
- func getGtfsFiles(input ...interface{}) (interface{}, error) {
- args := input[0].(result)
- names := []string{}
- for i, version := range args.missingVersions {
- name := fmt.Sprintf("%s_%d.zip", version.String(), i)
- zipPath := filepath.Join(args.tmpFeedPath, name)
- url := version.Link
- request, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return gott.Tuple{args}, fmt.Errorf("while creating request for %s: %w", url, err)
- }
- request.Header.Add("If-None-Match", args.etags[url])
- client := http.Client{} // todo timeout
- response, err := client.Do(request)
- if err != nil {
- return gott.Tuple{args}, fmt.Errorf("while downloading gtfs %s %w", name, err)
- }
- if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNotModified {
- return gott.Tuple{args}, fmt.Errorf("wrong response code %d for %s: %w", response.StatusCode, url, err)
- }
- if response.StatusCode == 200 {
- args.newEtags[url] = response.Header.Get("etag")
- } else {
- args.newEtags[url] = args.etags[url]
- continue
- }
- file, err := os.Create(zipPath)
- if err != nil {
- return gott.Tuple{args}, fmt.Errorf("while creating zip for %s %w", name, err)
- }
- defer file.Close()
- _, err = io.Copy(file, response.Body)
- if err != nil {
- return gott.Tuple{args}, fmt.Errorf("while copying gtfs %s %w", name, err)
- }
- names = append(names, name)
- }
- args.gtfsFilenames = names
- return gott.Tuple{args}, nil
- }
- func unzipGtfs(c feedConverter) error {
- return file.UnzipGtfs(c.TmpFeedPath, c.GtfsFilename)
- }
- func convertVehicles(c feedConverter) error { // ( -- >> vehicles.bare)
- result, err := os.Create(filepath.Join(c.TmpFeedPath, "vehicles.bare"))
- if err != nil {
- return fmt.Errorf("while creating file: %w", err)
- }
- defer result.Close()
- vehicles, err := c.Feed.ConvertVehicles()
- for _, vehicle := range vehicles {
- bytes, err := bare.Marshal(&vehicle)
- if err != nil {
- return fmt.Errorf("while marshalling: %w", err)
- }
- _, err = result.Write(bytes)
- if err != nil {
- return fmt.Errorf("while writing to file: %w", err)
- }
- }
- return nil
- }
- func prepareFeedGtfs(c feedConverter) error {
- return c.Feed.FeedPrepareZip(c.TmpFeedPath)
- }
- func createTrafficCalendarFile(c feedConverter) (feedConverter, error) {
- path := c.TmpFeedPath
- var err error
- c.TrafficCalendarFile, err = os.Create(filepath.Join(path, "calendar.bare"))
- return c, err
- }
- func recoverCalendar(c feedConverter, e error) (feedConverter, error) {
- var pathError *os.PathError
- if errors.As(e, &pathError) && errors.Is(pathError, fs.ErrNotExist) {
- return c, nil
- }
- return c, e
- }
- func convertCalendar(c feedConverter) (feedConverter, error) { // ( feedInfo -- schedules >> )
- c.schedules = map[string]Schedule{}
- path := c.TmpFeedPath
- calendarFile, err := os.Open(filepath.Join(path, "calendar.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- defer calendarFile.Close()
- r := csv.NewReader(bufio.NewReader(calendarFile))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- for {
- schedule := Schedule{}
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- schedule.Id = record[fields["service_id"]]
- startDate := record[fields["start_date"]]
- endDate := record[fields["end_date"]]
- schedule.DateRanges = []DateRange{
- DateRange{
- Start: startDate[:4] + "-" + startDate[4:6] + "-" + startDate[6:],
- End: endDate[:4] + "-" + endDate[4:6] + "-" + endDate[6:],
- },
- }
- if record[fields["monday"]] == "1" {
- schedule.DateRanges[0].Weekdays |= (1 << 1)
- }
- if record[fields["tuesday"]] == "1" {
- schedule.DateRanges[0].Weekdays |= (1 << 2)
- }
- if record[fields["wednesday"]] == "1" {
- schedule.DateRanges[0].Weekdays |= (1 << 3)
- }
- if record[fields["thursday"]] == "1" {
- schedule.DateRanges[0].Weekdays |= (1 << 4)
- }
- if record[fields["friday"]] == "1" {
- schedule.DateRanges[0].Weekdays |= (1 << 5)
- }
- if record[fields["saturday"]] == "1" {
- schedule.DateRanges[0].Weekdays |= (1 << 6)
- }
- if record[fields["sunday"]] == "1" {
- schedule.DateRanges[0].Weekdays |= (1 << 0)
- schedule.DateRanges[0].Weekdays |= (1 << 7)
- }
- c.schedules[schedule.Id] = schedule
- scheduleStart, err := time.ParseInLocation(DateFormat, schedule.DateRanges[0].Start, c.Timezone)
- if err != nil {
- c.ValidFromError = append(c.ValidFromError, err)
- }
- if err == nil && (c.ValidFrom.IsZero() || scheduleStart.Before(c.ValidFrom)) {
- c.ValidFrom = scheduleStart
- c.feedInfo.ValidSince = scheduleStart.Format(ValidityFormat)
- }
- scheduleEnd, err := time.ParseInLocation(DateFormat, schedule.DateRanges[0].End, c.Timezone)
- if err != nil {
- c.ValidTillError = append(c.ValidTillError, err)
- }
- if err == nil && (c.ValidTill.IsZero() || scheduleEnd.After(c.ValidTill)) {
- c.ValidTill = scheduleEnd
- c.feedInfo.ValidTill = scheduleEnd.Format(ValidityFormat)
- }
- }
- return c, nil
- }
- func convertCalendarDates(c feedConverter) (feedConverter, error) { // ( feedInfo -- schedules >> )
- path := c.TmpFeedPath
- datesFile, err := os.Open(filepath.Join(path, "calendar_dates.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- defer datesFile.Close()
- r := csv.NewReader(bufio.NewReader(datesFile))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- for {
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- if record[fields["exception_type"]] == "1" {
- id := record[fields["service_id"]]
- schedule := c.schedules[id]
- date := record[fields["date"]]
- dateRange := DateRange{
- Start: date[:4] + "-" + date[4:6] + "-" + date[6:],
- End: date[:4] + "-" + date[4:6] + "-" + date[6:],
- Weekdays: 0xff,
- }
- if len(schedule.DateRanges) == 0 {
- schedule.Id = id
- schedule.DateRanges = []DateRange{dateRange}
- } else {
- schedule.DateRanges = append(schedule.DateRanges, dateRange)
- sort.Slice(schedule.DateRanges, func(i, j int) bool {
- return schedule.DateRanges[i].Start < schedule.DateRanges[j].Start
- })
- }
- c.schedules[schedule.Id] = schedule
- } else {
- date := record[fields["date"]]
- formatedDate := date[:4] + "-" + date[4:6] + "-" + date[6:]
- scheduleToEdit := c.schedules[record[fields["service_id"]]]
- newDateRanges := []DateRange{}
- for i := 0; i < len(scheduleToEdit.DateRanges); i++ {
- dateRange := scheduleToEdit.DateRanges[i]
- if dateRange.Start == formatedDate {
- d, _ := time.ParseInLocation(DateFormat, dateRange.Start, c.Timezone)
- dateRange.Start = d.AddDate(0, 0, 1).Format(DateFormat)
- if dateRange.Start <= dateRange.End {
- newDateRanges = append(newDateRanges, dateRange)
- }
- continue
- }
- if dateRange.Start < formatedDate && formatedDate < dateRange.End {
- d, _ := time.ParseInLocation(DateFormat, formatedDate, c.Timezone)
- range1 := DateRange{dateRange.Start, d.AddDate(0, 0, -1).Format(DateFormat), dateRange.Weekdays}
- range2 := DateRange{d.AddDate(0, 0, 1).Format(DateFormat), dateRange.End, dateRange.Weekdays}
- newDateRanges = append(newDateRanges, range1)
- newDateRanges = append(newDateRanges, range2)
- continue
- }
- if formatedDate == dateRange.End {
- d, _ := time.ParseInLocation(DateFormat, dateRange.End, c.Timezone)
- dateRange.End = d.AddDate(0, 0, -1).Format(DateFormat)
- newDateRanges = append(newDateRanges, dateRange)
- continue
- }
- newDateRanges = append(newDateRanges, dateRange)
- }
- scheduleToEdit.DateRanges = newDateRanges
- c.schedules[record[fields["service_id"]]] = scheduleToEdit
- }
- }
- for _, schedule := range c.schedules {
- lastDateRange := len(schedule.DateRanges) - 1
- scheduleStart, err := time.ParseInLocation(DateFormat, schedule.DateRanges[0].Start, c.Timezone)
- if err != nil {
- c.ValidFromError = append(c.ValidFromError, err)
- }
- if err == nil && (c.ValidFrom.IsZero() || scheduleStart.Before(c.ValidFrom)) {
- c.ValidFrom = scheduleStart
- c.feedInfo.ValidSince = scheduleStart.Format(ValidityFormat)
- }
- scheduleEnd, err := time.ParseInLocation(DateFormat, schedule.DateRanges[lastDateRange].End, c.Timezone)
- if err != nil {
- c.ValidTillError = append(c.ValidTillError, err)
- }
- if err == nil && (c.ValidTill.IsZero() || scheduleEnd.After(c.ValidTill)) {
- c.ValidTill = scheduleEnd
- c.feedInfo.ValidTill = scheduleEnd.Format(ValidityFormat)
- }
- }
- return c, nil
- }
- func checkAnyCalendarConverted(c feedConverter) error {
- if len(c.schedules) == 0 {
- return fmt.Errorf("no calendar converted")
- }
- return nil
- }
- func saveSchedules(c feedConverter) error {
- resultFile := c.TrafficCalendarFile
- schedulesArray := make([]Schedule, len(c.schedules))
- i := 0
- for _, schedule := range c.schedules {
- schedulesArray[i] = schedule
- i++
- }
- sort.Slice(schedulesArray, func(i, j int) bool {
- return schedulesArray[i].DateRanges[0].Start < schedulesArray[j].DateRanges[0].Start
- })
- for _, schedule := range schedulesArray {
- bytes, err := bare.Marshal(&schedule)
- if err != nil {
- return fmt.Errorf("while marshalling: %w", err)
- }
- _, err = resultFile.Write(bytes)
- if err != nil {
- return fmt.Errorf("while writing: %w", err)
- }
- }
- c.schedules = map[string]Schedule{}
- return nil
- }
- func saveFeedInfo(c feedConverter) error {
- path := c.TmpFeedPath
- result, err := os.Create(filepath.Join(path, "feed_info.bare"))
- if err != nil {
- return fmt.Errorf("while creating file: %w", err)
- }
- defer result.Close()
- bytes, err := bare.Marshal(&c.feedInfo)
- if err != nil {
- return fmt.Errorf("while marshalling: %w", err)
- }
- _, err = result.Write(bytes)
- if err != nil {
- return fmt.Errorf("while writing: %w", err)
- }
- log.Printf("timetable is valid: %s to %s\n", c.feedInfo.ValidSince, c.feedInfo.ValidTill)
- c.feedInfo = FeedInfo{}
- return nil
- }
- func closeTrafficCalendarFile(c feedConverter, e error) (feedConverter, error) {
- if c.TrafficCalendarFile != nil {
- c.TrafficCalendarFile.Close()
- }
- return c, e
- }
- func clearDepartures(c feedConverter) feedConverter {
- c.Departures = map[string][]Departure{}
- return c
- }
- func convertDepartures(c feedConverter) (feedConverter, error) { // O(n:stop_times) ; ( -- departures:map[tripID][]departure, tripsThroughStop:map[stopID][]{tripID,order}, tripHeadsigns:map[tripID]stopID >> )
- path := c.TmpFeedPath
- file, err := os.Open(filepath.Join(path, "stop_times.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- defer file.Close()
- departures := map[string][]Departure{}
- r := csv.NewReader(bufio.NewReader(file))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- tripsThroughStop := map[string]map[string]StopOrder{}
- tripHeadsigns := map[string]string{}
- for {
- departure := Departure{}
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- stopID := record[fields["stop_id"]]
- tripID := record[fields["trip_id"]]
- fmt.Sscanf(record[fields["stop_sequence"]], "%d", &departure.StopSequence)
- fmt.Sscanf(record[fields["pickup_type"]], "%d", &departure.Pickup)
- fmt.Sscanf(record[fields["drop_off_type"]], "%d", &departure.Dropoff)
- if _, ok := tripsThroughStop[stopID]; !ok {
- tripsThroughStop[stopID] = map[string]StopOrder{}
- }
- tripsThroughStop[stopID][tripID] = StopOrder{
- Sequence: departure.StopSequence,
- }
- if c.Feed.Flags().Headsign == HeadsignTripLastStop {
- tripHeadsigns[tripID] = stopID
- }
- var hours, minutes uint
- fmt.Sscanf(record[fields["arrival_time"]], "%d:%d", &hours, &minutes)
- departure.Time = hours*60 + minutes
- departures[tripID] = append(departures[tripID], departure)
- }
- c.tripHeadsigns = tripHeadsigns
- c.Departures = departures
- c.TripsThroughStop = tripsThroughStop
- return c, nil
- }
- func clearLineNames(c feedConverter) feedConverter {
- c.LineNames = map[string]string{}
- return c
- }
- func getLineNames(c feedConverter) (feedConverter, error) { // O(n:routes) ; ( -- lineNames:map[routeID]lineName >> )
- path := c.TmpFeedPath
- file, err := os.Open(filepath.Join(path, "routes.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- defer file.Close()
- r := csv.NewReader(bufio.NewReader(file))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- names := map[string]string{}
- for {
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- routeID := record[fields["route_id"]]
- lineName := c.Feed.Flags().LineName
- for _, template := range []string{"route_short_name", "route_long_name"} {
- lineName = strings.Replace(lineName, "{{"+template+"}}", record[fields[template]], -1)
- }
- names[routeID] = lineName
- }
- c.LineNames = names
- return c, nil
- }
- func clearStopNames(c feedConverter) feedConverter {
- c.stopNames = map[string]string{}
- return c
- }
- func getStopNames(c feedConverter) (feedConverter, error) { // O(n:stops) ; ( -- stopNames[stopID]stopName >> )
- if c.Feed.Flags().Headsign != HeadsignTripLastStop {
- return c, nil
- }
- stopNames := map[string]string{}
- path := c.TmpFeedPath
- file, err := os.Open(filepath.Join(path, "stops.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- defer file.Close()
- r := csv.NewReader(bufio.NewReader(file))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- for {
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- stopID := record[fields["stop_id"]]
- stopName := record[fields["stop_name"]]
- stopNames[stopID] = stopName
- }
- c.stopNames = stopNames
- return c, nil
- }
- func clearTripsChangeOptions(c feedConverter) feedConverter {
- c.TripChangeOpts = map[string]ChangeOption{}
- return c
- }
- func clearTripsThroughStops(c feedConverter) feedConverter {
- c.TripsThroughStop = map[string]map[string]StopOrder{}
- return c
- }
- func convertTrips(c feedConverter) (feedConverter, error) { // O(n:trips) ; (departures, lineNames, stopNames -- tripsOffsets:map[tripID]offset, tripsChangeOpts:map[tripID]{lineID,headsign} >> trips)
- path := c.TmpFeedPath
- departures := c.Departures
- lineNames := c.LineNames
- file, err := os.Open(filepath.Join(path, "trips.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- defer file.Close()
- result, err := os.Create(filepath.Join(path, "trips.bare"))
- if err != nil {
- return c, fmt.Errorf("while creating file: %w", err)
- }
- defer result.Close()
- r := csv.NewReader(bufio.NewReader(file))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- var offset uint = 0
- tripsOffsets := map[string]uint{}
- tripChangeOpts := map[string]ChangeOption{}
- for {
- trip := Trip{}
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- trip.Id = record[fields["trip_id"]]
- switch c.Feed.Flags().Headsign {
- case HeadsignTripHeadsing:
- trip.Headsign = record[fields["trip_headsign"]]
- case HeadsignTripLastStop:
- trip.Headsign = c.stopNames[c.tripHeadsigns[trip.Id]]
- }
- trip.Departures = departures[trip.Id]
- trip.ScheduleID = record[fields["service_id"]]
- trip.LineID = record[fields["route_id"]]
- fmt.Sscanf(record[fields["direction_id"]], "%d", &trip.Direction)
- tripChangeOpts[trip.Id] = ChangeOption{
- LineName: lineNames[record[fields["route_id"]]],
- Headsign: translateFieldDefault(trip.Headsign, c.feedInfo.Language, c.defaultLanguage, c.translations),
- TranslatedHeadsigns: translateField(trip.Headsign, c.feedInfo.Language, c.defaultLanguage, c.translations),
- }
- bytes, err := bare.Marshal(&trip)
- if err != nil {
- return c, fmt.Errorf("while marshalling: %w", err)
- }
- b, err := result.Write(bytes)
- if err != nil {
- return c, fmt.Errorf("while writing: %w", err)
- }
- tripsOffsets[trip.Id] = offset
- offset += uint(b)
- }
- c.TripsOffsets = tripsOffsets
- c.TripChangeOpts = tripChangeOpts
- return c, nil
- }
- func clearStops(c feedConverter) feedConverter {
- c.Stops = map[string]string{}
- return c
- }
- func convertStops(c feedConverter) (feedConverter, error) { // O(n:stops) ; (translations, tripsThroughStop, tripChangeOpts, tripOffsets -- stopsOffsetsByCode:CodeIndex, stopsOffsetsByName:map[name][]offsets >> stops)
- path := c.TmpFeedPath
- tripsThroughStop := c.TripsThroughStop
- tripChangeOpts := c.TripChangeOpts
- tripsOffsets := c.TripsOffsets
- file, err := os.Open(filepath.Join(path, "stops.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- defer file.Close()
- result, err := os.Create(filepath.Join(path, "stops.bare"))
- if err != nil {
- return c, fmt.Errorf("while creating file: %w", err)
- }
- defer result.Close()
- r := csv.NewReader(bufio.NewReader(file))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- var offset uint = 0
- stopsOffsetsByName := map[string][]uint{}
- stopsOffsetsByCode := CodeIndex{}
- stops := map[string]string{}
- maxStopTripsLength := 0
- for {
- stop := Stop{}
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- 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]
- stopTripsLength := len(stopTrips)
- if maxStopTripsLength < stopTripsLength {
- maxStopTripsLength = stopTripsLength
- }
- stop.Id = stopID
- 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 {
- stop.Zone = record[field]
- }
- stop.NodeName = record[fields["stop_name"]]
- stops[record[fields["stop_id"]]] = stop.Code
- if field, ok := fields["stop_timezone"]; ok {
- stop.Timezone = record[field]
- }
- if c.feedInfo.Language == "mul" {
- key := record[fields["stop_name"]]
- if _, ok := c.translations[stop.NodeName][c.defaultLanguage]; !ok {
- stop.TranslatedNames = []Translation{{Language: c.defaultLanguage, Value: stop.Name}}
- stop.TranslatedNodeNames = []Translation{{Language: c.defaultLanguage, Value: stop.NodeName}}
- } else {
- stop.TranslatedNames = []Translation{{Language: c.defaultLanguage, Value: strings.ReplaceAll(stop.Name, key, c.translations[key][c.defaultLanguage])}}
- stop.TranslatedNodeNames = []Translation{{Language: c.defaultLanguage, Value: c.translations[key][c.defaultLanguage]}}
- }
- for language, value := range c.translations[key] {
- if language == c.defaultLanguage {
- continue
- }
- stop.TranslatedNames = append(stop.TranslatedNames, Translation{Language: c.defaultLanguage, Value: strings.ReplaceAll(stop.Name, key, value)})
- stop.TranslatedNodeNames = append(stop.TranslatedNodeNames, Translation{Language: c.defaultLanguage, Value: c.translations[key][value]})
- }
- }
- var lat, lon float64
- fmt.Sscanf(record[fields["stop_lat"]], "%f", &lat)
- fmt.Sscanf(record[fields["stop_lon"]], "%f", &lon)
- stop.Position = Position{lat, lon}
- changeOptionMap := map[string]ChangeOption{}
- stop.ChangeOptions = []ChangeOption{}
- stop.Order = map[string]StopOrder{}
- for tripID, stopTrip := range stopTrips {
- changeOption := tripChangeOpts[tripID]
- stopOrder := StopOrder{
- TripOffset: tripsOffsets[tripID],
- Sequence: stopTrip.Sequence,
- }
- stop.Order[tripID] = stopOrder
- changeOptionMap[changeOption.LineName+"->"+changeOption.Headsign] = changeOption
- }
- for _, option := range changeOptionMap {
- stop.ChangeOptions = append(stop.ChangeOptions, option)
- }
- sort.Slice(stop.ChangeOptions, func(i, j int) bool {
- var num1, num2 int
- _, err1 := fmt.Sscanf(stop.ChangeOptions[i].LineName, "%d", &num1)
- _, err2 := fmt.Sscanf(stop.ChangeOptions[j].LineName, "%d", &num2)
- if err1 != nil && err2 != nil {
- return stop.ChangeOptions[i].LineName < stop.ChangeOptions[j].LineName
- } else if err1 != nil {
- return false
- } else if err2 != nil {
- return true
- } else {
- return num1 < num2
- }
- })
- bytes, err := bare.Marshal(&stop)
- if err != nil {
- return c, fmt.Errorf("while marshalling: %w", err)
- }
- b, err := result.Write(bytes)
- if err != nil {
- return c, fmt.Errorf("while writing: %w", err)
- }
- if len(stop.TranslatedNames) == 0 {
- stopsOffsetsByName[stop.Name] = append(stopsOffsetsByName[stop.Name], offset)
- }
- for _, v := range stop.TranslatedNames {
- stopsOffsetsByName[v.Value] = append(stopsOffsetsByName[v.Value], offset)
- }
- stopsOffsetsByCode[stop.Code] = offset
- offset += uint(b)
- }
- if maxStopTripsLength > 8192 {
- log.Printf("maximum length of StopOrder is %d, more than 8192, which may need to be tweaked", maxStopTripsLength)
- }
- c.StopsCodeIndex = stopsOffsetsByCode
- c.StopsNameIndex = stopsOffsetsByName
- c.Stops = stops
- return c, nil
- }
- func clearTripOffsets(c feedConverter) feedConverter {
- c.TripsOffsets = map[string]uint{}
- return c
- }
- func clearLineGraphs(c feedConverter) feedConverter {
- c.LineGraphs = map[string]map[uint]LineGraph{}
- return c
- }
- func clearLineHeadsigns(c feedConverter) feedConverter {
- c.lineHeadsigns = map[string]map[uint][]string{}
- return c
- }
- func getTrips(c feedConverter) (feedConverter, error) {
- file, err := os.Open(filepath.Join(c.TmpFeedPath, "trips.bare"))
- if err != nil {
- return c, fmt.Errorf("while opening trips: %w", err)
- }
- trips := map[string]Trip{}
- for {
- var trip Trip
- err := bare.UnmarshalReader(file, &trip)
- trip.Departures = []Departure{}
- trips[trip.Id] = trip
- if err != nil {
- if err == io.EOF {
- break
- } else {
- return c, fmt.Errorf("while unmarshaling: %w", err)
- }
- }
- }
- c.trips = trips
- return c, nil
- }
- func convertLineGraphs(c feedConverter) (feedConverter, error) { // O(n:stop_times) ; (trips, stops -- lineGrapsh:map[lineID]map[direction]graph, lineHeadsigns:map[lineID]map[direction][]headsigns >> )
- path := c.TmpFeedPath
- trips := c.trips
- stops := c.Stops
- // lineID dire headsi
- lineHeadsignsMap := map[string]map[uint]map[string]struct{}{}
- // lineID dire headsi
- lineHeadsigns := map[string]map[uint][]string{}
- // lineNa dire
- graphs := map[string]map[uint]_LineGraph{}
- file, err := os.Open(filepath.Join(path, "stop_times.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening stop_times: %w", err)
- }
- defer file.Close()
- r := csv.NewReader(bufio.NewReader(file))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- previousTripID := ""
- previous := -1
- previousTrip := Trip{}
- for {
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- tripID := record[fields["trip_id"]]
- stop := stops[record[fields["stop_id"]]]
- trip := trips[tripID]
- if _, ok := lineHeadsignsMap[trip.LineID]; !ok {
- lineHeadsignsMap[trip.LineID] = map[uint]map[string]struct{}{}
- lineHeadsigns[trip.LineID] = map[uint][]string{}
- }
- if _, ok := lineHeadsignsMap[trip.LineID][trip.Direction.Value()]; !ok {
- lineHeadsignsMap[trip.LineID][trip.Direction.Value()] = map[string]struct{}{}
- lineHeadsigns[trip.LineID][trip.Direction.Value()] = []string{}
- }
- lineHeadsignsMap[trip.LineID][trip.Direction.Value()][trip.Headsign] = struct{}{}
- if _, ok := graphs[trip.LineID]; !ok {
- graphs[trip.LineID] = map[uint]_LineGraph{}
- }
- if previousTripID != tripID && previousTripID != "" {
- // last of previous trip
- graph := graphs[previousTrip.LineID][previousTrip.Direction.Value()]
- if graph.NextNodes == nil {
- graph.NextNodes = map[int]map[int]struct{}{}
- }
- if graph.NextNodes[previous] == nil {
- graph.NextNodes[previous] = map[int]struct{}{}
- }
- graphs[previousTrip.LineID][previousTrip.Direction.Value()] = graph
- graphs[previousTrip.LineID][previousTrip.Direction.Value()].NextNodes[previous][-1] = struct{}{}
- }
- graph := graphs[trip.LineID][trip.Direction.Value()]
- if graph.StopCodes == nil {
- graph.StopCodes = map[string]int{}
- }
- if graph.NextNodes == nil {
- graph.NextNodes = map[int]map[int]struct{}{}
- }
- current := -1
- current, ok := graph.StopCodes[stop]
- if !ok {
- current = len(graph.StopCodesArray)
- graph.StopCodesArray = append(graph.StopCodesArray, stop)
- graph.StopCodes[stop] = current
- }
- if previousTripID != tripID {
- // first of current trip
- if graph.NextNodes[-1] == nil {
- graph.NextNodes[-1] = map[int]struct{}{}
- }
- if _, ok := graph.NextNodes[-1][current]; !ok {
- graph.NextNodes[-1][current] = struct{}{}
- }
- } else {
- // second <- first to last <- penultimate of current trip
- if graph.NextNodes[previous] == nil {
- graph.NextNodes[previous] = map[int]struct{}{}
- }
- if _, ok := graph.NextNodes[previous][current]; !ok {
- graph.NextNodes[previous][current] = struct{}{}
- }
- }
- previous = current
- previousTripID = tripID
- previousTrip = trip
- graphs[trip.LineID][trip.Direction.Value()] = graph
- }
- g := graphs[previousTrip.LineID][previousTrip.Direction.Value()]
- if g.NextNodes[previous] == nil {
- g.NextNodes[previous] = map[int]struct{}{}
- }
- if _, ok := g.NextNodes[previous][-1]; !ok {
- g.NextNodes[previous][-1] = struct{}{}
- }
- for lineID, directions := range lineHeadsignsMap {
- for direction, headsigns := range directions {
- for headsign := range headsigns {
- lineHeadsigns[lineID][direction] = append(lineHeadsigns[lineID][direction], headsign)
- }
- }
- }
- c.LineGraphs = map[string]map[uint]LineGraph{}
- for lineID, graphByDirection := range graphs {
- c.LineGraphs[lineID] = map[uint]LineGraph{}
- for direction, graph := range graphByDirection {
- c.LineGraphs[lineID][direction] = LineGraph{
- StopCodes: graph.StopCodesArray,
- NextNodes: map[int][]int{},
- }
- for from, tos := range graph.NextNodes {
- for to := range tos {
- c.LineGraphs[lineID][direction].NextNodes[from] = append(c.LineGraphs[lineID][direction].NextNodes[from], to)
- }
- }
- }
- }
- c.lineHeadsigns = lineHeadsigns
- return c, nil
- }
- func convertLines(c feedConverter) (feedConverter, error) { // O(n:routes) ; (lineGraphs, lineHeadsigns -- lineIndex:map[lineName][]offsets, lineIdIndex:CodeIndex >> lines)
- path := c.TmpFeedPath
- feed := c.Feed
- file, err := os.Open(filepath.Join(path, "routes.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- defer file.Close()
- result, err := os.Create(filepath.Join(path, "lines.bare"))
- if err != nil {
- return c, fmt.Errorf("while creating file: %w", err)
- }
- defer result.Close()
- r := csv.NewReader(bufio.NewReader(file))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- var offset uint = 0
- index := map[string][]uint{}
- idIndex := CodeIndex{}
- for {
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- routeID := record[fields["route_id"]]
- lineName := c.Feed.Flags().LineName
- for _, template := range []string{"route_short_name", "route_long_name"} {
- lineName = strings.Replace(lineName, "{{"+template+"}}", record[fields[template]], -1)
- }
- var kind uint
- fmt.Sscanf(record[fields["route_type"]], "%d", &kind)
- colour := "ffffff"
- if colourIx, ok := fields["route_color"]; ok && record[colourIx] != "" {
- colour = record[colourIx]
- }
- directions := []uint{}
- for direction := range c.lineHeadsigns[routeID] {
- directions = append(directions, direction)
- }
- sort.Slice(directions, func(i, j int) bool {
- return directions[i] < directions[j]
- })
- headsigns := [][]string{}
- translatedHeadsigns := [][][]Translation{}
- for _, direction := range directions {
- dirHeadsigns := c.lineHeadsigns[routeID][direction]
- headsigns = append(headsigns, dirHeadsigns)
- translatedHeadsign := [][]Translation{}
- for _, headsign := range dirHeadsigns {
- translatedHeadsign = append(translatedHeadsign, translateField(headsign, c.feedInfo.Language, c.defaultLanguage, c.translations))
- }
- translatedHeadsigns = append(translatedHeadsigns, translatedHeadsign)
- }
- graphs := []LineGraph{}
- for _, direction := range directions {
- graphs = append(graphs, c.LineGraphs[routeID][direction])
- }
- line := Line{
- Id: routeID,
- Name: lineName,
- Colour: hex2colour(colour),
- Kind: LineType(kind),
- Graphs: graphs,
- Headsigns: headsigns,
- }
- if field, present := fields["agency_id"]; present {
- line.AgencyID = record[field]
- }
- bytes, err := bare.Marshal(&line)
- if err != nil {
- return c, fmt.Errorf("while marshalling: %w", err)
- }
- b, err := result.Write(bytes)
- if err != nil {
- return c, fmt.Errorf("while writing: %w", err)
- }
- cleanQuery, err := CleanQuery(line.Name, feed)
- if err != nil {
- return c, fmt.Errorf("while cleaning line name: %w", err)
- }
- index[cleanQuery] = append(index[cleanQuery], offset)
- idIndex[routeID] = offset
- offset += uint(b)
- }
- c.LineIdIndex = idIndex
- c.LineIndex = index
- return c, nil
- }
- func convertFeedInfo(c feedConverter) (feedConverter, error) { // O(1:feed_info) ; ( -- feed_info >> )
- path := c.TmpFeedPath
- feedInfo := FeedInfo{}
- file, err := os.Open(filepath.Join(path, "feed_info.txt"))
- if err != nil {
- if errors.Is(err, fs.ErrNotExist) {
- log.Println("[WARN] no feed_info.txt")
- file = nil
- } else {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- }
- if file != nil {
- defer file.Close()
- r := csv.NewReader(bufio.NewReader(file))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- record, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- feedInfo.Website = record[fields["feed_publisher_url"]]
- feedInfo.Language = record[fields["feed_lang"]]
- if defaultLanguageIndex, ok := fields["default_lang"]; ok {
- c.defaultLanguage = record[defaultLanguageIndex]
- }
- if ix, ok := fields["feed_start_date"]; ok {
- c.ValidFrom, err = time.ParseInLocation("20060102", record[ix], c.Timezone)
- if err != nil {
- c.ValidFromError = append(c.ValidFromError, err)
- }
- feedInfo.ValidSince = record[ix]
- }
- if ix, ok := fields["feed_end_date"]; ok {
- c.ValidTill, err = time.ParseInLocation("20060102", record[ix], c.Timezone)
- if err != nil {
- c.ValidTillError = append(c.ValidTillError, err)
- }
- feedInfo.ValidTill = record[ix]
- }
- }
- feedInfo.Timezone = c.Timezone.String()
- feedInfo.RealtimeFeeds = c.Feed.RealtimeFeeds()
- feedInfo.QrHost, feedInfo.QrLocation, feedInfo.QrSelector = c.Feed.QRInfo()
- feedInfo.Attributions, feedInfo.Descriptions, err = getAttrDesc(c.Feed.String(), c.feedTranslations)
- feedInfo.Name = c.Feed.Name()
- c.feedInfo = feedInfo
- return c, err
- }
- func convertLuaScripts(c feedConverter) error { // O(1) ; ( -- >> updates.lua, alerts.lua, vehicles.lua )
- filenames := []string{"updates", "vehicles", "alerts"}
- for _, filename := range filenames {
- t, err := template.ParseFS(luaScripts, "realtime_lua/"+c.Feed.String()+"_"+filename+".lua")
- if err != nil {
- if strings.Contains(err.Error(), "pattern matches no files") {
- log.Printf("%s.lua for this feed does not exist, ignoring\n", filename)
- continue
- }
- return fmt.Errorf("while parsing template %s: %w", filename, err)
- }
- path := c.TmpFeedPath
- writeFile, err := os.Create(filepath.Join(path, filename+".lua"))
- if err != nil {
- return fmt.Errorf("while creating %s: %w", filename, err)
- }
- defer writeFile.Close()
- err = t.Execute(writeFile, c.config.Auth[c.Feed.String()])
- if err != nil {
- return fmt.Errorf("while executing template %s: %w", filename, err)
- }
- }
- return nil
- }
- func getAttrDesc(feedID string, feedTranslations embed.FS) (map[string]string, map[string]string, error) {
- attributions := map[string]string{}
- descriptions := map[string]string{}
- dir, err := feedTranslations.ReadDir("translations")
- if err != nil {
- return attributions, descriptions, err
- }
- for _, f := range dir {
- translation := map[string]string{}
- name := f.Name()
- lang := strings.Split(name, ".")[1]
- fileContent, err := feedTranslations.ReadFile("translations/" + name)
- if err != nil {
- log.Printf("error reading translation %s\n", name)
- continue
- }
- yaml.Unmarshal(fileContent, &translation)
- attributions[lang] = translation[feedID+"_attribution"]
- descriptions[lang] = translation[feedID+"_description"]
- if lang == "en" {
- attributions["und"] = translation[feedID+"_attribution"]
- descriptions["und"] = translation[feedID+"_description"]
- }
- }
- return attributions, descriptions, nil
- }
- func readTranslations(c feedConverter) (feedConverter, error) { // O(n:translations) ; ( -- translations >>)
- path := c.TmpFeedPath
- file, err := os.Open(filepath.Join(path, "translations.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- defer file.Close()
- translations := map[string]map[string]string{}
- r := csv.NewReader(bufio.NewReader(file))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- for {
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- key := record[fields["field_value"]]
- language := record[fields["language"]]
- translation := record[fields["translation"]]
- if _, ok := translations[key]; !ok {
- translations[key] = map[string]string{}
- }
- translations[key][language] = translation
- }
- c.translations = translations
- return c, nil
- }
- func recoverTranslations(c feedConverter, e error) (feedConverter, error) {
- var pathError *os.PathError
- if errors.As(e, &pathError) && errors.Is(pathError, fs.ErrNotExist) {
- return c, nil
- }
- return c, e
- }
- func convertAgencies(c feedConverter) (feedConverter, error) { // O(n:agency) ; ( -- >> agencies)
- path := c.TmpFeedPath
- file, err := os.Open(filepath.Join(path, "agency.txt"))
- if err != nil {
- return c, fmt.Errorf("while opening file: %w", err)
- }
- defer file.Close()
- result, err := os.Create(filepath.Join(path, "agencies.bare"))
- if err != nil {
- return c, fmt.Errorf("while creating file: %w", err)
- }
- defer file.Close()
- r := csv.NewReader(bufio.NewReader(file))
- header, err := r.Read()
- if err != nil {
- return c, fmt.Errorf("while reading header: %w", err)
- }
- fields := map[string]int{}
- for i, headerField := range header {
- fields[headerField] = i
- }
- for {
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return c, fmt.Errorf("while reading a record: %w", err)
- }
- agency := Agency{
- Id: record[fields["agency_id"]],
- Name: record[fields["agency_name"]],
- TranslatedNames: translateField(record[fields["agency_name"]], c.feedInfo.Language, c.defaultLanguage, c.translations),
- Website: record[fields["agency_url"]],
- TranslatedWebsites: translateField(record[fields["agency_url"]], c.feedInfo.Language, c.defaultLanguage, c.translations),
- Timezone: record[fields["agency_timezone"]],
- }
- c.Timezone, _ = time.LoadLocation(agency.Timezone)
- if field, present := fields["agency_lang"]; present {
- agency.Language = record[field]
- }
- if field, present := fields["agency_phone"]; present {
- agency.PhoneNumber = record[field]
- agency.TranslatedPhoneNumbers = translateField(record[field], c.feedInfo.Language, c.defaultLanguage, c.translations)
- }
- if field, present := fields["agency_fare_url"]; present {
- agency.FareWebsite = record[field]
- agency.TranslatedFareWebsites = translateField(record[field], c.feedInfo.Language, c.defaultLanguage, c.translations)
- }
- if field, present := fields["agency_email"]; present {
- agency.Email = record[field]
- agency.TranslatedEmails = translateField(record[field], c.feedInfo.Language, c.defaultLanguage, c.translations)
- }
- bytes, err := bare.Marshal(&agency)
- if err != nil {
- return c, fmt.Errorf("while marshalling: %w", err)
- }
- _, err = result.Write(bytes)
- if err != nil {
- return c, fmt.Errorf("while writing: %w", err)
- }
- }
- return c, nil
- }
- func writeNameIndex(c feedConverter, index map[string][]uint, filename string, raw bool) error {
- path := c.TmpFeedPath
- feed := c.Feed
- result, err := os.Create(filepath.Join(path, filename))
- if err != nil {
- return fmt.Errorf("while creating file: %w", err)
- }
- defer result.Close()
- for name, offsets := range index {
- cleanQuery := name
- if !raw {
- cleanQuery, err = CleanQuery(name, feed)
- if err != nil {
- return fmt.Errorf("while cleaning name %s: %w", name, err)
- }
- }
- stopOffset := NameOffset{
- Name: cleanQuery,
- Offsets: offsets,
- }
- bytes, err := bare.Marshal(&stopOffset)
- if err != nil {
- return fmt.Errorf("while marshalling: %w", err)
- }
- _, err = result.Write(bytes)
- if err != nil {
- return fmt.Errorf("while writing: %w", err)
- }
- }
- return nil
- }
- func writeStopNameIndex(c feedConverter) error {
- err := writeNameIndex(c, c.StopsNameIndex, "ix_stop_names.bare", false)
- c.StopsNameIndex = map[string][]uint{}
- return err
- }
- func writeLineIndex(c feedConverter) error {
- err := writeNameIndex(c, c.LineIndex, "ix_lines.bare", false)
- c.LineIndex = map[string][]uint{}
- return err
- }
- func writeLineIdIndex(c feedConverter) error {
- err := writeCodeIndex(c, c.LineIdIndex, "ix_line_codes.bare")
- c.LineIndex = map[string][]uint{}
- return err
- }
- func writeTripIndex(c feedConverter) error {
- tripIndex := map[string][]uint{}
- for trip, offset := range c.TripsOffsets {
- tripIndex[trip] = []uint{offset}
- }
- err := writeNameIndex(c, tripIndex, "ix_trips.bare", true)
- c.TripsOffsets = map[string]uint{}
- return err
- }
- func writeStopCodeIndex(c feedConverter) error {
- err := writeCodeIndex(c, c.StopsCodeIndex, "ix_stop_codes.bare")
- c.StopsCodeIndex = CodeIndex{}
- return err
- }
- func writeCodeIndex(c feedConverter, i CodeIndex, filename string) error {
- path := c.TmpFeedPath
- result, err := os.Create(filepath.Join(path, filename))
- if err != nil {
- return fmt.Errorf("while creating file: %w", err)
- }
- defer result.Close()
- bytes, err := bare.Marshal(&i)
- if err != nil {
- return fmt.Errorf("while marshalling: %w", err)
- }
- _, err = result.Write(bytes)
- if err != nil {
- return fmt.Errorf("while writing: %w", err)
- }
- return nil
- }
- func deleteTxtFiles(c feedConverter) error {
- return nil
- return file.DeleteTxtFiles(c.TmpFeedPath, c.GtfsFilename)
- }
- func compressTraffic(c feedConverter) error {
- return file.CompressBare(c.TmpFeedPath, c.GtfsFilename)
- }
- func deleteBareFiles(c feedConverter) error {
- return file.DeleteBareFiles(c.TmpFeedPath)
- }
- func moveTraffic(c feedConverter) error {
- if err := append(c.ValidFromError, c.ValidTillError...); len(err) != 0 {
- return errors.Join(err...)
- }
- return file.MoveTraffic(c.GtfsFilename, c.ValidFrom.Format("20060102")+"_"+c.ValidTill.Format("20060102")+".txz", c.TmpFeedPath, c.HomeFeedPath)
- }
- func convert(input ...interface{}) (interface{}, error) {
- allErrors := []error{}
- args := input[0].(result)
- for _, gtfsFile := range args.gtfsFilenames {
- log.Printf("converting feed %s/%s\n", args.feed.Name(), gtfsFile)
- r := gott2.R[feedConverter]{
- S: feedConverter{
- TmpFeedPath: args.tmpFeedPath,
- GtfsFilename: gtfsFile,
- Feed: args.feed,
- HomeFeedPath: args.homeFeedPath,
- feedTranslations: args.feedTranslations,
- config: args.config,
- },
- LogLevel: gott2.Debug,
- }
- r = r.
- Tee(unzipGtfs).
- Tee(prepareFeedGtfs).
- Tee(convertVehicles).
- Bind(convertAgencies).
- Bind(convertFeedInfo).
- Tee(convertLuaScripts).
- Bind(readTranslations).
- Recover(recoverTranslations).
- Bind(createTrafficCalendarFile).
- Bind(convertCalendar).
- Recover(recoverCalendar).
- Bind(convertCalendarDates).
- Recover(recoverCalendar).
- Tee(checkAnyCalendarConverted).
- Tee(saveSchedules).
- Tee(saveFeedInfo).
- Recover(closeTrafficCalendarFile).
- Bind(convertDepartures).
- Bind(getLineNames).
- Bind(getStopNames).
- Bind(convertTrips).
- Map(clearDepartures).
- Map(clearStopNames).
- Map(clearLineNames).
- Bind(convertStops).
- Tee(writeTripIndex).
- Map(clearTripOffsets).
- Tee(writeStopNameIndex).
- Tee(writeStopCodeIndex).
- Map(clearTripsChangeOptions).
- Map(clearTripsThroughStops).
- Map(clearLineNames).
- Bind(getTrips).
- Bind(convertLineGraphs).
- Map(clearStops).
- Bind(convertLines).
- Tee(writeLineIndex).
- Tee(writeLineIdIndex).
- Map(clearLineGraphs).
- Map(clearLineHeadsigns).
- Tee(deleteTxtFiles).
- Tee(compressTraffic).
- Tee(deleteBareFiles).
- Tee(moveTraffic)
- if r.E != nil {
- log.Printf("Error converting %s: %v\n", args.feed.Name(), r.E)
- allErrors = append(allErrors, r.E)
- }
- if err := append(r.S.ValidFromError, r.S.ValidTillError...); len(err) != 0 {
- allErrors = append(allErrors, err...)
- log.Printf("Error converting %s: %v\n", args.feed.Name(), errors.Join(err...))
- }
- }
- if len(allErrors) > 0 {
- return gott.Tuple{args}, errors.Join(allErrors...)
- }
- return gott.Tuple{args}, nil
- }
- func signal(input ...interface{}) (interface{}, error) {
- args := input[0].(result)
- if len(args.gtfsFilenames) > 0 && args.pid > 0 {
- process, err := os.FindProcess(args.pid)
- if err != nil {
- return gott.Tuple{args}, err
- }
- err = process.Signal(syscall.SIGUSR1)
- if err != nil {
- return gott.Tuple{args}, err
- }
- }
- return gott.Tuple{args}, nil
- }
- func openLastUpdated(input ...interface{}) (interface{}, error) {
- args := input[0].(result)
- updatesFilename := filepath.Join(args.config.FeedsPath, "updated.bare")
- var err error
- args.updatesFile, err = os.OpenFile(updatesFilename, os.O_RDWR|os.O_CREATE, 0644)
- return gott.Tuple{args}, err
- }
- func isEmpty(input ...interface{}) error {
- args := input[0].(result)
- stat, err := os.Stat(args.updatesFile.Name())
- if err != nil {
- return err
- }
- if stat.Size() == 0 {
- return ErrEmpty{}
- }
- return nil
- }
- func unmarshalLastUpdated(input ...interface{}) (interface{}, error) {
- args := input[0].(result)
- var lastUpdated map[string]string
- err := bare.UnmarshalReader(args.updatesFile, &lastUpdated)
- args.updates = lastUpdated
- return gott.Tuple{args}, err
- }
- func recoverEmpty(input ...interface{}) (interface{}, error) {
- args := input[0].(result)
- err := input[1].(error)
- var emptyError ErrEmpty
- if errors.As(err, &emptyError) {
- return gott.Tuple{args}, nil
- } else {
- return gott.Tuple{args}, err
- }
- }
- func lastUpdated(input ...interface{}) interface{} {
- args := input[0].(result)
- args.updates[args.feed.String()] = time.Now().Format(time.RFC3339)
- return gott.Tuple{args}
- }
- func seekLastUpdated(input ...interface{}) (interface{}, error) {
- args := input[0].(result)
- _, err := args.updatesFile.Seek(0, 0)
- return gott.Tuple{args}, err
- }
- func marshalLastUpdated(input ...interface{}) error {
- args := input[0].(result)
- err := bare.MarshalWriter(bare.NewWriter(args.updatesFile), &args.updates)
- args.updatesFile.Close()
- return err
- }
- func Prepare(cfg config.Config, t Traffic, bimbaPid int, feedTranslations embed.FS) error { // todo(BAF18) remove pid
- etags, err := readEtags(cfg)
- if err != nil {
- return fmt.Errorf("while reading etags: %w", err)
- }
- newEtags := map[string]string{}
- for _, feed := range t.Feeds {
- log.Printf("converting %s\n", feed.Name())
- r := gott.Tuple{result{
- config: cfg,
- pid: bimbaPid,
- tmpPath: os.TempDir(),
- feed: feed,
- feedName: feed.String(),
- location: feed.getTimezone(),
- updates: map[string]string{},
- etags: etags,
- newEtags: newEtags,
- feedTranslations: feedTranslations,
- }}
- s, err := gott.NewResult(r).
- SetLevelLog(gott.Debug).
- Bind(createTmpPath).
- Bind(createFeedHome).
- Bind(listDownloadedVersions).
- Bind(getAllVersions).
- Map(findValidVersions).
- Bind(getGtfsFiles).
- Bind(convert).
- Bind(signal).
- Bind(openLastUpdated).
- Tee(isEmpty).
- Bind(unmarshalLastUpdated).
- Recover(recoverEmpty).
- Map(lastUpdated).
- Bind(seekLastUpdated).
- Tee(marshalLastUpdated).
- Finish()
- if err != nil {
- log.Printf("Error converting %s: %v\n", feed.String(), err)
- } else {
- etags = s.(gott.Tuple)[0].(result).etags
- newEtags = s.(gott.Tuple)[0].(result).newEtags
- }
- }
- return saveEtags(cfg, newEtags)
- }
|