From a493d2cf0147c855b347250e7ca44ff09d3d980d Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:09:15 +0700 Subject: [PATCH] Add map improvements: hover highlight, fitBounds, search checkbox --- app/components/catalog/CatalogMap.vue | 74 ++++++++++++++++++++++----- app/components/page/CatalogPage.vue | 47 +++++++++++++++-- i18n/locales/en/catalogMap.json | 1 + i18n/locales/ru/catalogMap.json | 1 + 4 files changed, 104 insertions(+), 19 deletions(-) diff --git a/app/components/catalog/CatalogMap.vue b/app/components/catalog/CatalogMap.vue index 7356cf0..714001c 100644 --- a/app/components/catalog/CatalogMap.vue +++ b/app/components/catalog/CatalogMap.vue @@ -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) => { diff --git a/app/components/page/CatalogPage.vue b/app/components/page/CatalogPage.vue index 99d8ac7..31c2383 100644 --- a/app/components/page/CatalogPage.vue +++ b/app/components/page/CatalogPage.vue @@ -24,7 +24,7 @@
- + {{ $t('common.values.not_available') }} @@ -48,6 +48,11 @@
+ + @@ -74,7 +80,7 @@
- + {{ $t('common.values.not_available') }} @@ -95,7 +101,12 @@
-
+
+ + @@ -207,12 +219,37 @@ const emit = defineEmits<{ // Server-side clustering const { clusteredNodes, fetchClusters } = useClusteredNodes() +// Search with map checkbox +const searchWithMap = ref(false) +const currentBounds = ref(null) + const onBoundsChange = (bounds: MapBounds) => { + currentBounds.value = bounds if (props.useServerClustering) { fetchClusters(bounds) } } +// Filtered items when searchWithMap is enabled +const displayItems = computed(() => { + if (!searchWithMap.value || !currentBounds.value) return props.items + return props.items.filter(item => { + if (item.latitude == null || item.longitude == null) return false + const { west, east, north, south } = currentBounds.value! + const lng = Number(item.longitude) + const lat = Number(item.latitude) + return lng >= west && lng <= east && lat >= south && lat <= north + }) +}) + +// Hovered item with coordinates for map highlight +const hoveredItem = computed(() => { + if (!props.hoveredId) return null + const item = props.items.find(i => i.uuid === props.hoveredId) + if (!item?.latitude || !item?.longitude) return null + return { latitude: Number(item.latitude), longitude: Number(item.longitude) } +}) + // Use mapItems if provided, otherwise fall back to items const itemsForMap = computed(() => props.mapItems || props.items) diff --git a/i18n/locales/en/catalogMap.json b/i18n/locales/en/catalogMap.json index a41103b..f0335ca 100644 --- a/i18n/locales/en/catalogMap.json +++ b/i18n/locales/en/catalogMap.json @@ -1,5 +1,6 @@ { "catalogMap": { + "searchWithMap": "Search as map moves", "states": { "loading": "Loading locations...", "no_hubs": "No hubs available", diff --git a/i18n/locales/ru/catalogMap.json b/i18n/locales/ru/catalogMap.json index 33ccda5..6e99c04 100644 --- a/i18n/locales/ru/catalogMap.json +++ b/i18n/locales/ru/catalogMap.json @@ -1,5 +1,6 @@ { "catalogMap": { + "searchWithMap": "Искать на карте", "states": { "loading": "Загружаем локации...", "no_hubs": "Нет доступных хабов",