• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hasadna / open-bus-map-search / 9437029040

09 Jun 2024 01:42PM UTC coverage: 73.148% (-0.06%) from 73.206%
9437029040

Pull #790

github

web-flow
Merge b1c58d7d7 into 4e58b09b6
Pull Request #790: feat: make us SEO compatible (#745)

323 of 557 branches covered (57.99%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

941 of 1171 relevant lines covered (80.36%)

137535.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.63
/src/api/useVehicleLocations.ts
1
/**
2
 * this is a custom hook that fetches the vehicle locations from the API.
3
 * it recieves an interval of two dates, and loads locations of all vehicles in that interval.
4
 * if some of the interval has already been loaded,
5
 */
6

7
import _ from 'lodash'
8
import moment, { Moment } from 'moment'
9
import { useEffect, useState } from 'react'
10
import { VehicleLocation } from 'src/model/vehicleLocation'
11

12
const config = {
18✔
13
  apiUrl: 'https://open-bus-stride-api.hasadna.org.il/siri_vehicle_locations/list?get_count=false',
14
  limit: 500, // the maximum number of vehicles to load in one request
15
  fromField: 'recorded_at_time_from',
16
  toField: 'recorded_at_time_to',
17
  lineRefField: 'siri_routes__line_ref',
18
  operatorRefField: 'siri_routes__operator_ref',
19
} as const
20

21
type Dateable = Date | number | string | Moment
22

23
function formatTime(time: Dateable) {
24
  if (moment.isMoment(time)) {
700!
25
    return time.toISOString()
700✔
26
  } else {
27
    const date = new Date(time).toISOString()
×
28
    return date
×
29
  }
30
}
31

32
const loadedLocations = new Map<
18✔
33
  string, // time interval
34
  LocationObservable
35
>()
36

37
/*
38
 * this class is an observable that loads the data from the API.
39
 * it notifies the observers every time new data is loaded.
40
 * it also caches the data, so if the same interval is requested again, it will not load it again.
41
 */
42
class LocationObservable {
43
  constructor({
44
    from,
45
    to,
46
    lineRef,
47
    operatorRef,
48
  }: {
49
    from: Dateable
50
    to: Dateable
51
    lineRef?: number
52
    operatorRef?: number
53
  }) {
54
    this.#loadData({ from, to, lineRef, operatorRef })
164✔
55
  }
56

57
  data: VehicleLocation[] = []
164✔
58
  loading = true
164✔
59

60
  async #loadData({
164✔
61
    from,
62
    to,
63
    lineRef,
64
    operatorRef,
65
  }: {
66
    from: Dateable
67
    to: Dateable
68
    lineRef?: number
69
    operatorRef?: number
70
  }) {
71
    let offset = 0
164✔
72
    for (let i = 1; this.loading; i++) {
164✔
73
      let url = config.apiUrl
174✔
74
      url += `&${config.fromField}=${formatTime(from)}&${config.toField}=${formatTime(to)}&limit=${
174✔
75
        config.limit * i
76
      }&offset=${offset}`
77
      if (operatorRef) url += `&${config.operatorRefField}=${operatorRef}`
174!
78
      if (lineRef) url += `&${config.lineRefField}=${lineRef}`
174✔
79

80
      const response = await fetchWithQueue(url)
174✔
81
      // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
82
      const data: VehicleLocation[] = await response!.json()
12✔
83
      if (data.length === 0) {
12✔
84
        this.loading = false
2✔
85
        this.#notifyObservers({
2✔
86
          finished: true,
87
        })
88
      } else {
89
        this.data = [...this.data, ...data]
10✔
90
        this.#notifyObservers(data)
10✔
91
        offset += config.limit * i
10✔
92
      }
93
    }
94
    this.#observers = []
2✔
95
  }
96

97
  #notifyObservers(data: VehicleLocation[] | { finished: true }) {
182✔
98
    const observers = this.#observers
12✔
99
    console.log('notifying observers', observers.length)
12✔
100
    observers.forEach((observer) => observer(data))
12✔
101
  }
102

103
  #observers: ((locations: VehicleLocation[] | { finished: true }) => void)[] = []
18✔
104

