Initial commit from monorepo
This commit is contained in:
281
app/components/RouteMap.vue
Normal file
281
app/components/RouteMap.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<ClientOnly>
|
||||
<MapboxMap
|
||||
:key="mapId"
|
||||
:map-id="mapId"
|
||||
:style="`height: ${height}px; width: 100%;`"
|
||||
class="rounded-lg border border-base-300"
|
||||
:options="mapOptions"
|
||||
@load="onMapCreated"
|
||||
>
|
||||
<MapboxNavigationControl position="top-right" />
|
||||
</MapboxMap>
|
||||
|
||||
<template #fallback>
|
||||
<div class="h-96 w-full bg-base-200 rounded-lg flex items-center justify-center">
|
||||
<p class="text-base-content/60">{{ t('routeMap.states.loading') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Map as MapboxMapType } from 'mapbox-gl'
|
||||
import { LngLatBounds, Popup } from 'mapbox-gl'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
stages: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 400
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const mapRef = ref<MapboxMapType | null>(null)
|
||||
const didFitBounds = ref(false)
|
||||
|
||||
const instanceId = getCurrentInstance()?.uid || Math.floor(Math.random() * 100000)
|
||||
const mapId = computed(() => `route-map-${instanceId}`)
|
||||
|
||||
const routePoints = computed(() => {
|
||||
const points: Array<{ id: string; name: string; lat: number; lng: number; companies: any[] }> = []
|
||||
|
||||
props.stages.forEach((stage: any) => {
|
||||
if (stage.stageType === 'transport') {
|
||||
if (stage.sourceLatitude && stage.sourceLongitude) {
|
||||
const existingPoint = points.find(p => p.lat === stage.sourceLatitude && p.lng === stage.sourceLongitude)
|
||||
if (!existingPoint) {
|
||||
points.push({
|
||||
id: `source-${stage.uuid}`,
|
||||
name: stage.sourceLocationName || t('routeMap.points.source'),
|
||||
lat: stage.sourceLatitude,
|
||||
lng: stage.sourceLongitude,
|
||||
companies: getStageCompanies(stage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (stage.destinationLatitude && stage.destinationLongitude) {
|
||||
const existingPoint = points.find(p => p.lat === stage.destinationLatitude && p.lng === stage.destinationLongitude)
|
||||
if (!existingPoint) {
|
||||
points.push({
|
||||
id: `dest-${stage.uuid}`,
|
||||
name: stage.destinationLocationName || t('routeMap.points.destination'),
|
||||
lat: stage.destinationLatitude,
|
||||
lng: stage.destinationLongitude,
|
||||
companies: getStageCompanies(stage)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (stage.locationLatitude && stage.locationLongitude) {
|
||||
const existingPoint = points.find(p => p.lat === stage.locationLatitude && p.lng === stage.locationLongitude)
|
||||
if (!existingPoint) {
|
||||
points.push({
|
||||
id: `service-${stage.uuid}`,
|
||||
name: stage.locationName || t('routeMap.points.service'),
|
||||
lat: stage.locationLatitude,
|
||||
lng: stage.locationLongitude,
|
||||
companies: getStageCompanies(stage)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return points
|
||||
})
|
||||
|
||||
const mapCenter = computed<[number, number]>(() => {
|
||||
if (!routePoints.value.length) return [0, 0]
|
||||
|
||||
const lats = routePoints.value.map(p => p.lat).filter(Boolean)
|
||||
const lngs = routePoints.value.map(p => p.lng).filter(Boolean)
|
||||
|
||||
if (!lats.length || !lngs.length) return [0, 0]
|
||||
|
||||
const avgLat = lats.reduce((a, b) => a + b, 0) / lats.length
|
||||
const avgLng = lngs.reduce((a, b) => a + b, 0) / lngs.length
|
||||
|
||||
return [avgLng, avgLat]
|
||||
})
|
||||
|
||||
const mapOptions = computed(() => ({
|
||||
style: 'mapbox://styles/mapbox/streets-v12',
|
||||
center: mapCenter.value,
|
||||
zoom: 4
|
||||
}))
|
||||
|
||||
const routeCoordinates = computed(() => {
|
||||
return routePoints.value.map(point => [point.lng, point.lat])
|
||||
})
|
||||
|
||||
const pointsFeatureCollection = computed(() => ({
|
||||
type: 'FeatureCollection' as const,
|
||||
features: routePoints.value.map(point => ({
|
||||
type: 'Feature' as const,
|
||||
properties: {
|
||||
id: point.id,
|
||||
name: point.name,
|
||||
companies: point.companies
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point' as const,
|
||||
coordinates: [point.lng, point.lat]
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
const lineFeatureCollection = computed(() => ({
|
||||
type: 'FeatureCollection' as const,
|
||||
features: routeCoordinates.value.length > 1
|
||||
? [
|
||||
{
|
||||
type: 'Feature' as const,
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'LineString' as const,
|
||||
coordinates: routeCoordinates.value
|
||||
}
|
||||
}
|
||||
]
|
||||
: []
|
||||
}))
|
||||
|
||||
const updateSources = () => {
|
||||
const map = mapRef.value
|
||||
if (!map) return
|
||||
|
||||
const lineSource = map.getSource('route-line') as mapboxgl.GeoJSONSource | undefined
|
||||
if (lineSource) {
|
||||
lineSource.setData(lineFeatureCollection.value)
|
||||
}
|
||||
|
||||
const pointSource = map.getSource('route-points') as mapboxgl.GeoJSONSource | undefined
|
||||
if (pointSource) {
|
||||
pointSource.setData(pointsFeatureCollection.value)
|
||||
}
|
||||
}
|
||||
|
||||
const fitMapToData = () => {
|
||||
const map = mapRef.value
|
||||
if (!map || didFitBounds.value) return
|
||||
|
||||
const coordinates = routeCoordinates.value
|
||||
if (!coordinates.length) return
|
||||
|
||||
const bounds = coordinates.reduce(
|
||||
(acc, coord) => acc.extend(coord as [number, number]),
|
||||
new LngLatBounds(
|
||||
coordinates[0] as [number, number],
|
||||
coordinates[0] as [number, number]
|
||||
)
|
||||
)
|
||||
|
||||
map.fitBounds(bounds, {
|
||||
padding: 40,
|
||||
maxZoom: 8
|
||||
})
|
||||
|
||||
didFitBounds.value = true
|
||||
}
|
||||
|
||||
const onMapCreated = (map: MapboxMapType) => {
|
||||
mapRef.value = map
|
||||
|
||||
const initMap = () => {
|
||||
map.addSource('route-line', {
|
||||
type: 'geojson',
|
||||
data: lineFeatureCollection.value
|
||||
})
|
||||
|
||||
map.addLayer({
|
||||
id: 'route-line-layer',
|
||||
type: 'line',
|
||||
source: 'route-line',
|
||||
layout: {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round'
|
||||
},
|
||||
paint: {
|
||||
'line-color': 'var(--color-primary, #3b82f6)',
|
||||
'line-width': 4,
|
||||
'line-opacity': 0.75
|
||||
}
|
||||
})
|
||||
|
||||
map.addSource('route-points', {
|
||||
type: 'geojson',
|
||||
data: pointsFeatureCollection.value
|
||||
})
|
||||
|
||||
map.addLayer({
|
||||
id: 'route-points-layer',
|
||||
type: 'circle',
|
||||
source: 'route-points',
|
||||
paint: {
|
||||
'circle-radius': 7,
|
||||
'circle-color': '#f97316',
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': '#ffffff'
|
||||
}
|
||||
})
|
||||
|
||||
map.on('click', 'route-points-layer', (e) => {
|
||||
const coordinates = (e.features?.[0].geometry as GeoJSON.Point).coordinates.slice() as [number, number]
|
||||
const props = e.features?.[0].properties
|
||||
const name = props?.name || t('routeMap.points.service')
|
||||
|
||||
new Popup()
|
||||
.setLngLat(coordinates)
|
||||
.setHTML(`<strong>${name}</strong>`)
|
||||
.addTo(map)
|
||||
})
|
||||
|
||||
map.on('mouseenter', 'route-points-layer', () => { map.getCanvas().style.cursor = 'pointer' })
|
||||
map.on('mouseleave', 'route-points-layer', () => { map.getCanvas().style.cursor = '' })
|
||||
|
||||
updateSources()
|
||||
fitMapToData()
|
||||
}
|
||||
|
||||
if (map.loaded()) {
|
||||
initMap()
|
||||
} else {
|
||||
map.on('load', initMap)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [routePoints.value, routeCoordinates.value],
|
||||
() => {
|
||||
didFitBounds.value = false
|
||||
updateSources()
|
||||
fitMapToData()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const getStageCompanies = (stage: any) => {
|
||||
const companies: any[] = []
|
||||
|
||||
if (stage.selectedCompany) {
|
||||
companies.push(stage.selectedCompany)
|
||||
}
|
||||
|
||||
const uniqueCompanies = new Set()
|
||||
stage.trips?.forEach((trip: any) => {
|
||||
if (trip.company && !uniqueCompanies.has(trip.company.uuid)) {
|
||||
uniqueCompanies.add(trip.company.uuid)
|
||||
companies.push(trip.company)
|
||||
}
|
||||
})
|
||||
|
||||
return companies
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user