feat: enhance NatureRWIAsset spatial filtering, location handling, and inspection map data binding

This commit is contained in:
kdeterme 2026-09-07 11:48:55 +02:00
parent 75dd0bd50e
commit 584df99605
5 changed files with 389 additions and 120 deletions

View file

@ -1399,6 +1399,7 @@
let modelName = props.model_name || props.location_type || ''; let modelName = props.model_name || props.location_type || '';
if (modelName === 'NatureRWIZ' || modelName === 'NatureRWIAsset' || modelName === 'naturerwiasset' || props.thematic === 'water' || props.location_type === 'NatureRWIZ') { if (modelName === 'NatureRWIZ' || modelName === 'NatureRWIAsset' || modelName === 'naturerwiasset' || props.thematic === 'water' || props.location_type === 'NatureRWIZ') {
modelName = 'naturerwiz'; modelName = 'naturerwiz';
window.currentGiepLocationId = props.location_id || (props.is_location ? objId : null) || objId;
} }
document.getElementById('insp_target_type').textContent = props.element_type || props.asset_type_display || props.model_name || 'Asset'; document.getElementById('insp_target_type').textContent = props.element_type || props.asset_type_display || props.model_name || 'Asset';

View file

@ -173,6 +173,9 @@ def water_assets_geojson(request):
categories = data.get('category') or [] categories = data.get('category') or []
if isinstance(categories, str): if isinstance(categories, str):
categories = [categories] categories = [categories]
geometry_types = data.get('geometry_types') or ([data.get('geometry_type')] if data.get('geometry_type') else [])
if not geometry_types and data.get('polygons_only'):
geometry_types = ['polygon']
# Récupération et parsing de la géométrie # Récupération et parsing de la géométrie
filter_geom_raw = data.get("filter_geom") filter_geom_raw = data.get("filter_geom")
@ -191,6 +194,9 @@ def water_assets_geojson(request):
sections_ids = [section_id] if section_id else [] sections_ids = [section_id] if section_id else []
statuses = request.GET.getlist('asset_status') statuses = request.GET.getlist('asset_status')
categories = request.GET.getlist('category', []) categories = request.GET.getlist('category', [])
geometry_types = request.GET.getlist('geometry_type')
if not geometry_types and request.GET.get('polygons_only') in ('1', 'true', 'True', 'yes'):
geometry_types = ['polygon']
filter_geom_3812 = None filter_geom_3812 = None
_search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' _search_q = (data.get('q') or '').strip() if request.method == 'POST' else ''
@ -202,8 +208,59 @@ def water_assets_geojson(request):
naturerwiasset_qs = NatureRWIAsset.objects.all() naturerwiasset_qs = NatureRWIAsset.objects.all()
POLYGON_STRUCTURE_TYPES = [
'bassin sec/infiltration', 'bassin en eau', 'fosse', 'jardin de pluie', 'noue',
'massif drainant', 'tranchee drainante', 'toiture stockante', 'toiture verte',
'bassin orage', 'saul-vide', 'saul-terre', 'citerne', 'espace vert sur dalle',
'bande filtrante', 'collecteur en beton poreux'
]
if geometry_types:
from django.db.models import Q
geom_q = Q()
for gt in geometry_types:
gt_lower = gt.lower()
if gt_lower == 'polygon':
geom_q |= Q(geometry_type__iexact='polygon') | Q(structure_type__in=POLYGON_STRUCTURE_TYPES)
elif gt_lower == 'point':
geom_q |= Q(geometry_type__iexact='point')
elif gt_lower == 'line':
geom_q |= Q(geometry_type__iexact='line')
else:
geom_q |= Q(geometry_type__iexact=gt_lower)
naturerwiasset_qs = naturerwiasset_qs.filter(geom_q)
if sections_ids: if sections_ids:
naturerwiasset_qs = naturerwiasset_qs.filter(location_id__in=sections_ids) from django.db.models import Q
zones = list(NatureRWIZ.objects.filter(id__in=sections_ids))
if zones:
zone_ids = [z.id for z in zones]
q_loc = Q(location_id__in=zone_ids)
for z in zones:
if z.geom:
q_loc |= Q(geom__intersects=z.geom)
naturerwiasset_qs = naturerwiasset_qs.filter(q_loc)
else:
target_asset = NatureRWIAsset.objects.filter(id__in=sections_ids).first()
if target_asset:
if target_asset.location_id:
loc = target_asset.location
q_loc = Q(location_id=target_asset.location_id)
if loc and loc.geom:
q_loc |= Q(geom__intersects=loc.geom)
naturerwiasset_qs = naturerwiasset_qs.filter(q_loc)
elif target_asset.geom:
zone = NatureRWIZ.objects.filter(geom__intersects=target_asset.geom).first()
if zone:
naturerwiasset_qs = naturerwiasset_qs.filter(
Q(location=zone) | (Q(geom__intersects=zone.geom) if zone.geom else Q())
)
else:
naturerwiasset_qs = naturerwiasset_qs.filter(id=target_asset.id)
else:
naturerwiasset_qs = naturerwiasset_qs.filter(id=target_asset.id)
else:
naturerwiasset_qs = naturerwiasset_qs.filter(location_id__in=sections_ids)
if categories: if categories:
naturerwiasset_qs = naturerwiasset_qs.filter(category__id__in=categories) naturerwiasset_qs = naturerwiasset_qs.filter(category__id__in=categories)
@ -226,9 +283,11 @@ def water_assets_geojson(request):
naturerwiasset_qs = apply_search(naturerwiasset_qs, data) naturerwiasset_qs = apply_search(naturerwiasset_qs, data)
if naturerwiasset_qs.count() > MAX_ASSET_FEATURES: if naturerwiasset_qs.count() > MAX_ASSET_FEATURES:
return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) err_msg = _("Trop d'objets à afficher")
return JsonResponse({"error": f"{err_msg} (> {MAX_ASSET_FEATURES})"}, status=413)
from django.db.models import FloatField, Value, CharField, F from django.db.models import FloatField, Value, CharField, F
from django.contrib.gis.db.models.functions import AsGeoJSON, Transform
from assets.views.core import get_element_type_label from assets.views.core import get_element_type_label
naturerwiasset_qs = (naturerwiasset_qs.annotate( naturerwiasset_qs = (naturerwiasset_qs.annotate(
@ -238,20 +297,22 @@ def water_assets_geojson(request):
color=Value(None, output_field=CharField()), color=Value(None, output_field=CharField()),
location_lon=F('location__lon'), location_lon=F('location__lon'),
location_lat=F('location__lat'), location_lat=F('location__lat'),
computed_geojson=AsGeoJSON(Transform('geom', 4326)),
) )
.values( .values(
'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', 'surface_area', 'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', 'surface_area',
'category_name', 'model_name', 'color', 'geojson', 'category_name', 'model_name', 'color', 'geojson', 'computed_geojson',
'category_id', 'location_lon', 'location_lat', 'structure_type' 'category_id', 'location_lon', 'location_lat', 'structure_type', 'geometry_type'
) )
.order_by('code') .order_by('code')
)[:MAX_ASSET_FEATURES] )[:MAX_ASSET_FEATURES]
def build_geometry_and_coords(obj): def build_geometry_and_coords(obj):
geometry = None geometry = None
if obj.get('geojson'): geojson_str = obj.get('computed_geojson') or obj.get('geojson')
if geojson_str:
try: try:
geometry = json.loads(obj['geojson']) geometry = json.loads(geojson_str)
except (TypeError, ValueError, json.JSONDecodeError): except (TypeError, ValueError, json.JSONDecodeError):
geometry = None geometry = None
@ -296,6 +357,7 @@ def water_assets_geojson(request):
"thematic": "water", "thematic": "water",
"is_location": False, "is_location": False,
"structure_type": obj.get('structure_type', ''), "structure_type": obj.get('structure_type', ''),
"geometry_type": obj.get('geometry_type', ''),
} }
} }
@ -320,6 +382,10 @@ def water_assets_autocomplete(request):
except (TypeError, ValueError): except (TypeError, ValueError):
category_id = None category_id = None
geometry_type = request.GET.get('geometry_type')
if not geometry_type and request.GET.get('polygons_only') in ('1', 'true', 'True', 'yes'):
geometry_type = 'polygon'
qs = NatureRWIAsset.objects.exclude(status='removed').annotate( qs = NatureRWIAsset.objects.exclude(status='removed').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')),
@ -336,13 +402,27 @@ def water_assets_autocomplete(request):
if category_id is not None: if category_id is not None:
qs = qs.filter(category_id=category_id) qs = qs.filter(category_id=category_id)
if geometry_type:
if geometry_type.lower() == 'polygon':
POLYGON_STRUCTURE_TYPES = [
'bassin sec/infiltration', 'bassin en eau', 'fosse', 'jardin de pluie', 'noue',
'massif drainant', 'tranchee drainante', 'toiture stockante', 'toiture verte',
'bassin orage', 'saul-vide', 'saul-terre', 'citerne', 'espace vert sur dalle',
'bande filtrante', 'collecteur en beton poreux'
]
qs = qs.filter(Q(geometry_type__iexact='polygon') | Q(structure_type__in=POLYGON_STRUCTURE_TYPES))
else:
qs = qs.filter(geometry_type__iexact=geometry_type)
if location_id: if location_id:
try: try:
loc = NatureRWIZ.objects.get(id=location_id) loc = NatureRWIZ.objects.get(id=location_id)
if loc.geom: if loc.geom:
qs = qs.filter(geom__intersects=loc.geom) qs = qs.filter(Q(location_id=location_id) | Q(geom__intersects=loc.geom))
else:
qs = qs.filter(location_id=location_id)
except NatureRWIZ.DoesNotExist: except NatureRWIZ.DoesNotExist:
pass qs = qs.filter(location_id=location_id)
items = [] items = []
for a in qs.select_related('category')[:100]: for a in qs.select_related('category')[:100]:

View file

@ -1,7 +1,7 @@
{% load i18n static %} {% load i18n static %}
<!-- Component Formulaire d'Inspection GIEP Natif Mobile-First (Recommandations avec 5 choix) --> <!-- Component Formulaire d'Inspection GIEP Natif Mobile-First (Recommandations avec 5 choix) -->
<div id="giepSpecificFields" class="border rounded-4 p-3 mb-3 bg-white shadow-sm"> <div id="giepSpecificFields" class="border rounded-4 p-3 mb-3 bg-white shadow-sm" data-location-id="{% if asset.location_id %}{{ asset.location_id }}{% elif asset.id %}{{ asset.id }}{% elif location.id %}{{ location.id }}{% endif %}" data-asset-id="{% if asset.id %}{{ asset.id }}{% endif %}">
<!-- En-tête --> <!-- En-tête -->
<div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2"> <div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2">
@ -64,7 +64,7 @@
</div> </div>
<!-- Modal pour Sélection des Équipements GIEP sur Carte --> <!-- Modal pour Sélection des Équipements GIEP sur Carte -->
<div class="modal fade" id="giepAssetMapModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="giepAssetMapModalLabel" aria-hidden="true" style="z-index: 1060;"> <div class="modal fade" id="giepAssetMapModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="giepAssetMapModalLabel" aria-hidden="true" style="z-index: 1090;">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable"> <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content rounded-4 shadow-lg border-0"> <div class="modal-content rounded-4 shadow-lg border-0">
<div class="modal-header bg-primary text-white py-2.5 px-3 rounded-top-4"> <div class="modal-header bg-primary text-white py-2.5 px-3 rounded-top-4">
@ -78,7 +78,7 @@
<div class="col-12 col-md-7"> <div class="col-12 col-md-7">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<span class="input-group-text bg-white"><i class="bi bi-search text-muted"></i></span> <span class="input-group-text bg-white"><i class="bi bi-search text-muted"></i></span>
<input type="text" id="giep_modal_search_input" class="form-control" placeholder="{% translate 'Filtrer par code (ex: BI-01...) ou par type...' %}"> <input type="text" id="giep_modal_search_input" class="form-control" placeholder="{% translate 'Filtrer par code ou type d\'ouvrage...' %}">
</div> </div>
</div> </div>
<div class="col-12 col-md-5"> <div class="col-12 col-md-5">
@ -243,63 +243,175 @@
const serializedInput = document.getElementById('giep_data_serialized'); const serializedInput = document.getElementById('giep_data_serialized');
const addBtn = document.getElementById('giep_add_ouvrage_btn'); const addBtn = document.getElementById('giep_add_ouvrage_btn');
function resolveCurrentLocationId() {
// 1. Check data-location-id on container
const giepFields = document.getElementById('giepSpecificFields');
if (giepFields && giepFields.getAttribute('data-location-id')) {
return giepFields.getAttribute('data-location-id');
}
// 2. Check window.currentGiepLocationId
if (window.currentGiepLocationId) {
return window.currentGiepLocationId;
}
// 3. Check quick inspection modal insp_object_id
const inspObjIdEl = document.getElementById('insp_object_id');
if (inspObjIdEl && inspObjIdEl.value) {
return inspObjIdEl.value;
}
// 4. Check edit inspection modal edit_insp_object_id
const editInspObjIdEl = document.getElementById('edit_insp_object_id');
if (editInspObjIdEl && editInspObjIdEl.value) {
return editInspObjIdEl.value;
}
// 5. Check trigger with data-object-id
const modalTrigger = document.querySelector('[data-object-id]');
if (modalTrigger && modalTrigger.getAttribute('data-object-id')) {
return modalTrigger.getAttribute('data-object-id');
}
// 6. Check URL patterns (e.g. /zirw/123/)
const urlMatch = window.location.pathname.match(/\/zirw\/(\d+)\//);
if (urlMatch) return urlMatch[1];
return null;
}
function parseFeatureToAsset(f) {
const props = f.properties || {};
let lon = props.lon;
let lat = props.lat;
if ((lon === null || lon === undefined || lat === null || lat === undefined || isNaN(lon) || isNaN(lat)) && f.geometry) {
if (f.geometry.type === 'Point' && Array.isArray(f.geometry.coordinates)) {
lon = f.geometry.coordinates[0];
lat = f.geometry.coordinates[1];
} else if (f.geometry.type === 'Polygon' && Array.isArray(f.geometry.coordinates) && f.geometry.coordinates.length > 0) {
const ring = f.geometry.coordinates[0];
if (Array.isArray(ring) && ring.length > 0) {
let sumLon = 0, sumLat = 0;
ring.forEach(pt => { sumLon += pt[0]; sumLat += pt[1]; });
lon = sumLon / ring.length;
lat = sumLat / ring.length;
}
} else if (f.geometry.type === 'MultiPolygon' && Array.isArray(f.geometry.coordinates) && f.geometry.coordinates.length > 0) {
const firstPoly = f.geometry.coordinates[0];
if (Array.isArray(firstPoly) && firstPoly.length > 0) {
const ring = firstPoly[0];
if (Array.isArray(ring) && ring.length > 0) {
let sumLon = 0, sumLat = 0;
ring.forEach(pt => { sumLon += pt[0]; sumLat += pt[1]; });
lon = sumLon / ring.length;
lat = sumLat / ring.length;
}
}
} else if (f.geometry.type === 'LineString' && Array.isArray(f.geometry.coordinates) && f.geometry.coordinates.length > 0) {
const midPt = f.geometry.coordinates[Math.floor(f.geometry.coordinates.length / 2)];
if (Array.isArray(midPt)) {
lon = midPt[0];
lat = midPt[1];
}
}
}
return {
id: f.id,
code: props.code || `Asset #${f.id}`,
name: props.name || props.structure_type || props.category || props.code || `Asset #${f.id}`,
structure_type: props.structure_type || '',
category: props.category || '',
geometry_type: props.geometry_type || (f.geometry ? f.geometry.type.toLowerCase() : ''),
lon: (lon !== null && lon !== undefined && !isNaN(lon)) ? parseFloat(lon) : null,
lat: (lat !== null && lat !== undefined && !isNaN(lat)) ? parseFloat(lat) : null,
geometry: f.geometry,
rawFeature: f
};
}
function isPolygonAsset(asset) {
if (!asset) return false;
const gt = (asset.geometry_type || '').toLowerCase();
if (gt === 'polygon' || gt === 'multipolygon') return true;
if (asset.geometry && (asset.geometry.type === 'Polygon' || asset.geometry.type === 'MultiPolygon')) return true;
if (asset.rawFeature && asset.rawFeature.geometry && (asset.rawFeature.geometry.type === 'Polygon' || asset.rawFeature.geometry.type === 'MultiPolygon')) return true;
const st = (asset.structure_type || '').toLowerCase();
const polygonStructureTypes = [
'bassin sec/infiltration', 'bassin en eau', 'fosse', 'jardin de pluie', 'noue',
'massif drainant', 'tranchee drainante', 'toiture stockante', 'toiture verte',
'bassin orage', 'saul-vide', 'saul-terre', 'citerne', 'espace vert sur dalle',
'bande filtrante', 'collecteur en beton poreux'
];
if (polygonStructureTypes.some(type => st.includes(type))) {
return true;
}
return false;
}
function extendBoundsFromGeom(bounds, geom) { function extendBoundsFromGeom(bounds, geom) {
if (!bounds || !geom || !geom.coordinates) return; if (!bounds || !geom || !geom.coordinates) return;
const type = geom.type; const type = geom.type;
if (type === 'Point') { if (type === 'Point' && Array.isArray(geom.coordinates)) {
bounds.extend(geom.coordinates); if (!isNaN(geom.coordinates[0]) && !isNaN(geom.coordinates[1])) {
bounds.extend(geom.coordinates);
}
} else if (type === 'LineString' || type === 'MultiPoint') { } else if (type === 'LineString' || type === 'MultiPoint') {
geom.coordinates.forEach(c => bounds.extend(c)); geom.coordinates.forEach(c => {
if (Array.isArray(c) && !isNaN(c[0]) && !isNaN(c[1])) bounds.extend(c);
});
} else if (type === 'Polygon' || type === 'MultiLineString') { } else if (type === 'Polygon' || type === 'MultiLineString') {
geom.coordinates.forEach(ring => ring.forEach(c => bounds.extend(c))); geom.coordinates.forEach(ring => {
if (Array.isArray(ring)) {
ring.forEach(c => {
if (Array.isArray(c) && !isNaN(c[0]) && !isNaN(c[1])) bounds.extend(c);
});
}
});
} else if (type === 'MultiPolygon') { } else if (type === 'MultiPolygon') {
geom.coordinates.forEach(poly => poly.forEach(ring => ring.forEach(c => bounds.extend(c)))); geom.coordinates.forEach(poly => {
if (Array.isArray(poly)) {
poly.forEach(ring => {
if (Array.isArray(ring)) {
ring.forEach(c => {
if (Array.isArray(c) && !isNaN(c[0]) && !isNaN(c[1])) bounds.extend(c);
});
}
});
}
});
} }
} }
function fetchLocationAssets() { function fetchLocationAssets(forceRefresh = false) {
if (locationGiepAssets.length > 0) return Promise.resolve(locationGiepAssets); if (!forceRefresh && locationGiepAssets.length > 0) return Promise.resolve(locationGiepAssets);
let locationId = null; const locationId = resolveCurrentLocationId();
const modalTrigger = document.querySelector('[data-object-id]'); let url = "{% url 'assets:water_assets_geojson' %}?polygons_only=1";
if (modalTrigger) { if (locationId) {
locationId = modalTrigger.getAttribute('data-object-id'); url += "&location=" + encodeURIComponent(locationId);
} else { } else {
const urlMatch = window.location.pathname.match(/\/zirw\/(\d+)\//); url += "&unified=1";
if (urlMatch) locationId = urlMatch[1];
} }
if (!locationId) return Promise.resolve([]);
return fetch("{% url 'assets:water_assets_geojson' %}?location=" + locationId) return fetch(url)
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if (data && data.features) { let features = (data && data.features) ? data.features : [];
locationGiepAssets = data.features.map(f => { // If location filter returned 0 features and locationId was set, fallback to unified polygon water assets
const props = f.properties || {}; if (features.length === 0 && locationId) {
let lon = props.lon; return fetch("{% url 'assets:water_assets_geojson' %}?polygons_only=1&unified=1")
let lat = props.lat; .then(r => r.json())
if ((lon === null || lon === undefined || lat === null || lat === undefined) && f.geometry) { .then(fallbackData => {
if (f.geometry.type === 'Point' && Array.isArray(f.geometry.coordinates)) { const fallbackFeatures = (fallbackData && fallbackData.features) ? fallbackData.features : [];
lon = f.geometry.coordinates[0]; locationGiepAssets = fallbackFeatures.map(parseFeatureToAsset).filter(isPolygonAsset);
lat = f.geometry.coordinates[1]; return locationGiepAssets;
} })
} .catch(() => {
return { locationGiepAssets = [];
id: f.id, return [];
code: props.code || `Asset #${f.id}`, });
name: props.name || props.structure_type || props.category || props.code || `Asset #${f.id}`,
structure_type: props.structure_type || '',
category: props.category || '',
lon: (lon !== null && lon !== undefined) ? parseFloat(lon) : null,
lat: (lat !== null && lat !== undefined) ? parseFloat(lat) : null,
geometry: f.geometry,
rawFeature: f
};
});
} }
locationGiepAssets = features.map(parseFeatureToAsset).filter(isPolygonAsset);
return locationGiepAssets; return locationGiepAssets;
}) })
.catch(err => { .catch(err => {
console.warn("Erreur de chargement des équipements GIEP :", err); console.warn("Erreur de chargement des équipements GIEP :", err);
locationGiepAssets = [];
return []; return [];
}); });
} }
@ -447,69 +559,72 @@
let hasCoords = false; let hasCoords = false;
locationGiepAssets.forEach(asset => { locationGiepAssets.forEach(asset => {
if (!isPolygonAsset(asset)) return;
const matchText = !filterText || asset.code.toLowerCase().includes(filterText) || (asset.structure_type && asset.structure_type.toLowerCase().includes(filterText)) || (asset.name && asset.name.toLowerCase().includes(filterText)); const matchText = !filterText || asset.code.toLowerCase().includes(filterText) || (asset.structure_type && asset.structure_type.toLowerCase().includes(filterText)) || (asset.name && asset.name.toLowerCase().includes(filterText));
const devType = inferDeviceType(asset); const devType = inferDeviceType(asset);
const matchType = !filterType || devType === filterType; const matchType = !filterType || devType === filterType;
if (matchText && matchType && asset.rawFeature && asset.rawFeature.geometry && typeof maplibregl !== 'undefined') { if (matchText && matchType && asset.rawFeature && typeof maplibregl !== 'undefined') {
const geom = asset.rawFeature.geometry; const geom = asset.rawFeature.geometry;
let color = '#0277bd'; // Bleu GIEP (naturerwiasset) let color = '#0277bd'; // Bleu GIEP (naturerwiasset)
if (devType === 'evc') color = '#2e7d32'; // Vert EVC if (devType === 'evc') color = '#2e7d32'; // Vert EVC
if (devType === 'oed') color = '#0277bd'; // Bleu OED if (devType === 'oed') color = '#0277bd'; // Bleu OED
if (devType === 'ta') color = '#00796b'; // Turquoise TA if (devType === 'ta') color = '#00796b'; // Turquoise TA
const featureClone = { if (geom) {
type: 'Feature', const featureClone = {
id: asset.id, type: 'Feature',
geometry: geom,
properties: {
...(asset.rawFeature.properties || {}),
id: asset.id, id: asset.id,
code: asset.code, geometry: geom,
name: asset.name, properties: {
structure_type: asset.structure_type, ...(asset.rawFeature.properties || {}),
device_type: devType, id: asset.id,
color: color code: asset.code,
} name: asset.name,
}; structure_type: asset.structure_type,
device_type: devType,
color: color
}
};
filteredFeatures.push(featureClone); filteredFeatures.push(featureClone);
if (isInitialFit) {
extendBoundsFromGeom(bounds, geom); extendBoundsFromGeom(bounds, geom);
hasCoords = true;
} }
hasCoords = true;
// Marqueurs ronds pour les géométries de type Point // Marqueurs ronds pour les assets ayant des coordonnées valides
if (geom.type === 'Point' && Array.isArray(geom.coordinates)) { const lon = asset.lon;
const lon = geom.coordinates[0]; const lat = asset.lat;
const lat = geom.coordinates[1]; if (lon !== null && lat !== null && !isNaN(lon) && !isNaN(lat)) {
if (!isNaN(lon) && !isNaN(lat)) { if (!geom) {
const el = document.createElement('div'); bounds.extend([lon, lat]);
el.className = 'giep-map-marker'; hasCoords = true;
el.style.width = '28px';
el.style.height = '28px';
el.style.borderRadius = '50%';
el.style.backgroundColor = color;
el.style.border = '2.5px solid white';
el.style.boxShadow = '0 3px 8px rgba(0,0,0,0.4)';
el.style.cursor = 'pointer';
el.style.display = 'flex';
el.style.alignItems = 'center';
el.style.justifyContent = 'center';
el.innerHTML = '<i class="bi bi-droplet-fill text-white" style="font-size: 14px;"></i>';
el.title = `${asset.code} - ${asset.structure_type || asset.name}`;
const marker = new maplibregl.Marker({ element: el })
.setLngLat([lon, lat])
.addTo(modalMapInstance);
el.addEventListener('click', (e) => {
e.stopPropagation();
showAssetPopup(asset, [lon, lat]);
});
modalMapMarkers.push(marker);
} }
const el = document.createElement('div');
el.className = 'giep-map-marker';
el.style.width = '30px';
el.style.height = '30px';
el.style.borderRadius = '50%';
el.style.backgroundColor = color;
el.style.border = '2.5px solid white';
el.style.boxShadow = '0 3px 8px rgba(0,0,0,0.4)';
el.style.cursor = 'pointer';
el.style.display = 'flex';
el.style.alignItems = 'center';
el.style.justifyContent = 'center';
el.innerHTML = '<i class="bi bi-droplet-fill text-white" style="font-size: 15px;"></i>';
el.title = `${asset.code} - ${asset.structure_type || asset.name}`;
const marker = new maplibregl.Marker({ element: el })
.setLngLat([lon, lat])
.addTo(modalMapInstance);
el.addEventListener('click', (e) => {
e.stopPropagation();
showAssetPopup(asset, [lon, lat]);
});
modalMapMarkers.push(marker);
} }
} }
}); });
@ -599,9 +714,9 @@
}); });
} }
// Seul le chargement initial modifie le zoom et cadrage de la carte // Cadrer la carte sur les assets
if (isInitialFit && hasCoords && bounds && !bounds.isEmpty()) { if (hasCoords && bounds && !bounds.isEmpty()) {
modalMapInstance.fitBounds(bounds, { padding: 50, maxZoom: 18 }); modalMapInstance.fitBounds(bounds, { padding: 50, maxZoom: 18, duration: 300 });
} }
} }
@ -632,7 +747,6 @@
modalMapInstance.on('load', () => { modalMapInstance.on('load', () => {
renderModalMapFeatures(true); renderModalMapFeatures(true);
geolocate.trigger();
}); });
} else { } else {
setTimeout(() => { setTimeout(() => {
@ -640,29 +754,11 @@
renderModalMapFeatures(true); renderModalMapFeatures(true);
}, 150); }, 150);
} }
// Tentative de géolocalisation de l'utilisateur si navigateur supporté
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(pos) => {
const uLon = pos.coords.longitude;
const uLat = pos.coords.latitude;
if (modalMapInstance) {
modalMapInstance.flyTo({ center: [uLon, uLat], zoom: 16 });
}
},
(err) => {
console.log("Géolocalisation utilisateur non disponible:", err);
},
{ enableHighAccuracy: true, timeout: 6000 }
);
}
} }
function openMapModal(idx) { function openMapModal(idx) {
activeOuvrageIdxForMap = idx; activeOuvrageIdxForMap = idx;
fetchLocationAssets().then(assets => { fetchLocationAssets().then(assets => {
populateModalAssetsList();
const modalEl = document.getElementById('giepAssetMapModal'); const modalEl = document.getElementById('giepAssetMapModal');
let modalInstance = bootstrap.Modal.getInstance(modalEl); let modalInstance = bootstrap.Modal.getInstance(modalEl);
if (!modalInstance) { if (!modalInstance) {
@ -1004,7 +1100,7 @@
<div class="position-relative"> <div class="position-relative">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<span class="input-group-text bg-white"><i class="bi bi-search text-muted"></i></span> <span class="input-group-text bg-white"><i class="bi bi-search text-muted"></i></span>
<input type="text" class="form-control asset-search-input" data-idx="${idx}" placeholder="{% filter escapejs %}{% translate "Rechercher par code (ex: BI-01...) ou par type..." %}{% endfilter %}" autocomplete="off"> <input type="text" class="form-control asset-search-input" data-idx="${idx}" placeholder="{% filter escapejs %}{% translate "Rechercher un ouvrage par code ou type..." %}{% endfilter %}" autocomplete="off">
<button type="button" class="btn btn-outline-primary open-map-modal-btn" data-idx="${idx}"> <button type="button" class="btn btn-outline-primary open-map-modal-btn" data-idx="${idx}">
<i class="bi bi-geo-alt-fill me-1"></i>{% translate "Choisir sur la carte" %} <i class="bi bi-geo-alt-fill me-1"></i>{% translate "Choisir sur la carte" %}
</button> </button>
@ -1103,7 +1199,7 @@
} }
fetchLocationAssets().then(assets => { fetchLocationAssets().then(assets => {
const matches = assets.filter(a => a.code.toLowerCase().includes(val) || (a.structure_type && a.structure_type.toLowerCase().includes(val)) || (a.name && a.name.toLowerCase().includes(val))).slice(0, 8); const matches = assets.filter(a => isPolygonAsset(a) && (a.code.toLowerCase().includes(val) || (a.structure_type && a.structure_type.toLowerCase().includes(val)) || (a.name && a.name.toLowerCase().includes(val)))).slice(0, 8);
if (matches.length === 0) { if (matches.length === 0) {
dropdown.classList.add('d-none'); dropdown.classList.add('d-none');
dropdown.innerHTML = ''; dropdown.innerHTML = '';
@ -1217,6 +1313,10 @@
window.setGiepFormData = function(data) { window.setGiepFormData = function(data) {
if (!data) return; if (!data) return;
if (data.location_id) {
window.currentGiepLocationId = data.location_id;
locationGiepAssets = [];
}
const dateRain = document.getElementById('giep_date_derniere_pluie'); const dateRain = document.getElementById('giep_date_derniere_pluie');
if (dateRain) { if (dateRain) {
dateRain.value = (data.date_derniere_pluie !== undefined && data.date_derniere_pluie !== null) ? String(data.date_derniere_pluie) : 'null'; dateRain.value = (data.date_derniere_pluie !== undefined && data.date_derniere_pluie !== null) ? String(data.date_derniere_pluie) : 'null';

View file

@ -15,6 +15,25 @@ User = get_user_model()
class InspectionModelAndViewsTestCase(TestCase): class InspectionModelAndViewsTestCase(TestCase):
def setUp(self): def setUp(self):
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("CREATE EXTENSION IF NOT EXISTS unaccent;")
cursor.execute("""
CREATE OR REPLACE FUNCTION public.unaccent_immutable(text)
RETURNS text
LANGUAGE sql
IMMUTABLE PARALLEL SAFE
AS $function$
SELECT public.unaccent('public.unaccent', $1)
$function$;
CREATE OR REPLACE FUNCTION public.unaccent_immutable(character varying)
RETURNS text
LANGUAGE sql
IMMUTABLE PARALLEL SAFE
AS $function$
SELECT public.unaccent('public.unaccent', $1)
$function$;
""")
self.client = Client() self.client = Client()
self.user = User.objects.create_user( self.user = User.objects.create_user(
username='inspector_user', username='inspector_user',
@ -209,6 +228,66 @@ class InspectionModelAndViewsTestCase(TestCase):
ctx = asset_location_link({'request': None}, rwi_asset) ctx = asset_location_link({'request': None}, rwi_asset)
self.assertEqual(ctx['location'], rwiz_poly) self.assertEqual(ctx['location'], rwiz_poly)
def test_water_assets_geojson_api(self):
from django.contrib.gis.geos import Point
self.client.login(username='inspector_user', password='password123')
asset_in_zone = NatureRWIAsset.objects.create(
code="RWI-ASSET-01",
location=self.rwiz,
geom=Point(4.35, 50.85, srid=3812),
structure_type='noue',
geometry_type='polygon'
)
point_asset = NatureRWIAsset.objects.create(
code="RWI-ASSET-POINT-01",
location=self.rwiz,
geom=Point(4.351, 50.851, srid=3812),
structure_type='avaloir',
geometry_type='point'
)
# 1. Test with location parameter
url_loc = reverse('assets:water_assets_geojson') + f'?location={self.rwiz.id}'
res_loc = self.client.get(url_loc)
self.assertEqual(res_loc.status_code, 200)
data_loc = res_loc.json()
self.assertIn('features', data_loc)
self.assertTrue(any(f['id'] == asset_in_zone.id for f in data_loc['features']))
# 2. Test with unified parameter
url_unified = reverse('assets:water_assets_geojson') + '?unified=1'
res_unified = self.client.get(url_unified)
self.assertEqual(res_unified.status_code, 200)
data_unified = res_unified.json()
self.assertTrue(any(f['id'] == asset_in_zone.id for f in data_unified['features']))
# 3. Test with polygons_only filter
url_poly = reverse('assets:water_assets_geojson') + f'?location={self.rwiz.id}&polygons_only=1'
res_poly = self.client.get(url_poly)
self.assertEqual(res_poly.status_code, 200)
data_poly = res_poly.json()
self.assertTrue(any(f['id'] == asset_in_zone.id for f in data_poly['features']))
self.assertFalse(any(f['id'] == point_asset.id for f in data_poly['features']))
feat = next(f for f in data_poly['features'] if f['id'] == asset_in_zone.id)
self.assertEqual(feat['properties']['geometry_type'], 'polygon')
# 4. Test autocomplete with polygons_only
url_auto = reverse('assets:water_autocomplete_assets') + f'?location_id={self.rwiz.id}&polygons_only=1'
res_auto = self.client.get(url_auto)
self.assertEqual(res_auto.status_code, 200)
auto_data = res_auto.json()
auto_ids = [item['id'] for item in auto_data]
self.assertIn(asset_in_zone.id, auto_ids)
self.assertNotIn(point_asset.id, auto_ids)
def test_giep_partial_form_context(self):
self.client.login(username='inspector_user', password='password123')
url = reverse('inspections:asset_inspection_form_partial') + f'?content_type_id={self.rwiz_ct.id}&object_id={self.rwiz.id}'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertIn(b'giepSpecificFields', response.content)
self.assertIn(f'data-location-id="{self.rwiz.id}"'.encode('utf-8'), response.content)
def test_can_edit_inspection_permissions(self): def test_can_edit_inspection_permissions(self):
other_user = User.objects.create_user( other_user = User.objects.create_user(
username='regular_user', username='regular_user',

View file

@ -796,6 +796,10 @@ def get_inspection_detail_api(request, inspection_id):
data['date_derniere_pluie'] = leaf.date_derniere_pluie or 'null' data['date_derniere_pluie'] = leaf.date_derniere_pluie or 'null'
data['orage_moins_une_heure'] = leaf.orage_moins_une_heure data['orage_moins_une_heure'] = leaf.orage_moins_une_heure
data['giep_data'] = leaf.data or {} data['giep_data'] = leaf.data or {}
if hasattr(leaf.asset, 'location_id') and leaf.asset.location_id:
data['location_id'] = leaf.asset.location_id
else:
data['location_id'] = leaf.asset_object_id
elif isinstance(leaf, SignPanelInspection): elif isinstance(leaf, SignPanelInspection):
data['is_signpanel'] = True data['is_signpanel'] = True
@ -1445,6 +1449,11 @@ def get_asset_inspection_form_partial(request):
return HttpResponse('') return HttpResponse('')
context = {'asset': asset} context = {'asset': asset}
if asset and hasattr(asset, 'location') and asset.location:
context['location'] = asset.location
elif asset and asset.__class__.__name__ == 'NatureRWIZ':
context['location'] = asset
if model_name == 'signpanel': if model_name == 'signpanel':
from assets.models import SignPanelType from assets.models import SignPanelType
context['panel_types'] = SignPanelType.objects.all().order_by('code') context['panel_types'] = SignPanelType.objects.all().order_by('code')