Merge branch 'main' of https://git.valogeo.be/Valogeo/loko
This commit is contained in:
commit
cd3f3999cb
20 changed files with 679 additions and 41 deletions
14
loko/assets/migrations/0032_merge_20261009_1348.py
Normal file
14
loko/assets/migrations/0032_merge_20261009_1348.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-10-09 11:48
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0031_itsgeoasset_extra_data_itsbollardsystem_and_more'),
|
||||||
|
('assets', '0031_naturetree_amenity_value_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
|
|
@ -261,6 +261,33 @@
|
||||||
glyphCodepoint: 'f30b',
|
glyphCodepoint: 'f30b',
|
||||||
mapImage: 'icon-naturerwiasset',
|
mapImage: 'icon-naturerwiasset',
|
||||||
},
|
},
|
||||||
|
naturetree_regional: {
|
||||||
|
thematic: 'nature',
|
||||||
|
color: '#1d4ed8',
|
||||||
|
iconUrl: '/static/assets/nature_tree_icon_regional.svg?v=20261009_round',
|
||||||
|
iconSize: 56,
|
||||||
|
iconClass: 'bi-tree',
|
||||||
|
glyphCodepoint: 'f5e1',
|
||||||
|
mapImage: 'icon-nature-regional',
|
||||||
|
},
|
||||||
|
naturetree_remarkable: {
|
||||||
|
thematic: 'nature',
|
||||||
|
color: '#d97706',
|
||||||
|
iconUrl: '/static/assets/nature_tree_icon_remarkable.svg?v=20261009_round',
|
||||||
|
iconSize: 56,
|
||||||
|
iconClass: 'bi-tree',
|
||||||
|
glyphCodepoint: 'f5e1',
|
||||||
|
mapImage: 'icon-nature-remarkable',
|
||||||
|
},
|
||||||
|
naturetree_dead: {
|
||||||
|
thematic: 'nature',
|
||||||
|
color: '#6b7280',
|
||||||
|
iconUrl: '/static/assets/nature_tree_icon_dead.svg?v=20261009_round',
|
||||||
|
iconSize: 56,
|
||||||
|
iconClass: 'bi-tree',
|
||||||
|
glyphCodepoint: 'f5e1',
|
||||||
|
mapImage: 'icon-nature-dead',
|
||||||
|
},
|
||||||
naturetree: {
|
naturetree: {
|
||||||
thematic: 'nature',
|
thematic: 'nature',
|
||||||
color: '#2e7d32',
|
color: '#2e7d32',
|
||||||
|
|
@ -578,6 +605,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Specific asset_type override
|
// 3. Specific asset_type override
|
||||||
|
if (assetType === 'naturetree' || (thematic === 'nature' && (assetType === 'naturetree' || !assetType))) {
|
||||||
|
const isDead = item.is_dead === true || item.is_dead === 'true' || item.is_dead === 1 ||
|
||||||
|
item.vitality === 'dead' || item.status === 'dead' || item.status === 'archived';
|
||||||
|
if (isDead && ASSET_TYPE_OVERRIDES.naturetree_dead) {
|
||||||
|
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_dead);
|
||||||
|
}
|
||||||
|
const isRemarkable = item.is_remarkable === true || item.is_remarkable === 'true' || item.is_remarkable === 1;
|
||||||
|
const isRegional = item.management_type === 'regional' ||
|
||||||
|
(typeof item.managing_authority === 'string' && (item.managing_authority.toLowerCase().includes('région') || item.managing_authority.toLowerCase().includes('mobilité')));
|
||||||
|
if (isRemarkable && ASSET_TYPE_OVERRIDES.naturetree_remarkable) {
|
||||||
|
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_remarkable);
|
||||||
|
}
|
||||||
|
if (isRegional && ASSET_TYPE_OVERRIDES.naturetree_regional) {
|
||||||
|
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree_regional);
|
||||||
|
}
|
||||||
|
if (ASSET_TYPE_OVERRIDES.naturetree) {
|
||||||
|
return Object.assign({}, ASSET_TYPE_OVERRIDES.naturetree);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (assetType === 'naturebench') {
|
if (assetType === 'naturebench') {
|
||||||
const isPlanter = (item.asset_sub_type === 'planter') ||
|
const isPlanter = (item.asset_sub_type === 'planter') ||
|
||||||
['Bac à fleurs / Jardinière', 'Bloembak', 'Jardinière', 'Bac à fleurs', 'Bac à plantes'].includes(item.model) ||
|
['Bac à fleurs / Jardinière', 'Bloembak', 'Jardinière', 'Bac à fleurs', 'Bac à plantes'].includes(item.model) ||
|
||||||
|
|
@ -625,12 +671,17 @@
|
||||||
|
|
||||||
const customStyle = options.style ? ` style="${options.style}"` : '';
|
const customStyle = options.style ? ` style="${options.style}"` : '';
|
||||||
|
|
||||||
|
const classList = ['asset-symbology-icon-img'];
|
||||||
|
if (options.imgClass) classList.push(options.imgClass);
|
||||||
|
const imgClass = classList.join(' ');
|
||||||
|
const defaultImgStyle = 'width: 20px; height: 20px; max-width: 20px; max-height: 20px; object-fit: contain; vertical-align: middle; display: inline-block;';
|
||||||
|
const finalStyle = options.style ? ` style="${defaultImgStyle} ${options.style}"` : ` style="${defaultImgStyle}"`;
|
||||||
|
|
||||||
if (symbology.iconUrl) {
|
if (symbology.iconUrl) {
|
||||||
const imgClass = options.imgClass || 'asset-symbology-icon-img';
|
|
||||||
const fallbackUrl = symbology.fallbackUrl || (symbology.iconUrl.includes('.svg') ? symbology.iconUrl.replace(/\.svg(\?.*)?$/, '.png$1') : '');
|
const fallbackUrl = symbology.fallbackUrl || (symbology.iconUrl.includes('.svg') ? symbology.iconUrl.replace(/\.svg(\?.*)?$/, '.png$1') : '');
|
||||||
const fallbackClass = options.iconClass || symbology.iconClass || 'bi-sign-stop-fill';
|
const fallbackClass = options.iconClass || symbology.iconClass || 'bi-sign-stop-fill';
|
||||||
const onerrorAttr = ` onerror="this.onerror=null; ${fallbackUrl ? `if(this.src && this.src.indexOf('.svg') !== -1){ this.src='${fallbackUrl}'; } else {` : ''} this.style.display='none'; if(this.nextElementSibling){ this.nextElementSibling.style.display=''; } ${fallbackUrl ? '}' : ''}"`;
|
const onerrorAttr = ` onerror="this.onerror=null; ${fallbackUrl ? `if(this.src && this.src.indexOf('.svg') !== -1){ this.src='${fallbackUrl}'; } else {` : ''} this.style.display='none'; if(this.nextElementSibling){ this.nextElementSibling.style.display=''; } ${fallbackUrl ? '}' : ''}"`;
|
||||||
return `<img src="${symbology.iconUrl}" alt="" class="${imgClass}"${customStyle}${onerrorAttr}><i class="bi ${fallbackClass}" style="display:none;${options.style || ''}"></i>`;
|
return `<img src="${symbology.iconUrl}" alt="" class="${imgClass}"${finalStyle}${onerrorAttr}><i class="bi ${fallbackClass}" style="display:none;${options.style || ''}"></i>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconClass = options.iconClass || symbology.iconClass || 'bi-pin-map';
|
const iconClass = options.iconClass || symbology.iconClass || 'bi-pin-map';
|
||||||
|
|
@ -653,6 +704,9 @@
|
||||||
publiclighting: injected.publiclighting || THEMATIC_CONFIG.publiclighting.iconUrl,
|
publiclighting: injected.publiclighting || THEMATIC_CONFIG.publiclighting.iconUrl,
|
||||||
publiclightingelectricalcabinet: injected.publiclightingelectricalcabinet || ASSET_TYPE_OVERRIDES.publiclightingelectricalcabinet.iconUrl,
|
publiclightingelectricalcabinet: injected.publiclightingelectricalcabinet || ASSET_TYPE_OVERRIDES.publiclightingelectricalcabinet.iconUrl,
|
||||||
nature: injected.nature || THEMATIC_CONFIG.nature.iconUrl,
|
nature: injected.nature || THEMATIC_CONFIG.nature.iconUrl,
|
||||||
|
natureregional: injected.natureregional || (ASSET_TYPE_OVERRIDES.naturetree_regional && ASSET_TYPE_OVERRIDES.naturetree_regional.iconUrl),
|
||||||
|
natureremarkable: injected.natureremarkable || (ASSET_TYPE_OVERRIDES.naturetree_remarkable && ASSET_TYPE_OVERRIDES.naturetree_remarkable.iconUrl),
|
||||||
|
naturedead: injected.naturedead || (ASSET_TYPE_OVERRIDES.naturetree_dead && ASSET_TYPE_OVERRIDES.naturetree_dead.iconUrl),
|
||||||
structures: injected.structures || THEMATIC_CONFIG.structures.iconUrl,
|
structures: injected.structures || THEMATIC_CONFIG.structures.iconUrl,
|
||||||
roaddrain: injected.roaddrain || ASSET_TYPE_OVERRIDES.roaddrain.iconUrl,
|
roaddrain: injected.roaddrain || ASSET_TYPE_OVERRIDES.roaddrain.iconUrl,
|
||||||
roadconcreteblockset: injected.roadconcreteblockset || ASSET_TYPE_OVERRIDES.roadconcreteblockset.iconUrl,
|
roadconcreteblockset: injected.roadconcreteblockset || ASSET_TYPE_OVERRIDES.roadconcreteblockset.iconUrl,
|
||||||
|
|
@ -773,6 +827,26 @@
|
||||||
['==', ['slice', ['get', 'code'], 0, 7], 'CSE-BAC'],
|
['==', ['slice', ['get', 'code'], 0, 7], 'CSE-BAC'],
|
||||||
], 'icon-natureplanter',
|
], 'icon-natureplanter',
|
||||||
['==', ['get', 'asset_type'], 'naturebench'], 'icon-naturebench',
|
['==', ['get', 'asset_type'], 'naturebench'], 'icon-naturebench',
|
||||||
|
['all',
|
||||||
|
['any', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'thematic'], 'nature']],
|
||||||
|
['any',
|
||||||
|
['==', ['get', 'is_dead'], true],
|
||||||
|
['==', ['get', 'is_dead'], 'true'],
|
||||||
|
['==', ['get', 'is_dead'], 1],
|
||||||
|
['==', ['get', 'status'], 'dead'],
|
||||||
|
['==', ['get', 'status'], 'archived'],
|
||||||
|
['==', ['get', 'vitality'], 'dead']
|
||||||
|
]
|
||||||
|
], 'icon-nature-dead',
|
||||||
|
['all',
|
||||||
|
['any', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'thematic'], 'nature']],
|
||||||
|
['any', ['==', ['get', 'is_remarkable'], true], ['==', ['get', 'is_remarkable'], 'true'], ['==', ['get', 'is_remarkable'], 1]]
|
||||||
|
], 'icon-nature-remarkable',
|
||||||
|
['all',
|
||||||
|
['any', ['==', ['get', 'asset_type'], 'naturetree'], ['==', ['get', 'thematic'], 'nature']],
|
||||||
|
['any', ['==', ['get', 'management_type'], 'regional'], ['==', ['get', 'management_type'], 'Régional']]
|
||||||
|
], 'icon-nature-regional',
|
||||||
|
['==', ['get', 'asset_type'], 'naturetree'], 'icon-nature',
|
||||||
['==', ['get', 'asset_type'], 'drain'], 'icon-roaddrain',
|
['==', ['get', 'asset_type'], 'drain'], 'icon-roaddrain',
|
||||||
['==', ['get', 'asset_type'], 'roaddrain'], 'icon-roaddrain',
|
['==', ['get', 'asset_type'], 'roaddrain'], 'icon-roaddrain',
|
||||||
['==', ['get', 'category'], 'Ralentisseur'], 'icon-speedbump',
|
['==', ['get', 'category'], 'Ralentisseur'], 'icon-speedbump',
|
||||||
|
|
|
||||||
20
loko/assets/static/assets/nature_tree_icon_dead.svg
Normal file
20
loko/assets/static/assets/nature_tree_icon_dead.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="foliage-dead" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#9ca3af"/>
|
||||||
|
<stop offset="100%" stop-color="#4b5563"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="trunk-dead" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#78716c"/>
|
||||||
|
<stop offset="100%" stop-color="#57534e"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<ellipse cx="24" cy="24" rx="12" ry="10" fill="url(#foliage-dead)"/>
|
||||||
|
<ellipse cx="40" cy="24" rx="12" ry="10" fill="url(#foliage-dead)"/>
|
||||||
|
<ellipse cx="32" cy="18" rx="13" ry="11" fill="url(#foliage-dead)"/>
|
||||||
|
<ellipse cx="32" cy="29" rx="15" ry="10" fill="url(#foliage-dead)"/>
|
||||||
|
|
||||||
|
<rect x="28" y="30" width="8" height="24" rx="2" fill="url(#trunk-dead)"/>
|
||||||
|
<rect x="25" y="54" width="14" height="5" rx="2" fill="#44403c"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 928 B |
20
loko/assets/static/assets/nature_tree_icon_regional.svg
Normal file
20
loko/assets/static/assets/nature_tree_icon_regional.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="foliage-reg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#60a5fa"/>
|
||||||
|
<stop offset="100%" stop-color="#1d4ed8"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="trunk" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#8d6e63"/>
|
||||||
|
<stop offset="100%" stop-color="#5d4037"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<ellipse cx="24" cy="24" rx="12" ry="10" fill="url(#foliage-reg)"/>
|
||||||
|
<ellipse cx="40" cy="24" rx="12" ry="10" fill="url(#foliage-reg)"/>
|
||||||
|
<ellipse cx="32" cy="18" rx="13" ry="11" fill="url(#foliage-reg)"/>
|
||||||
|
<ellipse cx="32" cy="29" rx="15" ry="10" fill="url(#foliage-reg)"/>
|
||||||
|
|
||||||
|
<rect x="28" y="30" width="8" height="24" rx="2" fill="url(#trunk)"/>
|
||||||
|
<rect x="25" y="54" width="14" height="5" rx="2" fill="#4e342e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 913 B |
20
loko/assets/static/assets/nature_tree_icon_remarkable.svg
Normal file
20
loko/assets/static/assets/nature_tree_icon_remarkable.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="foliage-rem" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#fbbf24"/>
|
||||||
|
<stop offset="100%" stop-color="#d97706"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="trunk" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#8d6e63"/>
|
||||||
|
<stop offset="100%" stop-color="#5d4037"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<ellipse cx="24" cy="24" rx="12" ry="10" fill="url(#foliage-rem)"/>
|
||||||
|
<ellipse cx="40" cy="24" rx="12" ry="10" fill="url(#foliage-rem)"/>
|
||||||
|
<ellipse cx="32" cy="18" rx="13" ry="11" fill="url(#foliage-rem)"/>
|
||||||
|
<ellipse cx="32" cy="29" rx="15" ry="10" fill="url(#foliage-rem)"/>
|
||||||
|
|
||||||
|
<rect x="28" y="30" width="8" height="24" rx="2" fill="url(#trunk)"/>
|
||||||
|
<rect x="25" y="54" width="14" height="5" rx="2" fill="#4e342e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 913 B |
|
|
@ -549,7 +549,7 @@ function buildColumns() {
|
||||||
const symb = AssetSymbology.getAssetSymbology(row);
|
const symb = AssetSymbology.getAssetSymbology(row);
|
||||||
const colorStyle = symb.color ? `color:${symb.color};` : '';
|
const colorStyle = symb.color ? `color:${symb.color};` : '';
|
||||||
iconPrefix = AssetSymbology.renderIconHtml(symb, {
|
iconPrefix = AssetSymbology.renderIconHtml(symb, {
|
||||||
style: `${colorStyle} font-size: 0.95rem; vertical-align: -1px; margin-right: 5px;`,
|
style: `${colorStyle} width: 20px; height: 20px; vertical-align: middle; margin-right: 6px;`,
|
||||||
imgClass: 'me-1'
|
imgClass: 'me-1'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -748,6 +748,14 @@ function applyInitialPreferences() {
|
||||||
window._restoredMapCenter = prefs.map_center;
|
window._restoredMapCenter = prefs.map_center;
|
||||||
window._restoredMapZoom = prefs.map_zoom;
|
window._restoredMapZoom = prefs.map_zoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Basemap preference
|
||||||
|
if (prefs.basemap) {
|
||||||
|
window._restoredBasemap = prefs.basemap;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('loko_active_basemap', prefs.basemap);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
} else if (DEFAULT_THEMATIC_CODE && DEFAULT_THEMATIC_CODE in THEMATIC_CONFIG) {
|
} else if (DEFAULT_THEMATIC_CODE && DEFAULT_THEMATIC_CODE in THEMATIC_CONFIG) {
|
||||||
// First visit: pre-select the user's default thematic
|
// First visit: pre-select the user's default thematic
|
||||||
activeThematics = new Set([DEFAULT_THEMATIC_CODE]);
|
activeThematics = new Set([DEFAULT_THEMATIC_CODE]);
|
||||||
|
|
@ -775,6 +783,9 @@ function savePreferences() {
|
||||||
).map(cb => cb.value);
|
).map(cb => cb.value);
|
||||||
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
|
||||||
const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true;
|
const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true;
|
||||||
|
const activeBasemap = (window.unifiedLayerSwitcher && window.unifiedLayerSwitcher.activeBasemapId)
|
||||||
|
|| localStorage.getItem('loko_active_basemap')
|
||||||
|
|| 'default';
|
||||||
const prefs = {
|
const prefs = {
|
||||||
thematics: Array.from(activeThematics),
|
thematics: Array.from(activeThematics),
|
||||||
categories: Array.from(activeCategories),
|
categories: Array.from(activeCategories),
|
||||||
|
|
@ -787,6 +798,7 @@ function savePreferences() {
|
||||||
include_no_geom: includeNoGeom,
|
include_no_geom: includeNoGeom,
|
||||||
map_center: (typeof map !== 'undefined' && map.getCenter) ? [map.getCenter().lng, map.getCenter().lat] : null,
|
map_center: (typeof map !== 'undefined' && map.getCenter) ? [map.getCenter().lng, map.getCenter().lat] : null,
|
||||||
map_zoom: (typeof map !== 'undefined' && map.getZoom) ? map.getZoom() : null,
|
map_zoom: (typeof map !== 'undefined' && map.getZoom) ? map.getZoom() : null,
|
||||||
|
basemap: activeBasemap,
|
||||||
};
|
};
|
||||||
fetch(SAVE_ASSETS_PREFS_URL, {
|
fetch(SAVE_ASSETS_PREFS_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -812,6 +824,12 @@ $(document).ready(function () {
|
||||||
|
|
||||||
$('#unified-table').bootstrapTable({
|
$('#unified-table').bootstrapTable({
|
||||||
columns: buildColumns(),
|
columns: buildColumns(),
|
||||||
|
rowStyle: function (row) {
|
||||||
|
if (row && (row.is_dead || row.vitality === 'dead' || row.status === 'dead' || row.status === 'archived')) {
|
||||||
|
return { classes: 'text-muted opacity-75' };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
height: calcHeight(),
|
height: calcHeight(),
|
||||||
search: false,
|
search: false,
|
||||||
pagination: true,
|
pagination: true,
|
||||||
|
|
@ -1593,6 +1611,11 @@ function buildAssetsGeojsonPostBody(thematicCode, bbox, includeNonGeoFilters) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (thematicCode === 'nature') {
|
if (thematicCode === 'nature') {
|
||||||
|
if (Array.isArray(thematicFilters.management_type) && thematicFilters.management_type.length) {
|
||||||
|
payload.management_type = thematicFilters.management_type;
|
||||||
|
} else if (thematicFilters.management_type) {
|
||||||
|
payload.management_type = [thematicFilters.management_type];
|
||||||
|
}
|
||||||
if (thematicFilters.location_type) {
|
if (thematicFilters.location_type) {
|
||||||
payload.location_type = thematicFilters.location_type;
|
payload.location_type = thematicFilters.location_type;
|
||||||
}
|
}
|
||||||
|
|
@ -2028,6 +2051,9 @@ function collectClickedAssets(clickEvent) {
|
||||||
detail_url: p.detail_url || null,
|
detail_url: p.detail_url || null,
|
||||||
feature_id: id,
|
feature_id: id,
|
||||||
extra_info: p.plots_count != null ? `${p.plots_count} plot(s)` : (p.bollard_type || ''),
|
extra_info: p.plots_count != null ? `${p.plots_count} plot(s)` : (p.bollard_type || ''),
|
||||||
|
management_type: p.management_type || '',
|
||||||
|
managing_authority: p.managing_authority || '',
|
||||||
|
is_remarkable: p.is_remarkable || false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2729,6 +2755,13 @@ function loadTableData(features) {
|
||||||
model: p.model || '',
|
model: p.model || '',
|
||||||
status: p.status || '',
|
status: p.status || '',
|
||||||
detail_url: p.detail_url || null,
|
detail_url: p.detail_url || null,
|
||||||
|
management_type: p.management_type || '',
|
||||||
|
managing_authority: p.managing_authority || '',
|
||||||
|
is_remarkable: p.is_remarkable || false,
|
||||||
|
scientific_name: p.scientific_name || '',
|
||||||
|
vernacular_name: p.vernacular_name || '',
|
||||||
|
is_dead: p.is_dead || false,
|
||||||
|
vitality: p.vitality || '',
|
||||||
lon: p.lon,
|
lon: p.lon,
|
||||||
lat: p.lat,
|
lat: p.lat,
|
||||||
length: Number.isFinite(length) ? length : 0,
|
length: Number.isFinite(length) ? length : 0,
|
||||||
|
|
@ -2916,7 +2949,12 @@ maploader.addEventListener('mapLoaded', () => {
|
||||||
|
|
||||||
// Layer switcher — Urbis & Vlaanderen orthos (top-right, topmost)
|
// Layer switcher — Urbis & Vlaanderen orthos (top-right, topmost)
|
||||||
if (typeof LayerSwitcherControl !== 'undefined') {
|
if (typeof LayerSwitcherControl !== 'undefined') {
|
||||||
|
const initialBasemap = window._restoredBasemap || localStorage.getItem('loko_active_basemap') || 'default';
|
||||||
const layerSwitcher = new LayerSwitcherControl({
|
const layerSwitcher = new LayerSwitcherControl({
|
||||||
|
initialBasemapId: initialBasemap,
|
||||||
|
onBasemapChange: function (basemapId) {
|
||||||
|
schedulePreferencesSave();
|
||||||
|
},
|
||||||
layers: [{
|
layers: [{
|
||||||
id: 'basemap',
|
id: 'basemap',
|
||||||
name: 'Fonds de plan & Orthophotos',
|
name: 'Fonds de plan & Orthophotos',
|
||||||
|
|
@ -2924,7 +2962,10 @@ maploader.addEventListener('mapLoaded', () => {
|
||||||
beforeLayer: (map && map.getLayer('territory-mask') ? 'territory-mask' : 'locations'),
|
beforeLayer: (map && map.getLayer('territory-mask') ? 'territory-mask' : 'locations'),
|
||||||
basemaps: window.MAP_BACKGROUNDS || [],
|
basemaps: window.MAP_BACKGROUNDS || [],
|
||||||
}],
|
}],
|
||||||
});// Reset view — fly back to initial Brussels Region view
|
});
|
||||||
|
window.unifiedLayerSwitcher = layerSwitcher;
|
||||||
|
|
||||||
|
// Reset view — fly back to initial Brussels Region view
|
||||||
map.addControl({
|
map.addControl({
|
||||||
onAdd(m) {
|
onAdd(m) {
|
||||||
this._map = m;
|
this._map = m;
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,10 @@ function loadUnifiedMapIcons(map) {
|
||||||
// ── File-based icons (full colour, non-SDF) ─────────────────────────────
|
// ── File-based icons (full colour, non-SDF) ─────────────────────────────
|
||||||
// UNIFIED_ICON_URLS is set in the inline <script> of unified_index.html
|
// UNIFIED_ICON_URLS is set in the inline <script> of unified_index.html
|
||||||
// using {% static %} so the URL is resolved server-side.
|
// using {% static %} so the URL is resolved server-side.
|
||||||
const iconUrls = (typeof UNIFIED_ICON_URLS !== 'undefined') ? UNIFIED_ICON_URLS : {};
|
const injected = (typeof UNIFIED_ICON_URLS !== 'undefined') ? UNIFIED_ICON_URLS : {};
|
||||||
|
const iconUrls = (typeof AssetSymbology !== 'undefined' && AssetSymbology.getIconUrls)
|
||||||
|
? Object.assign({}, AssetSymbology.getIconUrls(), injected)
|
||||||
|
: injected;
|
||||||
|
|
||||||
// trafficlights: traffic_light_icon.svg (56 px)
|
// trafficlights: traffic_light_icon.svg (56 px)
|
||||||
if (iconUrls.trafficlights && !map.hasImage('icon-trafficlights')) {
|
if (iconUrls.trafficlights && !map.hasImage('icon-trafficlights')) {
|
||||||
|
|
@ -140,6 +143,21 @@ function loadUnifiedMapIcons(map) {
|
||||||
.then(data => { if (!map.hasImage('icon-nature')) map.addImage('icon-nature', data); })
|
.then(data => { if (!map.hasImage('icon-nature')) map.addImage('icon-nature', data); })
|
||||||
.catch(e => console.warn('unified_map_layers: could not load nature icon', e));
|
.catch(e => console.warn('unified_map_layers: could not load nature icon', e));
|
||||||
}
|
}
|
||||||
|
if (iconUrls.natureregional && !map.hasImage('icon-nature-regional')) {
|
||||||
|
loadNormalisedIcon(iconUrls.natureregional, 56)
|
||||||
|
.then(data => { if (!map.hasImage('icon-nature-regional')) map.addImage('icon-nature-regional', data); })
|
||||||
|
.catch(e => console.warn('unified_map_layers: could not load nature regional icon', e));
|
||||||
|
}
|
||||||
|
if (iconUrls.natureremarkable && !map.hasImage('icon-nature-remarkable')) {
|
||||||
|
loadNormalisedIcon(iconUrls.natureremarkable, 56)
|
||||||
|
.then(data => { if (!map.hasImage('icon-nature-remarkable')) map.addImage('icon-nature-remarkable', data); })
|
||||||
|
.catch(e => console.warn('unified_map_layers: could not load nature remarkable icon', e));
|
||||||
|
}
|
||||||
|
if (iconUrls.naturedead && !map.hasImage('icon-nature-dead')) {
|
||||||
|
loadNormalisedIcon(iconUrls.naturedead, 56)
|
||||||
|
.then(data => { if (!map.hasImage('icon-nature-dead')) map.addImage('icon-nature-dead', data); })
|
||||||
|
.catch(e => console.warn('unified_map_layers: could not load nature dead icon', e));
|
||||||
|
}
|
||||||
|
|
||||||
// structures: dedicated bridge/tunnel SVG (52 px)
|
// structures: dedicated bridge/tunnel SVG (52 px)
|
||||||
if (iconUrls.structures && !map.hasImage('icon-structures')) {
|
if (iconUrls.structures && !map.hasImage('icon-structures')) {
|
||||||
|
|
@ -211,7 +229,12 @@ function loadUnifiedMapIcons(map) {
|
||||||
|
|
||||||
// ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ──────────
|
// ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ──────────
|
||||||
// codepoints and colors derived from AssetSymbology (single source of truth)
|
// codepoints and colors derived from AssetSymbology (single source of truth)
|
||||||
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),
|
||||||
|
'icon-nature-regional',
|
||||||
|
'icon-nature-remarkable',
|
||||||
|
'icon-nature-dead',
|
||||||
|
]);
|
||||||
|
|
||||||
const GLYPH_ICONS = (typeof AssetSymbology !== 'undefined')
|
const GLYPH_ICONS = (typeof AssetSymbology !== 'undefined')
|
||||||
? AssetSymbology.getGlyphIcons()
|
? AssetSymbology.getGlyphIcons()
|
||||||
|
|
|
||||||
|
|
@ -480,7 +480,7 @@
|
||||||
text-transform: uppercase !important;
|
text-transform: uppercase !important;
|
||||||
letter-spacing: 0.04em !important;
|
letter-spacing: 0.04em !important;
|
||||||
border-bottom: 1.5px solid #e2e8f0 !important;
|
border-bottom: 1.5px solid #e2e8f0 !important;
|
||||||
padding: 0.65rem 0.75rem !important;
|
padding: 0.45rem 0.65rem !important;
|
||||||
vertical-align: middle !important;
|
vertical-align: middle !important;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
}
|
}
|
||||||
|
|
@ -489,16 +489,28 @@
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
font-size: 0.74rem !important;
|
font-size: 0.74rem !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
line-height: 1.4 !important;
|
line-height: 1.3 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rows & Cells */
|
/* Rows & Cells */
|
||||||
.bootstrap-table.bootstrap5 #unified-table td {
|
.bootstrap-table.bootstrap5 #unified-table td {
|
||||||
padding: 0.65rem 0.75rem !important;
|
padding: 0.32rem 0.65rem !important;
|
||||||
vertical-align: middle !important;
|
vertical-align: middle !important;
|
||||||
font-size: 0.83rem !important;
|
font-size: 0.82rem !important;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
border-bottom: 1px solid #f1f5f9 !important;
|
border-bottom: 1px solid #f1f5f9 !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-symbology-icon-img {
|
||||||
|
width: 20px !important;
|
||||||
|
height: 20px !important;
|
||||||
|
max-width: 20px !important;
|
||||||
|
max-height: 20px !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
margin-right: 6px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bootstrap-table.bootstrap5 #unified-table tbody tr {
|
.bootstrap-table.bootstrap5 #unified-table tbody tr {
|
||||||
|
|
@ -1303,7 +1315,7 @@
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
|
||||||
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
|
<script src="{% static 'common/layer-switcher-control.js' %}?v=20261009_basemap"></script>
|
||||||
<script>
|
<script>
|
||||||
window.TERRITORY_MASK_URL = "{% url 'common:get_territory_mask_geojson' %}";
|
window.TERRITORY_MASK_URL = "{% url 'common:get_territory_mask_geojson' %}";
|
||||||
const preventive_draft_id = "{{ preventive_draft_id|default:'' }}";
|
const preventive_draft_id = "{{ preventive_draft_id|default:'' }}";
|
||||||
|
|
@ -1341,7 +1353,10 @@
|
||||||
trafficlightelectricalcabinet: "{% static 'assets/electrical-panel.png' %}",
|
trafficlightelectricalcabinet: "{% static 'assets/electrical-panel.png' %}",
|
||||||
publiclighting: "{% static 'assets/public_lamp_post_icon.svg' %}?v=20260521c",
|
publiclighting: "{% static 'assets/public_lamp_post_icon.svg' %}?v=20260521c",
|
||||||
publiclightingelectricalcabinet: "{% static 'assets/electrical-panel.png' %}",
|
publiclightingelectricalcabinet: "{% static 'assets/electrical-panel.png' %}",
|
||||||
nature: "{% static 'assets/nature_tree_icon.svg' %}?v=20260521a",
|
nature: "{% static 'assets/nature_tree_icon.svg' %}?v=20261009_round",
|
||||||
|
natureregional: "{% static 'assets/nature_tree_icon_regional.svg' %}?v=20261009_round",
|
||||||
|
natureremarkable: "{% static 'assets/nature_tree_icon_remarkable.svg' %}?v=20261009_round",
|
||||||
|
naturedead: "{% static 'assets/nature_tree_icon_dead.svg' %}?v=20261009_round",
|
||||||
structures: "{% static 'assets/structures_bridge_tunnel_icon.svg' %}?v=20260522b",
|
structures: "{% static 'assets/structures_bridge_tunnel_icon.svg' %}?v=20260522b",
|
||||||
roaddrain: "{% static 'assets/roaddrain_grille_icon.svg' %}?v=20260521a",
|
roaddrain: "{% static 'assets/roaddrain_grille_icon.svg' %}?v=20260521a",
|
||||||
speedbump: "{% static 'assets/road_speedbump_icon.svg' %}?v=20260920",
|
speedbump: "{% static 'assets/road_speedbump_icon.svg' %}?v=20260920",
|
||||||
|
|
@ -1358,7 +1373,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=20260920_grid_options"></script>
|
<script src="{% static 'assets/asset_symbology.js' %}?v=20261009_round_trees_v2"></script>
|
||||||
<script src="{% static 'assets/unified_map_layers.js' %}?v=20260920_grid_options"></script>
|
<script src="{% static 'assets/unified_map_layers.js' %}?v=20261009_round_trees_v2"></script>
|
||||||
<script src="{% static 'assets/unified_index.js' %}?v=20260920_grid_options"></script>
|
<script src="{% static 'assets/unified_index.js' %}?v=20261009_round_trees_v2"></script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
@ -5441,6 +5441,97 @@ class UserAccessibleMunicipalitiesTest(TestCase):
|
||||||
# When only 1 municipality is accessible, the filter should NOT be shown
|
# When only 1 municipality is accessible, the filter should NOT be shown
|
||||||
self.assertIsNone(muni_filter)
|
self.assertIsNone(muni_filter)
|
||||||
|
|
||||||
|
def test_nature_assets_geojson_restriction_and_filter(self):
|
||||||
|
from assets.models import NatureLocation, NatureTree
|
||||||
|
from common.models import Thematic, UserThematics
|
||||||
|
from django.urls import reverse
|
||||||
|
import json
|
||||||
|
|
||||||
|
thematic_nature, _ = Thematic.objects.get_or_create(code="nature", defaults={"name_fr": "Nature"})
|
||||||
|
UserThematics.objects.get_or_create(user_config=self.ext_config, thematic=thematic_nature, defaults={"can_view_assets": True})
|
||||||
|
|
||||||
|
loc_a = NatureLocation.objects.create(code="LOC-A", name_fr="Rue A", municipality=self.muni_a)
|
||||||
|
loc_c = NatureLocation.objects.create(code="LOC-C", name_fr="Rue C", municipality=self.muni_c)
|
||||||
|
|
||||||
|
tree_muni = NatureTree.objects.create(code="TR-A-MUNI", name_fr="Arbre Communal", location=loc_a, management_type="municipal")
|
||||||
|
tree_reg = NatureTree.objects.create(code="TR-A-REG", name_fr="Arbre Régional", location=loc_a, management_type="regional")
|
||||||
|
tree_c = NatureTree.objects.create(code="TR-C", name_fr="Arbre Inaccessible", location=loc_c, management_type="municipal")
|
||||||
|
|
||||||
|
self.client.force_login(self.user_ext)
|
||||||
|
|
||||||
|
# 1. Unfiltered request
|
||||||
|
resp = self.client.post(
|
||||||
|
reverse('assets:nature_assets_geojson') + '?unified=1',
|
||||||
|
data=json.dumps({}),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
data = resp.json()
|
||||||
|
ids = [f['id'] for f in data.get('features', [])]
|
||||||
|
self.assertIn(tree_muni.id, ids)
|
||||||
|
self.assertIn(tree_reg.id, ids)
|
||||||
|
self.assertNotIn(tree_c.id, ids)
|
||||||
|
|
||||||
|
# 2. Filter by municipal only
|
||||||
|
resp_muni = self.client.post(
|
||||||
|
reverse('assets:nature_assets_geojson') + '?unified=1',
|
||||||
|
data=json.dumps({'management_type': ['municipal']}),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data_muni = resp_muni.json()
|
||||||
|
muni_ids = [f['id'] for f in data_muni.get('features', [])]
|
||||||
|
self.assertIn(tree_muni.id, muni_ids)
|
||||||
|
self.assertNotIn(tree_reg.id, muni_ids)
|
||||||
|
self.assertNotIn(tree_c.id, muni_ids)
|
||||||
|
|
||||||
|
# 3. Filter by regional only
|
||||||
|
resp_reg = self.client.post(
|
||||||
|
reverse('assets:nature_assets_geojson') + '?unified=1',
|
||||||
|
data=json.dumps({'management_type': ['regional']}),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data_reg = resp_reg.json()
|
||||||
|
reg_ids = [f['id'] for f in data_reg.get('features', [])]
|
||||||
|
self.assertIn(tree_reg.id, reg_ids)
|
||||||
|
self.assertNotIn(tree_muni.id, reg_ids)
|
||||||
|
|
||||||
|
def test_nature_conditional_filter_config_dynamic_gestionnaire(self):
|
||||||
|
from assets.views.core import build_conditional_filter_config
|
||||||
|
from assets.models import NatureLocation, NatureTree
|
||||||
|
from common.models import Thematic, UserConfig, UserContractAccess
|
||||||
|
from contracts.models import Contract
|
||||||
|
|
||||||
|
thematic_nature, _ = Thematic.objects.get_or_create(code="nature", defaults={"name_fr": "Nature"})
|
||||||
|
|
||||||
|
# Single Walloon municipality user
|
||||||
|
self.muni_a.niscode = '25018' # Court-Saint-Étienne (Wallonia)
|
||||||
|
self.muni_a.name_fr = 'Court-Saint-Étienne'
|
||||||
|
self.muni_a.save()
|
||||||
|
|
||||||
|
user_cse = User.objects.create_user(username="test_cse_user", password="pwd")
|
||||||
|
cfg_cse = UserConfig.objects.create(user=user_cse, is_intern=False, limit_assets_to_contracts=True)
|
||||||
|
ctr_cse = Contract.objects.create(contract_number="CTR-CSE", company=self.company, start_date="2026-01-01", end_date="2026-12-31")
|
||||||
|
ctr_cse.municipalities.add(self.muni_a)
|
||||||
|
UserContractAccess.objects.create(user_config=cfg_cse, contract=ctr_cse, can_view_assets=True)
|
||||||
|
|
||||||
|
loc = NatureLocation.objects.create(code="LOC-CSE", name_fr="Rue CSE", municipality=self.muni_a)
|
||||||
|
NatureTree.objects.create(code="TR-CSE-M", location=loc, management_type="municipal")
|
||||||
|
NatureTree.objects.create(code="TR-CSE-R", location=loc, management_type="regional")
|
||||||
|
|
||||||
|
cfg = build_conditional_filter_config([thematic_nature], user=user_cse)
|
||||||
|
nat_filters = cfg.get('nature', [])
|
||||||
|
mgmt_filter = next((f for f in nat_filters if f['key'] == 'management_type'), None)
|
||||||
|
self.assertIsNotNone(mgmt_filter)
|
||||||
|
|
||||||
|
labels = [opt['label'] for opt in mgmt_filter['options']]
|
||||||
|
values = [opt['value'] for opt in mgmt_filter['options']]
|
||||||
|
self.assertIn('municipal', values)
|
||||||
|
self.assertIn('regional', values)
|
||||||
|
self.assertTrue(any('Court-Saint-Étienne' in lbl for lbl in labels))
|
||||||
|
self.assertTrue(any('Région wallonne' in lbl for lbl in labels))
|
||||||
|
self.assertFalse(any('Boitsfort' in lbl for lbl in labels))
|
||||||
|
self.assertFalse(any('Bruxelles Mobilité' in lbl for lbl in labels))
|
||||||
|
|
||||||
|
|
||||||
class NatureLocationDetailViewTest(TestCase):
|
class NatureLocationDetailViewTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
||||||
|
|
@ -681,7 +681,7 @@ def build_conditional_filter_config(accessible_thematics, user=None):
|
||||||
|
|
||||||
# Roads / public lighting / sign / artworks: municipality on locations (only if more than 1 option)
|
# Roads / public lighting / sign / artworks: municipality on locations (only if more than 1 option)
|
||||||
if len(municipality_options) > 1:
|
if len(municipality_options) > 1:
|
||||||
for thematic_code in ['roads', 'publiclighting', 'sign', 'artworks']:
|
for thematic_code in ['roads', 'publiclighting', 'sign', 'artworks', 'nature']:
|
||||||
if thematic_code in accessible_codes:
|
if thematic_code in accessible_codes:
|
||||||
config.setdefault(thematic_code, []).append({
|
config.setdefault(thematic_code, []).append({
|
||||||
'key': 'municipality',
|
'key': 'municipality',
|
||||||
|
|
@ -752,7 +752,114 @@ def build_conditional_filter_config(accessible_thematics, user=None):
|
||||||
|
|
||||||
# Nature
|
# Nature
|
||||||
if 'nature' in accessible_codes:
|
if 'nature' in accessible_codes:
|
||||||
config['nature'] = [
|
from assets.models import NatureTree
|
||||||
|
|
||||||
|
def _get_muni_region(m):
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
code = str(getattr(m, 'niscode', '') or '').strip()
|
||||||
|
if code.startswith('21'):
|
||||||
|
return 'BRU'
|
||||||
|
elif code[:2] in ('25', '51', '52', '53', '55', '56', '57', '61', '62', '63', '81', '82', '83', '84', '85', '91', '92', '93') or (code and code[0] in ('5', '6', '8', '9')):
|
||||||
|
return 'WAL'
|
||||||
|
elif (code and code[0] in ('1', '3', '4', '7')) or code[:2] in ('23', '24'):
|
||||||
|
return 'VLG'
|
||||||
|
name = (getattr(m, 'name_fr', '') or '').lower()
|
||||||
|
if any(b in name for b in ('bruxelles', 'watermael', 'boitsfort', 'berchem', 'ixelles', 'etterbeek', 'uccle', 'woluwe', 'schaerbeek', 'anderlecht', 'forest', 'jette', 'ganshoren', 'evere', 'saint-gilles', 'saint-josse', 'auderghem', 'molenbeek', 'koekelberg')):
|
||||||
|
return 'BRU'
|
||||||
|
return None
|
||||||
|
|
||||||
|
regional_labels = {
|
||||||
|
'BRU': _('Région (Bruxelles Mobilité)'),
|
||||||
|
'WAL': _('Région (Région wallonne)'),
|
||||||
|
'VLG': _('Région (Région flamande)'),
|
||||||
|
}
|
||||||
|
|
||||||
|
user_cfg = getattr(user, 'config', None)
|
||||||
|
user_is_admin = getattr(user, 'is_superuser', False) or (user_cfg and (user_cfg.has_role('admin') or user_cfg.has_role('top_manager')))
|
||||||
|
user_is_ext = (user_cfg is None) or (not user_cfg.is_intern)
|
||||||
|
user_must_limit = user_is_ext or (user_cfg and user_cfg.limit_assets_to_contracts)
|
||||||
|
|
||||||
|
visible_trees_qs = NatureTree.objects.exclude(status='removed')
|
||||||
|
if not user_is_admin and user_must_limit:
|
||||||
|
visible_trees_qs = visible_trees_qs.filter(location__municipality__in=municipalities)
|
||||||
|
|
||||||
|
tree_stats = list(visible_trees_qs.values('management_type', 'location__municipality_id').distinct())
|
||||||
|
has_trees = len(tree_stats) > 0
|
||||||
|
muni_list = list(municipalities)
|
||||||
|
management_type_options = []
|
||||||
|
|
||||||
|
if len(muni_list) == 1:
|
||||||
|
single_muni = muni_list[0]
|
||||||
|
reg = _get_muni_region(single_muni) or 'BRU'
|
||||||
|
reg_label = regional_labels.get(reg, _('Région'))
|
||||||
|
|
||||||
|
has_municipal = any(t['management_type'] == 'municipal' for t in tree_stats) if has_trees else True
|
||||||
|
has_regional = any(t['management_type'] == 'regional' for t in tree_stats) if has_trees else True
|
||||||
|
|
||||||
|
if has_municipal:
|
||||||
|
management_type_options.append({
|
||||||
|
'value': 'municipal',
|
||||||
|
'label': _('Commune (%(name)s)') % {'name': single_muni.name}
|
||||||
|
})
|
||||||
|
if has_regional:
|
||||||
|
management_type_options.append({
|
||||||
|
'value': 'regional',
|
||||||
|
'label': reg_label
|
||||||
|
})
|
||||||
|
elif len(muni_list) > 1:
|
||||||
|
muni_map = {m.id: m for m in muni_list}
|
||||||
|
|
||||||
|
# Municipal options for each municipality with municipal trees
|
||||||
|
for m in muni_list:
|
||||||
|
has_m_trees = any(
|
||||||
|
t['management_type'] == 'municipal' and t['location__municipality_id'] == m.id
|
||||||
|
for t in tree_stats
|
||||||
|
) if has_trees else True
|
||||||
|
if has_m_trees:
|
||||||
|
management_type_options.append({
|
||||||
|
'value': f'municipal_{m.id}',
|
||||||
|
'label': _('Commune (%(name)s)') % {'name': m.name}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Regional options grouped by region
|
||||||
|
regions_with_regional_trees = set()
|
||||||
|
for t in tree_stats:
|
||||||
|
if t['management_type'] == 'regional':
|
||||||
|
mid = t['location__municipality_id']
|
||||||
|
m_obj = muni_map.get(mid)
|
||||||
|
if m_obj:
|
||||||
|
reg = _get_muni_region(m_obj)
|
||||||
|
if reg:
|
||||||
|
regions_with_regional_trees.add(reg)
|
||||||
|
|
||||||
|
if not has_trees:
|
||||||
|
for m in muni_list:
|
||||||
|
reg = _get_muni_region(m)
|
||||||
|
if reg:
|
||||||
|
regions_with_regional_trees.add(reg)
|
||||||
|
|
||||||
|
for reg_code, reg_val in [('BRU', 'regional_bruxelles'), ('WAL', 'regional_wallonie'), ('VLG', 'regional_flandre')]:
|
||||||
|
if reg_code in regions_with_regional_trees:
|
||||||
|
management_type_options.append({
|
||||||
|
'value': reg_val,
|
||||||
|
'label': regional_labels[reg_code]
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
management_type_options = [
|
||||||
|
{'value': 'municipal', 'label': _('Communal')},
|
||||||
|
{'value': 'regional', 'label': _('Régional')},
|
||||||
|
]
|
||||||
|
|
||||||
|
nature_filters = []
|
||||||
|
if management_type_options:
|
||||||
|
nature_filters.append({
|
||||||
|
'key': 'management_type',
|
||||||
|
'label': _('Gestionnaire'),
|
||||||
|
'type': 'multi',
|
||||||
|
'options': management_type_options,
|
||||||
|
})
|
||||||
|
nature_filters.extend([
|
||||||
{
|
{
|
||||||
'key': 'location_type',
|
'key': 'location_type',
|
||||||
'label': _('Type de localisation'),
|
'label': _('Type de localisation'),
|
||||||
|
|
@ -768,7 +875,8 @@ def build_conditional_filter_config(accessible_thematics, user=None):
|
||||||
'type': 'checkbox',
|
'type': 'checkbox',
|
||||||
'value': '1',
|
'value': '1',
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
|
config['nature'] = nature_filters
|
||||||
|
|
||||||
# Water
|
# Water
|
||||||
if 'water' in accessible_codes:
|
if 'water' in accessible_codes:
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,29 @@ def nature_index(request):
|
||||||
category_choices = [(m.id, m.get_name()) for m in categories]
|
category_choices = [(m.id, m.get_name()) for m in categories]
|
||||||
|
|
||||||
selected_management_types = request.GET.getlist('management_type')
|
selected_management_types = request.GET.getlist('management_type')
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
_munis = list(get_user_accessible_municipalities(request.user))
|
||||||
|
if len(_munis) == 1:
|
||||||
|
_sm = _munis[0]
|
||||||
|
_code = str(getattr(_sm, 'niscode', '') or '').strip()
|
||||||
|
_is_wal = _code[:2] in ('25', '51', '52', '53', '55', '56', '57', '61', '62', '63', '81', '82', '83', '84', '85', '91', '92', '93') or (_code and _code[0] in ('5', '6', '8', '9')) or 'court-saint' in (_sm.name_fr or '').lower()
|
||||||
|
_is_vlg = (_code and _code[0] in ('1', '3', '4', '7')) or _code[:2] in ('23', '24')
|
||||||
|
if _is_wal:
|
||||||
|
_reg_lbl = _("Régional (Région wallonne)")
|
||||||
|
elif _is_vlg:
|
||||||
|
_reg_lbl = _("Régional (Région flamande)")
|
||||||
|
else:
|
||||||
|
_reg_lbl = _("Régional (Bruxelles Mobilité)")
|
||||||
|
management_type_choices = [
|
||||||
|
('municipal', _("Communal (%(name)s)") % {'name': _sm.name}),
|
||||||
|
('regional', _reg_lbl),
|
||||||
|
('community', _("Intercommunal / Communautaire")),
|
||||||
|
('private', _("Privé / Conventionné")),
|
||||||
|
]
|
||||||
|
else:
|
||||||
management_type_choices = [
|
management_type_choices = [
|
||||||
('municipal', _("Communal (Commune)")),
|
('municipal', _("Communal (Commune)")),
|
||||||
('regional', _("Régional (Bruxelles Mobilité)")),
|
('regional', _("Régional (Région / Bruxelles Mobilité)")),
|
||||||
('community', _("Intercommunal / Communautaire")),
|
('community', _("Intercommunal / Communautaire")),
|
||||||
('private', _("Privé / Conventionné")),
|
('private', _("Privé / Conventionné")),
|
||||||
]
|
]
|
||||||
|
|
@ -462,10 +482,30 @@ def nature_locations_geojson(request):
|
||||||
filter_geom = None
|
filter_geom = None
|
||||||
|
|
||||||
|
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
user = request.user
|
||||||
|
user_config = getattr(user, 'config', None)
|
||||||
|
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
||||||
|
is_external = (user_config is None) or (not user_config.is_intern)
|
||||||
|
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
||||||
|
|
||||||
|
municipalities = ensure_list(data.get('municipality')) if request.method == "POST" else request.GET.getlist('municipality')
|
||||||
|
|
||||||
include_naturelocation = location_type in ('', 'naturelocation')
|
include_naturelocation = location_type in ('', 'naturelocation')
|
||||||
|
|
||||||
nature_locations = NatureLocation.objects.all() if include_naturelocation else NatureLocation.objects.none()
|
nature_locations = NatureLocation.objects.all() if include_naturelocation else NatureLocation.objects.none()
|
||||||
|
|
||||||
|
if not is_admin and must_limit:
|
||||||
|
accessible_munis = get_user_accessible_municipalities(user)
|
||||||
|
accessible_muni_ids = list(accessible_munis.values_list('id', flat=True))
|
||||||
|
if municipalities:
|
||||||
|
valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids]
|
||||||
|
nature_locations = nature_locations.filter(municipality_id__in=valid_munis)
|
||||||
|
else:
|
||||||
|
nature_locations = nature_locations.filter(municipality_id__in=accessible_muni_ids)
|
||||||
|
elif municipalities:
|
||||||
|
nature_locations = nature_locations.filter(municipality_id__in=municipalities)
|
||||||
|
|
||||||
if highway:
|
if highway:
|
||||||
nature_locations = nature_locations.filter(is_highway=highway)
|
nature_locations = nature_locations.filter(is_highway=highway)
|
||||||
|
|
||||||
|
|
@ -681,6 +721,8 @@ def nature_assets_geojson(request):
|
||||||
"is_remarkable": obj.get('is_remarkable', False),
|
"is_remarkable": obj.get('is_remarkable', False),
|
||||||
"scientific_name": obj.get('scientific_name') or '',
|
"scientific_name": obj.get('scientific_name') or '',
|
||||||
"vernacular_name": obj.get('vernacular_name') or '',
|
"vernacular_name": obj.get('vernacular_name') or '',
|
||||||
|
"is_dead": bool(obj.get('is_dead') or obj.get('vitality') == 'dead' or obj.get('status') in ('dead', 'archived')),
|
||||||
|
"vitality": obj.get('vitality') or '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -707,6 +749,35 @@ def nature_assets_geojson(request):
|
||||||
NatureBench.objects.all()
|
NatureBench.objects.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
user = request.user
|
||||||
|
user_config = getattr(user, 'config', None)
|
||||||
|
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
||||||
|
is_external = (user_config is None) or (not user_config.is_intern)
|
||||||
|
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
||||||
|
|
||||||
|
municipalities = ensure_list(data.get('municipality')) if request.method == "POST" else request.GET.getlist('municipality')
|
||||||
|
|
||||||
|
if not is_admin and must_limit:
|
||||||
|
accessible_munis = get_user_accessible_municipalities(user)
|
||||||
|
accessible_muni_ids = list(accessible_munis.values_list('id', flat=True))
|
||||||
|
if municipalities:
|
||||||
|
valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids]
|
||||||
|
naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=valid_munis)
|
||||||
|
naturetree_qs = naturetree_qs.filter(location__municipality_id__in=valid_munis)
|
||||||
|
natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=valid_munis)
|
||||||
|
naturebench_qs = naturebench_qs.filter(location__municipality_id__in=valid_munis)
|
||||||
|
else:
|
||||||
|
naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=accessible_muni_ids)
|
||||||
|
naturetree_qs = naturetree_qs.filter(location__municipality_id__in=accessible_muni_ids)
|
||||||
|
natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=accessible_muni_ids)
|
||||||
|
naturebench_qs = naturebench_qs.filter(location__municipality_id__in=accessible_muni_ids)
|
||||||
|
elif municipalities:
|
||||||
|
naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=municipalities)
|
||||||
|
naturetree_qs = naturetree_qs.filter(location__municipality_id__in=municipalities)
|
||||||
|
natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=municipalities)
|
||||||
|
naturebench_qs = naturebench_qs.filter(location__municipality_id__in=municipalities)
|
||||||
|
|
||||||
include_naturelocation = location_type in ('', 'naturelocation')
|
include_naturelocation = location_type in ('', 'naturelocation')
|
||||||
|
|
||||||
if not include_naturelocation:
|
if not include_naturelocation:
|
||||||
|
|
@ -715,7 +786,6 @@ def nature_assets_geojson(request):
|
||||||
natureshrub_qs = natureshrub_qs.none()
|
natureshrub_qs = natureshrub_qs.none()
|
||||||
naturebench_qs = naturebench_qs.none()
|
naturebench_qs = naturebench_qs.none()
|
||||||
|
|
||||||
|
|
||||||
if sections_ids:
|
if sections_ids:
|
||||||
naturegreensurface_qs = naturegreensurface_qs.filter(location__id__in=sections_ids)
|
naturegreensurface_qs = naturegreensurface_qs.filter(location__id__in=sections_ids)
|
||||||
naturetree_qs = naturetree_qs.filter(location__id__in=sections_ids)
|
naturetree_qs = naturetree_qs.filter(location__id__in=sections_ids)
|
||||||
|
|
@ -734,7 +804,43 @@ def nature_assets_geojson(request):
|
||||||
naturebench_qs = naturebench_qs.filter(model__id__in=models)
|
naturebench_qs = naturebench_qs.filter(model__id__in=models)
|
||||||
|
|
||||||
if management_types:
|
if management_types:
|
||||||
naturetree_qs = naturetree_qs.filter(management_type__in=management_types)
|
q_mgmt = Q()
|
||||||
|
for val in management_types:
|
||||||
|
val_str = str(val).strip()
|
||||||
|
if val_str == 'municipal':
|
||||||
|
q_mgmt |= Q(management_type='municipal')
|
||||||
|
elif val_str == 'regional':
|
||||||
|
q_mgmt |= Q(management_type='regional')
|
||||||
|
elif val_str == 'regional_bruxelles':
|
||||||
|
q_mgmt |= Q(management_type='regional') & (
|
||||||
|
Q(location__municipality__niscode__startswith='21') |
|
||||||
|
Q(location__municipality__name_fr__icontains='Bruxelles') |
|
||||||
|
Q(location__municipality__name_fr__icontains='Watermael') |
|
||||||
|
Q(location__municipality__name_fr__icontains='Berchem')
|
||||||
|
)
|
||||||
|
elif val_str == 'regional_wallonie':
|
||||||
|
q_mgmt |= Q(management_type='regional') & (
|
||||||
|
Q(location__municipality__niscode__startswith=('25', '5', '6', '8', '9')) |
|
||||||
|
Q(location__municipality__name_fr__icontains='Court-Saint')
|
||||||
|
)
|
||||||
|
elif val_str == 'regional_flandre':
|
||||||
|
q_mgmt |= Q(management_type='regional') & (
|
||||||
|
Q(location__municipality__niscode__startswith=('1', '3', '4', '7', '23', '24'))
|
||||||
|
)
|
||||||
|
elif val_str.startswith('municipal_'):
|
||||||
|
m_id = val_str.split('_', 1)[1]
|
||||||
|
if m_id.isdigit():
|
||||||
|
q_mgmt |= Q(management_type='municipal', location__municipality_id=int(m_id))
|
||||||
|
elif val_str.startswith('authority:'):
|
||||||
|
auth_name = val_str.split(':', 1)[1]
|
||||||
|
q_mgmt |= Q(managing_authority__iexact=auth_name)
|
||||||
|
else:
|
||||||
|
q_mgmt |= Q(managing_authority__icontains=val_str) | Q(management_type=val_str)
|
||||||
|
|
||||||
|
naturetree_qs = naturetree_qs.filter(q_mgmt)
|
||||||
|
naturegreensurface_qs = naturegreensurface_qs.none()
|
||||||
|
natureshrub_qs = natureshrub_qs.none()
|
||||||
|
naturebench_qs = naturebench_qs.none()
|
||||||
if categories:
|
if categories:
|
||||||
naturegreensurface_qs = naturegreensurface_qs.filter(category__id__in=categories)
|
naturegreensurface_qs = naturegreensurface_qs.filter(category__id__in=categories)
|
||||||
naturetree_qs = naturetree_qs.filter(category__id__in=categories)
|
naturetree_qs = naturetree_qs.filter(category__id__in=categories)
|
||||||
|
|
@ -812,7 +918,8 @@ def nature_assets_geojson(request):
|
||||||
'category_name', 'model_name', 'color', 'geojson',
|
'category_name', 'model_name', 'color', 'geojson',
|
||||||
'category_id', 'model_id', 'location_lon', 'location_lat',
|
'category_id', 'model_id', 'location_lon', 'location_lat',
|
||||||
'managing_authority', 'management_type', 'is_remarkable',
|
'managing_authority', 'management_type', 'is_remarkable',
|
||||||
'scientific_name', 'vernacular_name'
|
'scientific_name', 'vernacular_name',
|
||||||
|
'is_dead', 'vitality'
|
||||||
)
|
)
|
||||||
.order_by('code')
|
.order_by('code')
|
||||||
)[:MAX_ASSET_FEATURES + 1]
|
)[:MAX_ASSET_FEATURES + 1]
|
||||||
|
|
@ -884,12 +991,24 @@ def nature_locations_autocomplete(request):
|
||||||
if len(raw_query) < 2:
|
if len(raw_query) < 2:
|
||||||
return JsonResponse([], safe=False)
|
return JsonResponse([], safe=False)
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
user_config = getattr(user, 'config', None)
|
||||||
|
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
||||||
|
is_external = (user_config is None) or (not user_config.is_intern)
|
||||||
|
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
||||||
|
|
||||||
# Normalise la requête (équivalent de unaccent+lower côté Python)
|
# Normalise la requête (équivalent de unaccent+lower côté Python)
|
||||||
qn = strip_accents_lower(raw_query)
|
qn = strip_accents_lower(raw_query)
|
||||||
|
|
||||||
# Prépare des colonnes "recherchables" unaccentées + minuscules
|
# Prépare des colonnes "recherchables" unaccentées + minuscules
|
||||||
|
qs = NatureLocation.objects.all()
|
||||||
|
if not is_admin and must_limit:
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
accessible_munis = get_user_accessible_municipalities(user)
|
||||||
|
qs = qs.filter(municipality__in=accessible_munis)
|
||||||
|
|
||||||
qs = (
|
qs = (
|
||||||
NatureLocation.objects
|
qs
|
||||||
.annotate(
|
.annotate(
|
||||||
code_u=Lower(Unaccent('code')),
|
code_u=Lower(Unaccent('code')),
|
||||||
name_fr_u=Lower(Unaccent('name_fr')),
|
name_fr_u=Lower(Unaccent('name_fr')),
|
||||||
|
|
@ -954,13 +1073,25 @@ def nature_assets_autocomplete(request):
|
||||||
location_filter = {'location_id': location_id}
|
location_filter = {'location_id': location_id}
|
||||||
|
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
user_config = getattr(user, 'config', None)
|
||||||
|
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
||||||
|
is_external = (user_config is None) or (not user_config.is_intern)
|
||||||
|
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
||||||
|
|
||||||
def base_qs(model, extra_filter=None):
|
def base_qs(model, extra_filter=None):
|
||||||
ann = {
|
ann = {
|
||||||
'code_u': Lower(Unaccent('code')),
|
'code_u': Lower(Unaccent('code')),
|
||||||
'name_fr_u': Lower(Unaccent('name_fr')),
|
'name_fr_u': Lower(Unaccent('name_fr')),
|
||||||
'name_nl_u': Lower(Unaccent('name_nl')),
|
'name_nl_u': Lower(Unaccent('name_nl')),
|
||||||
}
|
}
|
||||||
q = model.objects.annotate(**ann)
|
q = model.objects.all()
|
||||||
|
if not is_admin and must_limit:
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
accessible_munis = get_user_accessible_municipalities(user)
|
||||||
|
q = q.filter(location__municipality__in=accessible_munis)
|
||||||
|
|
||||||
|
q = q.annotate(**ann)
|
||||||
|
|
||||||
# Si un terme de recherche est fourni, filtrer dessus
|
# Si un terme de recherche est fourni, filtrer dessus
|
||||||
if has_search_term:
|
if has_search_term:
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -553,3 +553,8 @@ msgstr "Zoom in"
|
||||||
#: common/templates/common/components/document_viewer.html
|
#: common/templates/common/components/document_viewer.html
|
||||||
msgid "Réinitialiser"
|
msgid "Réinitialiser"
|
||||||
msgstr "Reset"
|
msgstr "Reset"
|
||||||
|
|
||||||
|
#: common/views.py:683
|
||||||
|
msgid "Format de fond de plan invalide"
|
||||||
|
msgstr "Invalid basemap format"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -539,3 +539,8 @@ msgstr "Zoomer"
|
||||||
#: common/templates/common/components/document_viewer.html
|
#: common/templates/common/components/document_viewer.html
|
||||||
msgid "Réinitialiser"
|
msgid "Réinitialiser"
|
||||||
msgstr "Réinitialiser"
|
msgstr "Réinitialiser"
|
||||||
|
|
||||||
|
#: common/views.py:683
|
||||||
|
msgid "Format de fond de plan invalide"
|
||||||
|
msgstr "Format de fond de plan invalide"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -539,3 +539,8 @@ msgstr "Inzoomen"
|
||||||
#: common/templates/common/components/document_viewer.html
|
#: common/templates/common/components/document_viewer.html
|
||||||
msgid "Réinitialiser"
|
msgid "Réinitialiser"
|
||||||
msgstr "Herstellen"
|
msgstr "Herstellen"
|
||||||
|
|
||||||
|
#: common/views.py:683
|
||||||
|
msgid "Format de fond de plan invalide"
|
||||||
|
msgstr "Ongeldig basiskaartformaat"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -381,7 +381,8 @@
|
||||||
|
|
||||||
// Extract or build basemaps list
|
// Extract or build basemaps list
|
||||||
this.basemaps = this._resolveBasemaps();
|
this.basemaps = this._resolveBasemaps();
|
||||||
this.activeBasemapId = localStorage.getItem('loko_active_basemap') || 'default';
|
this.activeBasemapId = options.initialBasemapId || localStorage.getItem('loko_active_basemap') || 'default';
|
||||||
|
this.onBasemapChange = typeof options.onBasemapChange === 'function' ? options.onBasemapChange : null;
|
||||||
|
|
||||||
this._injectStyles();
|
this._injectStyles();
|
||||||
this._handleOutsideClick = this._handleOutsideClick.bind(this);
|
this._handleOutsideClick = this._handleOutsideClick.bind(this);
|
||||||
|
|
@ -442,13 +443,26 @@
|
||||||
this.popover = this._createPopover();
|
this.popover = this._createPopover();
|
||||||
map.getContainer().appendChild(this.popover);
|
map.getContainer().appendChild(this.popover);
|
||||||
|
|
||||||
// Initialize active basemap on map load
|
// Initialize active basemap on map load (checking map._loaded, isStyleLoaded, or loaded)
|
||||||
if (map.loaded()) {
|
const isMapReady = Boolean(
|
||||||
|
map._loaded ||
|
||||||
|
(typeof map.isStyleLoaded === 'function' && map.isStyleLoaded()) ||
|
||||||
|
map.loaded()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMapReady) {
|
||||||
this._applyInitialBasemap();
|
this._applyInitialBasemap();
|
||||||
} else {
|
} else {
|
||||||
map.on('load', () => this._applyInitialBasemap());
|
map.once('load', () => this._applyInitialBasemap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-apply if the style reloads dynamically
|
||||||
|
map.on('style.load', () => {
|
||||||
|
if (this.activeBasemapId && this.activeBasemapId !== 'default') {
|
||||||
|
this._applyInitialBasemap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('click', this._handleOutsideClick);
|
document.addEventListener('click', this._handleOutsideClick);
|
||||||
|
|
||||||
return this.container;
|
return this.container;
|
||||||
|
|
@ -660,8 +674,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
selectBasemap(id) {
|
selectBasemap(id) {
|
||||||
|
const previousId = this.activeBasemapId;
|
||||||
this.activeBasemapId = id;
|
this.activeBasemapId = id;
|
||||||
|
try {
|
||||||
localStorage.setItem('loko_active_basemap', id);
|
localStorage.setItem('loko_active_basemap', id);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
// Update UI list items
|
// Update UI list items
|
||||||
if (this.popover) {
|
if (this.popover) {
|
||||||
|
|
@ -682,9 +699,20 @@
|
||||||
this._applyRasterBasemap(bgConfig);
|
this._applyRasterBasemap(bgConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.onBasemapChange && previousId !== id) {
|
||||||
|
try {
|
||||||
|
this.onBasemapChange(id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[LayerSwitcherControl] onBasemapChange error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyInitialBasemap() {
|
_applyInitialBasemap() {
|
||||||
|
if (!this.basemaps || this.basemaps.length === 0) {
|
||||||
|
this.basemaps = this._resolveBasemaps();
|
||||||
|
}
|
||||||
if (this.activeBasemapId && this.activeBasemapId !== 'default') {
|
if (this.activeBasemapId && this.activeBasemapId !== 'default') {
|
||||||
const bgConfig = this.basemaps.find(b => b.id === this.activeBasemapId);
|
const bgConfig = this.basemaps.find(b => b.id === this.activeBasemapId);
|
||||||
if (bgConfig) {
|
if (bgConfig) {
|
||||||
|
|
@ -693,14 +721,16 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if any basemap has is_default
|
// Check if any basemap has is_default only if activeBasemapId is not set
|
||||||
|
if (!this.activeBasemapId) {
|
||||||
const defaultBg = this.basemaps.find(b => b.is_default);
|
const defaultBg = this.basemaps.find(b => b.is_default);
|
||||||
if (defaultBg) {
|
if (defaultBg) {
|
||||||
this.selectBasemap(defaultBg.id);
|
this.selectBasemap(defaultBg.id);
|
||||||
} else {
|
return;
|
||||||
this._updateButtonState();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._updateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
_removeCurrentRasterLayer() {
|
_removeCurrentRasterLayer() {
|
||||||
if (!this.map) return;
|
if (!this.map) return;
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,35 @@ class AssetsIndexPreferencesTests(TestCase):
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertIn("error", response.json())
|
self.assertIn("error", response.json())
|
||||||
|
|
||||||
|
def test_save_assets_index_preferences_with_basemap(self):
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
save_url = reverse("common:save_assets_index_preferences")
|
||||||
|
payload = {
|
||||||
|
"thematics": ["sign"],
|
||||||
|
"basemap": "urbis_ortho",
|
||||||
|
}
|
||||||
|
response = self.client.post(save_url, json.dumps(payload), content_type="application/json")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(response.json().get("success"))
|
||||||
|
|
||||||
|
from common.models import UserConfig
|
||||||
|
config = UserConfig.objects.get(user=self.superuser)
|
||||||
|
self.assertEqual(
|
||||||
|
config.assets_index_preferences.get("basemap"),
|
||||||
|
"urbis_ortho"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_save_assets_index_preferences_invalid_basemap_returns_400(self):
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
save_url = reverse("common:save_assets_index_preferences")
|
||||||
|
payload = {
|
||||||
|
"basemap": 9999,
|
||||||
|
}
|
||||||
|
response = self.client.post(save_url, json.dumps(payload), content_type="application/json")
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn("error", response.json())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UserConfigCopyTests(TestCase):
|
class UserConfigCopyTests(TestCase):
|
||||||
|
|
|
||||||
|
|
@ -663,6 +663,7 @@ def save_assets_index_preferences(request):
|
||||||
include_no_geom = data.get('include_no_geom', True)
|
include_no_geom = data.get('include_no_geom', True)
|
||||||
map_center = data.get('map_center')
|
map_center = data.get('map_center')
|
||||||
map_zoom = data.get('map_zoom')
|
map_zoom = data.get('map_zoom')
|
||||||
|
basemap = data.get('basemap')
|
||||||
|
|
||||||
if not isinstance(thematics, list):
|
if not isinstance(thematics, list):
|
||||||
return JsonResponse({'error': _('Format de thématiques invalide')}, status=400)
|
return JsonResponse({'error': _('Format de thématiques invalide')}, status=400)
|
||||||
|
|
@ -680,13 +681,18 @@ def save_assets_index_preferences(request):
|
||||||
return JsonResponse({'error': _('Format de filtres statut invalide')}, status=400)
|
return JsonResponse({'error': _('Format de filtres statut invalide')}, status=400)
|
||||||
if not isinstance(validation_status_filters, list):
|
if not isinstance(validation_status_filters, list):
|
||||||
return JsonResponse({'error': _('Format de filtres statut de validation invalide')}, status=400)
|
return JsonResponse({'error': _('Format de filtres statut de validation invalide')}, status=400)
|
||||||
|
if basemap is not None and not isinstance(basemap, str):
|
||||||
|
return JsonResponse({'error': _('Format de fond de plan invalide')}, status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_config, _created = UserConfig.objects.get_or_create(user=request.user)
|
user_config, _created = UserConfig.objects.get_or_create(user=request.user)
|
||||||
existing_prefs = user_config.assets_index_preferences or {}
|
existing_prefs = user_config.assets_index_preferences or {}
|
||||||
existing_visible_columns = existing_prefs.get('visible_columns', []) if isinstance(existing_prefs, dict) else []
|
if not isinstance(existing_prefs, dict):
|
||||||
|
existing_prefs = {}
|
||||||
|
existing_visible_columns = existing_prefs.get('visible_columns', [])
|
||||||
if not isinstance(existing_visible_columns, list):
|
if not isinstance(existing_visible_columns, list):
|
||||||
existing_visible_columns = []
|
existing_visible_columns = []
|
||||||
|
existing_basemap = existing_prefs.get('basemap', 'default')
|
||||||
|
|
||||||
user_config.assets_index_preferences = {
|
user_config.assets_index_preferences = {
|
||||||
'thematics': thematics,
|
'thematics': thematics,
|
||||||
|
|
@ -700,6 +706,7 @@ def save_assets_index_preferences(request):
|
||||||
'include_no_geom': include_no_geom,
|
'include_no_geom': include_no_geom,
|
||||||
'map_center': map_center,
|
'map_center': map_center,
|
||||||
'map_zoom': map_zoom,
|
'map_zoom': map_zoom,
|
||||||
|
'basemap': basemap if basemap is not None else existing_basemap,
|
||||||
}
|
}
|
||||||
user_config.save(update_fields=['assets_index_preferences'])
|
user_config.save(update_fields=['assets_index_preferences'])
|
||||||
return JsonResponse({'success': True})
|
return JsonResponse({'success': True})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue