Source

api/historyApi.js

/**
 * @namespace api
 * @category api
 * @module history-api
 * @todo Refactor/Move all the logic into history-service
 * */

import moment from 'moment'
import api from '@/api'
import Vue from 'vue'
import { getFormattedAddress } from '@/utils/address'
import APIUrls, {
  APIV2RequestDatetimeFormat,
  APIV2RequestDateFormat,
  APIV3RequestDatetimeFormat,
} from '@/config/simpliciti-apis.js'
import i18n from '@/i18n'
import normalizeTripHistoryItem from '@/services/entities/trip-history-item.js'
import { getNestedValue } from '@/utils/object'
import { isUnitTest } from '@/utils/unit.js'

const hash = require('object-hash')

/**
 * @todo Those functions should be consumed/proxied-by by history-service only
 */
const scope = {
  getVehiclePositionsV2,
  getVehiclePositionsV3,
  getPositionsFromVehicle,
  getVehicleHistoryInfosFromAPIV2,
  getChartTripHistoryFromVehicleId,
  getTripHistoryFromVehicleId,
}

export default scope

async function getVehiclePositionsV3(vehicleId, date, options = {}) {
  const t = (d, h = 0, m = 0, s = 0) =>
    moment(date).hour(h).minute(m).second(s).format(APIV3RequestDatetimeFormat)
  const startDatetime = options.startDate
    ? moment(options.startDate).format(APIV3RequestDatetimeFormat)
    : t(date)
  const endDatetime = options.endDate
    ? moment(options.endDate).format(APIV3RequestDatetimeFormat)
    : t(date, 23, 59, 59)
  console.log('getVehiclePositionsV3', {
    vehicleId,
    date,
    startDatetime,
    endDatetime,
  })

  return (
    (isUnitTest() &&
      require('@/api/mocks/v3/positions/geored_get_sensors.json')[0]) ||
    ((
      await api.v3.get(APIUrls.APIV3_GET_SENSORS, {
        vehicleId: vehicleId,
        startDatetime,
        endDatetime,
        positions: true,
        /*       groups: [
        'complementary_informations',
        'sensors',
        'sensor_details',
        'can',
      ], */
        ...(options.queryParams || {}),
      })
    ).data || [null])[0]
  )
}

/**
 * deprecated
 async function getVehiclePositionsSensorsV2(vehicleId, date, options = {}) {
   let startDate = moment(date)
   .hour(0)
   .minute(0)
   .second(0)
   .format(APIV2RequestDatetimeFormat)
   let endDate = moment(date)
   .hour(23)
   .minute(59)
   .second(59)
   .format(APIV2RequestDatetimeFormat)
   return ((
     await api.v2.post(
       `${APIUrls.APIV2_POSITIONS_GPS}?dateheure_debut=${startDate}&dateheure_fin=${endDate}&vehicule_id=${vehicleId}&groups=infos_complementaires,capteurs,capteurs_details,can`
       )
       ).data || [null])[0]
      }
*/

/**
 *
 * Used by Location module (Trip history details - table) to load positions and sensors configuration
 *
 * @param {*} vehicleId
 * @param {*} date
 * @param {Object} options.startDate
 * @todo Remove fixtures feature without braking unit test
 * @returns {Array}
 */
async function getVehiclePositionsV2(vehicleId, date, options = {}) {
  const t = (d, h = 0, m = 0, s = 0) =>
    moment(date).hour(h).minute(m).second(s).format(APIV2RequestDatetimeFormat)

  let startDate = t(date)
  let endDate = t(date, 23, 59, 59)

  if (options.startDate) {
    startDate = moment(options.startDate).format(APIV2RequestDatetimeFormat)
  }
  if (options.endDate) {
    endDate = moment(options.endDate).format(APIV2RequestDatetimeFormat)
  }

  console.log('getVehiclePositionsV2', {
    vehicleId,
    date,
    startDate,
    endDate,
  })

  /*   return ((await sensorsService.getSensors(
    vehicleId,
    options.startDate,
    options.endDate
  )) || [null])[0] */

  return (
    (await Vue.$api.loadFixtures(
      `${APIUrls.APIV2_POSITIONS_CAPTEURS.substring(1)}/${vehicleId}_${moment(
        date
      ).format(APIV2RequestDateFormat)}`,
      (items) => items && items.length > 0 && items[0]
    )) ||
    ((
      await api.v2.post(
        `${APIUrls.APIV2_POSITIONS_CAPTEURS}?dateheure_debut=${startDate}&positions=true&capteurs=false&dateheure_fin=${endDate}&vehicule_id=${vehicleId}`
      )
    ).data || [null])[0]
  )
}

