Source

components/diagnostic/replay-mixin.js

import { mapGetters } from 'vuex'
import { getVehicleIconHTML } from '@/services/vehicle-service.js'
import { getBearingFromTwoLatAndLng } from '@/utils/map.js'
import { queueOperationOnce } from '@/utils/promise.js'
import mapMixin from '@/mixins/map.js'

const L = window.L //Attention, Leaflet should not be a global variable

/***
 * Used by Diagnostic module
 */
export default {
  inject: {
    replayController: {
      default: () => ({}),
    },
  },
  data() {
    return {
      replaySpeed: 1000,
      updateZoomEveryPositions: 3,
      isReplayPlaying: false,
    }
  },
  computed: {
    ...mapGetters({
      positions: 'diagnostics/positions',
      vehicleClassName: 'diagnostics/vehicleClassName',
      replayPositionEndIndex: 'diagnostics/replayPositionEndIndex',
      chartSelectedItemTrigger: 'diagnostics/chartSelectedItemTrigger',
    }),
    /**
     * Needs to be computed due to positions change
     * @returns
     */
    replayOptions() {
      console.log('computed::replayOptions')
      let self = this
      return {
        min: 0,
        max: this.$store.state.diagnostics.positions.length - 1,

        animationOptions: {
          index: this.positions
            .map((pos, index) => {
              return {
                replayIndex: index,
              }
            })
            .filter((pos) => {
              return (
                pos.replayIndex >=
                  self.$store.state.diagnostics.replayPositionIndex &&
                (self.$store.state.diagnostics.replayPositionEndIndex === -1 ||
                  pos.replayIndex <=
                    self.$store.state.diagnostics.replayPositionEndIndex)
              )
            })
            .map((pos) => {
              return {
                value: pos.replayIndex,
                duration: this.replaySpeed,
              }
            }),
        },

        get currentIndex() {
          return self.$store.state.diagnostics.replayPositionIndex
        },
        set currentIndex(value) {
          self.$store.state.diagnostics.replayPositionIndex = value
          self.$store.state.diagnostics.replayPosition =
            self.positions[self.$store.state.diagnostics.replayPositionIndex]
        },
        onToggle(val) {
          self.isReplayPlaying = val
        },
        onDestroy() {
          console.log('replay-mixin::options::onDestroy')
          updateMapVehicleMarker(self.$map.getLeafletWrapperVM(), null, {
            visible: false,
          })
        },
        /**
         * @todo Drawing operations could be split using requestAnimationFrame
         * @param {*} currentIndex
         * @returns
         */
        onAnimate(currentIndex) {
          //Hide position marker popup and remove (it available)
          let positionMarkerLayerGroup =
            self.$map.getLeafletWrapperVM().layerGroups?.positions
          if (positionMarkerLayerGroup) {
            let positionMarker = positionMarkerLayerGroup.getLayers()[0] || null
            if (positionMarker) {
              positionMarker.closePopup && positionMarker.closePopup()
            }
            positionMarkerLayerGroup.remove()
          }

          //This will open the position details on the left panel

          self.$store.state.diagnostics.chartSelectedItemTrigger = 'replay'

          self.$store.state.diagnostics.chartSelectedItem =
            self.$store.state.diagnostics.replayPosition

          if (!self.positions[currentIndex]) {
            console.log(`onAnimate::Can't find position`, {
              currentIndex: currentIndex,
            })
            return
          }

          let currentPosition = self.positions[currentIndex]

          //avoid drawing the lat/lng
          if (
            self.lastPosition &&
            self.lastPosition.lat == currentPosition.lat &&
            self.lastPosition.lng == currentPosition.lng
          ) {
            return
          }
          updateMapVehicleMarker(
            self.$map.getLeafletWrapperVM(),
            currentPosition,
            {
              lastPosition: self.lastPosition,
              slideDuration: self.replaySpeed,
              vehicleName: self.vehicleName,
              vehicleClassName: self.vehicleClassName,
            }
          )
          self.lastPosition = currentPosition

          self.timesSinceZoom =
            self.timesSinceZoom === undefined
              ? self.updateZoomEveryPositions
              : self.timesSinceZoom

          let zoomLevel = 16

          switch (self.replaySpeed) {
            case 1000: {
              zoomLevel = 15
              self.updateZoomEveryPositions = 3
              break
            }
            case 500: {
              zoomLevel = 14
              self.updateZoomEveryPositions = 2
              break
            }
            case 200: {
              zoomLevel = 13
              self.updateZoomEveryPositions = 1
              break
            }
            case 125: {
              zoomLevel = 12
              self.updateZoomEveryPositions = 1
              break
            }
            default: {
              zoomLevel = 10
              self.updateZoomEveryPositions = 0
              break
            }
          }

          if (self.timesSinceZoom >= self.updateZoomEveryPositions) {
            self.timesSinceZoom = 0
            self.$map
              .getLeafletWrapperVM()
              .map.flyTo([currentPosition.lat, currentPosition.lng], 16)
          } else {
            self.timesSinceZoom++
          }
        },
      }
    },
  },
  watch: {
    replaySpeed() {
      this.replayReset()
    },
    replayPositionEndIndex() {
      console.log('watch::replayPositionEndIndex')
      this.replayReset()
    },
    chartSelectedItemTrigger() {
      if (this.chartSelectedItemTrigger === 'chart') {
        console.log('watch::chartSelectedItemTrigger')
        this.replayReset()
      }
    },
  },
  methods: {
    /**
     * If playing: Restart playback (index might have changed)
     * If stopped: Stop playback / Remove marker
     */
    replayReset() {
      console.log('method::replayReset')
      if (this.replayController.currentAnimationPlaying) {
        this.replayController.destroyAnimation()
        this.replayPlayToggle()
      } else {
        this.replayController.destroyAnimation()
      }
    },
    replayPlayToggle() {
      this.replayController.playToggle(this.replayOptions)
    },
    replayPrev() {
      this.replayController.prev(this.replayOptions)
    },
    replayNext() {
      this.replayController.next(this.replayOptions)
    },
    terminateReplay() {
      this.replayController.playToggle({
        ...this.replayOptions,
        value: false,
      })
      this.replayController.destroyAnimation()

      updateMapVehicleMarker(this.$map.getLeafletWrapperVM(), null, {
        visible: false,
      })
    },
  },
  destroyed() {
    this.terminateReplay()
  },
}