105
  observe(observer: (locations: VehicleLocation[] | { finished: true }) => void) {
106
    if (this.loading) {
176!
107
      this.#observers.push(observer)
176✔
108
    }
109
    observer(this.data)
176✔
110
    return () => {
176✔
111
      this.#observers = this.#observers.filter((o) => o !== observer)
114✔
112
    }
113
  }
114
}
115

116
const pool = new Array(10).fill(0).map(() => Promise.resolve<void | Response>(void 0))
180✔
117
async function fetchWithQueue(url: string, retries = 10) {
174✔
118
  let queue = pool.shift()!
174✔
119
  queue = queue
174✔
120
    .then(() => fetch(url))
96✔
121
    .catch(async () => {
122
      await new Promise((resolve) => setTimeout(resolve, 10000 * Math.random() + 100))
84✔
UNCOV
123
      return fetchWithQueue(url, retries - 1)
×
124
    })
125
  pool.push(queue)
174✔
126
  return queue
174✔
127
}
128

129
// this function checks the cache for the data, and if it's not there, it loads it
130
function getLocations({
131
  from,
132
  to,
133
  lineRef,
134
  onUpdate,
135
  operatorRef,
136
}: {
137
  from: Dateable
138
  to: Dateable
139
  lineRef?: number
140
  operatorRef?: number
141
  onUpdate: (locations: VehicleLocation[] | { finished: true }) => void // the observer will be called every time with all the locations that were loaded
142
}) {
143
  const key = `${formatTime(from)}-${formatTime(to)}-${operatorRef}-${lineRef}`
176✔
144
  if (!loadedLocations.has(key)) {
176✔
145
    loadedLocations.set(key, new LocationObservable({ from, to, lineRef, operatorRef }))
164✔
146
  }
147
  const observable = loadedLocations.get(key)!
176✔
148
  return observable.observe(onUpdate)
176✔
149
}
150

151
function getMinutesInRange(from: Dateable, to: Dateable, gap = 1) {
×
152
  const start = moment(from).startOf('minute')
24✔
153
  const end = moment(to).startOf('minute')
24✔
154

155
  // array of minutes to load
156
  const minutes = Array.from({ length: end.diff(start, 'minutes') / gap }, (_, i) => ({
176✔
157
    from: start.clone().add(i * gap, 'minutes'),
158
    to: start.clone().add((i + 1) * gap, 'minutes'),
159
  }))
160
  return minutes
24✔
161
}
162

163
export default function useVehicleLocations({
164
  from,
165
  to,
166
  lineRef,
167
  operatorRef,
168
  splitMinutes: split = 1,
72✔
169
  pause = false,
72✔
170
}: {
171
  from: Dateable
172
  to: Dateable
173
  lineRef?: number
174
  operatorRef?: number
175
  splitMinutes?: false | number
176
  pause?: boolean
177
}) {
178
  const [locations, setLocations] = useState<VehicleLocation[]>([])
360✔
179
  const [isLoading, setIsLoading] = useState<boolean[]>([])
360✔
180
  useEffect(() => {
360✔
181
    if (pause) return
52!
182
    const range = split ? getMinutesInRange(from, to, split) : [{ from, to }]
24!
183
    setIsLoading(range.map(() => true))
176✔
184
    const unmounts = range.map(({ from, to }, i) =>
24✔
185
      getLocations({
176✔
186
        from,
187
        to,
188
        lineRef,
189
        operatorRef,
190
        onUpdate: (data) => {
191
          if ('finished' in data) {
188!
192
            setIsLoading((prev) => {
2✔
193
              const newIsLoading = [...prev]
4✔
194
              newIsLoading[i] = false
4✔
195
              return newIsLoading
4✔
196
            })
197
          } else {
198
            setLocations((prev) =>
186✔
199
              _.uniqBy(
366✔
200
                [...prev, ...data].sort((a, b) => a.id - b.id),
46,377✔
201
                (loc) => loc.id,
46,391✔
202
              ),
203
            )
204
          }
205
        },
206
      }),
207
    )
208
    return () => {
24✔
209
      setLocations([])
16✔
210
      unmounts.forEach((unmount) => unmount())
114✔
211
      setIsLoading([])
16✔
212
    }
213
  }, [from, to, lineRef, split])
214
  return {
360✔
215
    locations,
216
    isLoading: isLoading.some((loading) => loading),
96✔
217
  }
218
}
219

220
export {}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc