291 lines
12 KiB
Python
291 lines
12 KiB
Python
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}"))
|