convert_stops.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. // SPDX-FileCopyrightText: Adam Evyčędo
  2. //
  3. // SPDX-License-Identifier: AGPL-3.0-or-later
  4. package traffic
  5. import (
  6. "database/sql"
  7. "encoding/csv"
  8. "fmt"
  9. "io"
  10. "log"
  11. "os"
  12. "path/filepath"
  13. "sort"
  14. "strings"
  15. "git.sr.ht/~sircmpwn/go-bare"
  16. )
  17. func dropInputStopsIndex(c feedConverter) feedConverter {
  18. c.stopsInputIndex = map[string]int64{}
  19. return c
  20. }
  21. func readTripsThroughStopsIndex(c feedConverter) (feedConverter, error) {
  22. index := map[string]int64{}
  23. path := c.TmpFeedPath
  24. forEachRow(filepath.Join(path, "tripsthroughstop.csv"), func(offset int64, fields map[string]int, record []string) error {
  25. stopID := record[fields["stop_id"]]
  26. if _, ok := index[stopID]; !ok {
  27. index[stopID] = offset
  28. }
  29. return nil
  30. })
  31. c.stopsInputIndex = index
  32. return c, nil
  33. }
  34. func dropInputTripsIndex(c feedConverter) feedConverter {
  35. c.tripsInputIndex = map[string]int64{}
  36. return c
  37. }
  38. func dropInputRoutesIndex(c feedConverter) feedConverter {
  39. c.routesInputIndex = map[string]int64{}
  40. return c
  41. }
  42. func convertStops(c feedConverter) (feedConverter, error) { // O(n:stops) ; (translations, file:tripsThroughStop, file:tripChangeOpts, tripOffsets -- stopsOffsetsByCode:CodeIndex, stopsOffsetsByName:map[name][]offsets >> stops)
  43. path := c.TmpFeedPath
  44. var outputOffset uint = 0
  45. stopsOffsetsByName := map[string][]uint{}
  46. stopsOffsetsByCode := CodeIndex{}
  47. stops := map[string]string{}
  48. maxStopTripsLength := 0
  49. result, err := os.Create(filepath.Join(path, "stops.bare"))
  50. if err != nil {
  51. return c, fmt.Errorf("while creating file: %w", err)
  52. }
  53. defer result.Close()
  54. tripsThroughStopFile, err := os.Open(filepath.Join(path, "tripsthroughstop.csv"))
  55. if err != nil {
  56. return c, fmt.Errorf("while opening tripsThroughStop file: %w", err)
  57. }
  58. defer tripsThroughStopFile.Close()
  59. tripsThroughStop := csv.NewReader(tripsThroughStopFile)
  60. tripsThroughStopHeader, err := tripsThroughStop.Read()
  61. if err != nil {
  62. return c, fmt.Errorf("while reading tripsThroughStop header: %w", err)
  63. }
  64. tripsThroughStopFields := map[string]int{}
  65. for i, headerField := range tripsThroughStopHeader {
  66. tripsThroughStopFields[headerField] = i
  67. }
  68. cacheDir, err := os.UserCacheDir()
  69. if err != nil {
  70. return c, fmt.Errorf("while getting cache dir: %w", err)
  71. }
  72. db, err := sql.Open("sqlite3", filepath.Join(cacheDir, "turntable.db"))
  73. if err != nil {
  74. return c, fmt.Errorf("while opening db: %w", err)
  75. }
  76. err = forEachRow(filepath.Join(path, "stops.txt"), func(offset int64, fields map[string]int, record []string) error {
  77. if f, ok := fields["location_type"]; ok && record[f] != "" && record[f] != "0" {
  78. // NOTE for now ignore everything that’s not a stop/platform
  79. // TODO use Portals (location_type == 2) to show on map if platform has a parent (location_type == 1) that has a Portal
  80. // TODO use location_type in {3,4} for routing inside stations (with pathways, transfers, and levels)
  81. return nil
  82. }
  83. stop := Stop{}
  84. stopID := record[fields["stop_id"]]
  85. stopTrips := map[string]StopOrder{}
  86. if position, ok := c.stopsInputIndex[stopID]; ok {
  87. tripsThroughStopFile.Seek(position, 0)
  88. for {
  89. tripsThroughStopRecord, err := readCsvLine(tripsThroughStopFile, -1, len(tripsThroughStopHeader))
  90. if err != nil {
  91. if err == io.EOF {
  92. break
  93. } else {
  94. return fmt.Errorf("while reading tripsThroughStop record: %w", err)
  95. }
  96. }
  97. recordStopID := tripsThroughStopRecord[tripsThroughStopFields["stop_id"]]
  98. if stopID != recordStopID {
  99. break
  100. }
  101. tripID := tripsThroughStopRecord[tripsThroughStopFields["trip_id"]]
  102. var sequence int
  103. fmt.Sscanf(tripsThroughStopRecord[tripsThroughStopFields["sequence"]], "%d", &sequence)
  104. stopTrips[tripID] = StopOrder{
  105. Sequence: sequence,
  106. TripOffset: c.tripsOffsets[tripID],
  107. }
  108. }
  109. stopTripsLength := len(stopTrips)
  110. if maxStopTripsLength < stopTripsLength {
  111. maxStopTripsLength = stopTripsLength
  112. }
  113. }
  114. stop.Id = stopID
  115. templates := []string{"stop_code", "stop_id", "stop_name", "platform_code"}
  116. stop.Code = c.Feed.Flags().StopIdFormat
  117. for _, template := range templates {
  118. stop.Code = strings.Replace(stop.Code, "{{"+template+"}}", record[fields[template]], -1)
  119. }
  120. stop.Name = c.Feed.Flags().StopName
  121. for _, template := range templates {
  122. // TODO if '{{template}}' is empty
  123. stop.Name = strings.Replace(stop.Name, "{{"+template+"}}", record[fields[template]], -1)
  124. }
  125. if field, ok := fields["zone_id"]; ok {
  126. stop.Zone = record[field]
  127. }
  128. stop.NodeName = record[fields["stop_name"]]
  129. stops[record[fields["stop_id"]]] = stop.Code
  130. if field, ok := fields["stop_timezone"]; ok {
  131. stop.Timezone = record[field]
  132. }
  133. if c.feedInfo.Language == "mul" {
  134. key := record[fields["stop_name"]]
  135. if _, ok := c.translations[stop.NodeName][c.defaultLanguage]; !ok {
  136. stop.TranslatedNames = []Translation{{Language: c.defaultLanguage, Value: stop.Name}}
  137. stop.TranslatedNodeNames = []Translation{{Language: c.defaultLanguage, Value: stop.NodeName}}
  138. } else {
  139. stop.TranslatedNames = []Translation{{Language: c.defaultLanguage, Value: strings.ReplaceAll(stop.Name, key, c.translations[key][c.defaultLanguage])}}
  140. stop.TranslatedNodeNames = []Translation{{Language: c.defaultLanguage, Value: c.translations[key][c.defaultLanguage]}}
  141. }
  142. for language, value := range c.translations[key] {
  143. if language == c.defaultLanguage {
  144. continue
  145. }
  146. stop.TranslatedNames = append(stop.TranslatedNames, Translation{Language: c.defaultLanguage, Value: strings.ReplaceAll(stop.Name, key, value)})
  147. stop.TranslatedNodeNames = append(stop.TranslatedNodeNames, Translation{Language: c.defaultLanguage, Value: c.translations[key][value]})
  148. }
  149. }
  150. var lat, lon float64
  151. fmt.Sscanf(record[fields["stop_lat"]], "%f", &lat)
  152. fmt.Sscanf(record[fields["stop_lon"]], "%f", &lon)
  153. stop.Position = Position{lat, lon}
  154. stop.ChangeOptions = []ChangeOption{}
  155. stop.Order = stopTrips
  156. rows, err := db.Query("select line_name, headsign from change_options where stop_id = ?", stopID)
  157. if err != nil {
  158. return fmt.Errorf("while querrying change options: %w", err)
  159. }
  160. for rows.Next() {
  161. var (
  162. lineName string
  163. headsign string
  164. )
  165. rows.Scan(&lineName, &headsign)
  166. stop.ChangeOptions = append(stop.ChangeOptions, ChangeOption{
  167. LineName: lineName,
  168. Headsign: headsign,
  169. TranslatedHeadsigns: []Translation{},
  170. // TODO add translations
  171. })
  172. }
  173. sort.Slice(stop.ChangeOptions, func(i, j int) bool {
  174. var num1, num2 int
  175. _, err1 := fmt.Sscanf(stop.ChangeOptions[i].LineName, "%d", &num1)
  176. _, err2 := fmt.Sscanf(stop.ChangeOptions[j].LineName, "%d", &num2)
  177. if err1 != nil && err2 != nil {
  178. return stop.ChangeOptions[i].LineName < stop.ChangeOptions[j].LineName
  179. } else if err1 != nil {
  180. return false
  181. } else if err2 != nil {
  182. return true
  183. } else {
  184. return num1 < num2
  185. }
  186. })
  187. bytes, err := bare.Marshal(&stop)
  188. if err != nil {
  189. return fmt.Errorf("while marshalling: %w", err)
  190. }
  191. b, err := result.Write(bytes)
  192. if err != nil {
  193. return fmt.Errorf("while writing: %w", err)
  194. }
  195. if len(stop.TranslatedNames) == 0 {
  196. stopsOffsetsByName[stop.Name] = append(stopsOffsetsByName[stop.Name], outputOffset)
  197. }
  198. for _, v := range stop.TranslatedNames {
  199. stopsOffsetsByName[v.Value] = append(stopsOffsetsByName[v.Value], outputOffset)
  200. }
  201. stopsOffsetsByCode[stop.Code] = outputOffset
  202. outputOffset += uint(b)
  203. return nil
  204. })
  205. if maxStopTripsLength > 12288 {
  206. log.Printf("maximum length of StopOrder is %d, more than 12288, which may need to be tweaked", maxStopTripsLength)
  207. }
  208. c.StopsCodeIndex = stopsOffsetsByCode
  209. c.StopsNameIndex = stopsOffsetsByName
  210. c.Stops = stops
  211. os.Remove(filepath.Join(cacheDir, "turntable.db"))
  212. return c, err
  213. }