Source

composables/zones-module.js

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>`,
  })*/
}