refactor: extract asset symbology into a dedicated module and update map layers and templates

This commit is contained in:
kdeterme 2026-09-13 14:02:41 +02:00
parent 2ada4f88b7
commit 35a20a2936
6 changed files with 931 additions and 149 deletions

View file

@ -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 '<i class="bi bi-pin-map"></i>';
const customStyle = options.style ? ` style="${options.style}"` : '';
if (symbology.iconUrl) {
const imgClass = options.imgClass || 'asset-symbology-icon-img';
return `<img src="${symbology.iconUrl}" alt="" class="${imgClass}"${customStyle}>`;
}
const iconClass = options.iconClass || symbology.iconClass || 'bi-pin-map';
return `<i class="bi ${iconClass}"${customStyle}></i>`;
}
// ─── 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);

View file

@ -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
],

View file

@ -752,6 +752,7 @@
<script src="{% static 'common/base_script.js' %}"></script>
<script src="{% static 'common/base_map.js' %}"></script>
<script src="{% static 'assets/sign_panels_shared.js' %}?v=20260703c"></script>
<script src="{% static 'assets/asset_symbology.js' %}?v=20260913"></script>
<script src="{% static 'assets/unified_map_layers.js' %}?v=20260726_pb"></script>
<script src="{% static 'assets/unified_index.js' %}?v=20260726_pb"></script>
{% endblock script %}

View file