function updateMapVehicleMarker(leafletVM, position, options = {}) {
  position = { ...position }
  let lastPosition = options.lastPosition
  let degValue = lastPosition
    ? getBearingFromTwoLatAndLng(
        [lastPosition.lat, lastPosition.lng],
        [position.lat, position.lng]
      )
    : 90
  let vehicleClassName = options.vehicleClassName
  let vehicleName = options.vehicleName || 'Vehicle'
  let hasContactOn = position.hasContactOn
  const icon = L.divIcon({
    className: 'realtime__marker',
    html: `<div class="marker__content">
              <div>
              <img class="marker_icon" 
                  data-computed-degrees="${degValue}"
                  style="transform:translate(-50%, -50%) rotate(${
                    degValue // > 180 ? 270 : 90
                  }deg)" 
                  src="./lib/realtimeMap/assets/picto_pins/Pin.svg">
              ${getVehicleIconHTML(vehicleClassName, {
                flipHorizontally: degValue > 180,
              })}
              <img class="marker__contact_icon" 
                  style="background-color:var(--color-contact-${
                    hasContactOn ? 'on' : 'off'
                  })"
                  src="./lib/realtimeMap/assets/picto_status/Contact.svg" />
              </div>
              <p class="marker__label">
              ${vehicleName}
              </p >
          </div>
          `,
  })

  position.positionId = position.id
  position.id = 'vehicle'
  let visible = options.visible !== undefined ? options.visible : true

  let popupOptions = {
    style: 'min-width:267px;',
  }
  let popupComponentImport = () =>
    /* webpackChunkName "shared_components" */
    import('@/components/shared/SimplicitiMap/PositionMarkerPopup.vue')

  let smoothUpdateOptions = visible
    ? {
        update(marker, newPos) {
          marker.setIcon(icon)
          marker.closePopup && marker.closePopup()

          //After one second: Update popup component
          queueOperationOnce(
            `diagnostics_replay_marker_popup_update`,
            () => {
              marker.properties = newPos
              mapMixin.methods.bindLeafletPopupComponent(
                marker,
                popupComponentImport,
                {
                  ...popupOptions,
                  parent: leafletVM,
                  props: {
                    map: leafletVM.map,
                    data: newPos,
                  },
                }
              )
            },
            {
              clearPreviousTimeout: true,
              timeout: 700, //This should save resources by avoiding instantiating multiple components in a very short amount of time (i.g speed x16)
            }
          )

          marker.slideTo([newPos.lat, newPos.lng], {
            duration: options.slideDuration || 1000, //Should be the same as replaySpeed
            keepAtCenter: false,
          })
        },
      }
    : {}

  let data = position ? [position] : []

  if (!visible) {
    data = []
  }

  leafletVM.drawGeometries({
    visible,
    data,
    layerOptions: {
      zIndex: 999,
    },
    layer: 'diagnosticsReplayMarker',
    popupOptions,
    popup: popupComponentImport,
    preserveExistingLayers: true,
    ...smoothUpdateOptions,
    generate(pos) {
      return L.marker([pos.lat, pos.lng], {
        icon,
        zIndexOffset: 3,
      })
    },
  })
}