Render map points by entity type
Some checks failed
Build Docker Image / build (push) Has been cancelled
Some checks failed
Build Docker Image / build (push) Has been cancelled
This commit is contained in:
@@ -44,11 +44,13 @@ const props = withDefaults(defineProps<{
|
|||||||
mapId: string
|
mapId: string
|
||||||
items?: MapItem[]
|
items?: MapItem[]
|
||||||
clusteredPoints?: ClusterPointType[]
|
clusteredPoints?: ClusterPointType[]
|
||||||
|
clusteredPointsByType?: Partial<Record<'offer' | 'hub' | 'supplier', ClusterPointType[]>>
|
||||||
useServerClustering?: boolean
|
useServerClustering?: boolean
|
||||||
hoveredItemId?: string | null
|
hoveredItemId?: string | null
|
||||||
hoveredItem?: HoveredItem | null
|
hoveredItem?: HoveredItem | null
|
||||||
pointColor?: string
|
pointColor?: string
|
||||||
entityType?: 'offer' | 'hub' | 'supplier'
|
entityType?: 'offer' | 'hub' | 'supplier'
|
||||||
|
visibleTypes?: Array<'offer' | 'hub' | 'supplier'>
|
||||||
initialCenter?: [number, number]
|
initialCenter?: [number, number]
|
||||||
initialZoom?: number
|
initialZoom?: number
|
||||||
infoLoading?: boolean
|
infoLoading?: boolean
|
||||||
@@ -68,6 +70,8 @@ const props = withDefaults(defineProps<{
|
|||||||
infoLoading: false,
|
infoLoading: false,
|
||||||
items: () => [],
|
items: () => [],
|
||||||
clusteredPoints: () => [],
|
clusteredPoints: () => [],
|
||||||
|
clusteredPointsByType: undefined,
|
||||||
|
visibleTypes: undefined,
|
||||||
relatedPoints: () => []
|
relatedPoints: () => []
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -81,6 +85,11 @@ const { flyThroughSpace } = useMapboxFlyAnimation()
|
|||||||
const didFitBounds = ref(false)
|
const didFitBounds = ref(false)
|
||||||
const mapInitialized = ref(false)
|
const mapInitialized = ref(false)
|
||||||
|
|
||||||
|
const usesTypedClusters = computed(() => {
|
||||||
|
const typed = props.clusteredPointsByType
|
||||||
|
return !!typed && Object.keys(typed).length > 0
|
||||||
|
})
|
||||||
|
|
||||||
// Entity type icons - SVG data URLs with specific colors
|
// Entity type icons - SVG data URLs with specific colors
|
||||||
const createEntityIcon = (type: 'offer' | 'hub' | 'supplier', color: string) => {
|
const createEntityIcon = (type: 'offer' | 'hub' | 'supplier', color: string) => {
|
||||||
const icons = {
|
const icons = {
|
||||||
@@ -137,6 +146,8 @@ const ENTITY_COLORS = {
|
|||||||
offer: '#f97316' // orange
|
offer: '#f97316' // orange
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
const CLUSTER_TYPES: Array<'offer' | 'hub' | 'supplier'> = ['offer', 'hub', 'supplier']
|
||||||
|
|
||||||
// Load all icons for related points (each type with its standard color)
|
// Load all icons for related points (each type with its standard color)
|
||||||
const loadRelatedPointIcons = async (map: MapboxMapType) => {
|
const loadRelatedPointIcons = async (map: MapboxMapType) => {
|
||||||
const types: Array<'hub' | 'supplier' | 'offer'> = ['hub', 'supplier', 'offer']
|
const types: Array<'hub' | 'supplier' | 'offer'> = ['hub', 'supplier', 'offer']
|
||||||
@@ -216,6 +227,33 @@ const serverClusteredGeoJson = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const serverClusteredGeoJsonByType = computed(() => {
|
||||||
|
const build = (points: ClusterPointType[] | undefined, type: 'offer' | 'hub' | 'supplier') => ({
|
||||||
|
type: 'FeatureCollection' as const,
|
||||||
|
features: (points || []).filter(Boolean).map(point => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {
|
||||||
|
id: point!.id,
|
||||||
|
name: point!.name,
|
||||||
|
count: point!.count ?? 1,
|
||||||
|
expansionZoom: point!.expansionZoom,
|
||||||
|
isCluster: (point!.count ?? 1) > 1,
|
||||||
|
type
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: 'Point' as const,
|
||||||
|
coordinates: [point!.longitude ?? 0, point!.latitude ?? 0]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
offer: build(props.clusteredPointsByType?.offer, 'offer'),
|
||||||
|
hub: build(props.clusteredPointsByType?.hub, 'hub'),
|
||||||
|
supplier: build(props.clusteredPointsByType?.supplier, 'supplier')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Hovered point GeoJSON (separate layer on top)
|
// Hovered point GeoJSON (separate layer on top)
|
||||||
const hoveredPointGeoJson = computed(() => ({
|
const hoveredPointGeoJson = computed(() => ({
|
||||||
type: 'FeatureCollection' as const,
|
type: 'FeatureCollection' as const,
|
||||||
@@ -256,6 +294,17 @@ const sourceId = computed(() => `${props.mapId}-points`)
|
|||||||
const hoveredSourceId = computed(() => `${props.mapId}-hovered`)
|
const hoveredSourceId = computed(() => `${props.mapId}-hovered`)
|
||||||
const relatedSourceId = computed(() => `${props.mapId}-related`)
|
const relatedSourceId = computed(() => `${props.mapId}-related`)
|
||||||
|
|
||||||
|
const getServerSourceId = (type: 'offer' | 'hub' | 'supplier') => `${props.mapId}-server-${type}`
|
||||||
|
const getServerClusterLayerId = (type: 'offer' | 'hub' | 'supplier') => `${props.mapId}-server-${type}-clusters`
|
||||||
|
const getServerClusterCountLayerId = (type: 'offer' | 'hub' | 'supplier') => `${props.mapId}-server-${type}-cluster-count`
|
||||||
|
const getServerPointLayerId = (type: 'offer' | 'hub' | 'supplier') => `${props.mapId}-server-${type}-points`
|
||||||
|
const getServerPointLabelLayerId = (type: 'offer' | 'hub' | 'supplier') => `${props.mapId}-server-${type}-point-labels`
|
||||||
|
|
||||||
|
const isTypeVisible = (type: 'offer' | 'hub' | 'supplier') => {
|
||||||
|
if (!props.visibleTypes || props.visibleTypes.length === 0) return true
|
||||||
|
return props.visibleTypes.includes(type)
|
||||||
|
}
|
||||||
|
|
||||||
const emitBoundsChange = (map: MapboxMapType) => {
|
const emitBoundsChange = (map: MapboxMapType) => {
|
||||||
const bounds = map.getBounds()
|
const bounds = map.getBounds()
|
||||||
if (!bounds) return
|
if (!bounds) return
|
||||||
@@ -279,7 +328,11 @@ const onMapCreated = (map: MapboxMapType) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (props.useServerClustering) {
|
if (props.useServerClustering) {
|
||||||
await initServerClusteringLayers(map)
|
if (usesTypedClusters.value) {
|
||||||
|
await initServerClusteringLayersByType(map)
|
||||||
|
} else {
|
||||||
|
await initServerClusteringLayers(map)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await initClientClusteringLayers(map)
|
await initClientClusteringLayers(map)
|
||||||
}
|
}
|
||||||
@@ -677,15 +730,218 @@ const initServerClusteringLayers = async (map: MapboxMapType) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initServerClusteringLayersByType = async (map: MapboxMapType) => {
|
||||||
|
for (const type of CLUSTER_TYPES) {
|
||||||
|
await loadEntityIcon(map, type, ENTITY_COLORS[type])
|
||||||
|
|
||||||
|
const sourceIdByType = getServerSourceId(type)
|
||||||
|
map.addSource(sourceIdByType, {
|
||||||
|
type: 'geojson',
|
||||||
|
data: serverClusteredGeoJsonByType.value[type]
|
||||||
|
})
|
||||||
|
|
||||||
|
const clusterLayerId = getServerClusterLayerId(type)
|
||||||
|
const clusterCountLayerId = getServerClusterCountLayerId(type)
|
||||||
|
const pointLayerId = getServerPointLayerId(type)
|
||||||
|
const pointLabelLayerId = getServerPointLabelLayerId(type)
|
||||||
|
|
||||||
|
map.addLayer({
|
||||||
|
id: clusterLayerId,
|
||||||
|
type: 'circle',
|
||||||
|
source: sourceIdByType,
|
||||||
|
filter: ['>', ['get', 'count'], 1],
|
||||||
|
paint: {
|
||||||
|
'circle-color': ENTITY_COLORS[type],
|
||||||
|
'circle-radius': ['step', ['get', 'count'], 20, 10, 30, 50, 40],
|
||||||
|
'circle-opacity': 0.8,
|
||||||
|
'circle-stroke-width': 2,
|
||||||
|
'circle-stroke-color': '#ffffff'
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
visibility: isTypeVisible(type) ? 'visible' : 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
map.addLayer({
|
||||||
|
id: clusterCountLayerId,
|
||||||
|
type: 'symbol',
|
||||||
|
source: sourceIdByType,
|
||||||
|
filter: ['>', ['get', 'count'], 1],
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'count'],
|
||||||
|
'text-size': 14,
|
||||||
|
visibility: isTypeVisible(type) ? 'visible' : 'none'
|
||||||
|
},
|
||||||
|
paint: { 'text-color': '#ffffff' }
|
||||||
|
})
|
||||||
|
|
||||||
|
map.addLayer({
|
||||||
|
id: pointLayerId,
|
||||||
|
type: 'symbol',
|
||||||
|
source: sourceIdByType,
|
||||||
|
filter: ['==', ['get', 'count'], 1],
|
||||||
|
layout: {
|
||||||
|
'icon-image': `entity-icon-${type}`,
|
||||||
|
'icon-size': 1,
|
||||||
|
'icon-allow-overlap': true,
|
||||||
|
visibility: isTypeVisible(type) ? 'visible' : 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
map.addLayer({
|
||||||
|
id: pointLabelLayerId,
|
||||||
|
type: 'symbol',
|
||||||
|
source: sourceIdByType,
|
||||||
|
filter: ['==', ['get', 'count'], 1],
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'name'],
|
||||||
|
'text-offset': [0, 1.8],
|
||||||
|
'text-size': 12,
|
||||||
|
'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'],
|
||||||
|
visibility: isTypeVisible(type) ? 'visible' : 'none'
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': '#ffffff',
|
||||||
|
'text-halo-color': '#000000',
|
||||||
|
'text-halo-width': 1.5
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
map.on('click', clusterLayerId, (e) => {
|
||||||
|
const features = map.queryRenderedFeatures(e.point, { layers: [clusterLayerId] })
|
||||||
|
const feature = features[0]
|
||||||
|
if (!feature) return
|
||||||
|
const clusterId = feature.properties?.cluster_id
|
||||||
|
const source = map.getSource(sourceIdByType) as mapboxgl.GeoJSONSource
|
||||||
|
source.getClusterExpansionZoom(clusterId, (err, zoom) => {
|
||||||
|
if (err) return
|
||||||
|
const geometry = feature.geometry as GeoJSON.Point
|
||||||
|
map.easeTo({ center: geometry.coordinates as [number, number], zoom: zoom || 4 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
map.on('click', pointLayerId, (e) => {
|
||||||
|
const features = map.queryRenderedFeatures(e.point, { layers: [pointLayerId] })
|
||||||
|
const feature = features[0]
|
||||||
|
if (!feature) return
|
||||||
|
const props_data = feature.properties as Record<string, any> | undefined
|
||||||
|
emit('select-item', props_data?.id, props_data)
|
||||||
|
})
|
||||||
|
|
||||||
|
map.on('mouseenter', clusterLayerId, () => { map.getCanvas().style.cursor = 'pointer' })
|
||||||
|
map.on('mouseleave', clusterLayerId, () => { map.getCanvas().style.cursor = '' })
|
||||||
|
map.on('mouseenter', pointLayerId, () => { map.getCanvas().style.cursor = 'pointer' })
|
||||||
|
map.on('mouseleave', pointLayerId, () => { 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-ring',
|
||||||
|
type: 'circle',
|
||||||
|
source: hoveredSourceId.value,
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 20,
|
||||||
|
'circle-color': 'transparent',
|
||||||
|
'circle-stroke-width': 3,
|
||||||
|
'circle-stroke-color': '#ffffff'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
map.addLayer({
|
||||||
|
id: 'hovered-point-layer',
|
||||||
|
type: 'circle',
|
||||||
|
source: hoveredSourceId.value,
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 14,
|
||||||
|
'circle-color': props.pointColor,
|
||||||
|
'circle-stroke-width': 3,
|
||||||
|
'circle-stroke-color': '#ffffff'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Related points layer
|
||||||
|
await loadRelatedPointIcons(map)
|
||||||
|
|
||||||
|
map.addSource(relatedSourceId.value, {
|
||||||
|
type: 'geojson',
|
||||||
|
data: relatedPointsGeoJson.value
|
||||||
|
})
|
||||||
|
map.addLayer({
|
||||||
|
id: `${props.mapId}-related-points`,
|
||||||
|
type: 'symbol',
|
||||||
|
source: relatedSourceId.value,
|
||||||
|
layout: {
|
||||||
|
'icon-image': [
|
||||||
|
'match',
|
||||||
|
['get', 'type'],
|
||||||
|
'hub', 'related-icon-hub',
|
||||||
|
'supplier', 'related-icon-supplier',
|
||||||
|
'offer', 'related-icon-offer',
|
||||||
|
'related-icon-offer'
|
||||||
|
],
|
||||||
|
'icon-size': 1,
|
||||||
|
'icon-allow-overlap': true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
map.addLayer({
|
||||||
|
id: `${props.mapId}-related-labels`,
|
||||||
|
type: 'symbol',
|
||||||
|
source: relatedSourceId.value,
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'name'],
|
||||||
|
'text-size': 11,
|
||||||
|
'text-anchor': 'top',
|
||||||
|
'text-offset': [0, 1.5]
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': '#ffffff',
|
||||||
|
'text-halo-color': '#000000',
|
||||||
|
'text-halo-width': 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
map.on('click', `${props.mapId}-related-points`, (e) => {
|
||||||
|
const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-points`] })
|
||||||
|
const feature = features[0]
|
||||||
|
if (!feature) return
|
||||||
|
const props_data = feature.properties as Record<string, any> | undefined
|
||||||
|
emit('select-item', props_data?.uuid, props_data)
|
||||||
|
})
|
||||||
|
|
||||||
|
map.on('mouseenter', `${props.mapId}-related-points`, () => {
|
||||||
|
map.getCanvas().style.cursor = 'pointer'
|
||||||
|
})
|
||||||
|
|
||||||
|
map.on('mouseleave', `${props.mapId}-related-points`, () => {
|
||||||
|
map.getCanvas().style.cursor = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Update map data when items or clusteredPoints change
|
// Update map data when items or clusteredPoints change
|
||||||
watch(() => props.useServerClustering ? serverClusteredGeoJson.value : geoJsonData.value, (newData) => {
|
watch(() => props.useServerClustering ? serverClusteredGeoJson.value : geoJsonData.value, (newData) => {
|
||||||
if (!mapRef.value || !mapInitialized.value) return
|
if (!mapRef.value || !mapInitialized.value) return
|
||||||
|
if (usesTypedClusters.value) return
|
||||||
const source = mapRef.value.getSource(sourceId.value) as mapboxgl.GeoJSONSource | undefined
|
const source = mapRef.value.getSource(sourceId.value) as mapboxgl.GeoJSONSource | undefined
|
||||||
if (source) {
|
if (source) {
|
||||||
source.setData(newData)
|
source.setData(newData)
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(() => serverClusteredGeoJsonByType.value, (newData) => {
|
||||||
|
if (!mapRef.value || !mapInitialized.value) return
|
||||||
|
if (!usesTypedClusters.value) return
|
||||||
|
for (const type of CLUSTER_TYPES) {
|
||||||
|
const sourceIdByType = getServerSourceId(type)
|
||||||
|
const source = mapRef.value.getSource(sourceIdByType) as mapboxgl.GeoJSONSource | undefined
|
||||||
|
if (source) {
|
||||||
|
source.setData(newData[type])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
// Update hovered point layer when hoveredItem changes
|
// Update hovered point layer when hoveredItem changes
|
||||||
watch(() => props.hoveredItem, () => {
|
watch(() => props.hoveredItem, () => {
|
||||||
if (!mapRef.value || !mapInitialized.value) return
|
if (!mapRef.value || !mapInitialized.value) return
|
||||||
@@ -706,6 +962,25 @@ watch(() => props.relatedPoints, () => {
|
|||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(() => props.visibleTypes, () => {
|
||||||
|
if (!mapRef.value || !mapInitialized.value) return
|
||||||
|
if (!usesTypedClusters.value) return
|
||||||
|
for (const type of CLUSTER_TYPES) {
|
||||||
|
const visibility = isTypeVisible(type) ? 'visible' : 'none'
|
||||||
|
const layers = [
|
||||||
|
getServerClusterLayerId(type),
|
||||||
|
getServerClusterCountLayerId(type),
|
||||||
|
getServerPointLayerId(type),
|
||||||
|
getServerPointLabelLayerId(type)
|
||||||
|
]
|
||||||
|
layers.forEach((layerId) => {
|
||||||
|
if (mapRef.value?.getLayer(layerId)) {
|
||||||
|
mapRef.value.setLayoutProperty(layerId, 'visibility', visibility)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
// Fit bounds when info loading finishes (all related data loaded)
|
// Fit bounds when info loading finishes (all related data loaded)
|
||||||
watch(() => props.infoLoading, (loading, wasLoading) => {
|
watch(() => props.infoLoading, (loading, wasLoading) => {
|
||||||
// Only fit bounds when loading changes from true to false (data finished loading)
|
// Only fit bounds when loading changes from true to false (data finished loading)
|
||||||
@@ -731,10 +1006,12 @@ watch([() => props.pointColor, () => props.entityType], async ([newColor, newTyp
|
|||||||
|
|
||||||
// Update cluster circle colors
|
// Update cluster circle colors
|
||||||
if (props.useServerClustering) {
|
if (props.useServerClustering) {
|
||||||
|
if (usesTypedClusters.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (map.getLayer('server-clusters')) {
|
if (map.getLayer('server-clusters')) {
|
||||||
map.setPaintProperty('server-clusters', 'circle-color', newColor)
|
map.setPaintProperty('server-clusters', 'circle-color', newColor)
|
||||||
}
|
}
|
||||||
// Update icon reference for points
|
|
||||||
if (map.getLayer('server-points')) {
|
if (map.getLayer('server-points')) {
|
||||||
map.setLayoutProperty('server-points', 'icon-image', `entity-icon-${newType}`)
|
map.setLayoutProperty('server-points', 'icon-image', `entity-icon-${newType}`)
|
||||||
}
|
}
|
||||||
@@ -755,6 +1032,7 @@ watch([() => props.pointColor, () => props.entityType], async ([newColor, newTyp
|
|||||||
|
|
||||||
// fitBounds for server clustering when first data arrives
|
// fitBounds for server clustering when first data arrives
|
||||||
watch(() => props.clusteredPoints, (points) => {
|
watch(() => props.clusteredPoints, (points) => {
|
||||||
|
if (usesTypedClusters.value) return
|
||||||
if (!mapRef.value || !mapInitialized.value) return
|
if (!mapRef.value || !mapInitialized.value) return
|
||||||
if (!didFitBounds.value && points && points.length > 0) {
|
if (!didFitBounds.value && points && points.length > 0) {
|
||||||
const bounds = new LngLatBounds()
|
const bounds = new LngLatBounds()
|
||||||
@@ -770,6 +1048,26 @@ watch(() => props.clusteredPoints, (points) => {
|
|||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch([() => props.clusteredPointsByType, () => props.visibleTypes], () => {
|
||||||
|
if (!usesTypedClusters.value) return
|
||||||
|
if (!mapRef.value || !mapInitialized.value) return
|
||||||
|
if (didFitBounds.value) return
|
||||||
|
|
||||||
|
const bounds = new LngLatBounds()
|
||||||
|
const visible = props.visibleTypes && props.visibleTypes.length > 0 ? props.visibleTypes : CLUSTER_TYPES
|
||||||
|
visible.forEach(type => {
|
||||||
|
const points = serverClusteredGeoJsonByType.value[type]?.features ?? []
|
||||||
|
points.forEach((p) => {
|
||||||
|
const coords = (p.geometry as GeoJSON.Point).coordinates as [number, number]
|
||||||
|
bounds.extend(coords)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!bounds.isEmpty()) {
|
||||||
|
mapRef.value.fitBounds(bounds, { padding: 50, maxZoom: 6 })
|
||||||
|
didFitBounds.value = true
|
||||||
|
}
|
||||||
|
}, { deep: true, immediate: true })
|
||||||
|
|
||||||
// Expose flyTo method for external use (with space fly animation)
|
// Expose flyTo method for external use (with space fly animation)
|
||||||
const flyTo = async (lat: number, lng: number, zoom = 8) => {
|
const flyTo = async (lat: number, lng: number, zoom = 8) => {
|
||||||
if (!mapRef.value) return
|
if (!mapRef.value) return
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
ref="mapRef"
|
ref="mapRef"
|
||||||
:map-id="mapId"
|
:map-id="mapId"
|
||||||
:items="isInfoMode ? [] : (useServerClustering ? [] : itemsWithCoords)"
|
:items="isInfoMode ? [] : (useServerClustering ? [] : itemsWithCoords)"
|
||||||
:clustered-points="isInfoMode ? [] : (useServerClustering ? clusteredNodes : [])"
|
:clustered-points="isInfoMode ? [] : (useServerClustering && !useTypedClusters ? clusteredNodes : [])"
|
||||||
|
:clustered-points-by-type="isInfoMode ? undefined : (useServerClustering && useTypedClusters ? clusteredPointsByType : undefined)"
|
||||||
|
:visible-types="useServerClustering && useTypedClusters ? visibleTypes : undefined"
|
||||||
:use-server-clustering="useServerClustering && !isInfoMode"
|
:use-server-clustering="useServerClustering && !isInfoMode"
|
||||||
:point-color="activePointColor"
|
:point-color="activePointColor"
|
||||||
:entity-type="activeEntityType"
|
:entity-type="activeEntityType"
|
||||||
@@ -246,6 +248,7 @@ const props = withDefaults(defineProps<{
|
|||||||
loading?: boolean
|
loading?: boolean
|
||||||
useServerClustering?: boolean
|
useServerClustering?: boolean
|
||||||
clusterNodeType?: string
|
clusterNodeType?: string
|
||||||
|
useTypedClusters?: boolean
|
||||||
mapId?: string
|
mapId?: string
|
||||||
pointColor?: string
|
pointColor?: string
|
||||||
hoveredId?: string
|
hoveredId?: string
|
||||||
@@ -266,6 +269,7 @@ const props = withDefaults(defineProps<{
|
|||||||
loading: false,
|
loading: false,
|
||||||
useServerClustering: true,
|
useServerClustering: true,
|
||||||
clusterNodeType: 'offer',
|
clusterNodeType: 'offer',
|
||||||
|
useTypedClusters: false,
|
||||||
mapId: 'catalog-map',
|
mapId: 'catalog-map',
|
||||||
pointColor: '#f97316',
|
pointColor: '#f97316',
|
||||||
items: () => [],
|
items: () => [],
|
||||||
@@ -284,11 +288,59 @@ const emit = defineEmits<{
|
|||||||
'update:filter-by-bounds': [value: boolean]
|
'update:filter-by-bounds': [value: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Server-side clustering - use computed node type based on view mode
|
const useTypedClusters = computed(() => props.useTypedClusters && props.useServerClustering)
|
||||||
const { clusteredNodes, fetchClusters, loading: clusterLoading, clearNodes } = useClusteredNodes(undefined, activeClusterNodeType)
|
|
||||||
|
// Server-side clustering (single-type mode)
|
||||||
|
const { clusteredNodes, fetchClusters, loading: singleClusterLoading, clearNodes } = useClusteredNodes(undefined, activeClusterNodeType)
|
||||||
|
|
||||||
|
// Server-side clustering (typed mode)
|
||||||
|
const offerClusters = useClusteredNodes(undefined, ref('offer'))
|
||||||
|
const hubClusters = useClusteredNodes(undefined, ref('logistics'))
|
||||||
|
const supplierClusters = useClusteredNodes(undefined, ref('supplier'))
|
||||||
|
|
||||||
|
const clusteredPointsByType = computed(() => ({
|
||||||
|
offer: offerClusters.clusteredNodes.value,
|
||||||
|
hub: hubClusters.clusteredNodes.value,
|
||||||
|
supplier: supplierClusters.clusteredNodes.value
|
||||||
|
}))
|
||||||
|
|
||||||
|
const activeClusterType = computed<'offer' | 'hub' | 'supplier'>(() => {
|
||||||
|
if (mapViewMode.value === 'hubs') return 'hub'
|
||||||
|
if (mapViewMode.value === 'suppliers') return 'supplier'
|
||||||
|
return 'offer'
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleTypes = computed(() => [activeClusterType.value])
|
||||||
|
|
||||||
|
const clusterLoading = computed(() => {
|
||||||
|
if (!useTypedClusters.value) return singleClusterLoading.value
|
||||||
|
if (activeClusterType.value === 'hub') return hubClusters.loading.value
|
||||||
|
if (activeClusterType.value === 'supplier') return supplierClusters.loading.value
|
||||||
|
return offerClusters.loading.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchActiveClusters = async () => {
|
||||||
|
if (!currentBounds.value) return
|
||||||
|
if (activeClusterType.value === 'hub') {
|
||||||
|
await hubClusters.fetchClusters(currentBounds.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (activeClusterType.value === 'supplier') {
|
||||||
|
await supplierClusters.fetchClusters(currentBounds.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await offerClusters.fetchClusters(currentBounds.value)
|
||||||
|
}
|
||||||
|
|
||||||
// Refetch clusters when view mode changes
|
// Refetch clusters when view mode changes
|
||||||
watch(mapViewMode, async () => {
|
watch(mapViewMode, async () => {
|
||||||
|
if (!props.useServerClustering) return
|
||||||
|
if (useTypedClusters.value) {
|
||||||
|
if (currentBounds.value) {
|
||||||
|
await fetchActiveClusters()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
// Clear old data first
|
// Clear old data first
|
||||||
clearNodes()
|
clearNodes()
|
||||||
// Refetch with current bounds if available
|
// Refetch with current bounds if available
|
||||||
@@ -338,7 +390,11 @@ const onBoundsChange = (bounds: MapBounds) => {
|
|||||||
emit('bounds-change', bounds)
|
emit('bounds-change', bounds)
|
||||||
// Don't fetch clusters when in info mode
|
// Don't fetch clusters when in info mode
|
||||||
if (props.useServerClustering && !isInfoMode.value) {
|
if (props.useServerClustering && !isInfoMode.value) {
|
||||||
fetchClusters(bounds)
|
if (useTypedClusters.value) {
|
||||||
|
fetchActiveClusters()
|
||||||
|
} else {
|
||||||
|
fetchClusters(bounds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
ref="catalogPageRef"
|
ref="catalogPageRef"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
:use-server-clustering="true"
|
:use-server-clustering="true"
|
||||||
|
:use-typed-clusters="true"
|
||||||
:cluster-node-type="clusterNodeType"
|
:cluster-node-type="clusterNodeType"
|
||||||
map-id="unified-catalog-map"
|
map-id="unified-catalog-map"
|
||||||
:point-color="mapPointColor"
|
:point-color="mapPointColor"
|
||||||
@@ -410,10 +411,12 @@ const onMapSelect = async (item: MapSelectItem) => {
|
|||||||
|
|
||||||
const itemName = item.name || itemId.slice(0, 8) + '...'
|
const itemName = item.name || itemId.slice(0, 8) + '...'
|
||||||
|
|
||||||
|
const itemType = (item as MapSelectItem & { type?: 'hub' | 'supplier' | 'offer' }).type
|
||||||
|
|
||||||
// If in selection mode, use map click to fill the selector
|
// If in selection mode, use map click to fill the selector
|
||||||
if (selectMode.value) {
|
if (selectMode.value) {
|
||||||
// For hubs selection - click on hub fills hub selector
|
// For hubs selection - click on hub fills hub selector
|
||||||
if (selectMode.value === 'hub' && mapViewMode.value === 'hubs') {
|
if (selectMode.value === 'hub' && (itemType === 'hub' || mapViewMode.value === 'hubs')) {
|
||||||
selectItem('hub', itemId, itemName)
|
selectItem('hub', itemId, itemName)
|
||||||
showQuoteResults.value = false
|
showQuoteResults.value = false
|
||||||
offers.value = []
|
offers.value = []
|
||||||
@@ -421,7 +424,7 @@ const onMapSelect = async (item: MapSelectItem) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For supplier selection - click on supplier fills supplier selector
|
// For supplier selection - click on supplier fills supplier selector
|
||||||
if (selectMode.value === 'supplier' && mapViewMode.value === 'suppliers') {
|
if (selectMode.value === 'supplier' && (itemType === 'supplier' || mapViewMode.value === 'suppliers')) {
|
||||||
selectItem('supplier', itemId, itemName)
|
selectItem('supplier', itemId, itemName)
|
||||||
showQuoteResults.value = false
|
showQuoteResults.value = false
|
||||||
offers.value = []
|
offers.value = []
|
||||||
@@ -429,7 +432,7 @@ const onMapSelect = async (item: MapSelectItem) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For product selection viewing offers - fetch offer to get productUuid
|
// For product selection viewing offers - fetch offer to get productUuid
|
||||||
if (selectMode.value === 'product' && mapViewMode.value === 'offers') {
|
if (selectMode.value === 'product' && (itemType === 'offer' || mapViewMode.value === 'offers')) {
|
||||||
// Fetch offer details to get productUuid (not available in cluster data)
|
// Fetch offer details to get productUuid (not available in cluster data)
|
||||||
const data = await execute(GetOfferDocument, { uuid: itemId }, 'public', 'exchange')
|
const data = await execute(GetOfferDocument, { uuid: itemId }, 'public', 'exchange')
|
||||||
const offer = data?.getOffer
|
const offer = data?.getOffer
|
||||||
@@ -444,8 +447,8 @@ const onMapSelect = async (item: MapSelectItem) => {
|
|||||||
|
|
||||||
// NEW: Default behavior - open Info directly
|
// NEW: Default behavior - open Info directly
|
||||||
let infoType: 'hub' | 'supplier' | 'offer'
|
let infoType: 'hub' | 'supplier' | 'offer'
|
||||||
if (mapViewMode.value === 'hubs') infoType = 'hub'
|
if (itemType === 'hub' || mapViewMode.value === 'hubs') infoType = 'hub'
|
||||||
else if (mapViewMode.value === 'suppliers') infoType = 'supplier'
|
else if (itemType === 'supplier' || mapViewMode.value === 'suppliers') infoType = 'supplier'
|
||||||
else infoType = 'offer'
|
else infoType = 'offer'
|
||||||
|
|
||||||
openInfo(infoType, itemId)
|
openInfo(infoType, itemId)
|
||||||
|
|||||||
Reference in New Issue
Block a user