@ -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 %}
<script src="{% static 'assets/asset_symbology.js' %}?v=20260913"></script>
<script>
(function() {
'use strict';
@ -670,22 +692,24 @@
const sheetDetailLink = document.getElementById('sheetDetailLink');
const sheetRouteLink = document.getElementById('sheetRouteLink');
// Couleurs par thématique
const THEMATIC_COLORS = {
'nature': '#16a34a',
'sign': '#2563eb',
'trafficlights': '#ea580c',
'publiclighting': '#ca8a04',
'roads': '#475569',
'clean': '#059669',
'structures': '#7c3aed',
'artworks': '#db2777',
'controlcenters': '#0891b2',
'its': '#4f46e5',
'outdoorfacilities': '#65a30d',
'outdoor': '#65a30d',
'parking': '#0284c7',
};
// Couleurs par thématique (source unique : AssetSymbology)
const THEMATIC_COLORS = (typeof AssetSymbology !== 'undefined')
? AssetSymbology.THEMATIC_COLORS
: {
'nature': '#2e7d32',
'sign': '#0022ff',
'trafficlights': '#e53935',
'publiclighting': '#fdd835',
'roads': '#607d8b',
'clean': '#5e5e5e',
'structures': '#6d4c41',
'artworks': '#f24141',
'controlcenters': '#7b1fa2',
'its': '#0288d1',
'outdoorfacilities': '#198754',
'outdoor': '#198754',
'parking': '#0d6efd',
};
// Fonction pour obtenir une boîte englobante valide (non dégénérée) depuis la carte
function getValidMapBbox() {
@ -1028,12 +1052,17 @@
? `<span class="badge bg-primary-subtle text-primary ms-auto me-2 text-nowrap"><i class="bi bi-geo-alt me-1"></i>${formatDistance(asset.distance_m)}</span>`
: '';
const thematicColor = THEMATIC_COLORS[asset.thematic] || '#2f3e8b';
const iconClass = asset.thematic_icon || 'bi-pin-map';
const symbology = (typeof AssetSymbology !== 'undefined')
? AssetSymbology.getAssetSymbology(asset)
: { color: THEMATIC_COLORS[asset.thematic] || '#2f3e8b', iconClass: asset.thematic_icon || 'bi-pin-map' };
const thematicColor = symbology.color || THEMATIC_COLORS[asset.thematic] || '#2f3e8b';
const iconHtml = (typeof AssetSymbology !== 'undefined')
? AssetSymbology.renderIconHtml(symbology)
: `<i class="bi ${symbology.iconClass || 'bi-pin-map'}"></i>`;
card.innerHTML = `
<div class="asset-icon-circle" style="background-color: ${thematicColor}15; color: ${thematicColor};">
<i class="bi ${iconClass}"></i>
${iconHtml}
</div>
<div class="flex-grow-1 text-truncate">
<div class="d-flex align-items-center gap-1.5 flex-wrap">
@ -1059,12 +1088,19 @@
results.forEach(asset => {
if (asset.lon === null || asset.lat === null) return;
const color = THEMATIC_COLORS[asset.thematic] || '#2f3e8b';
const symbology = (typeof AssetSymbology !== 'undefined')
? AssetSymbology.getAssetSymbology(asset)
: { color: THEMATIC_COLORS[asset.thematic] || '#2f3e8b', iconClass: asset.thematic_icon || 'bi-pin-map' };
const color = symbology.color || THEMATIC_COLORS[asset.thematic] || '#2f3e8b';
const iconHtml = (typeof AssetSymbology !== 'undefined')
? AssetSymbology.renderIconHtml(symbology)
: `<i class="bi ${symbology.iconClass || 'bi-pin-map'}"></i>`;
const el = document.createElement('div');
el.className = 'asset-map-marker';
el.style.borderColor = color;
el.style.color = color;
el.innerHTML = `<i class="bi ${asset.thematic_icon || 'bi-pin-map'}"></i>`;
el.innerHTML = iconHtml;
el.addEventListener('click', (e) => {
e.stopPropagation();
@ -1087,10 +1123,17 @@
sheetName.textContent = asset.name || asset.element_type || '';
sheetThematicName.textContent = asset.thematic_name || '';
const color = THEMATIC_COLORS[asset.thematic] || '#2f3e8b';
sheetIcon.className = `bi ${asset.thematic_icon || 'bi-pin-map'}`;
document.getElementById('sheetIconCircle').style.color = color;
document.getElementById('sheetIconCircle').style.backgroundColor = `${color}18`;
const symbology = (typeof AssetSymbology !== 'undefined')
? AssetSymbology.getAssetSymbology(asset)
: { color: THEMATIC_COLORS[asset.thematic] || '#2f3e8b', iconClass: asset.thematic_icon || 'bi-pin-map' };
const color = symbology.color || THEMATIC_COLORS[asset.thematic] || '#2f3e8b';
const iconCircle = document.getElementById('sheetIconCircle');
iconCircle.style.color = color;
iconCircle.style.backgroundColor = `${color}18`;
iconCircle.innerHTML = (typeof AssetSymbology !== 'undefined')
? AssetSymbology.renderIconHtml(symbology)
: `<i class="bi ${symbology.iconClass || 'bi-pin-map'}" id="sheetIcon"></i>`;
sheetStatusBadge.className = `badge-status badge-status-${asset.status || 'active'}`;
sheetStatusBadge.textContent = asset.status_display || asset.status || 'Actif';

View file

@ -2,11 +2,12 @@ from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.contrib.gis.geos import Polygon
from django.utils import translation
import json
from common.models import UserConfig, Role, Thematic, UserThematics
from assets.models import NatureTree, NatureLocation, NatureShrub
from assets.models import NatureTree, NatureLocation, NatureShrub, ParkingSpot
User = get_user_model()
@ -24,6 +25,14 @@ class MobileAssetDetailTests(TestCase):
"icon": "bi-tree"
}
)
self.parking_thematic, _ = Thematic.objects.get_or_create(
code="parking",
defaults={
"name_fr": "Stationnement",
"name_nl": "Parkeren",
"icon": "bi-p-square"
}
)
# Create Roles
self.admin_role, _ = Role.objects.get_or_create(name="admin")
@ -56,6 +65,17 @@ class MobileAssetDetailTests(TestCase):
status="active",
)
# Create test parking spot asset
poly = Polygon(((648000, 670000), (648010, 670000), (648010, 670010), (648000, 670010), (648000, 670000)), srid=3812)
self.spot = ParkingSpot.objects.create(
code="PK-SPOT-001",
name_fr="Arceaux vélo Gare",
name_nl="Fietsbeugels Station",
status="active",
spot_type="bicycle",
geom=poly,
)
def test_anonymous_user_redirected_to_login(self):
"""Anonymous user trying to access mobile asset detail is redirected to login."""
url = reverse("mobile:mobile_asset_detail_direct", args=["naturetree", self.tree.id])
@ -277,6 +297,7 @@ class MobileAssetDetailTests(TestCase):
# Assert JS script block and initial view controls are rendered
self.assertContains(response, "initMap")
self.assertContains(response, "maplibre-gl.js")
self.assertContains(response, "asset_symbology.js")
self.assertContains(response, "noResultsInZoneAlert")
self.assertContains(response, "btnResetToInitialView")
self.assertContains(response, "btnInitialView")
@ -316,6 +337,13 @@ class MobileAssetDetailTests(TestCase):
codes = [item["code"] for item in data["results"]]
self.assertIn("ARB-TEST-001", codes)
self.assertIn("ARB-SHRUB-001", codes)
# Check asset_type in nature results and GeoJSON properties
for item in data["results"]:
self.assertIn("asset_type", item)
self.assertIn(item["asset_type"], ["naturetree", "natureshrub"])
for feature in data["geojson"]["features"]:
self.assertIn("asset_type", feature["properties"])
self.assertIn(feature["properties"]["asset_type"], ["naturetree", "natureshrub"])
# 3. Test limit reached logic with limit=1: when limit is reached, return empty results and empty geojson
response = self.client.get(search_url, {"thematic": "nature", "limit": "1"})
@ -342,6 +370,25 @@ class MobileAssetDetailTests(TestCase):
data = response.json()
self.assertGreaterEqual(data["count"], 1)
# 6. Test search with thematic 'parking' and check spot_type and asset_type
UserThematics.objects.create(
user_config=user_config,
thematic=self.parking_thematic,
can_view_assets=True
)
response = self.client.get(search_url, {"thematic": "parking"})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertGreaterEqual(data["count"], 1)
spot_item = next((item for item in data["results"] if item["code"] == "PK-SPOT-001"), None)
self.assertIsNotNone(spot_item)
self.assertEqual(spot_item["asset_type"], "parkingspot")
self.assertEqual(spot_item["spot_type"], "bicycle")
spot_feature = next((f for f in data["geojson"]["features"] if f["properties"].get("code") == "PK-SPOT-001"), None)
self.assertIsNotNone(spot_feature)
self.assertEqual(spot_feature["properties"]["asset_type"], "parkingspot")
self.assertEqual(spot_feature["properties"]["spot_type"], "bicycle")
def test_generic_asset_uses_mobile_generic_template(self):
"""An asset model without specific template (NatureShrub) uses detail_generic_mobile.html."""
user = User.objects.create_user(username="shrub_viewer", password="password")

View file

@ -1199,11 +1199,12 @@ def mobile_assets_search(request):
from django.db.models import Q
from common.models import Thematic, UserThematics, UserConfig
from assets.models import (
NatureTree, NatureGreenSurface, NatureShrub, NatureBench,
NatureTree, NatureGreenSurface, NatureShrub, NatureBench, NatureRWIAsset,
SignPanel, SignPole,
TrafficLightPole, TrafficLightLantern, TrafficLightElectricalCabinet,
TrafficLightRadar, TrafficLightController,
PublicLightingPole, PublicLightingLightFixture, PublicLightingElectricalCabinet,
Roadway, RoadSidewalk, RoadFurniture,
Roadway, RoadSidewalk, RoadFurniture, RoadDrain, RoadConcreteBlockSet,
CleanLitterBin, CleanGlassContainer,
ArtworkMonument, ArtworkFountain,
StructureGeoAsset, ControlCenterGeoAsset, ITSGeoAsset,
@ -1290,6 +1291,7 @@ def mobile_assets_search(request):
('naturegreensurface', NatureGreenSurface),
('natureshrub', NatureShrub),
('naturebench', NatureBench),
('naturerwiasset', NatureRWIAsset),
],
'sign': [
('signpanel', SignPanel),
@ -1297,6 +1299,8 @@ def mobile_assets_search(request):
],
'trafficlights': [
('trafficlightpole', TrafficLightPole),
('trafficlightradar', TrafficLightRadar),
('trafficlightcontroller', TrafficLightController),
('trafficlightlantern', TrafficLightLantern),
('trafficlightelectricalcabinet', TrafficLightElectricalCabinet),
],
@ -1309,6 +1313,8 @@ def mobile_assets_search(request):
('roadway', Roadway),
('roadsidewalk', RoadSidewalk),
('roadfurniture', RoadFurniture),
('roaddrain', RoadDrain),
('roadconcreteblockset', RoadConcreteBlockSet),
],
'clean': [
('cleanlitterbin', CleanLitterBin),
@ -1454,6 +1460,7 @@ def mobile_assets_search(request):
element_type = get_element_type_label(model_key)
detail_url = reverse('mobile:mobile_asset_detail_direct', kwargs={'asset_model': model_key, 'asset_id': item.id})
spot_type = getattr(item, 'spot_type', None)
res_item = {
'id': item.id,
@ -1463,6 +1470,8 @@ def mobile_assets_search(request):
'thematic_name': thematic_name,
'thematic_icon': thematic_icon,
'model_key': model_key,
'asset_type': model_key,
'spot_type': spot_type,
'element_type': element_type,
'status': status_val,
'status_display': status_display,
@ -1489,6 +1498,8 @@ def mobile_assets_search(request):
'thematic_name': thematic_name,
'thematic_icon': thematic_icon,
'model_key': model_key,
'asset_type': model_key,
'spot_type': spot_type,
'element_type': element_type,
'status': status_val,
'status_display': status_display,