From 35a20a29364635f2711c9920e846daa6e16942b3 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Sun, 13 Sep 2026 14:02:41 +0200 Subject: [PATCH] refactor: extract asset symbology into a dedicated module and update map layers and templates --- loko/assets/static/assets/asset_symbology.js | 751 ++++++++++++++++++ .../static/assets/unified_map_layers.js | 169 ++-- .../templates/assets/unified_index.html | 1 + .../mobile/assets/assets_index_mobile.html | 95 ++- loko/mobile/tests_asset_mobile.py | 49 +- loko/mobile/views.py | 15 +- 6 files changed, 931 insertions(+), 149 deletions(-) create mode 100644 loko/assets/static/assets/asset_symbology.js diff --git a/loko/assets/static/assets/asset_symbology.js b/loko/assets/static/assets/asset_symbology.js new file mode 100644 index 0000000..ac59306 --- /dev/null +++ b/loko/assets/static/assets/asset_symbology.js @@ -0,0 +1,751 @@ +/* asset_symbology.js + * Single source of truth for asset symbologies (colors, icon URLs, bootstrap icon glyphs) + * shared between Desktop (unified map layers & index) and Mobile. + */ +(function (global) { + 'use strict'; + + // ─── 1. Canonical Thematic Configurations ──────────────────────────────────── + const THEMATIC_CONFIG = { + publiclighting: { + color: '#fdd835', // jaune — street lights + iconClass: 'bi-lamp-fill', + glyphCodepoint: 'f453', + iconUrl: '/static/assets/public_lamp_post_icon.svg?v=20260521c', + iconSize: 56, + mapImage: 'icon-publiclighting', + }, + sign: { + color: '#0022ff', // bleu — road signs + iconClass: 'bi-sign-stop-fill', + glyphCodepoint: 'f82b', + iconUrl: null, + mapImage: 'icon-sign', + }, + trafficlights: { + color: '#e53935', // rouge — traffic signals + iconClass: 'bi-exclamation-octagon', + glyphCodepoint: 'f336', + iconUrl: '/static/assets/traffic_light_icon.svg', + iconSize: 48, + mapImage: 'icon-trafficlights', + }, + nature: { + color: '#2e7d32', // vert — green spaces + iconClass: 'bi-tree-fill', + glyphCodepoint: 'f5e1', + iconUrl: '/static/assets/nature_tree_icon.svg?v=20260521a', + iconSize: 56, + mapImage: 'icon-nature', + }, + structures: { + color: '#6d4c41', // marron — structures + iconClass: 'bi-building-fill', + glyphCodepoint: 'f876', + iconUrl: '/static/assets/structures_bridge_tunnel_icon.svg?v=20260522b', + iconSize: 52, + mapImage: 'icon-structures', + }, + its: { + color: '#0288d1', // cyan-blue — intelligent transport + iconClass: 'bi-broadcast-pin', + glyphCodepoint: 'f1d5', + iconUrl: null, + mapImage: 'icon-its', + }, + clean: { + color: '#5e5e5e', // gris — cleanliness + iconClass: 'bi-trash3-fill', + glyphCodepoint: 'f78a', + iconUrl: null, + mapImage: 'icon-clean', + }, + artworks: { + color: '#f24141', // corail — artworks / monuments + iconClass: 'bi-palette-fill', + glyphCodepoint: 'f4b0', + iconUrl: null, + mapImage: 'icon-artworks', + }, + controlcenters: { + color: '#7b1fa2', // violet — control centres + iconClass: 'bi-display-fill', + glyphCodepoint: 'f301', + iconUrl: null, + mapImage: 'icon-controlcenters', + }, + publicbuildings: { + color: '#2B579A', // navy blue — public buildings + iconClass: 'bi-building', + glyphCodepoint: 'f1dd', + iconUrl: null, + mapImage: 'icon-publicbuildings', + }, + buildings: { + color: '#2B579A', + iconClass: 'bi-building', + glyphCodepoint: 'f1dd', + iconUrl: null, + mapImage: 'icon-publicbuildings', + }, + outdoorfacilities: { + color: '#198754', // vert plein air + iconClass: 'bi-dribbble', + glyphCodepoint: 'f302', + iconUrl: null, + mapImage: 'icon-outdoorfacilities', + }, + outdoor: { + color: '#198754', + iconClass: 'bi-dribbble', + glyphCodepoint: 'f302', + iconUrl: null, + mapImage: 'icon-outdoorfacilities', + }, + parking: { + color: '#0d6efd', // bleu stationnement + iconClass: 'bi-p-square-fill', + glyphCodepoint: 'f806', + iconUrl: null, + mapImage: 'icon-parking', + }, + roads: { + color: '#607d8b', // bleu-gris + iconClass: 'bi-map-fill', + glyphCodepoint: 'f47e', + iconUrl: null, + mapImage: 'icon-roads', + }, + }; + + // Fast lookup for thematic colors: { nature: '#2e7d32', ... } + const THEMATIC_COLORS = {}; + Object.keys(THEMATIC_CONFIG).forEach(key => { + THEMATIC_COLORS[key] = THEMATIC_CONFIG[key].color; + }); + + // ─── 2. Specific Asset Type Overrides ─────────────────────────────────────── + // Keyed by asset_type or model_key + const ASSET_TYPE_OVERRIDES = { + // Traffic lights + trafficlightpole: { + thematic: 'trafficlights', + color: '#e53935', + iconUrl: '/static/assets/traffic_light_icon.svg', + iconSize: 48, + iconClass: 'bi-exclamation-octagon', + glyphCodepoint: 'f336', + mapImage: 'icon-trafficlights', + }, + trafficlightradar: { + thematic: 'trafficlights', + color: '#e53935', + iconUrl: '/static/assets/radar_preventif_icon.svg?v=20260710', + iconSize: 48, + iconClass: 'bi-broadcast-pin', + glyphCodepoint: 'f1d5', + mapImage: 'icon-trafficlightradar', + }, + trafficlightradarpole: { + thematic: 'trafficlights', + color: '#e53935', + iconUrl: '/static/assets/simple_pole_icon.svg', + iconSize: 32, + iconClass: 'bi-signpost-split', + glyphCodepoint: 'f4e0', + mapImage: 'icon-trafficlightradarpole', + }, + trafficlightelectricalcabinet: { + thematic: 'trafficlights', + color: '#331110', + iconUrl: '/static/assets/electrical-panel.png', + iconSize: 32, + iconClass: 'bi-broadcast-pin', + glyphCodepoint: 'f1d5', + mapImage: 'icon-trafficlightelectricalcabinet', + }, + trafficlightcontroller: { + thematic: 'trafficlights', + color: '#331110', + iconUrl: null, + iconClass: 'bi-broadcast-pin', + glyphCodepoint: 'f1d5', + mapImage: 'icon-trafficlightsequipment', + }, + trafficlightdetector: { + thematic: 'trafficlights', + color: '#331110', + iconUrl: null, + iconClass: 'bi-broadcast-pin', + glyphCodepoint: 'f1d5', + mapImage: 'icon-trafficlightsequipment', + }, + trafficlightcontrollerhardware: { + thematic: 'trafficlights', + color: '#331110', + iconUrl: null, + iconClass: 'bi-broadcast-pin', + glyphCodepoint: 'f1d5', + mapImage: 'icon-trafficlightsequipment', + }, + trafficlightnetworkhardware: { + thematic: 'trafficlights', + color: '#331110', + iconUrl: null, + iconClass: 'bi-broadcast-pin', + glyphCodepoint: 'f1d5', + mapImage: 'icon-trafficlightsequipment', + }, + trafficlightvoltagehardware: { + thematic: 'trafficlights', + color: '#331110', + iconUrl: null, + iconClass: 'bi-broadcast-pin', + glyphCodepoint: 'f1d5', + mapImage: 'icon-trafficlightsequipment', + }, + + // Public lighting + publiclightingelectricalcabinet: { + thematic: 'publiclighting', + color: '#fdd835', + iconUrl: '/static/assets/electrical-panel.png', + iconSize: 32, + iconClass: 'bi-lamp-fill', + glyphCodepoint: 'f453', + mapImage: 'icon-publiclightingelectricalcabinet', + }, + publiclightingpole: { + thematic: 'publiclighting', + color: '#fdd835', + iconUrl: '/static/assets/public_lamp_post_icon.svg?v=20260521c', + iconSize: 56, + iconClass: 'bi-lamp-fill', + glyphCodepoint: 'f453', + mapImage: 'icon-publiclighting', + }, + publiclightinglightfixture: { + thematic: 'publiclighting', + color: '#fdd835', + iconUrl: '/static/assets/public_lamp_post_icon.svg?v=20260521c', + iconSize: 56, + iconClass: 'bi-lamp-fill', + glyphCodepoint: 'f453', + mapImage: 'icon-publiclighting', + }, + + // Nature + naturebench: { + thematic: 'nature', + color: '#2e7d32', + iconUrl: '/static/assets/nature_bench_icon.svg', + iconSize: 36, + iconClass: 'bi-tree-fill', + glyphCodepoint: 'f5e1', + mapImage: 'icon-naturebench', + }, + naturerwiasset: { + thematic: 'nature', + color: '#0277bd', + iconUrl: null, + iconClass: 'bi-droplet-fill', + glyphCodepoint: 'f30b', + mapImage: 'icon-naturerwiasset', + }, + naturetree: { + thematic: 'nature', + color: '#2e7d32', + iconUrl: '/static/assets/nature_tree_icon.svg?v=20260521a', + iconSize: 56, + iconClass: 'bi-tree-fill', + glyphCodepoint: 'f5e1', + mapImage: 'icon-nature', + }, + naturegreensurface: { + thematic: 'nature', + color: '#2e7d32', + iconUrl: '/static/assets/nature_tree_icon.svg?v=20260521a', + iconSize: 56, + iconClass: 'bi-tree-fill', + glyphCodepoint: 'f5e1', + mapImage: 'icon-nature', + }, + natureshrub: { + thematic: 'nature', + color: '#2e7d32', + iconUrl: '/static/assets/nature_tree_icon.svg?v=20260521a', + iconSize: 56, + iconClass: 'bi-tree-fill', + glyphCodepoint: 'f5e1', + mapImage: 'icon-nature', + }, + + // Structures + structuregeoasset: { + thematic: 'structures', + color: '#6d4c41', + iconUrl: '/static/assets/structures_bridge_tunnel_icon.svg?v=20260522b', + iconSize: 52, + iconClass: 'bi-building-fill', + glyphCodepoint: 'f876', + mapImage: 'icon-structures', + }, + + // Roads + roaddrain: { + thematic: 'roads', + color: '#1e88e5', + iconUrl: '/static/assets/roaddrain_grille_icon.svg?v=20260521a', + iconSize: 46, + iconClass: 'bi-water', + glyphCodepoint: 'f617', + mapImage: 'icon-roaddrain', + }, + drain: { + thematic: 'roads', + color: '#1e88e5', + iconUrl: '/static/assets/roaddrain_grille_icon.svg?v=20260521a', + iconSize: 46, + iconClass: 'bi-water', + glyphCodepoint: 'f617', + mapImage: 'icon-roaddrain', + }, + roadconcreteblockset: { + thematic: 'roads', + color: '#607d8b', + iconUrl: '/static/assets/road_concrete_block_icon.svg?v=20260608a', + iconSize: 42, + iconClass: 'bi-map-fill', + glyphCodepoint: 'f47e', + mapImage: 'icon-roadconcreteblockset', + }, + roadway: { + thematic: 'roads', + color: '#4a4a4a', + iconUrl: null, + iconClass: 'bi-map-fill', + glyphCodepoint: 'f47e', + mapImage: 'icon-roads', + }, + roadsidewalk: { + thematic: 'roads', + color: '#979898', + iconUrl: null, + iconClass: 'bi-map-fill', + glyphCodepoint: 'f47e', + mapImage: 'icon-roads', + }, + sidewalk: { + thematic: 'roads', + color: '#979898', + iconUrl: null, + iconClass: 'bi-map-fill', + glyphCodepoint: 'f47e', + mapImage: 'icon-roads', + }, + roadfurniture: { + thematic: 'roads', + color: '#607d8b', + iconUrl: null, + iconClass: 'bi-map-fill', + glyphCodepoint: 'f47e', + mapImage: 'icon-roads', + }, + + // Clean + cleanlitterbin: { + thematic: 'clean', + color: '#5e5e5e', + iconUrl: null, + iconClass: 'bi-trash3-fill', + glyphCodepoint: 'f78a', + mapImage: 'icon-clean', + }, + cleanglasscontainer: { + thematic: 'clean', + color: '#5e5e5e', + iconUrl: null, + iconClass: 'bi-trash3-fill', + glyphCodepoint: 'f78a', + mapImage: 'icon-clean', + }, + + // Artworks + artworkfountain: { + thematic: 'artworks', + color: '#1e88e5', + iconUrl: '/static/assets/artwork_fountain_icon.svg?v=20260521a', + iconSize: 48, + iconClass: 'bi-water', + glyphCodepoint: 'f617', + mapImage: 'icon-artworkfountain', + }, + artworkmonument: { + thematic: 'artworks', + color: '#f24141', + iconUrl: '/static/assets/artwork_statue_icon.svg?v=20260521a', + iconSize: 48, + iconClass: 'bi-person-standing', + glyphCodepoint: 'f8f9', + mapImage: 'icon-artworkmonument', + }, + + // Signs + signpole: { + thematic: 'sign', + color: '#888888', + iconUrl: null, + iconClass: 'bi-signpost-split', + glyphCodepoint: 'f4e0', + mapImage: 'sign-pole-vertical', + }, + signpanel: { + thematic: 'sign', + color: '#0022ff', + iconUrl: null, + iconClass: 'bi-sign-stop-fill', + glyphCodepoint: 'f82b', + mapImage: 'icon-sign', + }, + }; + + // ─── 3. Specific Parking Spot Overrides by spot_type ───────────────────────── + const PARKING_SPOT_OVERRIDES = { + bicycle: { + thematic: 'parking', + color: '#10b981', // Vert mobilité active + iconUrl: null, + iconClass: 'bi-bicycle', + glyphCodepoint: 'f18d', + mapImage: 'icon-parkingspot-bicycle', + }, + cargo_bike: { + thematic: 'parking', + color: '#10b981', + iconUrl: null, + iconClass: 'bi-bicycle', + glyphCodepoint: 'f18d', + mapImage: 'icon-parkingspot-bicycle', + }, + scooter_drop_zone: { + thematic: 'parking', + color: '#10b981', + iconUrl: null, + iconClass: 'bi-scooter', + glyphCodepoint: 'f8ab', + mapImage: 'icon-parkingspot-scooter_drop_zone', + }, + delivery: { + thematic: 'parking', + color: '#f59e0b', // Ambre camion livraison + iconUrl: null, + iconClass: 'bi-truck-front-fill', + glyphCodepoint: 'f820', + mapImage: 'icon-parkingspot-delivery', + }, + pmr: { + thematic: 'parking', + color: '#0d6efd', + iconUrl: null, + iconClass: 'bi-person-wheelchair', + glyphCodepoint: 'f8fb', + mapImage: 'icon-parkingspot-pmr', + }, + ev_charging: { + thematic: 'parking', + color: '#0d6efd', + iconUrl: null, + iconClass: 'bi-ev-station-fill', + glyphCodepoint: 'f839', + mapImage: 'icon-parkingspot-ev_charging', + }, + kiss_and_ride: { + thematic: 'parking', + color: '#0d6efd', + iconUrl: null, + iconClass: 'bi-clock-fill', + glyphCodepoint: 'f291', + mapImage: 'icon-parkingspot-kiss_and_ride', + }, + carsharing: { + thematic: 'parking', + color: '#0d6efd', + iconUrl: null, + iconClass: 'bi-car-front-fill', + glyphCodepoint: 'f7e0', + mapImage: 'icon-parkingspot-carsharing', + }, + car_parking: { + thematic: 'parking', + color: '#0d6efd', + iconUrl: null, + iconClass: 'bi-p-square-fill', + glyphCodepoint: 'f806', + mapImage: 'icon-parkingspot', + }, + }; + + // ─── 4. Symbology Resolver ────────────────────────────────────────────────── + /** + * Resolves the full symbology (color, iconUrl, iconClass, mapImage) for a given asset or feature properties. + * + * @param {Object} item - Asset object or GeoJSON feature.properties with thematic, asset_type/model_key, spot_type + * @returns {Object} { color, iconUrl, iconClass, glyphCodepoint, mapImage, iconSize } + */ + function getAssetSymbology(item) { + if (!item) { + return { + color: '#2f3e8b', + iconUrl: null, + iconClass: 'bi-pin-map', + glyphCodepoint: 'f47e', + mapImage: 'icon-roads', + iconSize: 32, + }; + } + + const thematic = item.thematic || ''; + const assetType = item.asset_type || item.model_key || ''; + const spotType = item.spot_type || ''; + + // 1. Parking spot type override + if (thematic === 'parking' && spotType && PARKING_SPOT_OVERRIDES[spotType]) { + return Object.assign({}, PARKING_SPOT_OVERRIDES[spotType]); + } + + // 2. Specific asset_type override + if (assetType && ASSET_TYPE_OVERRIDES[assetType]) { + return Object.assign({}, ASSET_TYPE_OVERRIDES[assetType]); + } + + // 3. Fallback to thematic configuration + if (thematic && THEMATIC_CONFIG[thematic]) { + return Object.assign({}, THEMATIC_CONFIG[thematic]); + } + + // 4. Ultimate fallback + return { + color: THEMATIC_COLORS[thematic] || '#2f3e8b', + iconUrl: null, + iconClass: item.thematic_icon || 'bi-pin-map', + glyphCodepoint: 'f47e', + mapImage: 'icon-roads', + iconSize: 32, + }; + } + + /** + * Returns the hex color for a thematic. + */ + function getThematicColor(thematicCode) { + return THEMATIC_COLORS[thematicCode] || '#2f3e8b'; + } + + /** + * Renders an HTML string for displaying an asset icon (image SVG/PNG or Bootstrap icon). + * + * @param {Object} symbology - Result of getAssetSymbology(item) + * @param {Object} [options] - Additional HTML/styling options { imgClass, iconClass, style } + * @returns {string} HTML markup + */ + function renderIconHtml(symbology, options = {}) { + if (!symbology) return ''; + + const customStyle = options.style ? ` style="${options.style}"` : ''; + + if (symbology.iconUrl) { + const imgClass = options.imgClass || 'asset-symbology-icon-img'; + return ``; + } + + const iconClass = options.iconClass || symbology.iconClass || 'bi-pin-map'; + return ``; + } + + // ─── 5. MapLibre Expression & Config Generators ───────────────────────────── + + /** + * Generates the dictionary of static icon URLs for loadNormalisedIcon(). + * Allows custom / template-injected URLs to take precedence if already defined. + */ + function getIconUrls() { + const injected = (typeof global !== 'undefined' && global.UNIFIED_ICON_URLS) ? global.UNIFIED_ICON_URLS : {}; + const urls = { + trafficlights: injected.trafficlights || ASSET_TYPE_OVERRIDES.trafficlightpole.iconUrl, + trafficlightradar: injected.trafficlightradar || ASSET_TYPE_OVERRIDES.trafficlightradar.iconUrl, + trafficlightradarpole: injected.trafficlightradarpole || ASSET_TYPE_OVERRIDES.trafficlightradarpole.iconUrl, + trafficlightelectricalcabinet: injected.trafficlightelectricalcabinet || ASSET_TYPE_OVERRIDES.trafficlightelectricalcabinet.iconUrl, + publiclighting: injected.publiclighting || THEMATIC_CONFIG.publiclighting.iconUrl, + publiclightingelectricalcabinet: injected.publiclightingelectricalcabinet || ASSET_TYPE_OVERRIDES.publiclightingelectricalcabinet.iconUrl, + nature: injected.nature || THEMATIC_CONFIG.nature.iconUrl, + structures: injected.structures || THEMATIC_CONFIG.structures.iconUrl, + roaddrain: injected.roaddrain || ASSET_TYPE_OVERRIDES.roaddrain.iconUrl, + roadconcreteblockset: injected.roadconcreteblockset || ASSET_TYPE_OVERRIDES.roadconcreteblockset.iconUrl, + naturebench: injected.naturebench || ASSET_TYPE_OVERRIDES.naturebench.iconUrl, + artworkfountain: injected.artworkfountain || ASSET_TYPE_OVERRIDES.artworkfountain.iconUrl, + artworkmonument: injected.artworkmonument || ASSET_TYPE_OVERRIDES.artworkmonument.iconUrl, + }; + return urls; + } + + /** + * Generates the GLYPH_ICONS array for registerBootstrapGlyphSdfIcon() in MapLibre. + */ + function getGlyphIcons() { + return [ + { name: 'icon-publiclighting', codepoint: 'f453', color: '#fdd835' }, + { name: 'icon-publiclightingelectricalcabinet', codepoint: 'f453', color: '#fdd835' }, + { name: 'icon-trafficlights', codepoint: 'f336', color: '#e53935' }, + { name: 'icon-trafficlightsequipment', codepoint: 'f1d5', color: '#331110' }, + { name: 'icon-trafficlightradar', codepoint: 'f1d5', color: '#e53935' }, + { name: 'icon-trafficlightradarpole', codepoint: 'f4e0', color: '#e53935' }, + { name: 'icon-sign', codepoint: 'f82b', color: '#0022ff' }, + { name: 'icon-nature', codepoint: 'f5e1', color: '#2e7d32' }, + { name: 'icon-naturebench', codepoint: 'f5e1', color: '#2e7d32' }, + { name: 'icon-naturerwiasset', codepoint: 'f30b', color: '#0277bd' }, + { name: 'icon-roaddrain', codepoint: 'f617', color: '#1e88e5' }, + { name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' }, + { name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' }, + { name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' }, + { name: 'icon-artworks', codepoint: 'f4b0', color: '#f24141' }, + { name: 'icon-artworkfountain', codepoint: 'f617', color: '#1e88e5' }, + { name: 'icon-artworkmonument', codepoint: 'f8f9', color: '#f24141' }, + { name: 'icon-controlcenters', codepoint: 'f301', color: '#7b1fa2' }, + { name: 'icon-publicbuildings', codepoint: 'f1dd', color: '#2B579A' }, + { name: 'icon-publicbuilding', codepoint: 'f1dd', color: '#2B579A' }, + { name: 'icon-outdoorfacilities', codepoint: 'f302', color: '#198754' }, + { name: 'icon-outdoor', codepoint: 'f302', color: '#198754' }, + { name: 'icon-outdoorlocation', codepoint: 'f302', color: '#198754' }, + { name: 'icon-outdoorasset', codepoint: 'f302', color: '#198754' }, + { name: 'icon-roads', codepoint: 'f47e', color: '#607d8b' }, + { name: 'icon-parking', codepoint: 'f806', color: '#0d6efd' }, + { name: 'icon-parkingspot', codepoint: 'f806', color: '#0d6efd' }, + { name: 'icon-parkingspot-bicycle', codepoint: 'f18d', color: '#10b981' }, + { name: 'icon-parkingspot-scooter_drop_zone', codepoint: 'f8ab', color: '#10b981' }, + { name: 'icon-parkingspot-pmr', codepoint: 'f8fb', color: '#0d6efd' }, + { name: 'icon-parkingspot-ev_charging', codepoint: 'f839', color: '#0d6efd' }, + { name: 'icon-parkingspot-delivery', codepoint: 'f820', color: '#f59e0b' }, + { name: 'icon-parkingspot-kiss_and_ride', codepoint: 'f291', color: '#0d6efd' }, + { name: 'icon-parkingspot-carsharing', codepoint: 'f7e0', color: '#0d6efd' }, + { name: 'icon-parkinglocation', codepoint: 'f806', color: '#0d6efd' }, + ]; + } + + /** + * Generates MapLibre THEMATIC_COLOR_EXPR. + */ + function getThematicColorExpr() { + return [ + 'match', ['get', 'thematic'], + 'publiclighting', THEMATIC_CONFIG.publiclighting.color, + 'sign', THEMATIC_CONFIG.sign.color, + 'trafficlights', THEMATIC_CONFIG.trafficlights.color, + 'nature', THEMATIC_CONFIG.nature.color, + 'structures', THEMATIC_CONFIG.structures.color, + 'its', THEMATIC_CONFIG.its.color, + 'clean', THEMATIC_CONFIG.clean.color, + 'artworks', THEMATIC_CONFIG.artworks.color, + 'controlcenters', THEMATIC_CONFIG.controlcenters.color, + 'publicbuildings', THEMATIC_CONFIG.publicbuildings.color, + 'buildings', THEMATIC_CONFIG.buildings.color, + 'parking', THEMATIC_CONFIG.parking.color, + 'roads', [ + 'match', ['get', 'asset_type'], + 'roadway', ASSET_TYPE_OVERRIDES.roadway.color, + 'sidewalk', ASSET_TYPE_OVERRIDES.sidewalk.color, + THEMATIC_CONFIG.roads.color, + ], + '#0022ff', // fallback + ]; + } + + /** + * Generates the MapLibre icon-image case expression for _unified_assets_symbol. + */ + function getIconImageExpr() { + return [ + 'case', + ['==', ['get', 'asset_type'], 'trafficlightpole'], 'icon-trafficlights', + ['==', ['get', 'asset_type'], 'trafficlightradar'], 'icon-trafficlightradar', + ['==', ['get', 'asset_type'], 'trafficlightradarpole'], 'icon-trafficlightradarpole', + ['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'], 'icon-trafficlightelectricalcabinet', + ['==', ['get', 'asset_type'], 'trafficlightcontroller'], 'icon-trafficlightsequipment', + ['==', ['get', 'asset_type'], 'trafficlightdetector'], 'icon-trafficlightsequipment', + ['==', ['get', 'asset_type'], 'trafficlightcontrollerhardware'], 'icon-trafficlightsequipment', + ['==', ['get', 'asset_type'], 'trafficlightnetworkhardware'], 'icon-trafficlightsequipment', + ['==', ['get', 'asset_type'], 'trafficlightvoltagehardware'], 'icon-trafficlightsequipment', + ['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'], 'icon-publiclightingelectricalcabinet', + ['==', ['get', 'asset_type'], 'artworkfountain'], 'icon-artworkfountain', + ['==', ['get', 'asset_type'], 'artworkmonument'], 'icon-artworkmonument', + ['==', ['get', 'asset_type'], 'naturebench'], 'icon-naturebench', + ['==', ['get', 'asset_type'], 'drain'], 'icon-roaddrain', + ['==', ['get', 'asset_type'], 'roaddrain'], 'icon-roaddrain', + ['==', ['get', 'asset_type'], 'roadconcreteblockset'], 'icon-roadconcreteblockset', + ['==', ['get', 'asset_type'], 'naturerwiasset'], 'icon-naturerwiasset', + ['==', ['get', 'spot_type'], 'scooter_drop_zone'], 'icon-parkingspot-scooter_drop_zone', + ['==', ['get', 'spot_type'], 'pmr'], 'icon-parkingspot-pmr', + ['==', ['get', 'spot_type'], 'ev_charging'], 'icon-parkingspot-ev_charging', + ['==', ['get', 'spot_type'], 'delivery'], 'icon-parkingspot-delivery', + ['==', ['get', 'spot_type'], 'kiss_and_ride'], 'icon-parkingspot-kiss_and_ride', + ['==', ['get', 'spot_type'], 'carsharing'], 'icon-parkingspot-carsharing', + ['match', ['get', 'spot_type'], ['bicycle', 'cargo_bike'], true, false], 'icon-parkingspot-bicycle', + ['==', ['get', 'asset_type'], 'parkingspot'], 'icon-parkingspot', + + // Fallback: match by thematic + [ + 'match', ['get', 'thematic'], + 'publiclighting', 'icon-publiclighting', + 'sign', 'icon-sign', + 'trafficlights', 'icon-trafficlightsequipment', + 'nature', 'icon-nature', + 'structures', 'icon-structures', + 'its', 'icon-its', + 'clean', 'icon-clean', + 'artworks', 'icon-artworks', + 'controlcenters', 'icon-controlcenters', + 'publicbuildings', 'icon-publicbuildings', + 'publicbuilding', 'icon-publicbuilding', + 'parking', 'icon-parking', + 'roads', 'icon-roads', + 'icon-roads', // fallback + ], + ]; + } + + /** + * Generates MapLibre match expression for parking spot colors. + */ + function getParkingColorExpr() { + return [ + 'match', ['get', 'spot_type'], + ['bicycle', 'cargo_bike', 'scooter_drop_zone'], PARKING_SPOT_OVERRIDES.bicycle.color, // '#10b981' + 'delivery', PARKING_SPOT_OVERRIDES.delivery.color, // '#f59e0b' + THEMATIC_CONFIG.parking.color, // '#0d6efd' (voitures: PMR, IRVE, CD, carsharing, car_parking) + ]; + } + + // ─── 6. Public Export ─────────────────────────────────────────────────────── + global.AssetSymbology = { + THEMATIC_CONFIG, + THEMATIC_COLORS, + ASSET_TYPE_OVERRIDES, + PARKING_SPOT_OVERRIDES, + getAssetSymbology, + getThematicColor, + renderIconHtml, + getIconUrls, + getGlyphIcons, + getThematicColorExpr, + getIconImageExpr, + getParkingColorExpr, + }; + + // Ensure UNIFIED_ICON_URLS is globally available if not already defined + if (typeof global.UNIFIED_ICON_URLS === 'undefined') { + global.UNIFIED_ICON_URLS = getIconUrls(); + } else { + // Merge defaults with any properties already set + Object.assign(global.UNIFIED_ICON_URLS, Object.assign({}, getIconUrls(), global.UNIFIED_ICON_URLS)); + } + +})(typeof window !== 'undefined' ? window : this); diff --git a/loko/assets/static/assets/unified_map_layers.js b/loko/assets/static/assets/unified_map_layers.js index 4eae967..a1f0b04 100644 --- a/loko/assets/static/assets/unified_map_layers.js +++ b/loko/assets/static/assets/unified_map_layers.js @@ -183,50 +183,27 @@ function loadUnifiedMapIcons(map) { // If the SVG above is unavailable, publiclighting falls back to Bootstrap glyph. // ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ────────── - // codepoints extracted from bootstrap-icons.min.css - // Build the set of icon names that have a file-based URL being loaded asynchronously. - // These must be excluded from glyph registration entirely — not just skipped via - // map.hasImage() — because loadNormalisedIcon is async and the glyph loop runs - // synchronously before the file-based image has had a chance to register. + // codepoints and colors derived from AssetSymbology (single source of truth) const FILE_ICON_NAMES = new Set(Object.keys(iconUrls).map(k => 'icon-' + k)); - const GLYPH_ICONS = [ - { name: 'icon-publiclighting', codepoint: 'f453', color: '#fdd835' }, // lamp-fill fallback — jaune - { name: 'icon-publiclightingelectricalcabinet', codepoint: 'f453', color: '#fdd835' }, // lamp-fill fallback — jaune - { name: 'icon-trafficlights', codepoint: 'f336', color: '#e53935' }, // exclamation-octagon — rouge (fallback) - { name: 'icon-trafficlightsequipment', codepoint: 'f1d5', color: '#331110' }, // generic TL equipment — rouge - { name: 'icon-trafficlightradar', codepoint: 'f1d5', color: '#e53935' }, // radar preventif fallback — rouge - { name: 'icon-trafficlightradarpole', codepoint: 'f4e0', color: '#e53935' }, // radar pole fallback — rouge (signpost-split) - { name: 'icon-sign', codepoint: 'f82b', color: '#0022ff' }, // sign-stop-fill — bleu - { name: 'icon-nature', codepoint: 'f5e1', color: '#2e7d32' }, // tree-fill — vert - { name: 'icon-naturebench', codepoint: 'f5e1', color: '#2e7d32' }, // tree-fill fallback — vert - { name: 'icon-naturerwiasset', codepoint: 'f30b', color: '#0277bd' }, // droplet-fill — bleu GIEP - { name: 'icon-roaddrain', codepoint: 'f617', color: '#1e88e5' }, // water — bleu drain - { name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' }, // building-fill — marron - { name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' }, // broadcast-pin — bleu cyan - { name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' }, // trash3-fill — gris - { name: 'icon-artworks', codepoint: 'f4b0', color: '#f24141' }, // palette-fill — corail - { name: 'icon-artworkfountain', codepoint: 'f617', color: '#1e88e5' }, // water — bleu fontaine - { name: 'icon-artworkmonument', codepoint: 'f8f9', color: '#f24141' }, // person-standing — statue - { name: 'icon-controlcenters', codepoint: 'f301', color: '#7b1fa2' }, // display-fill — violet - { name: 'icon-publicbuildings', codepoint: 'f1dd', color: '#2B579A' }, // building — bleu nuit - { name: 'icon-publicbuilding', codepoint: 'f1dd', color: '#2B579A' }, // building — bleu nuit - { name: 'icon-outdoorfacilities', codepoint: 'f302', color: '#198754' }, // dribbble/dribbble icon — vert plein air - { name: 'icon-outdoor', codepoint: 'f302', color: '#198754' }, - { name: 'icon-outdoorlocation', codepoint: 'f302', color: '#198754' }, - { name: 'icon-outdoorasset', codepoint: 'f302', color: '#198754' }, - { name: 'icon-roads', codepoint: 'f47e', color: '#607d8b' }, // map-fill — bleu-gris - { name: 'icon-parking', codepoint: 'f806', color: '#0d6efd' }, // p-square-fill — bleu stationnement - { name: 'icon-parkingspot', codepoint: 'f806', color: '#0d6efd' }, // p-square-fill — bleu stationnement - { name: 'icon-parkingspot-bicycle', codepoint: 'f18d', color: '#10b981' }, // bicycle — vert mobilité active - { name: 'icon-parkingspot-scooter_drop_zone', codepoint: 'f8ab', color: '#10b981' }, // scooter — vert mobilité active - { name: 'icon-parkingspot-pmr', codepoint: 'f8fb', color: '#0d6efd' }, // person-wheelchair — bleu voiture PMR - { name: 'icon-parkingspot-ev_charging', codepoint: 'f839', color: '#0d6efd' }, // ev-station-fill — bleu voiture IRVE - { name: 'icon-parkingspot-delivery', codepoint: 'f820', color: '#f59e0b' }, // truck-front-fill — orange camion livraison - { name: 'icon-parkingspot-kiss_and_ride', codepoint: 'f291', color: '#0d6efd' }, // clock-fill — bleu voiture CD - { name: 'icon-parkingspot-carsharing', codepoint: 'f7e0', color: '#0d6efd' }, // car-front-fill — bleu voiture autopartage - { name: 'icon-parkinglocation', codepoint: 'f806', color: '#0d6efd' }, // p-square-fill — bleu stationnement - ]; + const GLYPH_ICONS = (typeof AssetSymbology !== 'undefined') + ? AssetSymbology.getGlyphIcons() + : [ + { name: 'icon-publiclighting', codepoint: 'f453', color: '#fdd835' }, + { name: 'icon-trafficlights', codepoint: 'f336', color: '#e53935' }, + { name: 'icon-trafficlightsequipment', codepoint: 'f1d5', color: '#331110' }, + { name: 'icon-sign', codepoint: 'f82b', color: '#0022ff' }, + { name: 'icon-nature', codepoint: 'f5e1', color: '#2e7d32' }, + { name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' }, + { name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' }, + { name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' }, + { name: 'icon-artworks', codepoint: 'f4b0', color: '#f24141' }, + { name: 'icon-controlcenters', codepoint: 'f301', color: '#7b1fa2' }, + { name: 'icon-publicbuildings', codepoint: 'f1dd', color: '#2B579A' }, + { name: 'icon-outdoorfacilities', codepoint: 'f302', color: '#198754' }, + { name: 'icon-roads', codepoint: 'f47e', color: '#607d8b' }, + { name: 'icon-parking', codepoint: 'f806', color: '#0d6efd' }, + ]; @@ -293,30 +270,32 @@ function registerCrossSdfIcons(map) { // ─── Shared colour expression ───────────────────────────────────────────────── // Used in both the asset fill layer and the asset symbol layer. +// Generated from AssetSymbology (single source of truth) // eslint-disable-next-line no-unused-vars (referenced by UNIFIED_LAYERS below) -const THEMATIC_COLOR_EXPR = [ - 'match', ['get', 'thematic'], - 'publiclighting', '#fdd835', // yellow — street lights - 'sign', '#0022ff', // blue — road signs - 'trafficlights', '#e53935', // red — traffic signals - 'nature', '#2e7d32', // green — green spaces - 'structures', '#6d4c41', // brown — structures - 'its', '#0288d1', // cyan-blue — intelligent transport - 'clean', '#5e5e5e', // grey — cleanliness - 'artworks', '#f24141', // coral — artworks / monuments - 'controlcenters', '#7b1fa2', // purple — control centres - 'publicbuildings', '#2B579A', // navy blue — public buildings - 'buildings', '#2B579A', // navy blue — public buildings - 'parking', '#0d6efd', // blue — parking & mobility - 'roads', [ - - 'match', ['get', 'asset_type'], - 'roadway', '#4a4a4a', - 'sidewalk', '#979898ff', - '#607d8b' // fallback for roads - ], - '#0022ff' // fallback -]; +const THEMATIC_COLOR_EXPR = (typeof AssetSymbology !== 'undefined') + ? AssetSymbology.getThematicColorExpr() + : [ + 'match', ['get', 'thematic'], + 'publiclighting', '#fdd835', + 'sign', '#0022ff', + 'trafficlights', '#e53935', + 'nature', '#2e7d32', + 'structures', '#6d4c41', + 'its', '#0288d1', + 'clean', '#5e5e5e', + 'artworks', '#f24141', + 'controlcenters', '#7b1fa2', + 'publicbuildings', '#2B579A', + 'buildings', '#2B579A', + 'parking', '#0d6efd', + 'roads', [ + 'match', ['get', 'asset_type'], + 'roadway', '#4a4a4a', + 'sidewalk', '#979898ff', + '#607d8b' + ], + '#0022ff' + ]; // ─── Layer 1 — location fill (polygon locations: streets, areas) ────────────── // id MUST stay 'locations': LayerSwitcherControl uses beforeLayer: 'locations'. @@ -356,12 +335,12 @@ const _unified_assets_fill = { ['==', ['get', 'thematic'], 'nature'], ['coalesce', ['get', 'color'], '#2e7d32'], ['==', ['get', 'thematic'], 'parking'], - [ + (typeof AssetSymbology !== 'undefined' ? AssetSymbology.getParkingColorExpr() : [ 'match', ['get', 'spot_type'], ['bicycle', 'cargo_bike', 'scooter_drop_zone'], '#10b981', // Vert mobilité active 'delivery', '#f59e0b', // Orange/Ambre camion livraison '#0d6efd' // Bleu voitures (PMR, CD, EV, carsharing, car_parking) - ], + ]), THEMATIC_COLOR_EXPR ], @@ -512,57 +491,7 @@ const _unified_assets_symbol = { ] ], layout: { - 'icon-image': [ - // asset_type takes priority (differentiates sub-types within a thematic) - 'case', - ['==', ['get', 'asset_type'], 'trafficlightpole'], 'icon-trafficlights', - ['==', ['get', 'asset_type'], 'trafficlightradar'], 'icon-trafficlightradar', - ['==', ['get', 'asset_type'], 'trafficlightradarpole'], 'icon-trafficlightradarpole', - ['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'], 'icon-trafficlightelectricalcabinet', - ['==', ['get', 'asset_type'], 'trafficlightcontroller'], 'icon-trafficlightsequipment', - ['==', ['get', 'asset_type'], 'trafficlightdetector'], 'icon-trafficlightsequipment', - ['==', ['get', 'asset_type'], 'trafficlightcontrollerhardware'], 'icon-trafficlightsequipment', - ['==', ['get', 'asset_type'], 'trafficlightnetworkhardware'], 'icon-trafficlightsequipment', - ['==', ['get', 'asset_type'], 'trafficlightvoltagehardware'], 'icon-trafficlightsequipment', - ['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'], 'icon-publiclightingelectricalcabinet', - ['==', ['get', 'asset_type'], 'artworkfountain'], 'icon-artworkfountain', - ['==', ['get', 'asset_type'], 'artworkmonument'], 'icon-artworkmonument', - ['==', ['get', 'asset_type'], 'naturebench'], 'icon-naturebench', - ['==', ['get', 'asset_type'], 'drain'], 'icon-roaddrain', - ['==', ['get', 'asset_type'], 'roaddrain'], 'icon-roaddrain', - ['==', ['get', 'asset_type'], 'roadconcreteblockset'], 'icon-roadconcreteblockset', - ['==', ['get', 'asset_type'], 'naturerwiasset'], 'icon-naturerwiasset', - ['==', ['get', 'spot_type'], 'scooter_drop_zone'], 'icon-parkingspot-scooter_drop_zone', - ['==', ['get', 'spot_type'], 'pmr'], 'icon-parkingspot-pmr', - ['==', ['get', 'spot_type'], 'ev_charging'], 'icon-parkingspot-ev_charging', - ['==', ['get', 'spot_type'], 'delivery'], 'icon-parkingspot-delivery', - ['==', ['get', 'spot_type'], 'kiss_and_ride'], 'icon-parkingspot-kiss_and_ride', - ['==', ['get', 'spot_type'], 'carsharing'], 'icon-parkingspot-carsharing', - ['match', ['get', 'spot_type'], ['bicycle', 'cargo_bike'], true, false], 'icon-parkingspot-bicycle', - ['==', ['get', 'asset_type'], 'parkingspot'], 'icon-parkingspot', - - - // fallback: match by thematic - [ - 'match', ['get', 'thematic'], - 'publiclighting', 'icon-publiclighting', - 'sign', 'icon-sign', - // Non-pole trafficlights assets use the alternate icon. - 'trafficlights', 'icon-trafficlightsequipment', - 'nature', 'icon-nature', - 'structures', 'icon-structures', - 'its', 'icon-its', - 'clean', 'icon-clean', - 'artworks', 'icon-artworks', - 'controlcenters', 'icon-controlcenters', - 'publicbuildings', 'icon-publicbuildings', - 'publicbuilding', 'icon-publicbuilding', - 'parking', 'icon-parking', - 'roads', 'icon-roads', - 'icon-roads' // fallback - ] - - ], + 'icon-image': (typeof AssetSymbology !== 'undefined' && AssetSymbology.getIconImageExpr) ? AssetSymbology.getIconImageExpr() : 'icon-roads', 'icon-size': [ 'interpolate', ['linear'], ['zoom'], 8, ['case', ['any', ['==', ['get', 'asset_type'], 'drain'], ['==', ['get', 'asset_type'], 'roaddrain'], ['==', ['get', 'asset_type'], 'naturerwiasset']], 0.12, ['==', ['get', 'asset_type'], 'roadconcreteblockset'], 0.18, 0.4], @@ -856,12 +785,12 @@ const _unified_assets_polygon_outline = { 'case', ['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b', ['==', ['get', 'thematic'], 'parking'], - [ + (typeof AssetSymbology !== 'undefined' ? AssetSymbology.getParkingColorExpr() : [ 'match', ['get', 'spot_type'], ['bicycle', 'cargo_bike', 'scooter_drop_zone'], '#10b981', // Vert mobilité active 'delivery', '#f59e0b', // Orange/Ambre camion livraison '#0d6efd' // Bleu voitures (PMR, CD, EV, carsharing, car_parking) - ], + ]), THEMATIC_COLOR_EXPR ], diff --git a/loko/assets/templates/assets/unified_index.html b/loko/assets/templates/assets/unified_index.html index 0804a3b..201fe51 100644 --- a/loko/assets/templates/assets/unified_index.html +++ b/loko/assets/templates/assets/unified_index.html @@ -752,6 +752,7 @@ + {% endblock script %} \ No newline at end of file diff --git a/loko/mobile/templates/mobile/assets/assets_index_mobile.html b/loko/mobile/templates/mobile/assets/assets_index_mobile.html index 7e259d3..cb9a090 100644 --- a/loko/mobile/templates/mobile/assets/assets_index_mobile.html +++ b/loko/mobile/templates/mobile/assets/assets_index_mobile.html @@ -253,6 +253,8 @@ font-size: 1rem; cursor: pointer; transition: none !important; + padding: 3px; + box-sizing: border-box; } .maplibregl-marker { transition: none !important; @@ -262,7 +264,17 @@ transition: transform 0.15s ease; pointer-events: none; } - .asset-map-marker:hover i, .asset-map-marker:active i { + .asset-map-marker img, + .asset-symbology-icon-img { + width: 20px; + height: 20px; + object-fit: contain; + display: block; + pointer-events: none; + transition: transform 0.15s ease; + } + .asset-map-marker:hover i, .asset-map-marker:active i, + .asset-map-marker:hover img, .asset-map-marker:active img { transform: scale(1.2); } .asset-map-marker:hover, .asset-map-marker:active { @@ -339,6 +351,15 @@ justify-content: center; font-size: 1.35rem; flex-shrink: 0; + padding: 5px; + box-sizing: border-box; + } + .asset-icon-circle img, + #sheetIconCircle img { + width: 24px; + height: 24px; + object-fit: contain; + display: block; } /* Badges de statuts */ @@ -567,6 +588,7 @@ {% endblock content %} {% block extra_scripts %} +