import os from django.core.management.base import BaseCommand from django.conf import settings from django.contrib.gis.gdal import DataSource from django.contrib.gis.geos import GEOSGeometry from django.utils.dateparse import parse_date from django.db import transaction from assets.models import NatureRWIAsset # Dutch translations for type_ouvrage TYPE_OUVRAGE_NL_MAP = { # Points 'avaloir': 'afvoerput', 'bordure sequencee': 'rand met sequentie', 'bouche injection': 'injectieopening', 'clapet anti-retour': 'terugslagklep', 'empierrement sedimentation': 'verstening sedimentatie', 'kerb cell': 'kerbcel', 'puits infiltration': 'infiltratieput', 'surverse': 'overloop', 'vanne isolation': 'afsluitklep', 'vanne regulation': 'regelklep', # Lines 'gargouille de trottoir': 'stoepgootsteen', 'filet eau': 'druppel water', 'avaloir de rail': 'spoorafvoer', 'bordure arasee': 'afgewerkte rand', 'caniveau infiltrant': 'infiltratiegoot', 'drain': 'afvoer', 'trop plein': 'overvol', 'reseau egout separatif': 'gescheiden rioolnetwerk', # Polygons 'bassin sec/infiltration': 'droog bassin / infiltratiebekken', 'bassin en eau': 'waterbak', 'fosse': 'greppel', 'jardin de pluie': 'regentuin', 'noue': 'knoop', 'massif drainant': 'drainageblok', 'tranchee drainante': 'afwateringsgreppel', 'toiture stockante': 'stokkend dak', 'toiture verte': 'groendak', 'bassin orage': 'regenwaterreservoir', 'SAUL-vide': 'ultralichte holle structuur', 'SAUL-terre': 'ultralichte honingraatstructuur aarde', 'citerne': 'tankwagen', 'espace vert sur dalle': 'groene ruimte op betonnen plaat', 'bande filtrante': 'filterband', 'collecteur en beton poreux': 'poreuze betonnen collector', } def get_conn(feature, keys): for k in keys: if k in feature.fields: val = feature.get(k) if val is not None: return str(val).strip() return None def get_field_if_exists(feature, name): if name in feature.fields: return feature.get(name) return None def get_rwi_asset_acronym(geometry_type, structure_type, code_name): struct_type_lower = str(structure_type).strip().lower() if structure_type else None if code_name: return str(code_name).strip().upper() mappings = { 'avaloir': 'AV', 'bordure sequencee': 'BS', 'bouche injection': 'BI', 'clapet anti-retour': 'CA', 'empierrement sedimentation': 'ES', 'kerb cell': 'KC', 'puits infiltration': 'PI', 'surverse': 'SU', 'vanne isolation': 'VI', 'vanne regulation': 'VR', 'gargouille de trottoir': 'GT', 'filet eau': 'FE', 'avaloir de rail': 'AR', 'bordure arasee': 'BA', 'caniveau infiltrant': 'CI', 'drain': 'D', 'trop plein': 'TP', 'reseau egout separatif': 'RES', 'bassin sec/infiltration': 'OED', 'bassin en eau': 'OED', 'fosse': 'EVC', 'jardin de pluie': 'EVC', 'noue': 'EVC', 'massif drainant': 'OED', 'tranchee drainante': 'OED', 'toiture stockante': 'OSI', 'toiture verte': 'OSI', 'bassin orage': 'OED', 'SAUL-vide': 'OED', 'SAUL-terre': 'TA', 'citerne': 'OED', 'espace vert sur dalle': 'EVC', 'bande filtrante': 'EVC', 'collecteur en beton poreux': 'OED', 'chambre de visite': 'CV', } if struct_type_lower in mappings: return mappings[struct_type_lower] if geometry_type: return geometry_type.upper() return 'UNK' class Command(BaseCommand): help = "Imports RWI (Rain Water Integration) assets from the GeoPackage database." def add_arguments(self, parser): parser.add_argument( "gpkg_path", nargs="?", type=str, help="Path to the GeoPackage file. Defaults to external_files/giep/inventaire_giep.gpkg if not provided.", ) parser.add_argument( "--dry-run", action="store_true", default=False, help="Simulates the import process without writing changes to the database.", ) def handle(self, *args, **options): dry_run = options["dry_run"] gpkg_path = options.get("gpkg_path") if not gpkg_path: gpkg_path = os.path.abspath(os.path.join(settings.BASE_DIR, "../external_files/giep/inventaire_giep.gpkg")) if not os.path.exists(gpkg_path): self.stderr.write(self.style.ERROR(f"Error: GeoPackage file not found at: {gpkg_path}")) return self.stdout.write(f"→ Opening GeoPackage file at: {gpkg_path}") ds = DataSource(gpkg_path) if dry_run: self.stdout.write(self.style.WARNING("⚠ Dry-run mode active – no database changes will be saved.")) try: with transaction.atomic(): layer_mappings = [ ('point', 'point'), ('ligne', 'line'), ('polygone', 'polygon'), ] total_created = 0 total_updated = 0 for layer_name, geometry_type in layer_mappings: layer = ds[layer_name] self.stdout.write(f"→ Importing layer: {layer_name} ({len(layer)} features)...") created_count = 0 updated_count = 0 for feature in layer: # Defensive geometry access try: if not feature.geom: continue ogr_geom = feature.geom.clone() except Exception as ge: self.stdout.write(self.style.WARNING(f" [Warning] Skipping feature {feature.fid} due to geometry error: {ge}")) continue # Force geometry to 2D ogr_geom.set_3d(False) # EPSG 31370 (Lambert 72) -> EPSG 3812 (Lambert 2008) geom = GEOSGeometry(ogr_geom.wkb, srid=31370) if geom.empty: continue geom.transform(3812) # Coordinate transformation to EPSG 4326 (WGS 84) to extract lon/lat geom_wgs84 = geom.clone() geom_wgs84.transform(4326) if geom_wgs84.empty: continue centroid = geom_wgs84.centroid lon = centroid.x lat = centroid.y type_ouvrage = get_field_if_exists(feature, 'type_ouvra') code_name = get_field_if_exists(feature, 'nom_code') # Format standard RWI codes with acronym acronym = get_rwi_asset_acronym(geometry_type, type_ouvrage, code_name) code = f"RWI_{acronym}_{feature.fid:03d}" name_fr = str(type_ouvrage).capitalize() if type_ouvrage else code name_nl = TYPE_OUVRAGE_NL_MAP.get(type_ouvrage, "").capitalize() or name_fr # Connection strings sewer_conn = get_conn(feature, ['connexion_egout', 'connexion egout', 'Conexion egout']) separate_conn = get_conn(feature, ['connexion_r.separatif', 'connexion r.separatif', 'connexion_r_separatif']) hydro_conn = get_conn(feature, ['connexion_r.hydro', 'connexion r.hydro', 'connexion_r_hydro']) # Filter mapping has_filter = None if 'filtre' in feature.fields: filtre_val = feature.get('filtre') if filtre_val is not None: has_filter = True if str(filtre_val).lower().strip() in ('oui', '1', 'true') else False # Commissioning dates parsing comm_date_str = None for k in ['date_mise_en_service', 'mise_serv', 'mise_servi']: if k in feature.fields: val = feature.get(k) if val: comm_date_str = str(val).strip() break commissioning_date = None if comm_date_str: commissioning_date = parse_date(comm_date_str) # Dimensional metrics surface_area = get_field_if_exists(feature, 'surf_m2') length = get_field_if_exists(feature, 'length_m') # Fetch existing or instantiate a new asset using external_id and geometry_type asset = NatureRWIAsset.objects.filter(external_id=feature.fid, geometry_type=geometry_type).first() is_new = asset is None if is_new: asset = NatureRWIAsset(code=code) else: # Update code to the new format if it changed asset.code = code # Populate attributes asset.name_fr = name_fr asset.name_nl = name_nl asset.geom = geom asset.lon = lon asset.lat = lat asset.geojson = geom.json asset.geometry_type = geometry_type asset.external_id = feature.fid asset.code_name = get_field_if_exists(feature, 'nom_code') asset.structure_type = type_ouvrage asset.project_manager = get_field_if_exists(feature, 'chef_proj') asset.construction_status = str(get_field_if_exists(feature, 'travaux')).strip() if get_field_if_exists(feature, 'travaux') else None asset.commissioning_date = commissioning_date asset.condition = get_field_if_exists(feature, 'etat') asset.administrator = get_field_if_exists(feature, 'admin') asset.monitoring = get_field_if_exists(feature, 'suivi') asset.comment = get_field_if_exists(feature, 'comment') or get_field_if_exists(feature, 'commentaire') asset.has_filter = has_filter asset.sewer_connection = sewer_conn asset.separate_network_connection = separate_conn asset.hydro_network_connection = hydro_conn asset.surface_area = surface_area asset.length = length asset.coating_category = get_field_if_exists(feature, 'cat_rev') asset.coating_type = get_field_if_exists(feature, 'type_revetement') asset.status = 'active' asset.save() if is_new: created_count += 1 else: updated_count += 1 self.stdout.write(self.style.SUCCESS(f"✓ Layer '{layer_name}': {created_count} created, {updated_count} updated.")) total_created += created_count total_updated += updated_count if dry_run: # Roll back changes so database remains unmodified transaction.set_rollback(True) self.stdout.write(self.style.WARNING("✓ Dry-run completed. All changes rolled back successfully.")) else: self.stdout.write(self.style.SUCCESS( f"✓ Import completed! Total: {total_created} assets created, {total_updated} assets updated in the database." )) except Exception as e: self.stderr.write(self.style.ERROR(f"Error during import: {e}"))