refactor: extract asset symbology into a dedicated module and update map layers and templates
This commit is contained in:
parent
2ada4f88b7
commit
35a20a2936
6 changed files with 931 additions and 149 deletions
751
loko/assets/static/assets/asset_symbology.js
Normal file
751
loko/assets/static/assets/asset_symbology.js
Normal 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);
|
||||||
|
|
@ -183,49 +183,26 @@ function loadUnifiedMapIcons(map) {
|
||||||
// If the SVG above is unavailable, publiclighting falls back to Bootstrap glyph.
|
// If the SVG above is unavailable, publiclighting falls back to Bootstrap glyph.
|
||||||
|
|
||||||
// ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ──────────
|
// ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ──────────
|
||||||
// codepoints extracted from bootstrap-icons.min.css
|
// codepoints and colors derived from AssetSymbology (single source of truth)
|
||||||
// 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.
|
|
||||||
const FILE_ICON_NAMES = new Set(Object.keys(iconUrls).map(k => 'icon-' + k));
|
const FILE_ICON_NAMES = new Set(Object.keys(iconUrls).map(k => 'icon-' + k));
|
||||||
|
|
||||||
const GLYPH_ICONS = [
|
const GLYPH_ICONS = (typeof AssetSymbology !== 'undefined')
|
||||||
{ name: 'icon-publiclighting', codepoint: 'f453', color: '#fdd835' }, // lamp-fill fallback — jaune
|
? AssetSymbology.getGlyphIcons()
|
||||||
{ name: 'icon-publiclightingelectricalcabinet', codepoint: 'f453', color: '#fdd835' }, // lamp-fill fallback — jaune
|
: [
|
||||||
{ name: 'icon-trafficlights', codepoint: 'f336', color: '#e53935' }, // exclamation-octagon — rouge (fallback)
|
{ name: 'icon-publiclighting', codepoint: 'f453', color: '#fdd835' },
|
||||||
{ name: 'icon-trafficlightsequipment', codepoint: 'f1d5', color: '#331110' }, // generic TL equipment — rouge
|
{ name: 'icon-trafficlights', codepoint: 'f336', color: '#e53935' },
|
||||||
{ name: 'icon-trafficlightradar', codepoint: 'f1d5', color: '#e53935' }, // radar preventif fallback — rouge
|
{ name: 'icon-trafficlightsequipment', codepoint: 'f1d5', color: '#331110' },
|
||||||
{ name: 'icon-trafficlightradarpole', codepoint: 'f4e0', color: '#e53935' }, // radar pole fallback — rouge (signpost-split)
|
{ name: 'icon-sign', codepoint: 'f82b', color: '#0022ff' },
|
||||||
{ name: 'icon-sign', codepoint: 'f82b', color: '#0022ff' }, // sign-stop-fill — bleu
|
{ name: 'icon-nature', codepoint: 'f5e1', color: '#2e7d32' },
|
||||||
{ name: 'icon-nature', codepoint: 'f5e1', color: '#2e7d32' }, // tree-fill — vert
|
{ name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' },
|
||||||
{ name: 'icon-naturebench', codepoint: 'f5e1', color: '#2e7d32' }, // tree-fill fallback — vert
|
{ name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' },
|
||||||
{ name: 'icon-naturerwiasset', codepoint: 'f30b', color: '#0277bd' }, // droplet-fill — bleu GIEP
|
{ name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' },
|
||||||
{ name: 'icon-roaddrain', codepoint: 'f617', color: '#1e88e5' }, // water — bleu drain
|
{ name: 'icon-artworks', codepoint: 'f4b0', color: '#f24141' },
|
||||||
{ name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' }, // building-fill — marron
|
{ name: 'icon-controlcenters', codepoint: 'f301', color: '#7b1fa2' },
|
||||||
{ name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' }, // broadcast-pin — bleu cyan
|
{ name: 'icon-publicbuildings', codepoint: 'f1dd', color: '#2B579A' },
|
||||||
{ name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' }, // trash3-fill — gris
|
{ name: 'icon-outdoorfacilities', codepoint: 'f302', color: '#198754' },
|
||||||
{ name: 'icon-artworks', codepoint: 'f4b0', color: '#f24141' }, // palette-fill — corail
|
{ name: 'icon-roads', codepoint: 'f47e', color: '#607d8b' },
|
||||||
{ name: 'icon-artworkfountain', codepoint: 'f617', color: '#1e88e5' }, // water — bleu fontaine
|
{ name: 'icon-parking', codepoint: 'f806', color: '#0d6efd' },
|
||||||
{ 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
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -293,30 +270,32 @@ function registerCrossSdfIcons(map) {
|
||||||
|
|
||||||
// ─── Shared colour expression ─────────────────────────────────────────────────
|
// ─── Shared colour expression ─────────────────────────────────────────────────
|
||||||
// Used in both the asset fill layer and the asset symbol layer.
|
// 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)
|
// eslint-disable-next-line no-unused-vars (referenced by UNIFIED_LAYERS below)
|
||||||
const THEMATIC_COLOR_EXPR = [
|
const THEMATIC_COLOR_EXPR = (typeof AssetSymbology !== 'undefined')
|
||||||
|
? AssetSymbology.getThematicColorExpr()
|
||||||
|
: [
|
||||||
'match', ['get', 'thematic'],
|
'match', ['get', 'thematic'],
|
||||||
'publiclighting', '#fdd835', // yellow — street lights
|
'publiclighting', '#fdd835',
|
||||||
'sign', '#0022ff', // blue — road signs
|
'sign', '#0022ff',
|
||||||
'trafficlights', '#e53935', // red — traffic signals
|
'trafficlights', '#e53935',
|
||||||
'nature', '#2e7d32', // green — green spaces
|
'nature', '#2e7d32',
|
||||||
'structures', '#6d4c41', // brown — structures
|
'structures', '#6d4c41',
|
||||||
'its', '#0288d1', // cyan-blue — intelligent transport
|
'its', '#0288d1',
|
||||||
'clean', '#5e5e5e', // grey — cleanliness
|
'clean', '#5e5e5e',
|
||||||
'artworks', '#f24141', // coral — artworks / monuments
|
'artworks', '#f24141',
|
||||||
'controlcenters', '#7b1fa2', // purple — control centres
|
'controlcenters', '#7b1fa2',
|
||||||
'publicbuildings', '#2B579A', // navy blue — public buildings
|
'publicbuildings', '#2B579A',
|
||||||
'buildings', '#2B579A', // navy blue — public buildings
|
'buildings', '#2B579A',
|
||||||
'parking', '#0d6efd', // blue — parking & mobility
|
'parking', '#0d6efd',
|
||||||
'roads', [
|
'roads', [
|
||||||
|
|
||||||
'match', ['get', 'asset_type'],
|
'match', ['get', 'asset_type'],
|
||||||
'roadway', '#4a4a4a',
|
'roadway', '#4a4a4a',
|
||||||
'sidewalk', '#979898ff',
|
'sidewalk', '#979898ff',
|
||||||
'#607d8b' // fallback for roads
|
'#607d8b'
|
||||||
],
|
],
|
||||||
'#0022ff' // fallback
|
'#0022ff'
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─── Layer 1 — location fill (polygon locations: streets, areas) ──────────────
|
// ─── Layer 1 — location fill (polygon locations: streets, areas) ──────────────
|
||||||
// id MUST stay 'locations': LayerSwitcherControl uses beforeLayer: 'locations'.
|
// id MUST stay 'locations': LayerSwitcherControl uses beforeLayer: 'locations'.
|
||||||
|
|
@ -356,12 +335,12 @@ const _unified_assets_fill = {
|
||||||
['==', ['get', 'thematic'], 'nature'],
|
['==', ['get', 'thematic'], 'nature'],
|
||||||
['coalesce', ['get', 'color'], '#2e7d32'],
|
['coalesce', ['get', 'color'], '#2e7d32'],
|
||||||
['==', ['get', 'thematic'], 'parking'],
|
['==', ['get', 'thematic'], 'parking'],
|
||||||
[
|
(typeof AssetSymbology !== 'undefined' ? AssetSymbology.getParkingColorExpr() : [
|
||||||
'match', ['get', 'spot_type'],
|
'match', ['get', 'spot_type'],
|
||||||
['bicycle', 'cargo_bike', 'scooter_drop_zone'], '#10b981', // Vert mobilité active
|
['bicycle', 'cargo_bike', 'scooter_drop_zone'], '#10b981', // Vert mobilité active
|
||||||
'delivery', '#f59e0b', // Orange/Ambre camion livraison
|
'delivery', '#f59e0b', // Orange/Ambre camion livraison
|
||||||
'#0d6efd' // Bleu voitures (PMR, CD, EV, carsharing, car_parking)
|
'#0d6efd' // Bleu voitures (PMR, CD, EV, carsharing, car_parking)
|
||||||
],
|
]),
|
||||||
THEMATIC_COLOR_EXPR
|
THEMATIC_COLOR_EXPR
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -512,57 +491,7 @@ const _unified_assets_symbol = {
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
layout: {
|
layout: {
|
||||||
'icon-image': [
|
'icon-image': (typeof AssetSymbology !== 'undefined' && AssetSymbology.getIconImageExpr) ? AssetSymbology.getIconImageExpr() : 'icon-roads',
|
||||||
// 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-size': [
|
'icon-size': [
|
||||||
'interpolate', ['linear'], ['zoom'],
|
'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],
|
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',
|
'case',
|
||||||
['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b',
|
['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b',
|
||||||
['==', ['get', 'thematic'], 'parking'],
|
['==', ['get', 'thematic'], 'parking'],
|
||||||
[
|
(typeof AssetSymbology !== 'undefined' ? AssetSymbology.getParkingColorExpr() : [
|
||||||
'match', ['get', 'spot_type'],
|
'match', ['get', 'spot_type'],
|
||||||
['bicycle', 'cargo_bike', 'scooter_drop_zone'], '#10b981', // Vert mobilité active
|
['bicycle', 'cargo_bike', 'scooter_drop_zone'], '#10b981', // Vert mobilité active
|
||||||
'delivery', '#f59e0b', // Orange/Ambre camion livraison
|
'delivery', '#f59e0b', // Orange/Ambre camion livraison
|
||||||
'#0d6efd' // Bleu voitures (PMR, CD, EV, carsharing, car_parking)
|
'#0d6efd' // Bleu voitures (PMR, CD, EV, carsharing, car_parking)
|
||||||
],
|
]),
|
||||||
THEMATIC_COLOR_EXPR
|
THEMATIC_COLOR_EXPR
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -752,6 +752,7 @@
|
||||||
<script src="{% static 'common/base_script.js' %}"></script>
|
<script src="{% static 'common/base_script.js' %}"></script>
|
||||||
<script src="{% static 'common/base_map.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/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_map_layers.js' %}?v=20260726_pb"></script>
|
||||||
<script src="{% static 'assets/unified_index.js' %}?v=20260726_pb"></script>
|
<script src="{% static 'assets/unified_index.js' %}?v=20260726_pb"></script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
@ -253,6 +253,8 @@
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
|
padding: 3px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.maplibregl-marker {
|
.maplibregl-marker {
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
|
|
@ -262,7 +264,17 @@
|
||||||
transition: transform 0.15s ease;
|
transition: transform 0.15s ease;
|
||||||
pointer-events: none;
|
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);
|
transform: scale(1.2);
|
||||||
}
|
}
|
||||||
.asset-map-marker:hover, .asset-map-marker:active {
|
.asset-map-marker:hover, .asset-map-marker:active {
|
||||||
|
|
@ -339,6 +351,15 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 1.35rem;
|
font-size: 1.35rem;
|
||||||
flex-shrink: 0;
|
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 */
|
/* Badges de statuts */
|
||||||
|
|
@ -567,6 +588,7 @@
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
|
<script src="{% static 'assets/asset_symbology.js' %}?v=20260913"></script>
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -670,21 +692,23 @@
|
||||||
const sheetDetailLink = document.getElementById('sheetDetailLink');
|
const sheetDetailLink = document.getElementById('sheetDetailLink');
|
||||||
const sheetRouteLink = document.getElementById('sheetRouteLink');
|
const sheetRouteLink = document.getElementById('sheetRouteLink');
|
||||||
|
|
||||||
// Couleurs par thématique
|
// Couleurs par thématique (source unique : AssetSymbology)
|
||||||
const THEMATIC_COLORS = {
|
const THEMATIC_COLORS = (typeof AssetSymbology !== 'undefined')
|
||||||
'nature': '#16a34a',
|
? AssetSymbology.THEMATIC_COLORS
|
||||||
'sign': '#2563eb',
|
: {
|
||||||
'trafficlights': '#ea580c',
|
'nature': '#2e7d32',
|
||||||
'publiclighting': '#ca8a04',
|
'sign': '#0022ff',
|
||||||
'roads': '#475569',
|
'trafficlights': '#e53935',
|
||||||
'clean': '#059669',
|
'publiclighting': '#fdd835',
|
||||||
'structures': '#7c3aed',
|
'roads': '#607d8b',
|
||||||
'artworks': '#db2777',
|
'clean': '#5e5e5e',
|
||||||
'controlcenters': '#0891b2',
|
'structures': '#6d4c41',
|
||||||
'its': '#4f46e5',
|
'artworks': '#f24141',
|
||||||
'outdoorfacilities': '#65a30d',
|
'controlcenters': '#7b1fa2',
|
||||||
'outdoor': '#65a30d',
|
'its': '#0288d1',
|
||||||
'parking': '#0284c7',
|
'outdoorfacilities': '#198754',
|
||||||
|
'outdoor': '#198754',
|
||||||
|
'parking': '#0d6efd',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fonction pour obtenir une boîte englobante valide (non dégénérée) depuis la carte
|
// Fonction pour obtenir une boîte englobante valide (non dégénérée) depuis la carte
|
||||||
|
|
@ -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>`
|
? `<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 symbology = (typeof AssetSymbology !== 'undefined')
|
||||||
const iconClass = asset.thematic_icon || 'bi-pin-map';
|
? 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 = `
|
card.innerHTML = `
|
||||||
<div class="asset-icon-circle" style="background-color: ${thematicColor}15; color: ${thematicColor};">
|
<div class="asset-icon-circle" style="background-color: ${thematicColor}15; color: ${thematicColor};">
|
||||||
<i class="bi ${iconClass}"></i>
|
${iconHtml}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1 text-truncate">
|
<div class="flex-grow-1 text-truncate">
|
||||||
<div class="d-flex align-items-center gap-1.5 flex-wrap">
|
<div class="d-flex align-items-center gap-1.5 flex-wrap">
|
||||||
|
|
@ -1059,12 +1088,19 @@
|
||||||
results.forEach(asset => {
|
results.forEach(asset => {
|
||||||
if (asset.lon === null || asset.lat === null) return;
|
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');
|
const el = document.createElement('div');
|
||||||
el.className = 'asset-map-marker';
|
el.className = 'asset-map-marker';
|
||||||
el.style.borderColor = color;
|
el.style.borderColor = color;
|
||||||
el.style.color = color;
|
el.style.color = color;
|
||||||
el.innerHTML = `<i class="bi ${asset.thematic_icon || 'bi-pin-map'}"></i>`;
|
el.innerHTML = iconHtml;
|
||||||
|
|
||||||
el.addEventListener('click', (e) => {
|
el.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -1087,10 +1123,17 @@
|
||||||
sheetName.textContent = asset.name || asset.element_type || '';
|
sheetName.textContent = asset.name || asset.element_type || '';
|
||||||
sheetThematicName.textContent = asset.thematic_name || '';
|
sheetThematicName.textContent = asset.thematic_name || '';
|
||||||
|
|
||||||
const color = THEMATIC_COLORS[asset.thematic] || '#2f3e8b';
|
const symbology = (typeof AssetSymbology !== 'undefined')
|
||||||
sheetIcon.className = `bi ${asset.thematic_icon || 'bi-pin-map'}`;
|
? AssetSymbology.getAssetSymbology(asset)
|
||||||
document.getElementById('sheetIconCircle').style.color = color;
|
: { color: THEMATIC_COLORS[asset.thematic] || '#2f3e8b', iconClass: asset.thematic_icon || 'bi-pin-map' };
|
||||||
document.getElementById('sheetIconCircle').style.backgroundColor = `${color}18`;
|
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.className = `badge-status badge-status-${asset.status || 'active'}`;
|
||||||
sheetStatusBadge.textContent = asset.status_display || asset.status || 'Actif';
|
sheetStatusBadge.textContent = asset.status_display || asset.status || 'Actif';
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@ from django.test import TestCase, Client
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.gis.geos import Polygon
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from common.models import UserConfig, Role, Thematic, UserThematics
|
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()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
@ -24,6 +25,14 @@ class MobileAssetDetailTests(TestCase):
|
||||||
"icon": "bi-tree"
|
"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
|
# Create Roles
|
||||||
self.admin_role, _ = Role.objects.get_or_create(name="admin")
|
self.admin_role, _ = Role.objects.get_or_create(name="admin")
|
||||||
|
|
@ -56,6 +65,17 @@ class MobileAssetDetailTests(TestCase):
|
||||||
status="active",
|
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):
|
def test_anonymous_user_redirected_to_login(self):
|
||||||
"""Anonymous user trying to access mobile asset detail is redirected to login."""
|
"""Anonymous user trying to access mobile asset detail is redirected to login."""
|
||||||
url = reverse("mobile:mobile_asset_detail_direct", args=["naturetree", self.tree.id])
|
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
|
# Assert JS script block and initial view controls are rendered
|
||||||
self.assertContains(response, "initMap")
|
self.assertContains(response, "initMap")
|
||||||
self.assertContains(response, "maplibre-gl.js")
|
self.assertContains(response, "maplibre-gl.js")
|
||||||
|
self.assertContains(response, "asset_symbology.js")
|
||||||
self.assertContains(response, "noResultsInZoneAlert")
|
self.assertContains(response, "noResultsInZoneAlert")
|
||||||
self.assertContains(response, "btnResetToInitialView")
|
self.assertContains(response, "btnResetToInitialView")
|
||||||
self.assertContains(response, "btnInitialView")
|
self.assertContains(response, "btnInitialView")
|
||||||
|
|
@ -316,6 +337,13 @@ class MobileAssetDetailTests(TestCase):
|
||||||
codes = [item["code"] for item in data["results"]]
|
codes = [item["code"] for item in data["results"]]
|
||||||
self.assertIn("ARB-TEST-001", codes)
|
self.assertIn("ARB-TEST-001", codes)
|
||||||
self.assertIn("ARB-SHRUB-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
|
# 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"})
|
response = self.client.get(search_url, {"thematic": "nature", "limit": "1"})
|
||||||
|
|
@ -342,6 +370,25 @@ class MobileAssetDetailTests(TestCase):
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self.assertGreaterEqual(data["count"], 1)
|
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):
|
def test_generic_asset_uses_mobile_generic_template(self):
|
||||||
"""An asset model without specific template (NatureShrub) uses detail_generic_mobile.html."""
|
"""An asset model without specific template (NatureShrub) uses detail_generic_mobile.html."""
|
||||||
user = User.objects.create_user(username="shrub_viewer", password="password")
|
user = User.objects.create_user(username="shrub_viewer", password="password")
|
||||||
|
|
|
||||||
|
|
@ -1199,11 +1199,12 @@ def mobile_assets_search(request):
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from common.models import Thematic, UserThematics, UserConfig
|
from common.models import Thematic, UserThematics, UserConfig
|
||||||
from assets.models import (
|
from assets.models import (
|
||||||
NatureTree, NatureGreenSurface, NatureShrub, NatureBench,
|
NatureTree, NatureGreenSurface, NatureShrub, NatureBench, NatureRWIAsset,
|
||||||
SignPanel, SignPole,
|
SignPanel, SignPole,
|
||||||
TrafficLightPole, TrafficLightLantern, TrafficLightElectricalCabinet,
|
TrafficLightPole, TrafficLightLantern, TrafficLightElectricalCabinet,
|
||||||
|
TrafficLightRadar, TrafficLightController,
|
||||||
PublicLightingPole, PublicLightingLightFixture, PublicLightingElectricalCabinet,
|
PublicLightingPole, PublicLightingLightFixture, PublicLightingElectricalCabinet,
|
||||||
Roadway, RoadSidewalk, RoadFurniture,
|
Roadway, RoadSidewalk, RoadFurniture, RoadDrain, RoadConcreteBlockSet,
|
||||||
CleanLitterBin, CleanGlassContainer,
|
CleanLitterBin, CleanGlassContainer,
|
||||||
ArtworkMonument, ArtworkFountain,
|
ArtworkMonument, ArtworkFountain,
|
||||||
StructureGeoAsset, ControlCenterGeoAsset, ITSGeoAsset,
|
StructureGeoAsset, ControlCenterGeoAsset, ITSGeoAsset,
|
||||||
|
|
@ -1290,6 +1291,7 @@ def mobile_assets_search(request):
|
||||||
('naturegreensurface', NatureGreenSurface),
|
('naturegreensurface', NatureGreenSurface),
|
||||||
('natureshrub', NatureShrub),
|
('natureshrub', NatureShrub),
|
||||||
('naturebench', NatureBench),
|
('naturebench', NatureBench),
|
||||||
|
('naturerwiasset', NatureRWIAsset),
|
||||||
],
|
],
|
||||||
'sign': [
|
'sign': [
|
||||||
('signpanel', SignPanel),
|
('signpanel', SignPanel),
|
||||||
|
|
@ -1297,6 +1299,8 @@ def mobile_assets_search(request):
|
||||||
],
|
],
|
||||||
'trafficlights': [
|
'trafficlights': [
|
||||||
('trafficlightpole', TrafficLightPole),
|
('trafficlightpole', TrafficLightPole),
|
||||||
|
('trafficlightradar', TrafficLightRadar),
|
||||||
|
('trafficlightcontroller', TrafficLightController),
|
||||||
('trafficlightlantern', TrafficLightLantern),
|
('trafficlightlantern', TrafficLightLantern),
|
||||||
('trafficlightelectricalcabinet', TrafficLightElectricalCabinet),
|
('trafficlightelectricalcabinet', TrafficLightElectricalCabinet),
|
||||||
],
|
],
|
||||||
|
|
@ -1309,6 +1313,8 @@ def mobile_assets_search(request):
|
||||||
('roadway', Roadway),
|
('roadway', Roadway),
|
||||||
('roadsidewalk', RoadSidewalk),
|
('roadsidewalk', RoadSidewalk),
|
||||||
('roadfurniture', RoadFurniture),
|
('roadfurniture', RoadFurniture),
|
||||||
|
('roaddrain', RoadDrain),
|
||||||
|
('roadconcreteblockset', RoadConcreteBlockSet),
|
||||||
],
|
],
|
||||||
'clean': [
|
'clean': [
|
||||||
('cleanlitterbin', CleanLitterBin),
|
('cleanlitterbin', CleanLitterBin),
|
||||||
|
|
@ -1454,6 +1460,7 @@ def mobile_assets_search(request):
|
||||||
element_type = get_element_type_label(model_key)
|
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})
|
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 = {
|
res_item = {
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
|
|
@ -1463,6 +1470,8 @@ def mobile_assets_search(request):
|
||||||
'thematic_name': thematic_name,
|
'thematic_name': thematic_name,
|
||||||
'thematic_icon': thematic_icon,
|
'thematic_icon': thematic_icon,
|
||||||
'model_key': model_key,
|
'model_key': model_key,
|
||||||
|
'asset_type': model_key,
|
||||||
|
'spot_type': spot_type,
|
||||||
'element_type': element_type,
|
'element_type': element_type,
|
||||||
'status': status_val,
|
'status': status_val,
|
||||||
'status_display': status_display,
|
'status_display': status_display,
|
||||||
|
|
@ -1489,6 +1498,8 @@ def mobile_assets_search(request):
|
||||||
'thematic_name': thematic_name,
|
'thematic_name': thematic_name,
|
||||||
'thematic_icon': thematic_icon,
|
'thematic_icon': thematic_icon,
|
||||||
'model_key': model_key,
|
'model_key': model_key,
|
||||||
|
'asset_type': model_key,
|
||||||
|
'spot_type': spot_type,
|
||||||
'element_type': element_type,
|
'element_type': element_type,
|
||||||
'status': status_val,
|
'status': status_val,
|
||||||
'status_display': status_display,
|
'status_display': status_display,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue