Source

components/shared/SimplicitiMap/SimplicitiMap.vue

<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>