<template lang="pug">
.simpliciti_map(v-if="!!$store.getters['auth/clientId']")
slot(name="before_map", v-bind:map="this")
MapOptions(
v-if="!disableMapOptions",
ref="mapOptions",
v-model="mapOptions",
@toggleLayer="toggleMapLayers",
@toggleAllLayers="toggleAllMapLayers",
@sensors="onSensorLayerToggleChange",
:initialContactCheckboxesValue="showTripStepMarkersByDefault",
:initialSensorsVisibilityValue="showPositionMarkersByDefault",
@zonesMarkersToggle="(v) => (zonesMarkersToggle = v)"
)
LeafletMap(ref="map", @zoomend="(v) => (zoomLevel = v)"
@moveend="moveend"
)
BaseMapsFloatingMenu
portal(to="MapToolboxPortalTarget")
MapToolbox
slot
</template>
<script>
import Vue, { ref } from 'vue'
import MapOptions from '../../location/MapOptions/MapOptions.vue'
import LeafletMap from './LeafletMap'
import L from 'leaflet'
import { mapGetters } from 'vuex'
import polylineZoomMixin from './polyline-zoom-mixin'
import drawSinglePolylinesMixin from './draw-single-polylines-mixin'
import drawEventsMarkersMixin from './draw-events-markers-mixin'
import drawAlertsMarkersMixin from './draw-alerts-markers-mixin'
import drawChronoMarkersMixin from './draw-chronomarkers-mixin'
import mittToMethodsMixin from './mitt-to-methods-mixin'
import genericZoomMixin from './generic-zoom-mixin'
import drawPositionsMarkersMixin from './draw-positions-markers-mixin'
import drawBacsMarkersMixin from './draw-bacs-markers-mixin'
import drawVehicleMarkersMixin from '@c/shared/SimplicitiMap/draw-vehicle-markers.js'
import drawZonesMixin from '@c/shared/SimplicitiMap/draw-zones-mixin.js'
import BaseMapsFloatingMenu from '@c/shared/SimplicitiMap/BaseMapsFloatingMenu.vue'
import { getFormattedAddress } from '@/utils/address.js'
import { isMapSelectablePolylinesFeatureEnabled } from '@/services/feature-service.js'
import MapToolbox from '@c/shared/MapToolbox/MapToolbox.vue'
import useMapVM from '@/composables/map.js'
import { hasFeature, featureNames } from '@/config/features.js'
const map = ref(null)
const {
toggleStepStartStopMarkers,
changeLeafletPanesOpacity,
moveLeafletGeometryIntoHighlightPane,
} = useMapVM(map)
/**
* @namespace components
* @category components
* @subcategory shared
* @module SimplicitiMap
* @alias SimplicitiMapComponent
* @description
* Note: LeafletMap communication: For performance reasons, we decide to use component programatically (i.g: $refs.map.drawGeometries) rather than representing leaflet geometries with vue components.
* Note: Z-Index: SimplicitiMap should be responsable of re-ordering layer types in the right order. I.g: Positions should always draw on top of linestrings.
* @todo Methods should not be used directly (i.g $refs.map.drawPositionsMarkersFromData) but instead, called from inside this component based on store changes (simpliciti_map store)
*/
export default {
name: 'SimplicitiMap',
components: {
MapToolbox,
LeafletMap,
MapOptions,
BaseMapsFloatingMenu,
},
mixins: [
mittToMethodsMixin,
polylineZoomMixin,
drawSinglePolylinesMixin,
drawEventsMarkersMixin,
drawAlertsMarkersMixin,
drawChronoMarkersMixin,
genericZoomMixin,
drawPositionsMarkersMixin,
drawBacsMarkersMixin,
drawVehicleMarkersMixin,
drawZonesMixin,
],
inject: {
isLocationMainSearch: {
default: false,
},
isLocationHistoryMap: {
default: false,
},
isCircuitMap: {
default: false,
},
isTripHistoryMap: {
default: false,
},
isChronoMap: {
default: false,
},
isZonesMap: {
default: false,
},
simplicitiMapName: {
default: '',
},
},
props: {
fitBounds: {
type: Array,
default: () => [],
},
disableMapOptions: {
type: Boolean,
default: false,
},
},
data() {
return {
mapOptions: {},
isInitialMapCenterDone: false,
zoomLevel: null,
zonesMarkersToggle: false,
moveendTimestamp: null,
}
},
computed: {
...mapGetters({
tripStepsmarkers: 'simpliciti_map/tripStepsmarkers',
speedPolylines: 'simpliciti_map/speedPolylines',
chronoMarkers: 'simpliciti_map/chronoMarkers',
vehiclePositionMarkers: 'simpliciti_map/vehiclePositionMarkers',
sensorsConfig: 'map_options/sensorsConfig',
circuitExecutionsPolylines: 'simpliciti_map/circuitExecutionsPolylines',
tripHistoryPolylines: 'simpliciti_map/tripHistoryPolylines',
activeSearchFormTabName: 'search_module/activeSearchFormTabName',
}),
locationModulePolylines() {
return this.circuitExecutionsPolylines.length > 0 &&
this.activeSearchFormTabName === 'circuit'
? this.circuitExecutionsPolylines
: this.tripHistoryPolylines
},
showTripStepMarkersByDefault() {
return this.isTripHistoryMap
},
},
watch: {
zoomLevel() {
let layerGroups =
this.getLeafletMapWrapper &&
this.getLeafletMapWrapper() &&
this.getLeafletMapWrapper().layerGroups
Object.keys(layerGroups).forEach((key) => {
if (this['onZoomLevelChange_' + key]) {
this['onZoomLevelChange_' + key](layerGroups[key].getLayers())
}
})
},
fitBounds: {
handler(bounds) {
if (bounds.length > 0) {
this.getMap().fitBounds(bounds)
}
},
immediate: true,
},
/**
* Used by:
* Location - History by Vehicle,Driver, Circuit - Main search (Overview)
*
* Used for trip histories and circuit execution (if circuit tab)
*
* @todo: Extract if trip histories
*/
locationModulePolylines: {
handler(items = []) {
let bounds = null
if (this.isLocationHistoryMap && items.length > 0) {
this.waitForMapRef(
async () => {
await this.drawPolylines(items, {
layer: 'linestrings',
itemOptions: (item) => ({
color: item.color || 'darkgray',
originalColor: item.color || 'darkgray',
}),
...(isMapSelectablePolylinesFeatureEnabled()
? {
onclick: (e) => {
this.$map.highlightPolylines(
e.target.options.stepNumber
)
},
popupOptions: {
style: 'min-width:267px;',
},
popup: () =>
/* webpackChunkName "location_module" */
import('@c/location/TripHistoryPolylinePopup.vue'),
}
: {}),
})
//Fit to bounds only if bounds length changes... (This will skip fit to bounds when highlighting elements)
bounds = items.reduce((a, v) => {
a = a.concat(v.polyline)
return a
}, [])
if (this.locationModulePolylinesBoundsCount != bounds.length) {
this.locationModulePolylinesBoundsCount = bounds.length
this.getMap().fitBounds(bounds)
}
},
{
id: 'circuitExecutionsPolylines',
metadata: { len: items.length },
}
)
}
},
deep: true,
immediate: true,
},
/**
* Used by:
* Location - Real-time by Vehicle/Driver/Circuit - Last circuit
* Location - History by circuit - Main search (Details)
*/
tripStepsmarkers: {
handler(newValue) {
this.waitForMapRef(() => this.drawTripStepsmarkers(newValue))
},
deep: true,
immediate: true,
},
/**
* Used by:
* Location - Real-time by Vehicle/Driver/Circuit - Last circuit
* Location - History by circuit - Main search (Details)
*/
chronoMarkers: {
handler(newValue) {
this.waitForMapRef(() => this.drawChronoMarkers(newValue))
},
deep: true,
immediate: true,
},
/**
* Used by:
* Location - Real-time by Vehicle/Driver/Circuit - Last trip history
*/
speedPolylines: {
handler(newValue) {
if (this.isLocationMainSearch) {
return
}
this.waitForMapRef(() => this.drawSpeedPolylines(newValue))
},
deep: true,
immediate: true,
},
},
created() {
this.$store.dispatch('simpliciti_map/initialize')
},
mounted() {
this.waitForMapRef(() => {
map.value = this.$refs.map
this.$map.registerVM(this.simplicitiMapName, this)
this.bindReverseGeocodingOnClickFeature()
})
},
destroyed() {
this.$map.unregisterVM(this)
},
methods: {
/**
* Function that binds reverse geocoding service to Leaflet map on click events.
* On single click it will return an address depending on the coordinates of the click, and on double click it will clear the timeout set by single click.
*/
bindReverseGeocodingOnClickFeature() {
let self = this
let clickTimeout
this.getLeafletMapWrapper().map.on('click', async (e) => {
let enabled =
self.$store.getters[
'simpliciti_map/mapReverseGeocodingOnMouseClickEnabled'
]
if (!enabled) {
return //Skip reverse geocoding on map mouse click if not enabled
}
clearTimeout(clickTimeout)
clickTimeout = setTimeout(async () => {
e.originalEvent.stopPropagation()
const coords = {
latitude: e.latlng.lat,
longitude: e.latlng.lng,
}
let resultMessage = this.$t('geocoding.not_address_found')
if (e.latlng) {
await this.$geocoding
.reverseGeocoding(coords)
.then(async (response) => {
if (response.city) {
resultMessage = getFormattedAddress(response, {
country: true,
})
}
})
}
L.popup()
.setLatLng(e.latlng)
.setContent('<p class="px-2">' + resultMessage + '</p>')
.openOn(this.getLeafletMapWrapper().map)
}, 300) // set the timeout to 300ms
})
this.getLeafletMapWrapper().map.on('dblclick', () => {
clearTimeout(clickTimeout)
})
},
moveend() {
this.moveendTimestamp = Date.now()
},
withLeafletMapLayerGroup(layerGroupName, callback) {
let layerGroups =
this.getLeafletMapWrapper &&
this.getLeafletMapWrapper() &&
this.getLeafletMapWrapper().layerGroups
if (layerGroups && layerGroups[layerGroupName]) {
callback(layerGroups[layerGroupName])
}
},
openLayerPopup({ groupName, layerId }) {
let layerGroups =
this.getLeafletMapWrapper &&
this.getLeafletMapWrapper() &&
this.getLeafletMapWrapper().layerGroups
if (layerGroups && layerGroups[groupName]) {
let layer = layerGroups[groupName]
.getLayers()
.find((layer) => layer.properties.id == layerId)
if (!!layer && !layer.isPopupOpen()) {
if (this.zoomLevel < 18) {
//The popup will not open if zoom out, wait for an animation to finish
setTimeout(() => layer.openPopup(), 1000)
} else {
layer.openPopup()
}
}
}
},
/**
* Get Leaflet map instance
*/
getMap() {
return this.$refs.map.getMap()
},
/**
* Get LeafletMap component (child)
*/
getLeafletMapWrapper() {
return this.$refs.map
},
/**
* Removes/Add all the available featureGroups (layers) from the map (LeafletMap::map) at once.
*/
toggleAllMapLayers(value) {
let layerGroups =
this.getLeafletMapWrapper &&
this.getLeafletMapWrapper() &&
this.getLeafletMapWrapper().layerGroups
if (layerGroups && this.getLeafletMapWrapper) {
Object.keys(layerGroups).forEach((key) => {
if (value) {
layerGroups[key].addTo(this.getLeafletMapWrapper().map)
} else {
layerGroups[key].remove()
}
})
}
},
/**
* Toggles the specified layer groups from LeafletMap.
* @param {Object} options The object containing layerName and enabled that determines which layer to toggle
*/
toggleMapLayers(options) {
let { layerName, enabled } = options
let mapWrapper = this.getLeafletMapWrapper()
if (!mapWrapper) {
return
}
const layerGroups = mapWrapper.layerGroups
if (!layerGroups) {
this.$log.warn('LeafletMap layerGroups is not available')
return
}
let layerGroup = layerGroups && layerGroups[layerName]
if (!layerGroup) {
return
}
if (enabled) {
layerGroup && layerGroup.addTo(mapWrapper.getMap())
} else {
layerGroup && layerGroup.remove()
}
},
/**
* @deprecated In favor of zIndex property when using drawGeometries (LeafletMap.vue)
* After deprecation, ensure all the layers below (tripHistoryPolylines, speedPolylines, circuitPolylines, etc) uses zIndex property
* Re-arrange the Z-index of map layers (predefined priority)
sortMapLayers() {
const layers = this.getLeafletMapWrapper().layerGroups
if (layers.tripHistoryPolylines) {
layers.tripHistoryPolylines.bringToFront()
}
if (layers.speedPolylines) {
layers.speedPolylines.bringToFront()
}
if (layers.circuitPolylines) {
layers.circuitPolylines.bringToFront()
}
if (layers.circuit_execution_arrows) {
layers.circuit_execution_arrows.bringToFront()
}
Object.keys(layers).forEach((name) => {
if (name.includes('position')) {
layers[name].bringToFront()
layers[name].getLayers().forEach((layer) => {
layer.bringToFront()
})
}
})
},
*/
/**
* Vue DOM references might take some extra time to initialize
* @todo: Refactor into re-usable helper (see LeafletMap::waitForProperty)
*/
waitForMapRef(cb, options = {}, start = Date.now(), iterations = 0) {
let waitTime = options.waitTime || 1000
let timeout = options.timeout || 10000
if (Date.now() - start > timeout) {
this.$log.warn('waitForMapRef timeout')
return
}
if (!!window._logoutAt && window._logoutAt > start) {
this.$log.warn('waitForMapRef skip because logout detected')
return
}
//Ignore old calls if similar (only if options.id is supplied)
if (options.id) {
this._waitForMapRefTimeouts = this._waitForMapRefTimeouts || {}
if (
!!this._waitForMapRefTimeouts[options.id] &&
this._waitForMapRefTimeouts[options.id] > start
) {
//Vue.$log.debug(`waitForMapRef::${options.id}::${start}::skipped`);
return
} else {
/* Vue.$log.debug(
`waitForMapRef::${options.id}::${start}::start(${iterations})`,
options.metadata || {},
this._waitForMapRefTimeouts[options.id]
);*/
}
if (
!this._waitForMapRefTimeouts[options.id] ||
this._waitForMapRefTimeouts[options.id] < start
) {
this._waitForMapRefTimeouts[options.id] = start
}
}
if (this.$refs.map) {
if (options.id) {
Vue.$log.debug(
`waitForMapRef::${options.id}::${start}::end`,
options.metadata || {}
)
}
if (this.$refs.map.waitForInitialization) {
this.$refs.map.waitForInitialization().then(() => cb())
} else {
cb()
}
} else {
setTimeout(
() => this.waitForMapRef(cb, options, start, iterations + 1),
waitTime
)
}
},
/**
* Speed polylines will always start hidden (Toggleable from MapOptions)
*/
drawSpeedPolylines(data) {
if (data.length > 0) {
this.drawPolylines(data, {
visible: false,
leafletType: 'polyline',
type: 'speedPolylines',
layer: 'speedPolylines',
zIndex: 2,
})
}
},
/**
* Draw all Chrono Markers on the SimplicitiMap
*/
drawChronoMarkers(chronoMarkersArray) {
if (
!Array.isArray(chronoMarkersArray) &&
chronoMarkersArray.length === 0
) {
return
}
this.drawChronoMarkersSteps(chronoMarkersArray)
},
/**
* Draw contact ON/OFF markers (Also know as trip history step markers) (Red/Green)
*/
drawTripStepsmarkers(array) {
const drawTripSteps = (data, layer) =>
this.$refs.map.drawGeometries({
type: 'trip_step_' + layer,
layer,
data,
visible: this.showTripStepMarkersByDefault,
generate(item) {
return L.marker([item.lat, item.lng], {
icon: L.divIcon({
className: 'trip_step',
html: `<div class="trip_step__content" style="color: white;
font-size: 12px;
height: 15px;
width: 15px;
background: ${item.color};
display: flex;
justify-content: center;
align-items: center;">
${item.number}
</div>`,
}),
zIndexOffset: 1,
})
},
})
drawTripSteps(
array.filter((i) => i.type === 'contact_on'),
'trip_steps_contact_on'
)
drawTripSteps(
array.filter((i) => i.type === 'contact_off'),
'trip_steps_contact_off'
)
},
/**
* Draw Polylines (Linestrings) on the map
* - Used by Location - History mode - History tab (Related history)
* - Used by Location - History mode - Circuit tab (Related circuit exec)
* - Used by Location - Realtime mode - History tab (Dernier historique)
* - Used by Location - Realtime mode - Circuit tab (Dernier circuit exec)
*/
drawPolylines(polylines = [], options = {}) {
let self = this
options.layer = options.layer || 'linestrings'
options.zIndex = options.zIndex || 1
if (!this.$refs.map) {
console.log('Map is not availalble. Unit-test?')
}
this.$refs.map &&
this.$refs.map.drawPolylines(
polylines.map((element) => {
element = { ...element }
element.color = element.highlightedColor || element.color
return element
}),
{
...options,
after({ geometries }) {
self.updateHighlightedPolylineItemIfAny(polylines, geometries)
options.after && options.after.apply(this, arguments)
},
}
)
},
/**
* Note: map polylines can be highlighted
*/
updateHighlightedPolylineItemIfAny(
polylineItems,
relatedLeafletGeometries
) {
let highlightedPolylineItem = polylineItems.find(
(d) => d.highlighted || d.highlightedColor
)
if (highlightedPolylineItem) {
//Only show direction arrows on highlighted geometry
relatedLeafletGeometries.forEach((g) => {
g.deleteArrowheads && g.deleteArrowheads()
})
console.log(
'Removing arrowheads from',
relatedLeafletGeometries.length,
'elements'
)
const item = relatedLeafletGeometries.find(
(g) => g.externalId == highlightedPolylineItem.id
)
this.highlightSingleLeafletPolylineGeometry(item)
console.log('highlighting item', {
item,
})
if (
hasFeature(
featureNames.CIRCUIT_EXEC_STEP_HIGHLIGHT_SHOW_START_STOP_MARKERS
)
) {
this.highlightPane = moveLeafletGeometryIntoHighlightPane(item)
toggleStepStartStopMarkers(true, item)
changeLeafletPanesOpacity(0.5)
}
} else {
if (
hasFeature(
featureNames.CIRCUIT_EXEC_STEP_HIGHLIGHT_SHOW_START_STOP_MARKERS
)
) {
this.highlightPane && this.highlightPane.remove()
this.highlightPane = null
changeLeafletPanesOpacity(1)
toggleStepStartStopMarkers(false)
}
}
},
/**
* Note: Highlighted polyline has also direction arrows
*/
highlightSingleLeafletPolylineGeometry(polyline) {
//setTimeout(() => polyline.bringToFront(), 1000) //LeafletMap (Wrapper) takes 500ms to sort the layers
polyline.setStyle({
color: getComputedStyle(
document.querySelector(':root')
).getPropertyValue('--color-polyline-highlight'),
})
require('leaflet-arrowheads') //This will only require once
polyline.arrowheads({
color: 'darkgray',
fillColor:
polyline.color ||
polyline?.options?.color ||
polyline?.options?.style?.color ||
'darkgray',
yawn: 40,
//frequency: "180m",
//frequency: 'allvertices',
frequency: '200px',
size: '10px',
//fill: true,
weight: 3,
opacity: 0.8,
})
},
/**
* Draw direction arrows (for single circuit execution)
*/
drawCircuitArrowsFromPolylines(polylines = [], options = {}) {
require('leaflet-arrowheads')
this.$refs.map.drawGeometries({
...options,
type: 'arrow',
layer: 'circuit_execution_arrows',
zIndex: 4,
data: polylines.map((p) => ({
...p,
color: 'transparent',
arrowColor: p.color,
})),
generate(item) {
return L.polyline([item.polyline], {
arrowheads: true,
}).arrowheads({
color: item.arrowColor || 'darkgray',
weight: 1,
size: '12px',
smoothFactor: 0.5,
fill: true,
yawn: 50,
frequency: 'endonly',
})
},
after() {
options.after && options.after.apply(this, arguments)
},
})
},
},
}
</script>
<style lang="scss" scoped>
.simpliciti_map {
height: 100%;
position: relative;
display: flex;
flex-direction: column;
}
</style>
Source