fix(assets): suppress popup during polygon draw and resolve superposed asset selection
This commit is contained in:
parent
9bc41dea80
commit
f440aa5740
2 changed files with 193 additions and 24 deletions
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
Loading…
Reference in a new issue