/**
 * Retrieves vehicle positions from a date (00:00:00 to 23:59:59)
 * @param {*} vehicleId
 * @param {*} date
 * @returns
 */
async function getPositionsFromVehicle(
  vehicleId,
  date = new Date(),
  fromTime = '',
  toTime = ''
) {
  Vue.$log.debug('getPositionsFromVehicle', vehicleId, date, fromTime, toTime)
  date = moment(date).format(APIV2RequestDateFormat)
  fromTime =
    (fromTime && encodeURIComponent(` ${fromTime}`)) || '%2000%3A00%3A00'
  toTime = (toTime && encodeURIComponent(` ${toTime}`)) || '%2023%3A59%3A59'
  let res

  res = (
    await api.v2.get(
      `${APIUrls.APIV2_POSITIONS}/${vehicleId}/${date}${fromTime}/${date}${toTime}`
    )
  ).data

  let data = (res && res.length > 0 && res[0]) || {} //response comes in first item

  if (res.error && res.error[0]) {
    Vue.$log.error(res.error[0])
    throw new Error(res.error[0].message || 'unknown error')
  }
  return data.positions || []
}

/**
 * Used by Diagnostics module to display analysis overview (After search validation)
 *
 * Retrieves history infos (such as averange speed, distance) and positions from a vehicle, date
 * @returns {Object} Vehicle infos or an empty object
 */
async function getVehicleHistoryInfosFromAPIV2(
  vehicleId,
  dateOrDates,
  options = {}
) {
  let datesArr = dateOrDates instanceof Array ? dateOrDates : [dateOrDates]

  if (!vehicleId) {
    throw new Error('At least one vehicle must be specified')
  }

  if (datesArr.length === 0) {
    throw new Error('At least one date is required')
  }

  let formattedDates = datesArr.map((date) =>
    moment(date).format(APIV2RequestDateFormat)
  )
  let formattedDatesAsString = formattedDates.join(',')

  let startTimeAsString = moment(datesArr[0]).format('HH:mm')
  let endTimeAsString = moment(datesArr[datesArr.length - 1]).format('HH:mm')

  if (datesArr.length === 1) {
    endTimeAsString = '23:59' //If one date is selected, take the whole day
  }

  let shouldApplyTheTimeToFirstAndLastDateOnly =
    options.shouldApplyTheTimeToFirstAndLastDateOnly !== undefined
      ? options.shouldApplyTheTimeToFirstAndLastDateOnly
      : true

  let data = (
    await api.v2.post(
      `${APIUrls.APIV2_HISTORIQUE_BY_VEHICLE_DATES}?vehicule_id=${vehicleId}&dates=${formattedDatesAsString}&linestring=true&heure_debut=${startTimeAsString}&heure_fin=${endTimeAsString}&premier_dernier_jour=${shouldApplyTheTimeToFirstAndLastDateOnly}`
    )
  ).data

  //Temporal fix: If multiple dates, cumulate results
  let cumulatedDataResult = null
  const cumulateData = (rawItem) => {
    console.log('cumulateData', {
      cumulatedDataResult: { ...cumulatedDataResult },
      rawItem,
    })
    if (!cumulatedDataResult) {
      cumulatedDataResult = rawItem
    } else {
      ;['duree_troncon', 'distance_troncon'].forEach((fieldToCumulate) => {
        cumulatedDataResult[fieldToCumulate] += rawItem[fieldToCumulate]
      })
      ;['dh_activ_fin', 'adresse_fin', 'cp_fin', 'ville_fin'].forEach(
        (fieldToAssign) => {
          cumulatedDataResult[fieldToAssign] = rawItem[fieldToAssign]
        }
      )
    }
  }

  let normalizedData = {}
  Object.keys(data || {}).forEach((d) => {
    //Temporal fix: If multiple dates, cumulate results
    if (datesArr.length > 1) {
      data[d].forEach((rawItem) => {
        cumulateData(rawItem)
      })
    }

    normalizedData[d] = (data[d] || []).map(normalizeTripHistoryItem)
  })

  /*console.log('getVehicleHistoryInfosFromAPIV2::normalizedData', {
    normalizedData,
  })*/

  let response = {}

  //Temporal fix: If multiple dates, cumulate results
  if (datesArr.length > 1) {
    response = normalizeTripHistoryItem(cumulatedDataResult || {})
    response.isCumulated = true
  } else {
    response =
      normalizedData[formattedDates[0]] &&
      normalizedData[formattedDates[0]].length > 0
        ? normalizedData[formattedDates[0]][0]
        : null
  }

  /*
  console.log('getVehicleHistoryInfosFromAPIV2::response', {
    response,
    cumulated: datesArr.length > 1,
    cumulatedDataResult,
  })*/

  return response
}

