Add map improvements: hover highlight, fitBounds, search checkbox
All checks were successful
Build Docker Image / build (push) Successful in 5m45s
All checks were successful
Build Docker Image / build (push) Successful in 5m45s
This commit is contained in:
@@ -35,12 +35,18 @@ export interface MapBounds {
|
||||
zoom: number
|
||||
}
|
||||
|
||||
interface HoveredItem {
|
||||
latitude: number
|
||||
longitude: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
mapId: string
|
||||
items?: MapItem[]
|
||||
clusteredPoints?: ClusterPointType[]
|
||||
useServerClustering?: boolean
|
||||
hoveredItemId?: string | null
|
||||
hoveredItem?: HoveredItem | null
|
||||
pointColor?: string
|
||||
initialCenter?: [number, number]
|
||||
initialZoom?: number
|
||||
@@ -100,7 +106,21 @@ const serverClusteredGeoJson = computed(() => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Hovered point GeoJSON (separate layer on top)
|
||||
const hoveredPointGeoJson = computed(() => ({
|
||||
type: 'FeatureCollection' as const,
|
||||
features: props.hoveredItem ? [{
|
||||
type: 'Feature' as const,
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'Point' as const,
|
||||
coordinates: [props.hoveredItem.longitude, props.hoveredItem.latitude]
|
||||
}
|
||||
}] : []
|
||||
}))
|
||||
|
||||
const sourceId = computed(() => `${props.mapId}-points`)
|
||||
const hoveredSourceId = computed(() => `${props.mapId}-hovered`)
|
||||
|
||||
const emitBoundsChange = (map: MapboxMapType) => {
|
||||
const bounds = map.getBounds()
|
||||
@@ -344,6 +364,23 @@ const initServerClusteringLayers = (map: MapboxMapType) => {
|
||||
map.on('mouseleave', 'server-clusters', () => { map.getCanvas().style.cursor = '' })
|
||||
map.on('mouseenter', 'server-points', () => { map.getCanvas().style.cursor = 'pointer' })
|
||||
map.on('mouseleave', 'server-points', () => { map.getCanvas().style.cursor = '' })
|
||||
|
||||
// Hovered point layer (on top of everything)
|
||||
map.addSource(hoveredSourceId.value, {
|
||||
type: 'geojson',
|
||||
data: hoveredPointGeoJson.value
|
||||
})
|
||||
map.addLayer({
|
||||
id: 'hovered-point-layer',
|
||||
type: 'circle',
|
||||
source: hoveredSourceId.value,
|
||||
paint: {
|
||||
'circle-radius': 14,
|
||||
'circle-color': '#3b82f6',
|
||||
'circle-stroke-width': 3,
|
||||
'circle-stroke-color': '#ffffff'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Update map data when items or clusteredPoints change
|
||||
@@ -355,22 +392,31 @@ watch(() => props.useServerClustering ? serverClusteredGeoJson.value : geoJsonDa
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// Update hover paint when hoveredItemId changes
|
||||
watch(() => props.hoveredItemId, (newId) => {
|
||||
// Update hovered point layer when hoveredItem changes
|
||||
watch(() => props.hoveredItem, () => {
|
||||
if (!mapRef.value || !mapInitialized.value) return
|
||||
|
||||
const layerId = props.useServerClustering ? 'server-points' : 'unclustered-point'
|
||||
const propName = props.useServerClustering ? 'id' : 'uuid'
|
||||
|
||||
if (mapRef.value.getLayer(layerId)) {
|
||||
mapRef.value.setPaintProperty(layerId, 'circle-color', [
|
||||
'case',
|
||||
['==', ['get', propName], newId || ''],
|
||||
'#facc15',
|
||||
props.pointColor
|
||||
])
|
||||
const source = mapRef.value.getSource(hoveredSourceId.value) as mapboxgl.GeoJSONSource | undefined
|
||||
if (source) {
|
||||
source.setData(hoveredPointGeoJson.value)
|
||||
}
|
||||
})
|
||||
}, { deep: true })
|
||||
|
||||
// fitBounds for server clustering when first data arrives
|
||||
watch(() => props.clusteredPoints, (points) => {
|
||||
if (!mapRef.value || !mapInitialized.value) return
|
||||
if (!didFitBounds.value && points && points.length > 0) {
|
||||
const bounds = new LngLatBounds()
|
||||
points.forEach(p => {
|
||||
if (p?.longitude && p?.latitude) {
|
||||
bounds.extend([p.longitude, p.latitude])
|
||||
}
|
||||
})
|
||||
if (!bounds.isEmpty()) {
|
||||
mapRef.value.fitBounds(bounds, { padding: 50, maxZoom: 6 })
|
||||
didFitBounds.value = true
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Expose flyTo method for external use (with space fly animation)
|
||||
const flyTo = async (lat: number, lng: number, zoom = 8) => {
|
||||
|
||||
Reference in New Issue
Block a user