import {
getZones,
saveZone,
getZoneCategories,
} from '@/services/zones-service.js'
import L from 'leaflet'
import * as R from 'ramda'
import api from '@/api'
import i18n from '@/i18n'
import { queueOperationOnce } from '@/utils/promise.js'
import { apiCacheStorage } from '@/api/api-cache.js'
import store from '@/store'
import { geocodingPlugin } from '@/plugins/vue-services'
import { useToast } from '@c/shared/Toast.vue'
import { getQueryStringValue } from '@/utils/querystring'
import { mitt } from '@/plugins/mitt.js'
const { show: showToast } = useToast()
const logging =
(getQueryStringValue('verbose') || '').includes('1') ||
(getQueryStringValue('verbose') || '').includes('zones')
export const zoneTypeIcons = {
drapeau_bleu: `drapeau_bleu_32x32.gif`,
drapeau_orange: `drapeau_orange_32x32.gif`,
drapeau_rouge: `drapeau_rouge_32x32.gif`,
drapeau_vert: `drapeau_vert_32x32.gif`,
icone_clientbleu: `icone_clientbleu_16x16.gif`,
icone_clientrouge: `icone_clientrouge_16x16.gif`,
icone_clientvert: `icone_clientvert_16x16.gif`,
icone_depot: `icone_depot_16x16.gif`,
icone_entrepot: `icone_entrepot_16x16.gif`,
icone_fournisseur: `icone_fournisseur_16x16.gif`,
icone_garage: `icone_garage_16x16.gif`,
icone_interdit: `icone_interdit_16x16.gif`,
icone_livraison: `icone_livraison_16x16.gif`,
icone_maison: `icone_maison_16x16.gif`,
}
/**
*
* @param {*} iconKey e.g drapeau_bleu
* @returns
*/
export function normalizeZoneTypeIconItem(iconKey = '') {
return (
(!!iconKey && {
id: iconKey,
name: i18n.t(`zones.zone_type_icons_labels.${iconKey}`),
iconSrc: `/img/zones/${zoneTypeIcons[iconKey]}`,
iconFileName: zoneTypeIcons[iconKey],
}) ||
null
)
}
const globalLeafletGeomanOptions = {
continueDrawing: false,
markerStyle: {
icon: L.divIcon({
className: 'zone_marker__access_point_icon',
html: `<svg xmlns="http: //www. W3. Org/2000/svg" width="18.385" height="18.385" viewbox="0 0 18.385 18.385" > <g class="a" transform="translate(9.192) rotate(45)" : fill="var(--color-dark-blue)" > <rect class="b" style="stroke: none" stroke="none" width="13" height="13" rx="2" /> <rect class="c" style="fill: none" fill="none" x="0.5" y="0.5" width="12" height="12" rx="1.5" /> </g> </svg>`,
}),
},
}
function configureGeomanTranslations(leafletMapInstance) {
const customTranslation = {
buttonTitles: {
drawMarkerButton: i18n.t('zones.map.buttons.set_access_point'),
drawPolyButton: i18n.t('zones.map.buttons.draw_polygon'),
drawCircleButton: i18n.t('zones.map.buttons.draw_circle'),
editButton: i18n.t('zones.map.buttons.edit_layers'),
dragButton: i18n.t('zones.map.buttons.move_layers'),
deleteButton: i18n.t('zones.map.buttons.remove_layers'),
rotateButton: i18n.t('zones.map.buttons.rotate_layers'),
},
}
leafletMapInstance.pm.setLang('customName', customTranslation, i18n.locale)
}
export function useZoneModuleProvider() {
const getDefaultZoneItem = () => ({
name: '',
isAnalysis: false,
isInternal: false,
isShared: false,
})
let lastEnableClustering = null
const state = reactive({
zoneTypesList: [],
zonesList: [],
zoneCategoriesList: [],
mapFilteredTypes: [],
simplicitiMapVM: ref(null),
mapVM: ref(null),
targetZoneItem: null, //Zone being zoomed
isMapConfigured: false,
stateFlags: [],
geomanControls: false,
currentZone: getDefaultZoneItem(),
currentZoneType: {},
leafletZoneLayer: null,
leafletZoneAccessLayer: null,
})
const interactionMode = ref('view') //view / edit / create
window.zms = state
watchEffect(watchEffectHandlerForLeafletGeomanToolbarToogle)
//These will configure the map interactions and update the zone markers
watch(() => state.mapVM, watchLog('mapVM', updateMapLayersHandler))
watch(() => state.zonesList, watchLog('zonesList', updateMapLayersHandler), {
deep: true,
})
watch(
() => state.mapFilteredTypes,
watchLog('mapFilteredTypes', updateMapLayersHandler),
{
deep: true,
}
)
watch(interactionMode, () => {
if (state.mapVM) {
let pm = state.mapVM?.map?.pm
if (pm) {
pm.disableDraw() //Disable drawing mode when user switch from/to view/edit/create
}
}
if (['edit', 'create'].includes(interactionMode.value)) {
store.state.simpliciti_map.mapReverseGeocodingOnMouseClickEnabled = false
//Issue: A single leaflet layer group do not support clustering a non-clustering mode at the same time
//Solution 1: If clustering is enabled, zoom is restricted to avoid rendering other layers in clustering mode (state.mapVM.map.setMinZoom(16) )
///state.mapVM.map.setMinZoom(16)
//Solution 2: Render clustering and non-clustering layers in different layer groups
//Solution 3: Remove clustering so that there is not zoom restriction (Active) (state.mapVM.map.setMinZoom(null))
state.mapVM.map.setMinZoom(null)
} else {
state.mapVM.map.setMinZoom(null)
store.state.simpliciti_map.mapReverseGeocodingOnMouseClickEnabled = true
}
if ('edit' === interactionMode.value) {
setTimeout(() => {
updateStateLeafletLayers() //Fix for leafletZoneLayer/leafletZoneAccessLayer undefined when editing a zone item
}, 1000)
}
})
watch(
() => state.simplicitiMapVM?.zoomLevel,
watchLog('zoomLevel', updateMapLayersHandler)
)
watch(
() => state.simplicitiMapVM?.moveendTimestamp,
watchLog('moveendTimestamp', updateMapLayersHandler)
)
function watchLog(message, handler) {
return () => {
logging && console.log('zm::watch::' + message)
handler()
}
}
const scope = {
state,
zoneTypeIcons,
interactionMode,
//Getters
isProcessing: computed(
() =>
!![
'savingZone',
'zonesListLoading',
'zoneTypesListLoading',
'zonesCategoriesLoading',
].find((name) => state.stateFlags.includes(name))
),
hasMutationInProgress: computed(() =>
state.stateFlags.includes('savingZone')
),
isZonesListLoading: computed(() =>
state.stateFlags.includes('zonesListLoading')
),
isZoneTypesListLoading: computed(() =>
state.stateFlags.includes('zoneTypesListLoading')
),
isZoneCategoriesListLoading: computed(() =>
state.stateFlags.includes('zonesCategoriesLoading')
),
//Actions,
mapZoneItemFitBounds,
/**
* @param {*} item
*/
async saveZone(item) {
const stateFlag = addStateFlag('savingZone')
try {
let newZone = await saveZone({ ...item })
scope.resetCurrentZoneItem()
scope.updateZoneInsideLocalZonesList(newZone)
stateFlag.clear()
scope.refreshMapLayers()
apiCacheStorage.invalidateCacheByKeyInclude('area/areas').then(() => {
getZones().then(() => {
logging && console.log('zones cache updated successfully')
})
})
} catch (err) {
stateFlag.clear()
throw err
}
},
resetCurrentZoneItem() {
state.currentZone = getDefaultZoneItem()
if (state.leafletZoneLayer) {
state.leafletZoneLayer.remove()
state.leafletZoneLayer = null
}
if (state.leafletZoneAccessLayer) {
state.leafletZoneAccessLayer.remove()
state.leafletZoneAccessLayer = null
}
},
refreshMapLayers: updateMapLayersHandler,
updateZoneInsideLocalZonesList(zoneItem) {
let others = state.zonesList.filter((item) => item.id !== zoneItem.id)
state.zonesList = [...others, Object.freeze(zoneItem)]
},
async updateZones() {
logging && console.log('updateZones')
let firstCallbackCalled = false
const stateFlag = addStateFlag('zonesListLoading')
state.zonesList.length = 0
state.zonesList = await getZones({
callback(items) {
if (firstCallbackCalled) {
return //We only grab the first pack of data (1000) and then we wait for the final response.
}
firstCallbackCalled = true
state.zonesList = [...state.zonesList, ...items]
logging && console.log('Progresively loading the zone list')
},
})
stateFlag.clear()
},
updateZoneTypes: async () => {
const stateFlag = addStateFlag('zoneTypesListLoading')
//state.zoneTypesList = await getZoneTypes()
state.zoneTypesList.length = 0
state.zoneTypesList = await api.zoneType.getAllPooling({
sort: (a, b) => (a.name < b.name ? -1 : 1),
})
stateFlag.clear()
state.mapFilteredTypes = [
...state.zoneTypesList.map((o) => Object.freeze(o)),
]
},
updateZoneCategories: async () => {
const stateFlag = addStateFlag('zonesCategoriesLoading')
state.zoneCategoriesList = await getZoneCategories()
stateFlag.clear()
},
editZoneItem,
updateStateLeafletLayers,
}
window.zmss = scope
return scope
function editZoneItem(zoneItem) {
state.currentZone = { ...zoneItem }
interactionMode.value = 'edit'
state.mapVM.map.setView([zoneItem.lat, zoneItem.lng], 18)
}
function watchEffectHandlerForLeafletGeomanToolbarToogle() {
logging && console.log('watch::add-or-remove-map-toolbar')
if (
['edit', 'create'].includes(interactionMode.value) &&
state.mapVM &&
!state.geomanControls
) {
state.mapVM.map.pm.addControls({
position: 'topleft',
drawCircle: true,
drawPolygon: true,
drawPolyline: false,
drawCircleMarker: false,
drawText: false,
cutPolygon: false,
drawRectangle: false,
})
state.geomanControls = true
}
if (interactionMode.value === 'view' && state.geomanControls) {
state.mapVM.map.pm.removeControls()
state.geomanControls = false
}
}
/**
* Removes the leaflet layer from the zones layer group (only if the layer group exists)
* @param {*} layer
*/
function removeLayerSafe(layer) {
if (state.mapVM.layerGroups.zones) {
state.mapVM.layerGroups.zones.removeLayer(layer)
}
}
/**
*
* @param {*} e Leaflet Geoman event
*/
async function onLeafletGeomanLayerCreated(e) {
logging && console.log('pm:create')
let newLayer = e.layer
let isZoneLayer = ['Circle', 'Polygon'].includes(e.shape)
let isZoneAccessLayer = ['Marker'].includes(e.shape)
newLayer.options.pmIgnore = false
Object.keys(state.currentZone || {}).forEach(
(key) => (newLayer.options[key] = state.currentZone[key])
)
if (isZoneLayer) {
newLayer.options.isZoneGeometry = true
if (state.leafletZoneLayer) {
state.leafletZoneLayer.remove()
removeLayerSafe(state.leafletZoneLayer)
}
state.leafletZoneLayer = newLayer
}
if (isZoneAccessLayer) {
//Validate: If no address found via reverse geocoding, cancel
let reversed = await geocodingPlugin.reverseGeocoding({
latitude: newLayer.getLatLng().lat,
longitude: newLayer.getLatLng().lng,
})
if (!reversed.city) {
newLayer.remove()
showToast('zones.draw_access_layer_reverse_geocoding_fail', 'warning')
return
}
if (state.leafletZoneAccessLayer) {
state.leafletZoneAccessLayer.remove()
removeLayerSafe(state.leafletZoneAccessLayer)
}
state.leafletZoneAccessLayer = newLayer
}
L.PM.reInitLayer(newLayer)
}
function mapZoneItemFitBounds(zoneItem) {
if (!state.mapVM) {
return
}
state.targetZoneItem = zoneItem
state.mapVM.map.flyTo([zoneItem.lat, zoneItem.lng], 18)
}
function configureMapInteractions() {
logging && console.log('zm::configureMapInteractions')
configureGeomanTranslations(state.mapVM.map)
state.mapVM.map.pm.setGlobalOptions(globalLeafletGeomanOptions)
state.mapVM.map.on('pm:create', onLeafletGeomanLayerCreated)
//state.mapVM.map.on('zoomend', updateMapLayersHandler)
state.mapVM.map.on('pm:remove', (e) => {
if (e.shape === 'Marker') {
state.leafletZoneAccessLayer = null
}
if (['Circle', 'Polygon'].includes(e.shape)) {
state.leafletZoneLayer = null
}
})
state.isMapConfigured = true
}
function updateMapLayersHandler() {
queueOperationOnce(
`zones_module__updateMapLayersHandlerRunSequential`,
updateMapLayersHandlerRunSequential,
{
clearPreviousTimeout: false, //Will skip if already queued
isSequential: true,
timeout: 200,
resolve(success) {
if (success === false) {
updateMapLayersHandler() //e.g if map is not ready yet, retry
}
},
}
)
}
async function updateMapLayersHandlerRunSequential() {
if (!state.mapVM?.map) {
return false //LeafletMap is not ready yet
}
logging && console.log('updateMapLayers::run')
if (state.zonesList.length === 0) {
logging && console.log('updateMapLayers::start::no-zones-skip')
return true
}
if (!state.isMapConfigured) {
configureMapInteractions()
}
let zonesList = state.zonesList.map((item) => ({ ...item })) //remove ref
let mapBounds = state.mapVM.map.getBounds()
//Update: Disable clustering at all levels for testing
let enableClustering = false //state.simplicitiMapVM.zoomLevel <= 14
//Clear markers when transitioning between clustering and non-clustering mode
if (
typeof lastEnableClustering === 'boolean' &&
lastEnableClustering !== enableClustering
) {
state.mapVM.clearLayers('zones', {
removeLayerGroup: true,
})
logging && console.log('updateMapLayers::clear')
}
lastEnableClustering = enableClustering
let clusteringOptions = {}
if (enableClustering) {
clusteringOptions = {
clusterOptions: {
singleMarkerMode: true,
iconCreateFunction: function (cluster) {
return getClusterIcon(cluster)
},
},
}
}
let mapZoom = state.mapVM?.map?._zoom || 5
let visible = true
let filteredZones = []
if (mapZoom < 6) {
visible = false
} else {
filteredZones = zonesList.filter((zoneItem) => {
const typeMatch = state.mapFilteredTypes.some(
(typeItem) => typeItem.id === zoneItem.typeId
)
const boundsMatch = mapBounds.contains(
L.latLng(zoneItem.lat, zoneItem.lng)
)
const hasCoordinates = zoneItem.lat && zoneItem.lng
return typeMatch && boundsMatch && hasCoordinates
})
if (filteredZones.length > 1000) {
filteredZones = filteredZones.slice(0, 1000) //Only show 1k elements at time
}
/*
let filteredZonesOut = zonesList.filter(
(zoneItem) =>
!state.mapFilteredTypes.some(
(typeItem) => typeItem.id === zoneItem.typeId
)
)
let filteredZonesOutsideCurrentViewport = filteredZones.filter(
(item) => !mapBounds.contains(L.latLng(item.lat, item.lng))
)*/
/*
let invalidZones = filteredZones.filter(
(zoneItem) => !zoneItem.lat || !zoneItem.lng
)
if (invalidZones.length > 0) {
console.warn('Invalid zones', {
invalidZones: invalidZones,
})
}
logging &&
console.log('updateMapLayers::draw', {
invalidZonesLen: invalidZones.length,
})
*/
}
await state.mapVM.drawGeometries({
...clusteringOptions,
data: filteredZones,
layer: 'zones',
visible,
generate(item) {
if (enableClustering) {
return L.marker([item.lat, item.lng]) //Dummy marker
}
let layers = generateLeafletZoneGeometriesFromItem(item, state)
let [zoneLayer] = layers
zoneLayer.on('mouseover', (e) => {
if (e.originalEvent) {
e.originalEvent.stopPropagation()
} else {
e.stopPropagation && e.stopPropagation()
}
item = state.zonesList.find((z) => z.id === item.id) //Ensure item is up to date
mitt.emit('zone_module__zone_layer__hover', item)
})
zoneLayer.on('dblclick', (e) => {
if (e.originalEvent) {
e.originalEvent.stopPropagation()
} else {
e.stopPropagation && e.stopPropagation()
}
item = state.zonesList.find((z) => z.id === item.id) //Ensure item is up to date
editZoneItem(item)
})
return layers
},
preserveExistingLayers: !enableClustering,
update(layer, item, layers) {
item = state.zonesList.find((z) => z.id === item.id) //Ensure item is up to date
let isVisible =
!enableClustering &&
state.mapFilteredTypes.some((typeItem) => typeItem.id === item.typeId)
if (!isVisible) {
layers.forEach((l) => l.remove()) //This will remove the layer from the map bet will be keep in the layer group
return true
} else {
layers.forEach((layer) => {
if (!layer._map) {
layer.addTo(state.mapVM.map) //Add again to the map if removed (e.g toggle zones using map filters)
}
})
}
let accessPointLayer = layers.find((l) => l.options.isAccessPoint)
let textLayer = layers.find((l) => l.options.isText)
let zoneLayer = layers.find((l) => l.options.isZoneGeometry)
function skipLayerUpdateAndGenerate() {
if (zoneLayer) {
zoneLayer.remove()
removeLayerSafe(zoneLayer)
}
if (accessPointLayer) {
accessPointLayer.remove()
removeLayerSafe(accessPointLayer)
}
if (textLayer) {
textLayer.remove()
removeLayerSafe(textLayer)
}
return false //generate zone from scratch
}
let shouldTextAndAccessLayerBeVisible = mapZoom > 14
//Zone text and access icon are show/hide based on zoom. If sync is needed, generate the zone again.
if (
(!shouldTextAndAccessLayerBeVisible &&
(!!textLayer || !!accessPointLayer)) ||
(shouldTextAndAccessLayerBeVisible &&
(!textLayer || !accessPointLayer))
) {
return skipLayerUpdateAndGenerate()
}
if (accessPointLayer) {
accessPointLayer.setLatLng([item.accessLat, item.accessLng])
}
if (textLayer) {
textLayer.setLatLng([item.lat, item.lng])
let showLabel = mapZoom >= 17
if (
item.iconSrc !== zoneLayer.options.iconSrc ||
zoneLayer.options.name !== item.name
) {
textLayer.setIcon(
createZoneTextLayerIcon(item.iconSrc, item.name, showLabel)
)
} else {
if (textLayer.showLabel !== showLabel) {
textLayer.setIcon(
createZoneTextLayerIcon(item.iconSrc, item.name, showLabel)
)
}
textLayer.showLabel = showLabel //Why storing label in layer?
}
}
//Circle zone
if (zoneLayer.setLatLng) {
zoneLayer.setLatLng([item.lat, item.lng])
}
//Polygon zone
if (zoneLayer.setLatLngs) {
zoneLayer.setLatLngs(item.polygon)
}
zoneLayer.setStyle({ color: item.color, fillColor: item.color })
if (!zoneLayer.setLatLng) {
logging &&
console.log(
'updateMapLayers::drawGeometries::update: zoneLayer update fail',
{
zoneLayer,
}
)
}
return true
},
afterUpdate({ updatedLayers, notUpdatedLayers }) {
notUpdatedLayers.forEach((l) => l.remove()) //i.g If we delete a zone it means the associated layer will not be updated when fetching zones again. We remove those orphan layers.
let shouldCheckTextCollisions = (state.mapVM?.map?._zoom || 5) >= 17
if (shouldCheckTextCollisions) {
setTimeout(() => {
//Without timeout, zones layers are not updated somehow
toggleTextLayersBasedOnCollisions({
items: filteredZones,
layerGroup: state.mapVM.layerGroups.zones,
})
logging && console.log('toggleTextLayersBasedOnCollisions')
}, 500)
}
if (
interactionMode.value === 'edit' &&
(!state.leafletZoneAccessLayer || !state.leafletZoneLayer)
) {
updateStateLeafletLayers()
}
logging &&
console.log('updateMapLayers::drawGeometries:afterUpdate', {
updatedLayers: updatedLayers.length,
notUpdatedLayers: notUpdatedLayers.length,
})
},
})
logging && console.log('updateMapLayers::draw:end')
return true
}
/**
* Hides text layers if overlaps with other text layers (Only layers visible in the current viewport)
*
*/
function toggleTextLayersBasedOnCollisions({ items, layerGroup }) {
let allTextLayers = layerGroup
.getLayers()
.filter((layer) => layer.options.isText)
let track = console.trackTime('Checking collisions (text layers)', false)
track.count('allTextLayers', {
allTextLayers: allTextLayers,
allLayers: layerGroup.getLayers(),
})
let itemsAndTextLayers = items.map((zoneItem) => ({
textLayer: allTextLayers.find(
(layer) => layer.externalId === zoneItem.id
),
zoneItem,
}))
track.count('itemsAndTextLayers', {
itemsAndTextLayers: itemsAndTextLayers,
})
let itemsWithoutTextLayer = itemsAndTextLayers.filter((o) => !o.textLayer)
let itemsWithTextLayer = itemsAndTextLayers.filter((o) => o.textLayer)
itemsWithoutTextLayer.forEach(({ zoneItem }) =>
track.count(`has-no-text-layer (${zoneItem.name})`)
)
itemsWithTextLayer.forEach(({ zoneItem }) =>
track.count(`has-text-layer (${zoneItem.name})`)
)
//Traverse items
itemsWithTextLayer.forEach(({ zoneItem, textLayer }) => {
let overlap = doesTextOverlapInViewport({
zoneItem,
textLayer,
itemsWithTextLayer,
})
if (overlap) {
if (state.targetZoneItem?.id === zoneItem.id) {
track.count(
'overlap for ' + zoneItem.name,
'skip because is a zone being targeted'
)
return
}
track.count('overlap for ' + zoneItem.name)
textLayer.setIcon(
createZoneTextLayerIcon(zoneItem.iconSrc, zoneItem.name, false)
) //Hide text node
} else {
track.count('no-overlap for ' + zoneItem.name)
}
})
track()
function doesTextOverlapInViewport({
zoneItem,
textLayer,
itemsWithTextLayer,
}) {
function collide(elementA, elementB) {
//Get the bounding rectangles of the two elements
var rectA = elementA.getBoundingClientRect()
var rectB = elementB.getBoundingClientRect()
//Check if the two rectangles overlap
return (
rectA.left < rectB.right &&
rectA.right > rectB.left &&
rectA.top < rectB.bottom &&
rectA.bottom > rectB.top
)
}
let textLayersToCompare = itemsWithTextLayer.filter(
({ zoneItem: item }) => {
return item.id !== zoneItem.id
}
)
logging &&
console.log('doesTextOverlapInViewport', {
textLayersToCompare,
textLayer,
})
return textLayersToCompare.some(({ textLayer: currTextLayer }) => {
//lealfet bounds intersects doesn't seem to work ok on markers
//return currTextLayer.getBounds().intersects(textLayer.getBounds())
//lets compare the name dom node for collisions
let span1 = currTextLayer.getElement().querySelector('span')
let span2 = textLayer.getElement().querySelector('span')
if (!span1 || !span2) {
return false //At some point, if we start hiding dom nodes, they will not be available here
}
/*console.log('doesTextOverlapInViewport', {
span1: span1.innerHTML,
span2: span2.innerHTML,
value: collide(span1, span2),
})*/
return collide(span1, span2)
})
}
}
/**
* Sync Leaflet layers (zone and access) into state (Edit)
*/
function updateStateLeafletLayers() {
logging && console.log('updateStateLeafletLayers')
let id = state.currentZone.id
let layers = []
state.mapVM.map.eachLayer((l) => layers.push(l))
if (!state.leafletZoneAccessLayer) {
state.leafletZoneAccessLayer = layers.find(
(l) =>
(l.externalId == id || l.options.id == id) && l.options.isAccessPoint
)
if (state.leafletZoneAccessLayer) {
//throw new Error('Associated layer not found (Access point)')
state.leafletZoneAccessLayer.options.pmIgnore = false
L.PM.reInitLayer(state.leafletZoneAccessLayer)
}
}
if (!state.leafletZoneLayer) {
state.leafletZoneLayer = layers.find(
(l) =>
(l.externalId == id || l.options.id == id) && l.options.isZoneGeometry
)
if (state.leafletZoneLayer) {
//throw new Error('Associated layer not found (Zone geometry)')
state.leafletZoneLayer.options.pmIgnore = false
L.PM.reInitLayer(state.leafletZoneLayer)
}
}
}
function addStateFlag(name) {
state.stateFlags = [...[name], ...R.without([name], state.stateFlags)]
return {
clear() {
state.stateFlags = [...R.without([name], state.stateFlags)]
},
}
}
}
/**
* Will generate a Circle/Polygon along with an access point icon and the Zone name (Text node)
* @param {*} item
* @param {*} param1
* @returns
*/
function generateLeafletZoneGeometriesFromItem(item, state) {
let pathOptions = {
weight: 2,
color: item.color,
fill: true,
fillOpacity: 0.8,
fillColor: item.color,
}
let geometry = item.isPolygon
? L.polygon(item.polygon, {
...pathOptions,
isZoneGeometry: true,
})
: L.circle([item.lat, item.lng], {
...pathOptions,
radius: item.radius,
isZoneGeometry: true,
})
let accessPointGeometry =
item.accessLat && item.accessLng
? L.marker([item.accessLat, item.accessLng], {
icon: L.divIcon({
className: 'zone_marker__access_point_icon',
html: `<svg xmlns="http: //www. W3. Org/2000/svg" width="18.385" height="18.385" viewbox="0 0 18.385 18.385" > <g class="a" transform="translate(9.192) rotate(45)" : fill="var(--color-dark-blue)" > <rect class="b" style="stroke: none" stroke="none" width="13" height="13" rx="2" /> <rect class="c" style="fill: none" fill="none" x="0.5" y="0.5" width="12" height="12" rx="1.5" /> </g> </svg>`,
}),
isAccessPoint: true,
id: item.id,
})
: null
let mapZoom = state.mapVM?.map?._zoom || 5
let showLabel = mapZoom > 16
let textGeometry = new L.marker([item.lat, item.lng], {
opacity: 1,
icon: createZoneTextLayerIcon(item.iconSrc, item.name, showLabel),
isText: true,
})
const layers = [geometry]
//Render rules:
//- Zone layer: Zoom level below 50km
//- Zone icon: Zoom level below 500m
//- Zone text: Zoom level below 100m
if (mapZoom > 14) {
layers.push(textGeometry, accessPointGeometry)
}
return layers.filter((g) => !!g)
}
function createZoneTextLayerIcon(iconSrc, name = '', showName = true) {
let nameHTML = `<div><span>${showName ? name : ''}</span></div>`
return L.divIcon({
html: `<div>
<img src="${iconSrc}" title="${name}"/>
${nameHTML}
</div>
`,
iconAnchor: [16, 2],
className: 'zone_marker__text_icon',
})
}
/**
* Uses the same icon as identification module
* @param {*} cluster
* @returns
*/
function getClusterIcon(cluster) {
return L.divIcon({
className: 'cluster_marker',
html: `<div class="cluster_position_marker cluster_identification_marker">${cluster.getChildCount()}</div>`,
})
/*
let size = 21
size = cluster.getChildCount() > 99 ? 16 : size
size = cluster.getChildCount() > 999 ? 12 : size
return L.divIcon({
className: 'cluster_marker',
html: `<div class="marker__content">
<div>
<img class="marker_icon alert_marker_icon"
src="./lib/realtimeMap/assets/picto_pins/Pin.svg">
<div class="alert_marker_icon_inner marker_icon_cluster_text" style="font-size:${size}px;">
${cluster.getChildCount()}
</div>
</img>
</div>`,
})*/
}
Source