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 %}
+