/**
* @namespace Stores
* @category Stores
* @module diagnostics-store
* */
import {
getVehiclePositions,
getVehicleHistoryFromDate,
} from '@/services/history-service.js'
import moment from 'moment'
import { getVehicleChrono } from '@/services/chrono-service.js'
import { boxAssignmentColorsTable } from '@/services/box-service.js'
import colors from '@/styles/colors.js'
import { hasRight } from '@/services/rights-service.js'
import {
fetchVehicleConfiguration,
getVehicleParameter,
} from '@/services/vehicle-service.js'
import envService from '@/services/env-service.js'
import { getTerrestrialDistance } from '@/utils/geo.js'
import { normalizePositionTorsForDiagnosticsModule } from '@/services/diagnostics-service.js'
function injectPositionFakeSensorData(pos) {
pos.sensorTor = [
{
name: 'PDF',
n: 1,
},
{
name: 'TOR2',
n: 2,
},
{
name: 'Bras',
n: 3,
},
{
name: 'porte ouverte',
n: 4,
},
{
name: 'Embrayage',
n: 5,
},
{
name: 'Regulateur Vitesse',
n: 6,
},
{
name: 'Pedale Frein',
n: 7,
},
]
pos.sensorAna = [
{
name: 'Conso',
n: 1,
},
{
name: 'Régime',
n: 2,
},
{
name: 'Accelération',
n: 3,
},
]
}
function newState() {
return {
vehicleId: null,
vehicleConfiguration: {},
vehicleHistoryOverview: {},
analysisResult: {},
isVehicleHistoryOverviewLoaded: false,
positions: [],
brushStartTimestamp: null,
brushEndTimestamp: null,
chartSelectedItem: null,
chartSelectedItemTrigger: '', //chart/replay
chronoData: {},
isTest: false,
isLoading: false,
lateralMenuMode: 'search', //options
chartsData: [],
menuCollapsed: false,
replayPosition: null,
replayPositionIndex: 0,
replayPositionEndIndex: -1,
selectedTimeRangeInMinutes: [], //Slider dataset: Filter positions (only if single day)
//Charts have some vertical lines when we select one position or a range
echartMarkLineData: [],
echartDataZoom: {
type: 'dataZoom',
dataZoomIndex: 0,
start: null,
end: null,
},
}
}
const isMomentToday = (momentValue) => momentValue.isSame(new Date(), 'day')
export default {
namespaced: true,
state: newState(),
getters: {
filterChartDataItemsUsingSliderDataset(state, getters) {
return (dataItems) => {
if (getters['doesPeriodContainsMultipleDays']) {
return dataItems //Skip slider data filter if multiple days
}
let startDayMs = moment(dataItems[0].value[0])
.hour(0)
.minute(0)
.second(0)
._d.getTime()
let [startMinutes, endMinutes] = state.selectedTimeRangeInMinutes
return dataItems.filter((pos) => {
return (
pos.value[0] > startDayMs + startMinutes * 60 * 1000 &&
pos.value[0] < startDayMs + endMinutes * 60 * 1000
)
})
}
},
getDatetimeRangeGivenSelectedDate: (state) => (selectedDate) => {
let startDatetime = moment(selectedDate)
let endDatetime = moment(startDatetime)
.add(23, 'h')
.add(59, 'm')
.add(59, 's')
//If today, endDatetime is set to today at 23:59
if (isMomentToday(startDatetime)) {
endDatetime = moment(startDatetime).hour(23).minute(59).second(59)
}
return { startDatetime, endDatetime }
},
/** Used by the slider to filter positions */
sliderMin: (state) => {
if (state.positions.length === 0) {
return 0
} else {
let pos = state.positions[0]
let startDayTimestamp = moment(pos.timestamp)
.hour(0)
.minute(0)
.second(0)
._d.getTime()
return Math.floor((pos.timestamp - startDayTimestamp) / 1000 / 60)
}
},
sliderMax: (state) => {
if (state.positions.length === 0) {
return 60 * 24 //End of day
} else {
let pos = state.positions[state.positions.length - 1]
let startDayTimestamp = moment(pos.timestamp)
.hour(0)
.minute(0)
.second(0)
._d.getTime()
return Math.round((pos.timestamp - startDayTimestamp) / 1000 / 60)
}
},
doesPeriodContainsMultipleDays: (state) =>
state.vehicleHistoryOverview?.isCumulated || false,
chartSelectedItemTrigger: (state) => state.chartSelectedItemTrigger,
replayPositionEndIndex: (state) => state.replayPositionEndIndex,
hasValidEchartDataZoom: (state) =>
Object.keys(state.echartDataZoom).length > 0 &&
state.echartDataZoom.start !== null &&
state.echartDataZoom.end !== null,
isCanConfiguredForSelectedVehicle: (state) =>
state.vehicleConfiguration?.isCanConfigured || false,
positions: (state, getters, rootState, rootGetters) => {
return state.positions
},
/**
* Used by Echart chart xAxis min/max (30 minutes left/right margin improves mouse selection)
* @param {*} state
* @returns
*/
positionsMinMaxDatetimes: (state) => {
if (state.positions.length === 0) {
return { min: moment(), max: moment() }
} else {
return {
min: state.positions[0].timestamp - 1000 * 60 * 30,
max:
state.positions[state.positions.length - 1].timestamp +
1000 * 60 * 30,
}
}
},
vehicleName: (state) => state.vehicleHistoryOverview?.vehicleName,
vehicleClassName: (state) =>
state.vehicleConfiguration?.vehicle?.category?.class || '30070',
vehicleSpeedAlert: (state) =>
envService.getDevOnlyQueryStringValue('vehicleSpeedAlert')
? parseInt(envService.getDevOnlyQueryStringValue('vehicleSpeedAlert'))
: state.vehicleConfiguration?.vehicleSpeedAlert || -1,
},
mutations: {},
actions: {
resetStore({ state, dispatch }) {
let _newState = newState()
for (var key in state) {
state[key] = _newState[key]
}
dispatch(
'simpliciti_map/setDataset',
{
type: 'singleTripHistoryPolylines',
data: [],
},
{ root: true }
)
dispatch(
'simpliciti_map/setDataset',
{
type: 'vehiclePositionMarkers',
data: [],
},
{ root: true }
)
dispatch('search_module/setHasResults', false, { root: true })
},
/**
* Overview Information is displayed after search selection validation (At the same time while loading positions)
* @param {*} param0
* @param {*} param1
*/
async updateVehicleHistoryOverview(
{ state, dispatch, getters },
{ vehicleId, date }
) {
state.isVehicleHistoryOverviewLoaded = false
state.vehicleHistoryOverview = {}
let { startDatetime, endDatetime } =
getters['getDatetimeRangeGivenSelectedDate'](date)
/*
if (isMomentToday(startDatetime)) {
startDatetime = startDatetime.hour(0).minute(0).second(0) //Otherwise API might bring nothing back
}*/
let dates = null
if (startDatetime.day() !== endDatetime.day()) {
//If multiple dates, send both dates as request parameters
dates = [startDatetime._d, endDatetime._d]
} else {
dates = startDatetime._d
}
state.vehicleHistoryOverview = await getVehicleHistoryFromDate(
vehicleId,
dates
)
state.isVehicleHistoryOverviewLoaded = true
},
/**
* Vehicle configuration brings useful box configuration information used to compute ANA values.
*
* @param {*} param0
* @param {*} vehicleId
*/
async updateVehicleConfiguration({ state }, vehicleId) {
let vehicleConfiguration = await fetchVehicleConfiguration(vehicleId)
let vehicleGlobalConsumptionRef = await getVehicleParameter(
vehicleId,
'RefConsoGlobale'
)
vehicleConfiguration.vehicleGlobalConsumptionRef =
vehicleGlobalConsumptionRef
? parseInt(vehicleGlobalConsumptionRef.value || 0)
: null
state.vehicleConfiguration = vehicleConfiguration
},
/**
* Used to render Chrono chart and Analysis Chrono section (Synthesis)
*
* @param {Number} params.vehicleId (Required)
* @param {Date|Moment} params.date (or startDate/endDate)
* @param {Date|Moment} params.startDate (or date)
* @param {Date|Moment} params.endDate (or date)
* @param {Boolean} params.onlySynthesis (False by default)
*/
async updateChronoData({ state, rootGetters }, params) {
let onlySynthesis =
params.onlySynthesis !== undefined ? params.onlySynthesis : false
let hasRightToCronoChart = hasRight(
rootGetters['auth/rightsList'],
'diagnostics_chart_chrono',
rootGetters['auth/loginName']
)
if (hasRightToCronoChart) {
let chronoData = await getVehicleChrono({
...params,
context: 'updateChronoData',
})
if (onlySynthesis) {
state.chronoData.synthesis = chronoData.synthesis
} else {
state.chronoData = chronoData
}
} else {
state.chronoData = {}
}
},
/**
* - Fetch vehicle configuration
* - Fetch vehicle positions (and normalize them)
* - Send polyline information to map store (to display trip history on the map based on positions data)
* @param {*} param0
* @param {*} param1
*/
async updateStore(
{ state, dispatch, rootGetters, getters },
{ vehicleId, date, timeFrom, timeTo }
) {
/*console.log('store:diagnostics:dispatch:updateStore', {
vehicleId,
date,
timeFrom,
timeTo,
})*/
state.vehicleId = vehicleId
let vehicleConfigPromise = dispatch(
'updateVehicleConfiguration',
vehicleId
)
let chronoPromise = dispatch('updateChronoData', {
vehicleId,
date,
})
//Positions (GPS Positions, TOR, ANA, Speed, CAN, SPE)
let positions = []
let { startDatetime, endDatetime } =
getters['getDatetimeRangeGivenSelectedDate'](date)
let positionsPromise = getVehiclePositions(
vehicleId,
startDatetime._d,
endDatetime._d
)
//Retrieve data in parallel
await Promise.all([
vehicleConfigPromise,
chronoPromise,
positionsPromise.then((value) => (positions = value)),
])
state.positions = Object.freeze(
getNormalizedPositionsForChart(
positions,
state.vehicleConfiguration,
getters['doesPeriodContainsMultipleDays'],
state.isTest
)
)
if (state.positions.length > 0) {
//Set the initial data zoom for the charts using the raw positions (first and last)
state.echartDataZoom = {
type: 'dataZoom',
dataZoomIndex: 0,
start: 0,
end: 100,
}
}
//Add trip history polylines based on raw positions
let polyline = null
dispatch(
'simpliciti_map/setDataset',
{
type: 'singleTripHistoryPolylines',
data: state.positions.reduce((a, v) => {
if (v.hasContactOn && polyline === null) {
polyline = {
polyline: [[v.lat, v.lng]],
smoothFactor: 0.5,
weight:
parseInt(process.env.VUE_APP_LOCATION_MAP_POLYLINE_WEIGHT) ||
5,
color: colors.color_main,
}
a.push(polyline)
}
if (polyline) {
let lastLatLng = polyline.polyline[polyline.polyline.length - 1]
if (v.lat != lastLatLng[0] && v.lng != lastLatLng[1]) {
polyline.polyline.push([v.lat, v.lng])
}
if (!v.hasContactOn) {
polyline = null
}
}
return a
}, []),
},
{ root: true }
)
},
},
}
/**
* If CAN are stored inside ANAs
* vehicleConfiguration.isCanInformationInsideANAs equals true
* @param {*} pos
*/
function injectCanInformationWhenBoxConfigCanSetToCapteurs(pos) {
pos.sensorCanConsumptionLiters =
pos.sensorAna.find((item) => item.n === 1)?.value || 0
pos.sensorCanRPM = pos.sensorAna.find((item) => item.n === 2)?.value || 0
pos.sensorCanThrottle = pos.sensorAna.find((item) => item.n === 3)?.value || 0
//Filter out ANAs containing CAN information
pos.sensorAna = pos.sensorAna.filter((item) => {
if ([1, 2, 3].includes(item.n)) {
return false
} else {
return true
}
})
}
/**
* - Add Contact ON position at start
* - Filter out Contact OFF (Except first)
* - Add Contact OFF at end
* - Computes shouldComputeConsumptionPer100Km
* @param {Array} positions Array of positions from APIV3
* @returns {Array} normalized positions array
*/
function getNormalizedPositionsForChart(
positions,
vehicleConfig,
doesPeriodContainsMultipleDays,
isTest
) {
let lastPos = null
let totalDistanceMeters = 0
let activeTorPositions = [] //Helper to compute startTimestmap/endTimestamp on TORs with meastureType 'temps'
let mappedPositions = positions.map((freezedPosition, positionIndex) => {
let pos = { ...freezedPosition }
//If CAN are stored inside ANAs
if (vehicleConfig.isCanInformationInsideANAs) {
injectCanInformationWhenBoxConfigCanSetToCapteurs(pos)
}
//Total distance: CAN value, fallback to compute using previous position
if (pos.distanceMeters) {
totalDistanceMeters += pos.distanceMeters
} else {
if (lastPos) {
totalDistanceMeters += getTerrestrialDistance(
lastPos.lat,
lastPos.lng,
pos.lat,
pos.lng
)
}
}
//- Consumption in liters exists in both current position and previous position
//- Current position consumption in liters is greater than previous position consumption in liters
//- Current position vehicle state is moving (speed is greater than stop threshold)
let shouldComputeConsumptionPer100Km =
pos.sensorCanConsumptionLiters !== '' &&
lastPos &&
lastPos.sensorCanConsumptionLiters !== '' &&
pos.sensorCanConsumptionLiters > lastPos.sensorCanConsumptionLiters &&
pos.speed > vehicleConfig.vehicleStopSpeedThreshold
if (shouldComputeConsumptionPer100Km) {
//todo: if conso ref (vehicle config), assign only if
//"if ($liDistanceTot > ((100 / $liConsoRef * $liConso) / 3)) {"
let consoDiff = Math.abs(
pos.sensorCanConsumptionLiters - lastPos.sensorCanConsumptionLiters
)
shouldComputeConsumptionPer100Km = false
if (vehicleConfig.vehicleGlobalConsumptionRef) {
if (
totalDistanceMeters >=
((100 / vehicleConfig.vehicleGlobalConsumptionRef) * consoDiff) / 3
) {
shouldComputeConsumptionPer100Km = true
} else {
shouldComputeConsumptionPer100Km = false //Skip compute
}
} else {
shouldComputeConsumptionPer100Km = true
}
if (shouldComputeConsumptionPer100Km) {
pos.computedConsumptionPer100Km = (
((consoDiff / (totalDistanceMeters / 1000)) * 100) /
1000
).toFixed(2)
totalDistanceMeters = 0 //Reset total distance each time conso100 is computed
}
} else {
pos.computedConsumptionPer100Km = 0
}
if (isTest) {
injectPositionFakeSensorData(pos)
}
if (pos.sensorAna.length > 0) {
pos.sensorAna = normalizePositionItemANASensors(
pos.sensorAna,
vehicleConfig,
isTest
).filter((ana) => !!ana.boxAssignment /*Skip ANA if no box assignment*/)
}
normalizePositionTorsForDiagnosticsModule(
pos,
activeTorPositions,
vehicleConfig.boxConfigSensorAssignments
)
lastPos = pos
//Last position should be contact off
if (positionIndex === positions.length - 1) {
pos.hasContactOn = false
}
return Object.freeze(pos)
})
return filterOutConsecutiveContactOffPositions(
mappedPositions,
doesPeriodContainsMultipleDays
)
}
/**
* ANA: Filter & Normalize values for Diagnostic Chart.
* Values are normalized using associated sensor assignment item
*/
function normalizePositionItemANASensors(
anaSensors,
vehicleConfiguration,
isTest
) {
return anaSensors.map((sensorItem) => {
/*
//Deprecated: Use reference APIs (box)
let boxAssignment = rootGetters['box/boxAssignmentForSensor'](
'ANA',
sensorItem.n, //number
pos.boxNumber,
state.vehicleId
)*/
let boxAssignment = vehicleConfiguration.boxConfigSensorAssignments.find(
(saItem) =>
saItem.sensorType.toUpperCase() === 'ANA' &&
saItem.sensorNumber == sensorItem.n
)
//If test (fake sensors), grab any sensor assignment item
if (isTest && !boxAssignment) {
boxAssignment = vehicleConfiguration.boxConfigSensorAssignments[0]
}
//Use name from box configuration assignment and fallback to name from positions response
sensorItem.name = boxAssignment.name || sensorItem.name
sensorItem.boxAssignment = boxAssignment
if (!boxAssignment) {
//throw new Error('Expected box assignment item to be found')
return sensorItem //Skip ANA normalization
}
if (isTest && !sensorItem.value) {
sensorItem.value = Math.random() * (Math.random() > 0.9 ? -1 : 1)
}
if (boxAssignment.precision >= 0) {
sensorItem.normalizedValue = parseFloat(
sensorItem.value.toFixed(boxAssignment.precision)
)
}
//Factor
let factor = boxAssignment.factor || 1
sensorItem.normalizedValue = sensorItem.normalizedValue * factor
//Shift
//V2: $lfvalCptAna = $lfvalCptAna * $aaConfigANA[$liC]['facteur'] + $aaConfigANA[$liC]['decalage'];
//sensorItem.normalizedValue =sensorItem.normalizedValue * Math.pow(10, boxAssignment.shift)
sensorItem.normalizedValue += boxAssignment.shift
let value =
sensorItem.normalizedValue !== undefined
? sensorItem.normalizedValue
: sensorItem.value
//lowerDisplay / upperDisplay
sensorItem.display =
value >= boxAssignment.lowerDisplay &&
value <= (boxAssignment.upperDisplay || 999999999)
//measureType
sensorItem.unit = boxAssignment.unitLabel
//color
sensorItem.color =
boxAssignment.color ||
boxAssignmentColorsTable[boxAssignment.colorCode || boxAssignment.colorHP]
return sensorItem
})
}
/**
* In GeoredV2, consecutive contact off positions are skipped
* @param {*} positions
* @returns
*/
export function filterOutConsecutiveContactOffPositions(
positions,
doesPeriodContainsMultipleDays
) {
const filteredPositions = []
//Filter out contact off (Except first)
var isSameDay = function (date, otherDate) {
return date.toDateString() === otherDate.toDateString()
}
let lastPos = null
let firstPosition = null
for (let pos of positions) {
if (!doesPeriodContainsMultipleDays) {
//Filter out positions from next day: Otherwise the time range slider will be broken (This should be fixed server side if possible)
if (!firstPosition) {
firstPosition = pos
} else {
if (
!isSameDay(new Date(pos.timestamp), new Date(firstPosition.timestamp))
//moment(pos.timestamp).diff(moment(firstPosition.timestamp), 'days') > 0
) {
continue
}
}
}
if (lastPos === null) {
if (pos.hasContactOn) {
filteredPositions.push(
Object.freeze({
...pos,
hasContactOn: false,
timestamp: pos.timestamp,
})
)
}
filteredPositions.push(pos)
} else {
//Case 1: Contact OFF => Contact ON or Contact ON => Contact OFF
if (
(lastPos.hasContactOn === false && pos.hasContactOn === true) ||
(lastPos.hasContactOn === true && pos.hasContactOn === false)
) {
filteredPositions.push(pos)
}
//Case 2: Contact ON
if (lastPos.hasContactOn === true && pos.hasContactOn === true) {
filteredPositions.push(pos)
}
}
lastPos = pos
}
return filteredPositions
}
Source