feat: add unified index, map layers, and ITS asset management views and symbology

This commit is contained in:
Karl Determe 2026-10-08 16:45:33 +02:00
parent 7d5718dec2
commit 80c54a083d
5 changed files with 164 additions and 25 deletions

View file

@ -427,6 +427,33 @@
glyphCodepoint: 'f82b',
mapImage: 'icon-sign',
},
// ITS
itsgeoasset: {
thematic: 'its',
color: '#0288d1',
iconUrl: null,
iconClass: 'bi-broadcast-pin',
glyphCodepoint: 'f1d5',
mapImage: 'icon-its',
},
itsbollardsystem: {
thematic: 'its',
color: '#0288d1',
iconUrl: null,
iconClass: 'bi-border-width',
glyphCodepoint: 'f1ca',
mapImage: 'icon-itsbollardsystem',
},
itsbollardplot: {
thematic: 'its',
color: '#0288d1',
iconUrl: '/static/assets/its_bollard_plot_icon.svg?v=20261008',
iconSize: 42,
iconClass: 'bi-cone-striped',
glyphCodepoint: 'f2aa',
mapImage: 'icon-itsbollardplot',
},
};
// ─── 3. Specific Parking Spot Overrides by spot_type ─────────────────────────
@ -633,6 +660,7 @@
natureplanter: injected.natureplanter || (ASSET_TYPE_OVERRIDES.natureplanter && ASSET_TYPE_OVERRIDES.natureplanter.iconUrl),
artworkfountain: injected.artworkfountain || ASSET_TYPE_OVERRIDES.artworkfountain.iconUrl,
artworkmonument: injected.artworkmonument || ASSET_TYPE_OVERRIDES.artworkmonument.iconUrl,
itsbollardplot: injected.itsbollardplot || (ASSET_TYPE_OVERRIDES.itsbollardplot && ASSET_TYPE_OVERRIDES.itsbollardplot.iconUrl),
};
return urls;
}
@ -656,6 +684,9 @@
{ name: 'icon-roaddrain', codepoint: 'f617', color: '#1e88e5' },
{ name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' },
{ name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' },
{ name: 'icon-itsgeoasset', codepoint: 'f1d5', color: '#0288d1' },
{ name: 'icon-itsbollardsystem', codepoint: 'f1ca', color: '#0288d1' },
{ name: 'icon-itsbollardplot', codepoint: 'f2aa', color: '#0288d1' },
{ name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' },
{ name: 'icon-cleanglasscontainer', codepoint: 'f78a', color: '#16a34a' },
{ name: 'icon-artworks', codepoint: 'f4b0', color: '#f24141' },
@ -749,6 +780,9 @@
['==', ['get', 'asset_type'], 'roadfurniture'], 'icon-speedbump',
['==', ['get', 'asset_type'], 'roadconcreteblockset'], 'icon-roadconcreteblockset',
['==', ['get', 'asset_type'], 'naturerwiasset'], 'icon-naturerwiasset',
['==', ['get', 'asset_type'], 'itsbollardplot'], 'icon-itsbollardplot',
['==', ['get', 'asset_type'], 'itsbollardsystem'], 'icon-itsbollardsystem',
['==', ['get', 'asset_type'], 'itsgeoasset'], 'icon-its',
['==', ['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',
@ -924,14 +958,20 @@
'#0288d1'
],
['==', ['get', 'thematic'], 'nature'], '#2e7d32',
['==', ['get', 'asset_type'], 'itsbollardsystem'], '#0288d1',
['match', ['get', 'asset_type'], 'roadway', '#334155', 'sidewalk', '#64748b', getThematicColorExpr()]
],
'line-width': [
'case',
['any', ['boolean', ['feature-state', 'highlighted'], false], ['boolean', ['feature-state', 'selected'], false]], 6,
['==', ['get', 'asset_type'], 'itsbollardsystem'], 5,
3.5
],
'line-opacity': 0.88,
'line-opacity': [
'case',
['==', ['get', 'asset_type'], 'itsbollardsystem'], 0.95,
0.88
],
},
minzoom: 8,
};

View file

@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
<ellipse cx="32" cy="54" rx="22" ry="5" fill="#1e293b" opacity="0.3"/>
<ellipse cx="32" cy="52" rx="18" ry="4" fill="#334155"/>
<ellipse cx="32" cy="51" rx="16" ry="3.5" fill="#64748b"/>
<ellipse cx="32" cy="50.5" rx="13" ry="2.8" fill="#1e293b"/>
<defs>
<linearGradient id="bollardGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#0284c7"/>
<stop offset="25%" stop-color="#38bdf8"/>
<stop offset="60%" stop-color="#0284c7"/>
<stop offset="100%" stop-color="#0369a1"/>
</linearGradient>
<linearGradient id="capGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#7dd3fc"/>
<stop offset="100%" stop-color="#0284c7"/>
</linearGradient>
<linearGradient id="bandGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="35%" stop-color="#fef08a"/>
<stop offset="70%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#d97706"/>
</linearGradient>
</defs>
<path d="M 21,24 L 21,50 C 21,53 43,53 43,50 L 43,24 Z" fill="url(#bollardGrad)"/>
<path d="M 21,21 L 21,27 C 21,29.5 43,29.5 43,27 L 43,21 C 43,23.5 21,23.5 21,21 Z" fill="url(#bandGrad)"/>
<path d="M 21,15 L 21,21 C 21,23.5 43,23.5 43,21 L 43,15 Z" fill="url(#bollardGrad)"/>
<ellipse cx="32" cy="15" rx="11" ry="3.5" fill="url(#capGrad)" stroke="#bae6fd" stroke-width="0.8"/>
<ellipse cx="32" cy="14.2" rx="7" ry="2" fill="#ffffff" opacity="0.6"/>
<circle cx="26" cy="24.5" r="1" fill="#ffffff"/>
<circle cx="32" cy="25.2" r="1.1" fill="#ffffff"/>
<circle cx="38" cy="24.5" r="1" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -535,7 +535,7 @@ function buildColumns() {
}
},
{
field: 'element_type', title: gettext('Type'), sortable: true, width: 130, widthUnit: 'px', valign: 'middle',
field: 'element_type', title: gettext('Type'), sortable: true, width: 145, widthUnit: 'px', valign: 'middle',
formatter: function (v, row) {
const table = $('#unified-table');
let isCatVisible = false;
@ -543,10 +543,21 @@ function buildColumns() {
isCatVisible = table.length && typeof table.bootstrapTable === 'function' &&
table.bootstrapTable('getVisibleColumns').some(c => c.field === 'category');
} catch (e) {}
if (!isCatVisible && row && row.is_location && row.category) {
return `${escapeHtml(v || '-')} <span class="badge bg-light text-secondary border fw-normal ms-1" style="font-size:0.75rem;">${escapeHtml(row.category)}</span>`;
let iconPrefix = '';
if (typeof AssetSymbology !== 'undefined' && row && !row.is_location && !row.is_management_zone) {
const symb = AssetSymbology.getAssetSymbology(row);
const colorStyle = symb.color ? `color:${symb.color};` : '';
iconPrefix = AssetSymbology.renderIconHtml(symb, {
style: `${colorStyle} font-size: 0.95rem; vertical-align: -1px; margin-right: 5px;`,
imgClass: 'me-1'
});
}
return escapeHtml(v || '-');
if (!isCatVisible && row && row.is_location && row.category) {
return `${iconPrefix}${escapeHtml(v || '-')} <span class="badge bg-light text-secondary border fw-normal ms-1" style="font-size:0.75rem;">${escapeHtml(row.category)}</span>`;
}
return `${iconPrefix}${escapeHtml(v || '-')}`;
}
},
{ field: 'thematic_name', title: gettext('Thématique'), sortable: true, width: 140, widthUnit: 'px', valign: 'middle' },
@ -2016,6 +2027,7 @@ function collectClickedAssets(clickEvent) {
element_type: p.element_type || '',
detail_url: p.detail_url || null,
feature_id: id,
extra_info: p.plots_count != null ? `${p.plots_count} plot(s)` : (p.bollard_type || ''),
});
});
@ -2149,15 +2161,26 @@ function showAssetClickPopup(clickEvent) {
const type = escapeHtml(a.element_type || a.asset_type || '');
const thematic = escapeHtml(a.thematic_name || a.thematic || '');
const name = escapeHtml(a.name || '');
const extra = a.extra_info ? ` &bull; <span class="badge bg-light text-primary border" style="font-size:0.7rem;">${escapeHtml(a.extra_info)}</span>` : '';
const detailAnchor = a.detail_url
? `<a href="${escapeHtml(a.detail_url)}">${code}</a>`
: `<span>${code}</span>`;
let iconPrefix = '';
if (typeof AssetSymbology !== 'undefined') {
const symb = AssetSymbology.getAssetSymbology(a);
const colorStyle = symb.color ? `color:${symb.color};` : '';
iconPrefix = AssetSymbology.renderIconHtml(symb, {
style: `${colorStyle} font-size: 1rem; vertical-align: -1px; margin-right: 5px;`,
imgClass: 'me-1'
});
}
return `
<div class="asset-popup-item" style="display:flex; gap:0.5rem; align-items:flex-start; justify-content:space-between; padding:0.25rem 0; border-bottom:1px solid #f1f3f5;">
<div style="min-width:0;">
<div style="font-weight:600; line-height:1.1;">${detailAnchor}</div>
<div style="font-size:0.75rem; color:#6c757d; line-height:1.2;">${type}${thematic ? ' - ' + thematic : ''}</div>
<div style="font-weight:600; line-height:1.1;">${iconPrefix}${detailAnchor}</div>
<div style="font-size:0.75rem; color:#6c757d; line-height:1.2;">${type}${thematic ? ' - ' + thematic : ''}${extra}</div>
${name ? `<div style="font-size:0.75rem; color:#495057; line-height:1.2;">${name}</div>` : ''}
</div>
<button type="button"

View file

@ -201,6 +201,12 @@ function loadUnifiedMapIcons(map) {
.then(data => { if (!map.hasImage('icon-artworkmonument')) map.addImage('icon-artworkmonument', data); })
.catch(e => console.warn('unified_map_layers: could not load artworkmonument icon', e));
}
// itsbollardplot: dedicated automatic bollard / plot SVG (42 px)
if (iconUrls.itsbollardplot && !map.hasImage('icon-itsbollardplot')) {
loadNormalisedIcon(iconUrls.itsbollardplot, 42)
.then(data => { if (!map.hasImage('icon-itsbollardplot')) map.addImage('icon-itsbollardplot', data); })
.catch(e => console.warn('unified_map_layers: could not load itsbollardplot icon', e));
}
// If the SVG above is unavailable, publiclighting falls back to Bootstrap glyph.
// ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ──────────
@ -634,6 +640,7 @@ const _unified_assets_symbol = {
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
['==', ['get', 'trafficlightelectricalcabinet'], 'trafficlightelectricalcabinet'],
['==', ['get', 'asset_type'], 'naturerwiasset'],
['==', ['get', 'asset_type'], 'itsbollardplot'],
['==', ['get', 'category'], 'Avaloir'],
['==', ['get', 'category'], 'Corbeille de rue'],
['==', ['get', 'category'], 'Corbeille'],
@ -669,6 +676,7 @@ const _unified_assets_symbol = {
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
['==', ['get', 'trafficlightelectricalcabinet'], 'trafficlightelectricalcabinet'],
['==', ['get', 'asset_type'], 'naturerwiasset'],
['==', ['get', 'asset_type'], 'itsbollardplot'],
['==', ['get', 'category'], 'Avaloir'],
['==', ['get', 'category'], 'Corbeille de rue'],
['==', ['get', 'category'], 'Corbeille'],
@ -704,6 +712,7 @@ const _unified_assets_symbol = {
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
['==', ['get', 'asset_type'], 'naturerwiasset'],
['==', ['get', 'asset_type'], 'itsbollardplot'],
['==', ['get', 'category'], 'Avaloir'],
['==', ['get', 'category'], 'Corbeille de rue'],
['==', ['get', 'category'], 'Corbeille'],
@ -739,6 +748,7 @@ const _unified_assets_symbol = {
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
['==', ['get', 'asset_type'], 'naturerwiasset'],
['==', ['get', 'asset_type'], 'itsbollardplot'],
['==', ['get', 'category'], 'Avaloir'],
['==', ['get', 'category'], 'Corbeille de rue'],
['==', ['get', 'category'], 'Corbeille'],
@ -774,6 +784,7 @@ const _unified_assets_symbol = {
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
['==', ['get', 'asset_type'], 'naturerwiasset'],
['==', ['get', 'asset_type'], 'itsbollardplot'],
['==', ['get', 'category'], 'Avaloir'],
['==', ['get', 'category'], 'Corbeille de rue'],
['==', ['get', 'category'], 'Corbeille'],
@ -809,6 +820,7 @@ const _unified_assets_symbol = {
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
['==', ['get', 'asset_type'], 'naturerwiasset'],
['==', ['get', 'asset_type'], 'itsbollardplot'],
['==', ['get', 'category'], 'Avaloir'],
['==', ['get', 'category'], 'Corbeille de rue'],
['==', ['get', 'category'], 'Corbeille'],
@ -844,6 +856,7 @@ const _unified_assets_symbol = {
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
['==', ['get', 'asset_type'], 'naturerwiasset'],
['==', ['get', 'asset_type'], 'itsbollardplot'],
['==', ['get', 'category'], 'Avaloir'],
['==', ['get', 'category'], 'Corbeille de rue'],
['==', ['get', 'category'], 'Corbeille'],
@ -876,6 +889,7 @@ const _unified_assets_symbol = {
['==', ['get', 'thematic'], 'publiclighting'], 'bottom',
['==', ['get', 'thematic'], 'trafficlights'], 'bottom',
['==', ['get', 'thematic'], 'nature'], 'bottom',
['==', ['get', 'asset_type'], 'itsbollardplot'], 'bottom',
'center'
],
'icon-offset': [
@ -912,6 +926,8 @@ const _unified_assets_symbol = {
['all', ['==', ['get', 'thematic'], 'roads'], ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false]], 0,
['all', ['==', ['get', 'asset_type'], 'naturerwiasset'], ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false]], 0,
['==', ['get', 'asset_type'], 'naturegreensurface'], 0,
['==', ['get', 'asset_type'], 'itsbollardsystem'], 0,
['match', ['geometry-type'], ['LineString', 'MultiLineString'], true, false], 0,
['boolean', ['feature-state', 'highlighted'], false], 1,
0.85
],
@ -1084,14 +1100,20 @@ const _unified_assets_line = (typeof AssetSymbology !== 'undefined' && AssetSymb
'line-color': [
'case',
['==', ['get', 'thematic'], 'nature'], '#2e7d32',
['==', ['get', 'asset_type'], 'itsbollardsystem'], '#0288d1',
THEMATIC_COLOR_EXPR
],
'line-width': [
'case',
['boolean', ['feature-state', 'highlighted'], false], 5,
['boolean', ['feature-state', 'highlighted'], false], 6,
['==', ['get', 'asset_type'], 'itsbollardsystem'], 5,
3
],
'line-opacity': 0.8,
'line-opacity': [
'case',
['==', ['get', 'asset_type'], 'itsbollardsystem'], 0.95,
0.8
],
},
minzoom: 8,
};

View file

@ -10,7 +10,7 @@ from django.contrib.gis.db.models.functions import Transform
from django.contrib.gis.geos import GEOSGeometry, Point
from django.contrib.gis.db.models.functions import Distance
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, Case, When, IntegerField, CharField
from django.db.models import Q, Case, When, IntegerField, CharField, Count
from django.db.models.functions import Lower
from django.db.models import Func, Prefetch
from django.db import connection
@ -324,25 +324,42 @@ def its_assets_geojson(request):
def obj_to_feature(obj, asset_type=None):
geometry, lon, lat = build_geometry_and_coords(obj)
props = {
"code": obj.code,
"name": obj.get_name(),
"category": obj.category.get_name() if obj.category else None,
"category_id": obj.category_id,
"model": obj.model.get_name() if obj.model else None,
"model_id": obj.model_id,
"status": obj.status,
"validation_status": obj.validation_status,
"lon": lon,
"lat": lat,
"asset_type": asset_type,
"detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None,
"element_type": get_element_type_label(asset_type or ''),
"thematic": "its",
"is_location": False,
}
if asset_type == "itsbollardsystem":
props["width"] = obj.width
props["direction"] = obj.get_direction_display() if hasattr(obj, 'get_direction_display') else obj.direction
props["plots_count"] = getattr(obj, 'plots_total', None)
if props["plots_count"] is None and hasattr(obj, 'plots'):
props["plots_count"] = obj.plots.count()
if obj.geom:
props["length"] = round(obj.geom.length, 2)
elif asset_type == "itsbollardplot":
props["position"] = obj.position
props["bollard_type"] = obj.get_bollard_type_display() if hasattr(obj, 'get_bollard_type_display') else obj.bollard_type
if getattr(obj, 'system', None):
props["system_code"] = obj.system.code
props["system_name"] = obj.system.name
return {
"type": "Feature",
"id": obj.id,
"geometry": geometry,
"properties": {
"code": obj.code,
"name": obj.get_name(),
"category": obj.category.get_name() if obj.category else None,
"model": obj.model.get_name() if obj.model else None,
"status": obj.status,
"validation_status": obj.validation_status,
"lon": lon,
"lat": lat,
"asset_type": asset_type,
"detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None,
"element_type": get_element_type_label(asset_type or ''),
"thematic": "its",
"is_location": False,
},
"properties": props,
}
def add_assets_to_features(queryset, asset_type, features, feature_count):
@ -362,6 +379,7 @@ def its_assets_geojson(request):
bollard_system_qs = (
ITSBollardSystem.objects
.select_related('location', 'category', 'model')
.annotate(plots_total=Count('plots'))
.all()
)
bollard_plot_qs = (