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,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,
|
|
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
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
110
|
-
const
|
|
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
|
|
113
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
137
|
+
await processDevice(driversData, device, userData, { route, trips }, deviceAlarmEvents, deviceOverspeedEvents)
|
|
138
|
+
deviceCount++
|
|
139
|
+
}
|
|
131
140
|
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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,
|