/**
 * Feth from APIV2
 * Support fixtures
 * @param {*} vehicleId
 * @param {*} date
 * @todo Remove fixtures feature without braking unit test
 * @returns
 */
async function getTripHistoryFromVehicleId(vehicleId, date = new Date()) {
  return (
    (await Vue.$api.loadFixtures(
      `${APIUrls.APIV2_HISTORIQUE_TRONCONS_DETAILS.substring(
        1
      )}/${vehicleId}_${moment(date).format(APIV2RequestDateFormat)}`
    )) ||
    (
      await api.v2.post(
        `${
          APIUrls.APIV2_HISTORIQUE_TRONCONS_DETAILS
        }?vehicule_id=${vehicleId}&dateheure_debut=${encodeURI(
          moment(date)
            .hour(0)
            .minute(0)
            .second(0)
            .format(APIV2RequestDatetimeFormat)
        )}&dateheure_fin=${encodeURI(
          moment(date)
            .hour(23)
            .minute(59)
            .second(59)
            .format(APIV2RequestDatetimeFormat)
        )}&linestring=true&linestring_couleur=true&linestring_mouvement=true`
      )
    ).data
  )
}

/**
 * Retrieves one vehicle trip history (normalized for a vertical chart)
 * @param {*} vehicleId
 * @param {*} date
 * @returns {Array} Normalized chart dataset
 * @todo Refactor: Instead of historyApi, Chart component (TripHistoryDetails) should be responsable of transofrming API response into chart normalized dataset
 */
