import Vue from 'vue'
import moment from 'moment'
import { createCircuitColorsManager } from '@/services/location-service.js'
import { generateShortId } from '@/utils/crypto.js'
import APIUrls from '@/config/simpliciti-apis.js'
const LocationServiceTripHistoryColorManager = createCircuitColorsManager()
/**
* User can stop search in progress
*/
Vue.use({
install() {
let scope = (Vue.$locationModuleSearchManager = {
_searchs: [],
createNew() {
let item = {
id: generateShortId('id-'),
isCanceled: false,
isRemoved: false,
}
scope._searchs.push(item)
return {
item,
remove() {
item.isRemoved = true
scope.remove(item)
},
shouldContinue() {
if (!item.isCanceled) {
return true
} else {
!item.isRemoved && scope.remove(item)
return false
}
},
}
},
remove(idOrItem) {
let id = idOrItem.id || id
console.warn('location: search abort (untracking)', id)
scope._searchs.splice(
scope._searchs.findIndex((item) => item.id == id),
1
)
},
cancelAnySearchInProgress() {
scope._searchs.forEach((item) => {
item.isCanceled = true
})
},
})
},
})
import {
parseLocationItem,
getHistoryAPIQueryStringDatePart,
getItemsFromHistoryVehicleDriverAPIV2Response,
getItemsFromHistoryCircuitAPIV2Response,
getItemsFromHistoryDriverAPIV2Response,
} from './helpers'
const sequential = require('promise-sequential')
const searchModuleSelectedKeys = {
vehicle: 'selectedVehiclesIds',
driver: 'selectedDriversIds',
circuit: 'selectedCircuitsIds',
}
function hasSelectionTypeAnySelectedID(selectionType, searchSelection) {
return (
searchSelection[searchModuleSelectedKeys[selectionType]] &&
searchSelection[searchModuleSelectedKeys[selectionType]].length > 0
)
}
/***
* @namespace Stores
* @category Stores
* @module location-module-store-main-search
* @memberof location-module-store
*/
export default {
actions: {
/**
* @internal
* Split by items feature: Requests are split if items (vehicle/driver/circuit) are > 5
* Merge strategy: Merge results if not first request
* Sequential: Yes
* Parallel: No
*
* @returns
*/
async performSearchDivideRequests({ dispatch, rootGetters }, payload = {}) {
return new Promise(async (resolve, reject) => {
const { selectionType } = payload
let searchSelection = rootGetters['search_module/getSelection']
let allIds =
payload.ids ||
searchSelection[searchModuleSelectedKeys[selectionType]]
let groups = []
let group = []
let groupLengths = {
vehicle:
process.env
.VUE_APP_API_REALTIME_BY_VEHICULE_MAX_ITEMS_PER_REQUEST || 5,
driver:
process.env
.VUE_APP_API_REALTIME_BY_VEHICULE_MAX_ITEMS_PER_REQUEST || 5,
circuit:
process.env
.VUE_APP_API_REALTIME_BY_VEHICULE_MAX_ITEMS_PER_REQUEST || 5,
}
allIds.forEach((id) => {
group.push(id)
if (group.length == groupLengths[selectionType]) {
groups.push(group)
group = []
}
})
if (group.length > 0) {
groups.push(group)
}
let stats = {
requestsDone: 0,
groupsCount: groups.length,
requestsTotal: groups.length,
totalReqTime: 0,
searchStart: moment(),
}
let searchItem = Vue.$locationModuleSearchManager.createNew()
LocationServiceTripHistoryColorManager.resetState()
//Vue.$log.debug("performSearchDivideRequests::groups", groups.length);
await sequential(
groups.map((ids, index) => {
return async () => {
if (!searchItem.shouldContinue()) {
console.warn(
'location: search abort (skip series)',
searchItem.id
)
return null
}
let r = await dispatch('performSearch', {
...payload,
selectionType,
ids,
isSplitByItems: true,
merge: index > 0,
stats,
searchItem,
metadata: {
splitGroup: `${index + 1}/${groups.length}`,
group: index + 1,
},
})
return r
}
})
)
searchItem.remove()
if (!searchItem.shouldContinue()) {
console.warn('location: search abort (resolve)', searchItem.id)
resolve()
return
}
//Used by unit-test
if (payload.onSuccessSplitItems) {
await payload.onSuccessSplitItems({
dispatch,
rootGetters,
})
}
resolve()
})
},
stopSearchInProgress() {},
/**
* Performs search by vehicle/circuit/drivers using real-time/history API.
* @returns
*/
async performSearch(
{ dispatch, rootGetters, rootState, commit },
payload = {}
) {
if (!payload.selectionType) {
throw new Error('selectionType required')
}
const { selectionType } = payload
let searchSelection = {
...rootGetters['search_module/getSelection'],
}
if (payload.selectedDateRanges) {
searchSelection.selectedDateRanges = payload.selectedDateRanges
}
//Do not check/validate selected ids in search_module store if mock (jest)
if (
payload.isMock !== true &&
!payload.categoryIds &&
!hasSelectionTypeAnySelectedID(selectionType, searchSelection)
) {
return
}
if (payload.isSplitByItems !== true) {
return dispatch('performSearchDivideRequests', payload)
}
let allIds =
payload.categoryIds ||
payload.ids ||
searchSelection[searchModuleSelectedKeys[selectionType]]
let infos = []
const baseUrlData = {
vehicle: [
APIUrls.APIV2_TEMPS_REEL_BY_VEHICLE,
APIUrls.APIV2_HISTORIQUE_BY_VEHICLE_DATES,
],
driver: [
APIUrls.APIV2_TEMPS_REEL_BY_DRIVER,
APIUrls.APIV2_HISTORIQUE_BY_DRIVER_DATES,
],
circuit: [
APIUrls.APIV2_TEMPS_REEL_BY_CIRCUIT,
APIUrls.APIV2_HISTORIQUE_BY_CIRCUIT_DATES,
],
}
const isHistoryMode = searchSelection.selectedDateRanges.length > 0
const baseUrl = baseUrlData[selectionType][isHistoryMode ? 1 : 0]
let extraParams = ''
if (isHistoryMode) {
/*
* Split by date feature: Requests are split if multiple dates
* Merge strategy: Merge results if not first request from first group (split by items feature)
* Sequential: No
* Parallel: Yes
* */
let dateRanges = searchSelection.selectedDateRanges
if (dateRanges.length > 1) {
let firstSplitByDateSetResultPromise = null
payload.stats.requestsTotal =
payload.stats.groupsCount * dateRanges.length
//Execute in parallel
await Promise.all(
dateRanges.map((singleDateRange, index) => {
let lastDateRange = dateRanges[dateRanges.length - 1]
//Time-range is stored in the last date range
singleDateRange[0] = moment(singleDateRange[0])
.hour(moment(lastDateRange[0]).hour())
.minute(moment(lastDateRange[0]).minute())._d
singleDateRange[1] = moment(singleDateRange[1])
.hour(moment(lastDateRange[1]).hour())
.minute(moment(lastDateRange[1]).minute())._d
return (async () => {
/*
Vue.$log.debug(
"performSearch:split-by-date executing date range: ",
singleDateRange.map((date) =>
moment(date).format("YYYY-MM-DD HH:mm:ss")
),
"for items",
allIds
);*/
const requestId = generateShortId('req-')
let r = await dispatch('performSearch', {
...payload,
requestId,
selectionType,
ids: allIds,
selectedDateRanges: [singleDateRange],
merge: payload.merge,
stats: payload.stats,
metadata: {
...(payload.metadata || {}),
splitGroupDate: `${index + 1}/${dateRanges.length}`,
reqCount: payload.stats.requestsTotal,
},
//Override Assign/Merge
setHandler: async (infos) => {
//Update completed metadata (debug only)
if (!Vue.$env.isProduction()) {
commit(
'api/updateCompletedRequestMetadata',
{
id: requestId,
payload: {
shouldMergeResults:
payload.merge || firstSplitByDateSetResultPromise,
req: `${payload.stats.requestsDone}/${payload.stats.requestsTotal}`,
totalReqTime: moment
.utc(payload.stats.totalReqTime)
.format('HH:mm:ss.SSS'),
searchElapsed: moment
.utc(moment().diff(payload.stats.searchStart))
.format('HH:mm:ss.SSS'),
},
},
{
root: true,
}
)
}
//Set (Assign/Merge) can be mock
if (payload.mockSet) {
await mockSet({ commit, dispatch }, infos)
} else {
const shouldMerge =
payload.merge || !!firstSplitByDateSetResultPromise
if (firstSplitByDateSetResultPromise) {
await firstSplitByDateSetResultPromise
}
let setPromise = dispatch('setResultsFromAPIResponse', {
selectionType,
infos,
disableInitializeSearchModule:
payload.disableInitializeSearchModule,
merge: shouldMerge,
})
if (!firstSplitByDateSetResultPromise) {
firstSplitByDateSetResultPromise = setPromise
}
}
},
})
return r
})() //Promise.all expects promises; sequential expects ()=>Promise
})
)
//If split by date, return immediately
return
} else {
const isFirstAndLastDay = rootState.search_module.isFirstAndLastDay
extraParams += `${getHistoryAPIQueryStringDatePart(
searchSelection.selectedDateRanges,
isFirstAndLastDay
)}`
extraParams += '&linestring=1&linestring_troncons=1' // Bring circuit execution linestrings
}
if (['vehicle', 'driver'].includes(selectionType)) {
extraParams += '&linestring_multiple=true'
}
}
//Override fetch/set
if (payload.mockFetchAndSet !== undefined) {
payload.mockFetchAndSet(payload)
return
}
//Fetch can be override
if (payload.mockFetch) {
infos = await payload.mockFetch(payload)
} else {
//I.g: If selectionType=circuit and payload.categoryIds, paramName equals circuit_categorie_id
let paramName = {
vehicle: ['vehicule_id', 'vehicule_categorie_id'],
driver: ['chauffeur_id', 'chauffeur_categorie_id'],
circuit: ['circuit_id', 'circuit_categorie_id'],
}[selectionType][payload.ids ? 0 : 1]
if (!Vue.$env.isProduction()) {
payload.reqStart = moment()
}
let fetchPromise = dispatch(
'api/post',
{
name: `main_search_${isHistoryMode ? 'history' : 'realtime'}`,
url: `${baseUrl}?${paramName}=${allIds}${extraParams}`,
debugHasResultsHandler: (apiResponse) =>
getNormalizedResults(apiResponse, selectionType, rootGetters)
.length > 0,
debugMetadata: !Vue.$env.isProduction() && {
shouldMergeResults: payload.merge,
...(payload.metadata || {}),
totalReqTime: moment
.utc(payload.stats.totalReqTime)
.format('HH:mm:ss.SSS'),
},
debugRequestId: payload.requestId,
},
{
root: true,
}
)
infos = (await fetchPromise).data
}
payload.stats.requestsDone++
if (!Vue.$env.isProduction()) {
payload.stats.totalReqTime =
payload.stats.totalReqTime + moment().diff(payload.reqStart)
}
if (payload.searchItem && !payload.searchItem.shouldContinue()) {
console.warn(
'location: search abort (skip result)',
payload.searchItem.id
)
return
}
//Set (Assign/Merge) can be override
if (payload.setHandler) {
return payload.setHandler(infos)
}
//Set (Assign/Merge) can be mock
if (payload.mockSet) {
await mockSet({ commit, dispatch }, infos)
} else {
//Results are Assigned/Merged into store
await dispatch('setResultsFromAPIResponse', {
selectionType,
infos,
disableInitializeSearchModule: payload.disableInitializeSearchModule,
merge: payload.merge !== undefined ? payload.merge : false,
})
}
},
/**
*
* Transform/Normalize and set/merge real-time/history APIV2 responses into Location store
*
* @param {Object} VuexContext
* @param {String} options.selectionType Required (vehicle/driver/circuit)
* @param {Array} options.infos Required
* @param {Boolean} options.merge Whenever to merge results into existing results or overwrite them
*/
async setResultsFromAPIResponse(
{ commit, rootGetters, state, dispatch },
options = {}
) {
if (options.infos === null) {
return
}
//Only for unit test
if (options.disableInitializeSearchModule !== true) {
//Normalization requires search_module to be initialized
await dispatch(
'search_module/initialize',
{},
{
root: true,
}
)
} else {
Vue.$log.info('disableInitializeSearchModule')
}
let { selectionType, infos } = options
const normalizedResults = getNormalizedResults(
infos,
selectionType,
rootGetters
)
commit(options.merge ? 'mergeResults' : 'setResults', {
type: selectionType,
value: normalizedResults,
})
let hasResults =
state.vehicleResults.length > 0 ||
state.driverResults.length > 0 ||
state.circuitResults.length > 0
dispatch('search_module/setHasResults', hasResults, {
root: true,
})
},
},
}
/**
* Normalizes API responses into a flat array
*
* -APIV2 Responses are not flat and there are different (history/realtime)
*/
function getNormalizedResults(apiResponse, selectionType, rootGetters) {
let options = { selectionType }
options.isHistoryMode = !(apiResponse instanceof Array)
let flatResults = []
if (options.isHistoryMode) {
if (['vehicle'].includes(selectionType)) {
flatResults = getItemsFromHistoryVehicleDriverAPIV2Response(apiResponse)
}
if (['driver'].includes(selectionType)) {
flatResults = getItemsFromHistoryDriverAPIV2Response(apiResponse)
}
if (['circuit'].includes(selectionType)) {
flatResults = getItemsFromHistoryCircuitAPIV2Response(apiResponse)
}
flatResults = flatResults.map((item) => {
item.couleur =
LocationServiceTripHistoryColorManager.getUnusedColorAsString(
item.vehicule_id
)
//console.info('Grabing color for vehicle', item.vehicule_id, item.couleur)
return item
})
} else {
flatResults = apiResponse //Realtime API already brings a flat array
}
return (flatResults || []).map((item) =>
parseLocationItem(item, options, rootGetters)
)
}
Source