Source

components/nearby_vehicles/NearbyVehiclesFeature.vue

<template>
  <div>
    <div class="title">{{ $t('nearby_vehicles.feature_title') }}</div>
    <NearbyVehiclesForm v-model="radius" class="mt-2"> </NearbyVehiclesForm>
  </div>
</template>
<script setup>
import NearbyVehiclesForm from './NearbyVehiclesForm.vue'
import { onMounted, onBeforeUnmount, inject } from 'vue'
import { fetchNearbyVehicles } from '@/services/nearby-vehicles-service.js'

const props = defineProps({
  lat: {
    type: Number,
    default: 0,
  },
  lng: {
    type: Number,
    default: 0,
  },
  formattedAddress: {
    type: String,
    default: '',
  },
})
const radius = ref(0)

const setMapToolboxContentMaxWidth = inject(
  'setMapToolboxContentMaxWidth',
  null
)

watchEffect(() => {
  if (setMapToolboxContentMaxWidth) {
    if (providerState.shouldRender && providerState.radius > 0) {
      setMapToolboxContentMaxWidth('initial')
    } else {
      setMapToolboxContentMaxWidth('465px')
    }
  }
})

watchEffect(() => {
  providerState.lat = props.lat
  providerState.lng = props.lng
  providerState.radius = radius.value
  providerState.formattedAddress = props.formattedAddress
})

onMounted(() => {
  resetProviderState()
})
onBeforeUnmount(() => {
  resetProviderState()
})
</script>
<script>
import { ref, reactive, watchEffect, computed } from 'vue'
import L from 'leaflet'
import { createMarkerFromRealtimeItem } from '@/mixins/map.js'
import { drawLocateAddressMarker } from '@/components/shared/geocoding/geocoding-mixin.js'

let mapVmRef = ref(null)

/**
 * Module context pattern
 */
const providerState = reactive(newProviderState())

const filteredMapVehiclesList = computed(() => {
  return providerState.vehiclesList.filter(
    (item) => !providerState.hiddenVehicleMarkers.includes(item.id)
  )
})

const vehicleMarkersLayerName = 'nearbyVehiclesVehiclesMarkersLayer'
const radiusCircleLayerName = 'nearbyVehiclesRadiusCircleLayer'

const MapOperations = {
  showLocateAddressResultMarker(show = true) {
    if (show === false) {
      return mapVmRef.value.removeLayerGroup(
        'nearbyVehicleLocateAddressResultMarker'
      )
    }

    drawLocateAddressMarker(
      mapVmRef.value,
      providerState.lat,
      providerState.lng,
      providerState.formattedAddress,
      {
        layer: 'nearbyVehicleLocateAddressResultMarker',
        after: () =>
          mapVmRef.value.map.flyTo([providerState.lat, providerState.lng], 14),
      }
    )
  },
  showRadiusCircle(show = true) {
    if (show === false) {
      return mapVmRef.value.removeLayerGroup(radiusCircleLayerName)
    }
    mapVmRef.value.drawGeometries({
      layer: radiusCircleLayerName,
      data: [
        {
          id: 'nearbyVehiclesRadiusCircle',
        },
      ],
      generate(item) {
        return L.circle([providerState.lat, providerState.lng], {
          color: 'rgba(0,0,0,0.5)',
          fillColor: 'black',
          fillOpacity: 0.02,
          radius: providerState.radius * 1000,
        })
      },
    })
  },
  showVehicleMarkers(show = true) {
    if (show === false) {
      return mapVmRef.value.removeLayerGroupBy((layerGroup) =>
        layerGroup.name.includes('nearbyVehicles')
      )
    }
    let data = filteredMapVehiclesList.value.map((item) => {
      //We reuse createMarkerFromRealtimeItem which requires a realtime-like object
      let realtimeIsoItem = {
        ...item,
        lat: item.lat,
        lng: item.lng,
        cap: parseInt(item.vehicleOrientationDegrees),
        //vehicleName: item.vehicleName,
        //vehicleCategoryClassName: item.vehicleCategoryClassName,
        //vehicleStatusColor: item.vehicleStatusColor,
        driverChronoEnabled: false, //if true, add: driverChornoStatusOriginal
        raw: {},
      }
      return createMarkerFromRealtimeItem(realtimeIsoItem)
    })

    mapVmRef.value.drawGeometries({
      layer: vehicleMarkersLayerName,
      data,
      generate(item) {
        return L.marker([item.normalizedItem.lat, item.normalizedItem.lng], {
          icon: item.icon,
          zIndexOffset: 3,
        })
      },
      popupOptions: {
        style: 'min-width:267px;',
      },
      popup: () =>
        /* webpackChunkName "location_module" */
        import(
          '@c/location/LocationMap/LocationRealtimeMap/VehicleMarkerPopup.vue'
        ),
      /**
       * Fit bounds over vehicle markers
       */
      after: ({ map }) => {
        let latLngs = data
          .reduce(
            (a, v) =>
              a.concat([
                [
                  parseFloat(v.normalizedItem.lat),
                  parseFloat(v.normalizedItem.lng),
                ],
              ]),
            []
          )
          .filter((arr) => !!arr[0] && !!arr[1])
        if (!!latLngs && latLngs.length > 0) {
          map.flyToBounds(latLngs)
        }
      },
    })
  },
}

