feat: enhance NatureRWIAsset spatial filtering, location handling, and inspection map data binding
This commit is contained in:
parent
75dd0bd50e
commit
584df99605
5 changed files with 389 additions and 120 deletions
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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,7 +208,58 @@ 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:
|
||||||
|
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)
|
naturerwiasset_qs = naturerwiasset_qs.filter(location_id__in=sections_ids)
|
||||||
|
|
||||||
if categories:
|
if 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]:
|
||||||
|
|
|
||||||
|
|
@ -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,44 +243,71 @@
|
||||||
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 extendBoundsFromGeom(bounds, geom) {
|
function resolveCurrentLocationId() {
|
||||||
if (!bounds || !geom || !geom.coordinates) return;
|
// 1. Check data-location-id on container
|
||||||
const type = geom.type;
|
const giepFields = document.getElementById('giepSpecificFields');
|
||||||
if (type === 'Point') {
|
if (giepFields && giepFields.getAttribute('data-location-id')) {
|
||||||
bounds.extend(geom.coordinates);
|
return giepFields.getAttribute('data-location-id');
|
||||||
} else if (type === 'LineString' || type === 'MultiPoint') {
|
|
||||||
geom.coordinates.forEach(c => bounds.extend(c));
|
|
||||||
} else if (type === 'Polygon' || type === 'MultiLineString') {
|
|
||||||
geom.coordinates.forEach(ring => ring.forEach(c => bounds.extend(c)));
|
|
||||||
} else if (type === 'MultiPolygon') {
|
|
||||||
geom.coordinates.forEach(poly => poly.forEach(ring => ring.forEach(c => bounds.extend(c))));
|
|
||||||
}
|
}
|
||||||
|
// 2. Check window.currentGiepLocationId
|
||||||
|
if (window.currentGiepLocationId) {
|
||||||
|
return window.currentGiepLocationId;
|
||||||
}
|
}
|
||||||
|
// 3. Check quick inspection modal insp_object_id
|
||||||
function fetchLocationAssets() {
|
const inspObjIdEl = document.getElementById('insp_object_id');
|
||||||
if (locationGiepAssets.length > 0) return Promise.resolve(locationGiepAssets);
|
if (inspObjIdEl && inspObjIdEl.value) {
|
||||||
let locationId = null;
|
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]');
|
const modalTrigger = document.querySelector('[data-object-id]');
|
||||||
if (modalTrigger) {
|
if (modalTrigger && modalTrigger.getAttribute('data-object-id')) {
|
||||||
locationId = modalTrigger.getAttribute('data-object-id');
|
return modalTrigger.getAttribute('data-object-id');
|
||||||
} else {
|
|
||||||
const urlMatch = window.location.pathname.match(/\/zirw\/(\d+)\//);
|
|
||||||
if (urlMatch) locationId = urlMatch[1];
|
|
||||||
}
|
}
|
||||||
if (!locationId) return Promise.resolve([]);
|
// 6. Check URL patterns (e.g. /zirw/123/)
|
||||||
|
const urlMatch = window.location.pathname.match(/\/zirw\/(\d+)\//);
|
||||||
|
if (urlMatch) return urlMatch[1];
|
||||||
|
|
||||||
return fetch("{% url 'assets:water_assets_geojson' %}?location=" + locationId)
|
return null;
|
||||||
.then(r => r.json())
|
}
|
||||||
.then(data => {
|
|
||||||
if (data && data.features) {
|
function parseFeatureToAsset(f) {
|
||||||
locationGiepAssets = data.features.map(f => {
|
|
||||||
const props = f.properties || {};
|
const props = f.properties || {};
|
||||||
let lon = props.lon;
|
let lon = props.lon;
|
||||||
let lat = props.lat;
|
let lat = props.lat;
|
||||||
if ((lon === null || lon === undefined || lat === null || lat === undefined) && f.geometry) {
|
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)) {
|
if (f.geometry.type === 'Point' && Array.isArray(f.geometry.coordinates)) {
|
||||||
lon = f.geometry.coordinates[0];
|
lon = f.geometry.coordinates[0];
|
||||||
lat = f.geometry.coordinates[1];
|
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 {
|
return {
|
||||||
|
|
@ -289,17 +316,102 @@
|
||||||
name: props.name || props.structure_type || props.category || props.code || `Asset #${f.id}`,
|
name: props.name || props.structure_type || props.category || props.code || `Asset #${f.id}`,
|
||||||
structure_type: props.structure_type || '',
|
structure_type: props.structure_type || '',
|
||||||
category: props.category || '',
|
category: props.category || '',
|
||||||
lon: (lon !== null && lon !== undefined) ? parseFloat(lon) : null,
|
geometry_type: props.geometry_type || (f.geometry ? f.geometry.type.toLowerCase() : ''),
|
||||||
lat: (lat !== null && lat !== undefined) ? parseFloat(lat) : null,
|
lon: (lon !== null && lon !== undefined && !isNaN(lon)) ? parseFloat(lon) : null,
|
||||||
|
lat: (lat !== null && lat !== undefined && !isNaN(lat)) ? parseFloat(lat) : null,
|
||||||
geometry: f.geometry,
|
geometry: f.geometry,
|
||||||
rawFeature: f
|
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) {
|
||||||
|
if (!bounds || !geom || !geom.coordinates) return;
|
||||||
|
const type = geom.type;
|
||||||
|
if (type === 'Point' && Array.isArray(geom.coordinates)) {
|
||||||
|
if (!isNaN(geom.coordinates[0]) && !isNaN(geom.coordinates[1])) {
|
||||||
|
bounds.extend(geom.coordinates);
|
||||||
|
}
|
||||||
|
} else if (type === 'LineString' || type === 'MultiPoint') {
|
||||||
|
geom.coordinates.forEach(c => {
|
||||||
|
if (Array.isArray(c) && !isNaN(c[0]) && !isNaN(c[1])) bounds.extend(c);
|
||||||
|
});
|
||||||
|
} else if (type === 'Polygon' || type === 'MultiLineString') {
|
||||||
|
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') {
|
||||||
|
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(forceRefresh = false) {
|
||||||
|
if (!forceRefresh && locationGiepAssets.length > 0) return Promise.resolve(locationGiepAssets);
|
||||||
|
const locationId = resolveCurrentLocationId();
|
||||||
|
let url = "{% url 'assets:water_assets_geojson' %}?polygons_only=1";
|
||||||
|
if (locationId) {
|
||||||
|
url += "&location=" + encodeURIComponent(locationId);
|
||||||
|
} else {
|
||||||
|
url += "&unified=1";
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
let features = (data && data.features) ? data.features : [];
|
||||||
|
// If location filter returned 0 features and locationId was set, fallback to unified polygon water assets
|
||||||
|
if (features.length === 0 && locationId) {
|
||||||
|
return fetch("{% url 'assets:water_assets_geojson' %}?polygons_only=1&unified=1")
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(fallbackData => {
|
||||||
|
const fallbackFeatures = (fallbackData && fallbackData.features) ? fallbackData.features : [];
|
||||||
|
locationGiepAssets = fallbackFeatures.map(parseFeatureToAsset).filter(isPolygonAsset);
|
||||||
|
return locationGiepAssets;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
locationGiepAssets = [];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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,17 +559,19 @@
|
||||||
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
|
||||||
|
|
||||||
|
if (geom) {
|
||||||
const featureClone = {
|
const featureClone = {
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
|
|
@ -474,20 +588,22 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
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) {
|
||||||
|
bounds.extend([lon, lat]);
|
||||||
|
hasCoords = true;
|
||||||
|
}
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'giep-map-marker';
|
el.className = 'giep-map-marker';
|
||||||
el.style.width = '28px';
|
el.style.width = '30px';
|
||||||
el.style.height = '28px';
|
el.style.height = '30px';
|
||||||
el.style.borderRadius = '50%';
|
el.style.borderRadius = '50%';
|
||||||
el.style.backgroundColor = color;
|
el.style.backgroundColor = color;
|
||||||
el.style.border = '2.5px solid white';
|
el.style.border = '2.5px solid white';
|
||||||
|
|
@ -496,7 +612,7 @@
|
||||||
el.style.display = 'flex';
|
el.style.display = 'flex';
|
||||||
el.style.alignItems = 'center';
|
el.style.alignItems = 'center';
|
||||||
el.style.justifyContent = 'center';
|
el.style.justifyContent = 'center';
|
||||||
el.innerHTML = '<i class="bi bi-droplet-fill text-white" style="font-size: 14px;"></i>';
|
el.innerHTML = '<i class="bi bi-droplet-fill text-white" style="font-size: 15px;"></i>';
|
||||||
el.title = `${asset.code} - ${asset.structure_type || asset.name}`;
|
el.title = `${asset.code} - ${asset.structure_type || asset.name}`;
|
||||||
|
|
||||||
const marker = new maplibregl.Marker({ element: el })
|
const marker = new maplibregl.Marker({ element: el })
|
||||||
|
|
@ -511,7 +627,6 @@
|
||||||
modalMapMarkers.push(marker);
|
modalMapMarkers.push(marker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const geojsonPayload = {
|
const geojsonPayload = {
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue