fix(assets): suppress popup during polygon draw and resolve superposed asset selection

This commit is contained in:
kdeterme 2026-10-12 19:22:00 +02:00
parent 9bc41dea80
commit f440aa5740
2 changed files with 193 additions and 24 deletions

View file

@ -2134,20 +2134,28 @@ function collectClickedAssets(clickEvent) {
const layers = [
'assets',
'assets-point-selection',
'assets-fill',
'assets-polygon-outline',
'assets-line',
'assets-point',
'sign-panels-circles',
'sign-panels-symbols',
'sign-poles-orientation',
'sign-poles',
'sign-poles-vertical',
'assets-line',
'assets-polygon-outline',
'assets-fill',
].filter(layerId => map.getLayer(layerId));
if (!layers.length) return [];
const rendered = map.queryRenderedFeatures(clickEvent.point, { layers });
// Query with a tolerance bounding box so small point/circle symbols are easily selected
const tolerance = 6;
const pt = clickEvent.point;
const bbox = [
[pt.x - tolerance, pt.y - tolerance],
[pt.x + tolerance, pt.y + tolerance]
];
const rendered = map.queryRenderedFeatures(bbox, { layers });
const unique = new Map();
rendered.forEach(f => {
@ -2156,6 +2164,23 @@ function collectClickedAssets(clickEvent) {
if (id == null) return;
const key = `${p.thematic || ''}|${p.asset_type || ''}|${id}`;
if (unique.has(key)) return;
const geomType = f.geometry ? f.geometry.type : '';
const lType = f.layer ? f.layer.type : '';
let dist = 100;
if (geomType.includes('Point') && f.geometry.coordinates) {
try {
const coords = Array.isArray(f.geometry.coordinates[0]) ? f.geometry.coordinates[0] : f.geometry.coordinates;
const screenPt = map.project(coords);
dist = Math.hypot(pt.x - screenPt.x, pt.y - screenPt.y);
} catch (e) {
dist = 0;
}
} else if (geomType.includes('Line')) {
dist = 50;
}
unique.set(key, {
id: id,
code: p.code || '',
@ -2170,6 +2195,10 @@ function collectClickedAssets(clickEvent) {
management_type: p.management_type || '',
managing_authority: p.managing_authority || '',
is_remarkable: p.is_remarkable || false,
geometry_type: geomType,
layer_type: lType,
layer_id: f.layer ? f.layer.id : '',
click_dist: dist,
});
});
@ -2177,10 +2206,17 @@ function collectClickedAssets(clickEvent) {
}
function collectClickedManagementZones(clickEvent) {
const layers = ['management-zones-fill', 'management-zones-line'].filter(layerId => map.getLayer(layerId));
const layers = ['management-zones-line', 'management-zones-fill'].filter(layerId => map.getLayer(layerId));
if (!layers.length) return [];
const rendered = map.queryRenderedFeatures(clickEvent.point, { layers });
const tolerance = 6;
const pt = clickEvent.point;
const bbox = [
[pt.x - tolerance, pt.y - tolerance],
[pt.x + tolerance, pt.y + tolerance]
];
const rendered = map.queryRenderedFeatures(bbox, { layers });
const unique = new Map();
rendered.forEach(f => {
@ -2201,6 +2237,9 @@ function collectClickedManagementZones(clickEvent) {
color: p.color || '',
feature_id: id,
is_management_zone: true,
geometry_type: f.geometry ? f.geometry.type : '',
layer_type: f.layer ? f.layer.type : '',
click_dist: 150,
});
});
@ -2208,10 +2247,17 @@ function collectClickedManagementZones(clickEvent) {
}
function collectClickedLocations(clickEvent) {
const layers = ['locations', 'locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline'].filter(layerId => map.getLayer(layerId));
const layers = ['locations-circle', 'locations-radar', 'locations-radar-outline', 'locations-outline', 'locations'].filter(layerId => map.getLayer(layerId));
if (!layers.length) return [];
const rendered = map.queryRenderedFeatures(clickEvent.point, { layers });
const tolerance = 6;
const pt = clickEvent.point;
const bbox = [
[pt.x - tolerance, pt.y - tolerance],
[pt.x + tolerance, pt.y + tolerance]
];
const rendered = map.queryRenderedFeatures(bbox, { layers });
const unique = new Map();
rendered.forEach(f => {
@ -2220,6 +2266,21 @@ function collectClickedLocations(clickEvent) {
if (id == null) return;
const key = `${p.thematic || ''}|${id}`;
if (unique.has(key)) return;
const geomType = f.geometry ? f.geometry.type : '';
const lType = f.layer ? f.layer.type : '';
let dist = 100;
if (geomType.includes('Point') && f.geometry.coordinates) {
try {
const coords = Array.isArray(f.geometry.coordinates[0]) ? f.geometry.coordinates[0] : f.geometry.coordinates;
const screenPt = map.project(coords);
dist = Math.hypot(pt.x - screenPt.x, pt.y - screenPt.y);
} catch (e) {
dist = 0;
}
}
unique.set(key, {
id: id,
code: p.code || '',
@ -2230,12 +2291,46 @@ function collectClickedLocations(clickEvent) {
usage_type_display: p.usage_type_display || '',
detail_url: p.detail_url || null,
feature_id: id,
is_location: true,
geometry_type: geomType,
layer_type: lType,
click_dist: dist,
});
});
return Array.from(unique.values());
}
function sortClickedFeatures(features) {
return features.slice().sort((a, b) => {
const getGeomScore = (item) => {
const g = (item.geometry_type || '').toLowerCase();
const lt = (item.layer_type || '').toLowerCase();
if (g.includes('point') || lt === 'symbol' || lt === 'circle') return 1;
if (g.includes('line')) return 2;
return 3; // polygon / fill
};
const getTypeScore = (item) => {
if (item.is_management_zone) return 3;
if (item.is_location) return 2;
return 1; // assets
};
const gA = getGeomScore(a);
const gB = getGeomScore(b);
if (gA !== gB) return gA - gB;
const tA = getTypeScore(a);
const tB = getTypeScore(b);
if (tA !== tB) return tA - tB;
const dA = a.click_dist ?? 999;
const dB = b.click_dist ?? 999;
return dA - dB;
});
}
function bindAssetPopupActions(popupNode) {
if (!popupNode) return;
@ -2326,15 +2421,70 @@ function bindAssetPopupActions(popupNode) {
}
function showAssetClickPopup(clickEvent) {
const assets = collectClickedAssets(clickEvent);
const locations = collectClickedLocations(clickEvent);
const managementZones = collectClickedManagementZones(clickEvent);
if (!assets.length && !locations.length && !managementZones.length) return;
// 1. Never show popup during polygon selection, drawing mode, or right after polygon completes
if (mapSelectionMode === 'polygon') return;
if (mapDrawInstance && typeof mapDrawInstance.getMode === 'function') {
const mode = mapDrawInstance.getMode();
if (mode && mode.startsWith('draw')) return;
}
if (window._polygonSelectionJustFinished && (Date.now() - window._polygonSelectionJustFinished < 500)) {
return;
}
// 2. Prevent duplicate execution across multiple layer listeners on the same click
if (clickEvent && clickEvent.originalEvent) {
if (clickEvent.originalEvent._lokoClickHandled) return;
clickEvent.originalEvent._lokoClickHandled = true;
} else if (clickEvent && clickEvent.point) {
const now = Date.now();
if (window._lastMapClickTime && (now - window._lastMapClickTime < 50) &&
window._lastMapClickPoint &&
window._lastMapClickPoint.x === clickEvent.point.x &&
window._lastMapClickPoint.y === clickEvent.point.y) {
return;
}
window._lastMapClickTime = now;
window._lastMapClickPoint = clickEvent.point;
}
const rawAssets = collectClickedAssets(clickEvent);
const rawLocations = collectClickedLocations(clickEvent);
const rawManagementZones = collectClickedManagementZones(clickEvent);
const assets = sortClickedFeatures(rawAssets);
const locations = sortClickedFeatures(rawLocations);
const managementZones = sortClickedFeatures(rawManagementZones);
const allCandidates = sortClickedFeatures([...assets, ...locations, ...managementZones]);
if (!allCandidates.length) return;
if (mapSelectionMode === 'click') {
const target = assets[0] || locations[0] || managementZones[0];
let target = allCandidates[0];
// If there are multiple candidates of the same geometry category (e.g. two overlapping points),
// and the topmost one is already selected, allow selecting the next unselected one so both can be selected.
if (allCandidates.length > 1) {
const topGeom = (target.geometry_type || '').toLowerCase();
const isTopPoint = topGeom.includes('point');
const isTopSelected = selectedIds.has(target.id) || selectedIds.has(String(target.id));
if (isTopSelected) {
const sameGeomUnselected = allCandidates.find(c => {
const cGeom = (c.geometry_type || '').toLowerCase();
const isCPoint = cGeom.includes('point');
if (isTopPoint !== isCPoint) return false;
return !selectedIds.has(c.id) && !selectedIds.has(String(c.id));
});
if (sameGeomUnselected) {
target = sameGeomUnselected;
}
}
}
if (target && target.id != null) {
toggleAssetSelection(target.id, target);
const isNowSel = selectedIds.has(target.id) || selectedIds.has(String(target.id)) || (typeof target.id === 'string' && !isNaN(target.id) && selectedIds.has(Number(target.id)));
const label = target.code || target.name || target.element_type || gettext('Asset');
showToast(`${label} : ${isNowSel ? gettext('sélectionné') : gettext('désélectionné')}`, isNowSel ? 'primary' : 'secondary');
}
return;
}
@ -2999,8 +3149,8 @@ function resetAllHighlightStates(specificId = null) {
function setMapFeatureSelectionState(rowOrId, isSelected) {
if (typeof map === 'undefined' || !map) return;
const isObj = rowOrId && typeof rowOrId === 'object';
const id = isObj ? rowOrId.id : rowOrId;
if (id == null) return;
const rawId = isObj ? rowOrId.id : rowOrId;
if (rawId == null) return;
let targetSource = null;
if (isObj) {
@ -3008,21 +3158,29 @@ function setMapFeatureSelectionState(rowOrId, isSelected) {
}
const sources = targetSource ? [targetSource] : ['assets', 'locations', 'management_zones'];
const idsToSet = [rawId];
if (typeof rawId === 'string' && !isNaN(rawId) && rawId.trim() !== '') {
idsToSet.push(Number(rawId));
} else if (typeof rawId === 'number') {
idsToSet.push(String(rawId));
}
sources.forEach(src => {
try {
if (map.getSource(src)) {
if (isSelected) {
map.setFeatureState({ source: src, id: id }, { selected: true });
} else {
map.setFeatureState({ source: src, id: id }, { selected: false, highlighted: false });
}
idsToSet.forEach(fid => {
if (isSelected) {
map.setFeatureState({ source: src, id: fid }, { selected: true });
} else {
map.setFeatureState({ source: src, id: fid }, { selected: false, highlighted: false });
}
});
}
} catch (e) {}
});
if (!isSelected) {
resetAllHighlightStates(id);
resetAllHighlightStates(rawId);
}
}
@ -3300,8 +3458,14 @@ maploader.addEventListener('mapLoaded', () => {
if (!map.getLayer(layerId)) return;
map.on('click', layerId, showAssetClickPopup);
if (['assets', 'assets-point-selection', 'assets-fill', 'assets-line', 'assets-point', 'sign-panels-circles', 'sign-panels-symbols', 'sign-poles-orientation', 'sign-poles', 'sign-poles-vertical', 'management-zones-fill'].includes(layerId)) {
map.on('mouseenter', layerId, function () { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', layerId, function () { map.getCanvas().style.cursor = ''; });
map.on('mouseenter', layerId, function () {
if (mapSelectionMode === 'polygon') return;
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', layerId, function () {
if (mapSelectionMode === 'polygon') return;
map.getCanvas().style.cursor = '';
});
}
});
@ -3717,6 +3881,7 @@ function selectAssetsInsidePolygon(polygonGeom) {
}
function handlePolygonSelectionCreated(e) {
window._polygonSelectionJustFinished = Date.now();
if (!e || !e.features || !e.features.length) return;
const geom = e.features[0].geometry;
const count = selectAssetsInsidePolygon(geom);
@ -3743,6 +3908,10 @@ function handleDrawModeChange(e) {
function setMapSelectionMode(mode) {
mapSelectionMode = mode;
if (assetClickPopup && assetClickPopup.isOpen()) {
assetClickPopup.remove();
}
const clickBtn = document.getElementById('unified-map-click-sel-btn');
const polyBtn = document.getElementById('unified-map-poly-sel-btn');
const hintEl = document.getElementById('map-selection-hint');

View file

@ -1683,5 +1683,5 @@
<script src="{% static 'assets/sign_panels_shared.js' %}?v=20260703c"></script>
<script src="{% static 'assets/asset_symbology.js' %}?v=20261012_circle_select_v5"></script>
<script src="{% static 'assets/unified_map_layers.js' %}?v=20261012_circle_select_v5"></script>
<script src="{% static 'assets/unified_index.js' %}?v=20261012_map_selection_v1"></script>
<script src="{% static 'assets/unified_index.js' %}?v=20261012_map_selection_v2"></script>
{% endblock script %}