function newProviderState() {
  return {
    lat: 0, //<- Locate address lng
    lng: 0, //<- Locate address lng
    formattedAddress: '', //<- Locate address marker popup text
    radius: 0, //<- radius input from user
    vehiclesList: [], //<- Holds nearby vehicles items
    shouldRender: false, //<- Indicates the map should be rendered
    isProcessing: false, //<- Prevent us to call API twice
    lastPayloadId: null, //<- Prevent us to call API twice with the same parameters (soft cache)
    hiddenVehicleMarkers: [], //<- Allow us to filter out items hidden by users
  }
}

function resetProviderState(ignoreKeys = []) {
  let newState = newProviderState()
  for (var key of Object.keys(newState)) {
    if (ignoreKeys.length > 0 && ignoreKeys.includes(key)) {
      continue
    } else {
      providerState[key] = newState[key]
    }
  }
}

watchEffect(() => {
  if (!mapVmRef.value) {
    return false
  }
  if (providerState.shouldRender) {
    MapOperations.showRadiusCircle(
      providerState.lat && providerState.lng && providerState.radius
    )
    MapOperations.showLocateAddressResultMarker(
      providerState.lat && providerState.lng && providerState.formattedAddress
    )
    MapOperations.showVehicleMarkers(filteredMapVehiclesList.value.length > 0)
  } else {
    mapVmRef.value.removeLayerGroupBy((layerGroup) =>
      layerGroup.name.includes('nearbyVehicles')
    )
  }
})

export function useNearbyVehicles() {
  return {
    setMapVmRef(ref) {
      mapVmRef.value = ref ? ref.value : null
    },
    isProcessing: computed(() => providerState.isProcessing),
    isMapRequired: computed(() => providerState.shouldRender),
    vehiclesList: computed(() => providerState.vehiclesList),
    radiusKM: computed(() => providerState.radius),
    clearNearbyVehicles: () => resetProviderState(),
    clearNearbyVehiclesResults: () =>
      resetProviderState(['lat', 'lng', 'radius', 'formattedAddress']),
    updateNearbyVehicles,
    isVehicleMarkerVisible: (vehicleId) => {
      return !providerState.hiddenVehicleMarkers.includes(vehicleId)
    },
    toggleVehicleMarkerVisibility(vehicleId) {
      if (providerState.hiddenVehicleMarkers.includes(vehicleId)) {
        providerState.hiddenVehicleMarkers.splice(
          providerState.hiddenVehicleMarkers.indexOf(vehicleId),
          1
        )
      } else {
        providerState.hiddenVehicleMarkers.push(vehicleId)
      }
    },
    mapFlyTo(lat, lng) {
      if (mapVmRef.value) {
        mapVmRef.value.map.flyTo([lat, lng], 15)
      }
    },
  }
}

export function updateNearbyVehicles() {
  if (providerState.radius === 0) {
    providerState.vehiclesList = []
  }

  if (!providerState.lat || !providerState.lng || !providerState.radius) {
    return console.warn('Lat, Lng, and radius are required')
  }
  if (providerState.isProcessing) {
    return
  }
  providerState.isProcessing = true
  providerState.shouldRender = true
  let payload = {
    lat: providerState.lat,
    lng: providerState.lng,
    radius: providerState.radius,
  }

  let payloadId = window.btoa(JSON.stringify(payload))

  if (providerState.lastPayloadId === payloadId) {
    //Ignore if result is already in the provider context
    return console.log(
      'Ignore request, a response with the same payload is already in the provider context'
    )
  }

  fetchNearbyVehicles(payload.lat, payload.lng, payload.radius)
    .then((res = []) => {
      providerState.vehiclesList = res
    })
    .finally(() => {
      providerState.lastPayloadId = payloadId
      providerState.isProcessing = false
    })
}

export default {
  name: 'NearbyVehiclesFeature',
}
</script>
<style lang="scss" scoped>
.title {
  font: normal normal bold 14px/19px Open Sans;
  letter-spacing: 0px;
  color: var(--color-main);
}
</style>