diff --git a/app/components/catalog/CatalogMap.vue b/app/components/catalog/CatalogMap.vue index e97b9e3..e1194e6 100644 --- a/app/components/catalog/CatalogMap.vue +++ b/app/components/catalog/CatalogMap.vue @@ -128,6 +128,53 @@ const loadEntityIcon = async (map: MapboxMapType, type: 'offer' | 'hub' | 'suppl }) } +// Standard colors for entity types +const ENTITY_COLORS = { + hub: '#22c55e', // green + supplier: '#3b82f6', // blue + offer: '#f97316' // orange +} as const + +// Load all icons for related points (each type with its standard color) +const loadRelatedPointIcons = async (map: MapboxMapType) => { + const types: Array<'hub' | 'supplier' | 'offer'> = ['hub', 'supplier', 'offer'] + for (const type of types) { + const iconName = `related-icon-${type}` + if (map.hasImage(iconName)) { + map.removeImage(iconName) + } + + const svg = createEntityIcon(type, ENTITY_COLORS[type]) + const img = new Image(32, 32) + + await new Promise((resolve) => { + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = 32 + canvas.height = 32 + const ctx = canvas.getContext('2d') + if (ctx) { + ctx.beginPath() + ctx.arc(16, 16, 15, 0, 2 * Math.PI) + ctx.fillStyle = ENTITY_COLORS[type] + ctx.fill() + ctx.strokeStyle = 'white' + ctx.lineWidth = 2 + ctx.stroke() + ctx.drawImage(img, 4, 4, 24, 24) + } + + const imageData = ctx?.getImageData(0, 0, 32, 32) + if (imageData) { + map.addImage(iconName, { width: 32, height: 32, data: imageData.data }) + } + resolve() + } + img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) + }) + } +} + const mapOptions = computed(() => ({ style: 'mapbox://styles/mapbox/satellite-streets-v12', center: props.initialCenter, @@ -372,27 +419,28 @@ const initClientClusteringLayers = async (map: MapboxMapType) => { } }) - // Related points layer (for Info mode - colored by type) + // Related points layer (for Info mode - icons by type) + await loadRelatedPointIcons(map) + map.addSource(relatedSourceId.value, { type: 'geojson', data: relatedPointsGeoJson.value }) map.addLayer({ - id: `${props.mapId}-related-circles`, - type: 'circle', + id: `${props.mapId}-related-points`, + type: 'symbol', source: relatedSourceId.value, - paint: { - 'circle-radius': 8, - 'circle-color': [ + layout: { + 'icon-image': [ 'match', ['get', 'type'], - 'hub', '#22c55e', // green - 'supplier', '#3b82f6', // blue - 'offer', '#f97316', // orange - '#06b6d4' // default cyan + 'hub', 'related-icon-hub', + 'supplier', 'related-icon-supplier', + 'offer', 'related-icon-offer', + 'related-icon-offer' // default ], - 'circle-stroke-width': 2, - 'circle-stroke-color': '#ffffff' + 'icon-size': 1, + 'icon-allow-overlap': true } }) map.addLayer({ @@ -403,7 +451,7 @@ const initClientClusteringLayers = async (map: MapboxMapType) => { 'text-field': ['get', 'name'], 'text-size': 11, 'text-anchor': 'top', - 'text-offset': [0, 1.2] + 'text-offset': [0, 1.5] }, paint: { 'text-color': '#ffffff', @@ -413,19 +461,19 @@ const initClientClusteringLayers = async (map: MapboxMapType) => { }) // Click handlers for related points - map.on('click', `${props.mapId}-related-circles`, (e) => { - const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-circles`] }) + 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 | undefined emit('select-item', props_data?.uuid, props_data) }) - map.on('mouseenter', `${props.mapId}-related-circles`, () => { + map.on('mouseenter', `${props.mapId}-related-points`, () => { map.getCanvas().style.cursor = 'pointer' }) - map.on('mouseleave', `${props.mapId}-related-circles`, () => { + map.on('mouseleave', `${props.mapId}-related-points`, () => { map.getCanvas().style.cursor = '' }) @@ -564,27 +612,28 @@ const initServerClusteringLayers = async (map: MapboxMapType) => { } }) - // Related points layer (for Info mode - colored by type) + // Related points layer (for Info mode - icons by type) + await loadRelatedPointIcons(map) + map.addSource(relatedSourceId.value, { type: 'geojson', data: relatedPointsGeoJson.value }) map.addLayer({ - id: `${props.mapId}-related-circles`, - type: 'circle', + id: `${props.mapId}-related-points`, + type: 'symbol', source: relatedSourceId.value, - paint: { - 'circle-radius': 8, - 'circle-color': [ + layout: { + 'icon-image': [ 'match', ['get', 'type'], - 'hub', '#22c55e', // green - 'supplier', '#3b82f6', // blue - 'offer', '#f97316', // orange - '#06b6d4' // default cyan + 'hub', 'related-icon-hub', + 'supplier', 'related-icon-supplier', + 'offer', 'related-icon-offer', + 'related-icon-offer' // default ], - 'circle-stroke-width': 2, - 'circle-stroke-color': '#ffffff' + 'icon-size': 1, + 'icon-allow-overlap': true } }) map.addLayer({ @@ -595,7 +644,7 @@ const initServerClusteringLayers = async (map: MapboxMapType) => { 'text-field': ['get', 'name'], 'text-size': 11, 'text-anchor': 'top', - 'text-offset': [0, 1.2] + 'text-offset': [0, 1.5] }, paint: { 'text-color': '#ffffff', @@ -605,19 +654,19 @@ const initServerClusteringLayers = async (map: MapboxMapType) => { }) // Click handlers for related points - map.on('click', `${props.mapId}-related-circles`, (e) => { - const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-circles`] }) + 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 | undefined emit('select-item', props_data?.uuid, props_data) }) - map.on('mouseenter', `${props.mapId}-related-circles`, () => { + map.on('mouseenter', `${props.mapId}-related-points`, () => { map.getCanvas().style.cursor = 'pointer' }) - map.on('mouseleave', `${props.mapId}-related-circles`, () => { + map.on('mouseleave', `${props.mapId}-related-points`, () => { map.getCanvas().style.cursor = '' }) }