export async function getChartTripHistoryFromVehicleId(
  vehicleId,
  date = new Date(),
  data = null
) {
  //  Helper functions:

  function pushItem(item) {
    item.id = hash(item)
    nodesGroupedByDate.push(item)
  }

  function createNode(tripStepNumber, trip, type = 'contact_on') {
    let label = ``,
      nodeDate = ``

    let prefix = ['contact_on', 'flag'].includes(type) ? '_deb' : '_fin'

    let address = getFormattedAddress(trip, {
      streetAddress: `adresse${prefix}`,
      zipCode: `cp${prefix}`,
      city: `ville${prefix}`,
      useZone: trip[`in_zoneinterne${prefix}`],
      zone: `zone${prefix}`,
    })

    if (type === 'contact_on') {
      label = `${i18n.t('location.history_tab.item.contact_on')}
      <br/><br/>
      ${address}
      `
      nodeDate = Vue.$date.formatTimeWithSeconds(
        moment(trip.dh_contact_on, APIV2RequestDatetimeFormat)
      )
    }
    if (type === 'play') {
      label = `${i18n.t('common.Départ')} ${address}
      <br/><br/>
      Durée du trajet : ${getTimeDuration(
        trip.dh_trajet_deb,
        trip.dh_arret_deb
      )}  &nbsp;&nbsp;  ${i18n.t(
        'location.history_tab.item.traveled_distance'
      )} : ${trip.distance} Km
      `
      nodeDate = Vue.$date.formatTimeWithSeconds(
        moment(trip.dh_trajet_deb, APIV2RequestDatetimeFormat)
      )
    }

    if (type === 'flag') {
      nodeDate = Vue.$date.formatTimeWithSeconds(
        moment(trip.dh_trajet_fin, APIV2RequestDatetimeFormat)
      )
      label = `${i18n.t('common.Arrivée')} ${address}`
    }

    if (type === 'contact_off') {
      label = `${i18n.t('location.history_tab.item.contact_off')}
                <br/><br/>
                ${address}
                `
    }

    let object = {
      step: tripStepNumber,
      date: nodeDate,
      label: label,
      type,
      lng: trip[`longitude${prefix}`],
      lat: trip[`latitude${prefix}`],
      linestring: trip.linestring,
    }

    delete object.id
    object.id = hash(object)
    return object
  }

  async function parseItem(item) {
    let newItem = {}
    let trip

    for (let index in item.troncons) {
      trip = item.troncons[index]

      let title = Vue.$date.formatDate(
        moment(trip.date, APIV2RequestDateFormat)
      )

      if (newItem.title && newItem.title != title) {
        pushItem(newItem)
        newItem = {}
      }

      if (!newItem.title) {
        newItem.title = title
        newItem.nodes = newItem.nodes || []

        //Trip history head infos (averages)

        newItem.fromDatetime = moment(
          getNestedValue(item, 'historique.dh_contact_on')
        )
        newItem.toDatetime = moment(
          getNestedValue(item, 'historique.dh_contact_off')
        )

        newItem.tronconDistance = getNestedValue(item, [
          'historique.distance_troncons', //invalid?
          'historique.distance_troncon',
        ])
        newItem.averageSpeed = item.historique.vitesse || ''
      }

      let stepNumber = parseInt(index) + 1

      if (trip.dh_contact_on) {
        newItem.nodes.push(createNode(stepNumber, trip, 'contact_on'))
      }

      newItem.nodes.push(createNode(stepNumber, trip, 'play'))

      newItem.nodes.push(createNode(stepNumber, trip, 'flag'))

      if (trip.dh_contact_off) {
        newItem.nodes.push(createNode(stepNumber, trip, 'contact_off'))
      }
    }
    pushItem(newItem)
  }

  //Get data
  data = data || (await getTripHistoryFromVehicleId(vehicleId, date))
  let nodesGroupedByDate = []
  //Group data by date
  await Promise.all(data.map(parseItem))

  //Group data by steps
  //  nodesGroupedByDate = {title,nodes:{step}} ==> {title (date) ,steps:[{stepNumber,nodes:[]}]}
  nodesGroupedByDate.forEach((grouped) => {
    grouped.steps = []
    let stepGroup = {
      stepNumber: 1,
      nodes: [],
    }

    grouped.nodes.forEach((node) => {
      if (stepGroup.stepNumber != node.step) {
        grouped.steps.push(stepGroup)
        stepGroup = {
          stepNumber: node.step,
          nodes: [],
        }
      }
      stepGroup.nodes.push(node)
    })
    grouped.steps.push(stepGroup)
    delete grouped.nodes
  })

  return nodesGroupedByDate
}

function getTimeDuration(from, to) {
  let then = moment(from, APIV2RequestDatetimeFormat)._d
  let ms = moment(to, APIV2RequestDatetimeFormat).diff(moment(then))
  let d = moment.duration(ms)
  return Math.floor(d.asHours()) + moment.utc(ms).format('[h] mm[m] ss[s]')
}