Improves the visual representation of traffic events on the globe: - Optimizes arc animation parameters for smoother transitions - Adds source and target ring animations for better event tracking - Skips visualization for nearby locations to reduce visual clutter - Updates color scheme to dark orange for better visibility - Synchronizes animation timings with event delays
202 lines
5.4 KiB
Vue
202 lines
5.4 KiB
Vue
<script setup>
|
|
import { useElementSize } from '@vueuse/core'
|
|
import { scaleSequentialSqrt } from 'd3-scale'
|
|
import { interpolateYlOrRd } from 'd3-scale-chromatic'
|
|
import Globe from 'globe.gl'
|
|
import { debounce } from 'lodash-es'
|
|
import { MeshPhongMaterial } from 'three'
|
|
|
|
const props = defineProps({
|
|
minutes: {
|
|
type: Number,
|
|
default: 60,
|
|
},
|
|
})
|
|
|
|
const time = inject('time')
|
|
const filters = inject('filters')
|
|
|
|
const countries = ref({})
|
|
const locations = ref([])
|
|
const colos = ref({})
|
|
const currentLocation = ref({})
|
|
|
|
const el = useTemplateRef('globeEl')
|
|
const { width } = useElementSize(el)
|
|
const size = computed(() => ({
|
|
width: width.value,
|
|
height: width.value > 768 ? width.value * 0.6 : width.value,
|
|
}))
|
|
|
|
const globeEl = ref()
|
|
const hexAltitude = ref(0.001)
|
|
const highest = computed(() => {
|
|
return locations.value.reduce((acc, curr) => Math.max(acc, curr.count), 0) || 1
|
|
})
|
|
|
|
let globe = null
|
|
|
|
async function getGlobeJSON() {
|
|
const data = await $fetch('/countries.geojson')
|
|
countries.value = data
|
|
}
|
|
|
|
async function getColosJSON() {
|
|
const data = await $fetch('/colos.json')
|
|
colos.value = data
|
|
}
|
|
|
|
async function getCurrentLocation() {
|
|
const data = await useAPI('/api/location')
|
|
currentLocation.value = data
|
|
}
|
|
|
|
async function getLiveLocations() {
|
|
const { data } = await useAPI('/api/logs/locations', {
|
|
query: {
|
|
startAt: time.value.startAt,
|
|
endAt: time.value.endAt,
|
|
...filters.value,
|
|
},
|
|
})
|
|
locations.value = data?.map(e => ({
|
|
lat: e.latitude,
|
|
lng: e.longitude,
|
|
count: Math.max(1, +e.count / highest.value),
|
|
}))
|
|
}
|
|
|
|
let cleanArcsDataTimer = null
|
|
|
|
function trafficEvent({ props }, { delay = 0 }) {
|
|
const arc = {
|
|
startLat: props.item.latitude,
|
|
startLng: props.item.longitude,
|
|
endLat: colos.value[props.item.COLO]?.lat,
|
|
endLng: colos.value[props.item.COLO]?.lon,
|
|
color: 'darkOrange',
|
|
arcAltitude: 0.2,
|
|
}
|
|
console.info(`from ${props.item.city}(${arc.startLat}, ${arc.startLng}) to ${props.item.COLO}(${arc.endLat}, ${arc.endLng})`)
|
|
const isNear = Math.abs(arc.endLat - arc.startLat) < 5 && Math.abs(arc.endLng - arc.startLng) < 5
|
|
if (isNear) {
|
|
console.info(`from ${props.item.city} to ${props.item.COLO} is near, skip`)
|
|
return
|
|
}
|
|
const random = Math.random()
|
|
globe.arcsData([arc])
|
|
.arcColor('color')
|
|
.arcDashLength(() => random)
|
|
.arcDashGap(() => 1 - random)
|
|
.arcDashAnimateTime(delay * 2)
|
|
.ringRepeatPeriod(delay * 2)
|
|
|
|
const srcRing = { lat: arc.startLat, lng: arc.startLng }
|
|
globe.ringsData([...globe.ringsData(), srcRing])
|
|
setTimeout(() => globe.ringsData(globe.ringsData().filter(r => r !== srcRing)), delay * 2)
|
|
|
|
setTimeout(() => {
|
|
const targetRing = { lat: arc.endLat, lng: arc.endLng }
|
|
globe.ringsData([...globe.ringsData(), targetRing])
|
|
setTimeout(() => globe.ringsData(globe.ringsData().filter(r => r !== targetRing)), delay * 2)
|
|
}, delay * 2)
|
|
|
|
clearTimeout(cleanArcsDataTimer)
|
|
cleanArcsDataTimer = setTimeout(() => {
|
|
globe.arcsData([])
|
|
}, delay * 2)
|
|
}
|
|
|
|
const normalized = 5 / props.minutes
|
|
const weightColor = scaleSequentialSqrt(interpolateYlOrRd).domain([0, highest.value * normalized * 15])
|
|
function initGlobe() {
|
|
globe = new Globe(globeEl.value)
|
|
.width(size.value.width)
|
|
.height(size.value.height)
|
|
// .globeOffset([width.value > 768 ? -100 : 0, width.value > 768 ? 0 : 100])
|
|
.atmosphereColor('rgba(170, 170, 200, 0.8)')
|
|
.globeMaterial(new MeshPhongMaterial({
|
|
color: 'rgb(228, 228, 231)',
|
|
transparent: false,
|
|
opacity: 1,
|
|
}))
|
|
.backgroundColor('rgba(0,0,0,0)')
|
|
.hexPolygonsData(countries.value.features)
|
|
.hexPolygonResolution(3)
|
|
.hexPolygonMargin(0.2)
|
|
.hexBinResolution(3)
|
|
.hexBinPointsData(locations.value)
|
|
.hexPolygonAltitude(() => hexAltitude.value)
|
|
.hexBinMerge(true)
|
|
.hexBinPointWeight('count')
|
|
.hexPolygonColor(() => `rgba(54, 211, 153, ${Math.random() / 1.5 + 0.5})`)
|
|
.ringColor(() => t => `rgba(255,100,50,${1 - t})`)
|
|
.ringMaxRadius(3)
|
|
.ringPropagationSpeed(3)
|
|
.onGlobeReady(() => {
|
|
globe.pointOfView({ lat: currentLocation.value.latitude, lng: currentLocation.value.longitude, altitude: width.value > 768 ? 2 : 3 })
|
|
globe.controls().autoRotate = true
|
|
globe.controls().autoRotateSpeed = 0.3
|
|
})
|
|
|
|
globe.controls().addEventListener('end', debounce(() => {
|
|
const distance = Math.round(globe.controls().getDistance())
|
|
let nextAlt = 0.005
|
|
if (distance <= 300)
|
|
nextAlt = 0.001
|
|
else if (distance >= 600)
|
|
nextAlt = 0.02
|
|
if (nextAlt !== hexAltitude.value)
|
|
hexAltitude.value = nextAlt
|
|
}, 200))
|
|
|
|
globalTrafficEvent.on(trafficEvent)
|
|
}
|
|
|
|
function stopRotation() {
|
|
if (globe) {
|
|
globe.controls().autoRotate = false
|
|
}
|
|
}
|
|
|
|
watch([time, filters], getLiveLocations, {
|
|
deep: true,
|
|
})
|
|
|
|
watch(width, () => {
|
|
if (globe) {
|
|
globe.width(size.value.width)
|
|
globe.height(size.value.height)
|
|
}
|
|
})
|
|
|
|
watch(locations, () => {
|
|
if (globe) {
|
|
globe.hexBinPointsData(locations.value)
|
|
globe.hexTopColor(d => weightColor(d.sumWeight))
|
|
globe.hexSideColor(d => weightColor(d.sumWeight))
|
|
}
|
|
})
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([
|
|
getLiveLocations(),
|
|
getCurrentLocation(),
|
|
getGlobeJSON(),
|
|
getColosJSON(),
|
|
])
|
|
initGlobe()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
globalTrafficEvent.off(trafficEvent)
|
|
if (globe) {
|
|
globe._destructor?.()
|
|
globe = null
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="globeEl" @mousedown="stopRotation" />
|
|
</template>
|