fleetmap-reports 2.0.363 → 2.0.365

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetmap-reports",
3
- "version": "2.0.363",
3
+ "version": "2.0.365",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1,8 +1,9 @@
1
+ const axios = require('axios')
1
2
  const traccarHelper = require('../util/traccar')
2
3
  const { getDriverData, devicesByDriver } = require('../util/driver')
3
4
  const { getCanDriverStyleMessages, parseCanDriverStyleMessage } = require('../util/xpert')
4
5
  const { calculateRPMSections } = require('./performance-report')
5
- const { getTranslations, isClientSide, convertToLocaleString, convertMS, executeServerSide } = require('../util/utils')
6
+ const { getTranslations, isClientSide, convertToLocaleString, convertMS, getServerHost } = require('../util/utils')
6
7
  const jsPDF = require('jspdf')
7
8
  const { headerFromUser, addTable } = require('../util/pdfDocument')
8
9
  const { getStyle } = require('../reportStyle')
@@ -57,6 +58,20 @@ function toGrade (score) {
57
58
  return grades.find(g => score >= g.min).grade
58
59
  }
59
60
 
61
+ // Splits [from, to] into consecutive sub-ranges of at most chunkDays each
62
+ function splitDateRange (from, to, chunkDays) {
63
+ const chunkMs = chunkDays * 24 * 60 * 60 * 1000
64
+ const end = new Date(to).getTime()
65
+ const chunks = []
66
+ let chunkStart = new Date(from).getTime()
67
+ while (chunkStart < end) {
68
+ const chunkEnd = Math.min(chunkStart + chunkMs, end)
69
+ chunks.push({ from: new Date(chunkStart), to: new Date(chunkEnd) })
70
+ chunkStart = chunkEnd
71
+ }
72
+ return chunks.length ? chunks : [{ from: new Date(from), to: new Date(to) }]
73
+ }
74
+
60
75
  async function create (from, to, userData, traccar) {
61
76
  const reportData = {
62
77
  drivers: []
@@ -73,25 +88,8 @@ async function create (from, to, userData, traccar) {
73
88
  }
74
89
 
75
90
  if (isClientSide() && (userData.drivers.length > 50 || ((new Date(to).getTime() - new Date(from).getTime()) > (1000 * 60 * 60 * 24 * 15)))) {
76
- const sliced = automaticReports.sliceArray(userData.drivers, 5)
77
- userData.requestToRunServerSide = true
78
- userData.byDriver = true
79
- // Narrow the devices sent per-slice to just the ones those drivers actually used, so a single
80
- // server invocation doesn't have to hold the whole fleet's route data in memory at once (was
81
- // causing Runtime.OutOfMemory on large fleets).
82
- const allDriversData = await executeServerSide('driverranking-report', sliced, from, to, userData, userData.drivers.length, traccar, 5, (driversSlice) => {
83
- const deviceIds = new Set()
84
- driversSlice.forEach(d => {
85
- const ids = deviceIdsByDriver.get(d.uniqueId)
86
- if (ids) {
87
- ids.forEach(id => deviceIds.add(id))
88
- }
89
- })
90
- return { devices: userData.devices.filter(device => deviceIds.has(device.id)) }
91
- })
92
-
93
- allDriversData.sort((a, b) => b.overallScore - a.overallScore)
94
- allDriversData.forEach((d, index) => { d.rank = index + 1 })
91
+ const rawRows = await runServerSideGrid(from, to, userData, deviceIdsByDriver, traccar)
92
+ const allDriversData = scoreAndRankDrivers(mergeRawRows(rawRows), userData)
95
93
 
96
94
  reportData.drivers = allDriversData
97
95
  reportData.target = buildTargets(userData)
@@ -100,44 +98,70 @@ async function create (from, to, userData, traccar) {
100
98
  }
101
99
 
102
100
  const devices = userData.devices
103
- let deviceCount = 0
104
101
  const driversData = new Map()
105
- const overspeedEvents = await getOverspeedEvents(from, to, userData, devices, userData, traccar)
106
102
 
107
- const sliced = automaticReports.sliceArray(devices, 5)
103
+ // Process the period in chunks so we never hold more than ~a week of raw GPS positions for the
104
+ // whole device set in memory at once - a single busy device can have tens of thousands of
105
+ // positions over a month, which was causing Runtime.OutOfMemory even after narrowing devices/drivers.
106
+ const dateChunks = splitDateRange(from, to, 7)
107
+ const totalDeviceOperations = devices.length * dateChunks.length
108
+ let deviceCount = 0
108
109
 
109
- for (const slice of sliced) {
110
- const allInOne = await traccarHelper.getAllInOne(traccar, from, to, slice, true, true, false, false, deviceCount, devices.length)
110
+ for (const dateChunk of dateChunks) {
111
+ const overspeedEvents = await getOverspeedEvents(dateChunk.from, dateChunk.to, userData, devices, userData, traccar)
112
+ const sliced = automaticReports.sliceArray(devices, 5)
111
113
 
112
- const result = []
113
- const requests = []
114
- for (const _chunk of automaticReports.sliceArray(slice.map(d => d.id), 1)) {
115
- requests.push(traccar.reports.reportsEventsGet(from, to, _chunk, null, ['alarm']))
116
- }
114
+ for (const slice of sliced) {
115
+ const allInOne = await traccarHelper.getAllInOne(traccar, dateChunk.from, dateChunk.to, slice, true, true, false, false, deviceCount, totalDeviceOperations)
117
116
 
118
- try {
119
- result.push(...(await Promise.all(requests)))
120
- } catch (e) {
121
- console.log(e)
122
- }
123
- const alarmEvents = result.flatMap(d => d.data)
117
+ const result = []
118
+ const requests = []
119
+ for (const _chunk of automaticReports.sliceArray(slice.map(d => d.id), 1)) {
120
+ requests.push(traccar.reports.reportsEventsGet(dateChunk.from, dateChunk.to, _chunk, null, ['alarm']))
121
+ }
122
+
123
+ try {
124
+ result.push(...(await Promise.all(requests)))
125
+ } catch (e) {
126
+ console.log(e)
127
+ }
128
+ const alarmEvents = result.flatMap(d => d.data)
129
+
130
+ for (const device of slice) {
131
+ const route = allInOne.route.filter(t => t.deviceId === device.id)
132
+ const trips = allInOne.trips.filter(t => t.deviceId === device.id)
124
133
 
125
- for (const device of slice) {
126
- const route = allInOne.route.filter(t => t.deviceId === device.id)
127
- const trips = allInOne.trips.filter(t => t.deviceId === device.id)
134
+ const deviceOverspeedEvents = overspeedEvents.find(e => e.device.id === device.id)
135
+ const deviceAlarmEvents = alarmEvents.filter(e => e.deviceId === device.id)
128
136
 
129
- const deviceOverspeedEvents = overspeedEvents.find(e => e.device.id === device.id)
130
- const deviceAlarmEvents = alarmEvents.filter(e => e.deviceId === device.id)
137
+ await processDevice(driversData, device, userData, { route, trips }, deviceAlarmEvents, deviceOverspeedEvents)
138
+ deviceCount++
139
+ }
131
140
 
132
- await processDevice(driversData, device, userData, { route, trips }, deviceAlarmEvents, deviceOverspeedEvents)
133
- deviceCount++
141
+ console.log('LOADING_MESSAGE:' + slice[0].name)
142
+ console.log(`PROGRESS_PERC:${deviceCount / totalDeviceOperations * 100}`)
134
143
  }
144
+ }
145
+
146
+ const rawRows = Array.from(driversData.values())
135
147
 
136
- console.log('LOADING_MESSAGE:' + slice[0].name)
137
- console.log(`PROGRESS_PERC:${deviceCount / devices.length * 100}`)
148
+ // When invoked as one cell of the client's (driverSlice x dateChunk) grid, return the raw
149
+ // accumulated totals for just this chunk - the client sums them across chunks for the same
150
+ // driver before computing scores, since scores are relative to the whole requested period.
151
+ if (userData.rawMode) {
152
+ reportData.drivers = rawRows
153
+ return reportData
138
154
  }
139
155
 
140
- const allDriversData = (Array.from(driversData.values())).filter(d => d.distance > 0)
156
+ reportData.drivers = scoreAndRankDrivers(rawRows, userData)
157
+ reportData.target = buildTargets(userData)
158
+ reportData.coefficient = buildCoefficients()
159
+
160
+ return reportData
161
+ }
162
+
163
+ function scoreAndRankDrivers (rawRows, userData) {
164
+ const allDriversData = rawRows.filter(d => d.distance > 0)
141
165
  allDriversData.forEach(d => {
142
166
  d.avgFuelConsumption = getCanAvgConsumption(d.distance, d.spentFuel).byKms
143
167
 
@@ -163,12 +187,86 @@ async function create (from, to, userData, traccar) {
163
187
  })
164
188
  allDriversData.sort((a, b) => b.overallScore - a.overallScore)
165
189
  allDriversData.forEach((d, index) => { d.rank = index + 1 })
190
+ return allDriversData
191
+ }
166
192
 
167
- reportData.drivers.push(...allDriversData)
168
- reportData.target = buildTargets(userData)
169
- reportData.coefficient = buildCoefficients()
193
+ const mergedFields = ['distance', 'highEngineRPM', 'hardBraking', 'hardAcceleration', 'hardCornering',
194
+ 'overspeed', 'spentFuel', 'geofenceAlarm', 'continuesDrivingAlarm', 'reverseAlarm', 'otherAlarm']
195
+
196
+ // Sums each driver's raw totals across every (driverSlice x dateChunk) grid cell they appeared in
197
+ function mergeRawRows (rawRows) {
198
+ const merged = new Map()
199
+ rawRows.forEach(row => {
200
+ let acc = merged.get(row.driverId)
201
+ if (!acc) {
202
+ acc = { driverId: row.driverId, name: row.name }
203
+ mergedFields.forEach(field => { acc[field] = 0 })
204
+ merged.set(row.driverId, acc)
205
+ }
206
+ mergedFields.forEach(field => { acc[field] += row[field] || 0 })
207
+ })
208
+ return Array.from(merged.values())
209
+ }
170
210
 
171
- return reportData
211
+ // Fans the report out across a grid of (driverSlice x dateChunk) requests to the backend, so no
212
+ // single Lambda invocation has to hold more than a handful of drivers' worth of one week of route
213
+ // data in memory at once, and progress feedback arrives once per grid cell rather than once per
214
+ // driver-slice (which could take minutes on its own with a long date range).
215
+ async function runServerSideGrid (from, to, userData, deviceIdsByDriver, traccar) {
216
+ const driverSlices = automaticReports.sliceArray(userData.drivers, 5)
217
+ const dateChunks = splitDateRange(from, to, 7)
218
+
219
+ const cells = []
220
+ driverSlices.forEach(driverSlice => {
221
+ dateChunks.forEach(dateChunk => cells.push({ driverSlice, dateChunk }))
222
+ })
223
+
224
+ const cookie = await axios.get('/pinmeapi/cookie/get', { withCredentials: true }).then(d => d.data)
225
+ const url = `https://${getServerHost()}/pinmeapi/reports/driverranking-report`
226
+ const rawRows = []
227
+ let completed = 0
228
+ const maxConcurrent = 10
229
+
230
+ for (let i = 0; i < cells.length; i += maxConcurrent) {
231
+ const batch = cells.slice(i, i + maxConcurrent)
232
+ await Promise.all(batch.map(async ({ driverSlice, dateChunk }) => {
233
+ const deviceIds = new Set()
234
+ driverSlice.forEach(d => {
235
+ const ids = deviceIdsByDriver.get(d.uniqueId)
236
+ if (ids) {
237
+ ids.forEach(id => deviceIds.add(id))
238
+ }
239
+ })
240
+ const slicedDevices = userData.devices.filter(device => deviceIds.has(device.id))
241
+
242
+ try {
243
+ const response = await axios.post(url, {
244
+ from: dateChunk.from,
245
+ to: dateChunk.to,
246
+ userData: {
247
+ ...userData,
248
+ drivers: driverSlice,
249
+ devices: slicedDevices,
250
+ geofences: [],
251
+ requestToRunServerSide: true,
252
+ rawMode: true
253
+ },
254
+ cookie
255
+ }, { withCredentials: true })
256
+ const result = response.data[0]
257
+ if (result) {
258
+ rawRows.push(...(result.drivers || []))
259
+ }
260
+ } catch (e) {
261
+ console.error(e)
262
+ }
263
+ completed++
264
+ console.log('LOADING_MESSAGE:' + driverSlice[0].name)
265
+ console.log(`PROGRESS_PERC:${(completed / cells.length) * 100}`)
266
+ }))
267
+ }
268
+
269
+ return rawRows
172
270
  }
173
271
 
174
272
  function buildTargets (userData) {
@@ -219,6 +317,7 @@ async function processDevice (driversData, device, userData, allInOne, alarmEven
219
317
  let driverData = driversData.get(d.id)
220
318
  if (!driverData) {
221
319
  driverData = {
320
+ driverId: d.id,
222
321
  name: d.name,
223
322
  distance: 0,
224
323
  highEngineRPM: 0,