Add map improvements: hover highlight, fitBounds, search checkbox
All checks were successful
Build Docker Image / build (push) Successful in 5m45s

This commit is contained in:
Ruslan Bakiev
2026-01-14 12:09:15 +07:00
parent c458f851dc
commit a493d2cf01
4 changed files with 104 additions and 19 deletions

View File

@@ -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) => {