Source

store/diagnostics-store.js

/**
 * @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
}