import json import ssl import urllib.request from django.contrib.gis.geos import Point from django.contrib.gis.gdal import CoordTransform, SpatialReference from django.core.management.base import BaseCommand from django.db import transaction from django.db.models import Q from django.utils import timezone from assets.models.trafficlights import ( TrafficLightIntersection, TrafficLightPole, TrafficLightController, TrafficLightControllerHardware, TrafficLightDetector, TrafficLightNetworkHardware, TrafficLightVoltageHardware, TrafficLightElectricalCabinet, TrafficLightElectricalCabinetContent, TrafficLightAccessory, ) def clean_code(code): c = code.upper().strip() if c.startswith('Y_'): c = c[2:] return c def get_keys_to_try(code): cleaned = clean_code(code) keys = [code, cleaned] if cleaned and cleaned[0] in ('S', 'R', 'C'): suffix = cleaned[1:] keys.extend([suffix, 'S' + suffix, 'R' + suffix, 'C' + suffix]) return list(dict.fromkeys(keys)) # remove duplicates keeping order def find_coords(code, coords_map): # Try exact / core variations for k in get_keys_to_try(code): if k in coords_map: return coords_map[k] # Try trailing '0' normalization # If code ends with '0' cleaned = clean_code(code) if cleaned.endswith('0'): stripped = cleaned[:-1] for k in get_keys_to_try(stripped): if k in coords_map: return coords_map[k] # If WFS key in coords_map ends with '0' for wfs_key, coords in coords_map.items(): if wfs_key.endswith('0'): stripped_wfs = wfs_key[:-1] # Check if our code core matches stripped WFS key for k in get_keys_to_try(code): if k == stripped_wfs: return coords return None class Command(BaseCommand): help = "Update TrafficLightIntersection and equipment objects where geometry is null using Brussels Mobility WFS GeoJSON data." def handle(self, *args, **options): # 1. Fetch WFS GeoJSON url = ( "https://data.mobility.brussels/geoserver/bm_equipment/wfs?" "service=wfs&version=1.1.0&request=GetFeature&" "typeName=bm_equipment:traffic_lights&outputFormat=json&srsName=EPSG:4326" ) self.stdout.write(f"Fetching GeoJSON from: {url}") try: req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) context = ssl._create_unverified_context() with urllib.request.urlopen(req, context=context) as response: data = json.loads(response.read().decode('utf-8')) except Exception as e: self.stdout.write(self.style.ERROR(f"Failed to fetch GeoJSON data: {e}")) return features = data.get('features', []) self.stdout.write(f"Successfully fetched {len(features)} features.") # 2. Map coordinates by intersection code (key) # Coordinates in GeoJSON are [lon, lat] (EPSG:4326) coords_map = {} for feature in features: props = feature.get('properties', {}) key = (props.get('key') or '').strip().upper() geom = feature.get('geometry') if key and geom and geom.get('type') == 'Point': coordinates = geom.get('coordinates') if len(coordinates) == 2: coords_map[key] = coordinates self.stdout.write(f"Mapped coordinates for {len(coords_map)} intersections.") # 3. Setup coordinate transformation to EPSG:3812 (Belgian Lambert 72) srs_3812 = SpatialReference(3812) srs_4326 = SpatialReference(4326) ct_to_3812 = CoordTransform(srs_4326, srs_3812) # 4. Find all TrafficLightIntersection where geom is null null_intersections = TrafficLightIntersection.objects.filter(geom__isnull=True) self.stdout.write(f"Found {null_intersections.count()} intersections with null geometry in the database.") updated_intersections_count = 0 now = timezone.now() # We perform updates in a transaction block with transaction.atomic(): for intersection in null_intersections: code = intersection.code.strip() coords = find_coords(code, coords_map) if coords: lon, lat = coords try: # Create EPSG:4326 point and transform to EPSG:3812 geom_wgs84 = Point(lon, lat, srid=4326) geom_3812 = geom_wgs84.clone() geom_3812.transform(ct_to_3812) geojson_str = json.dumps( {"type": "Point", "coordinates": [round(lon, 8), round(lat, 8)]} ) intersection.geom = geom_3812 intersection.lon = lon intersection.lat = lat intersection.geojson = geojson_str # updated_at will be set automatically by save() auto_now=True intersection.save() updated_intersections_count += 1 except Exception as e: self.stdout.write( self.style.WARNING(f"Error transforming coordinates for {code}: {e}") ) self.stdout.write( self.style.SUCCESS(f"Updated {updated_intersections_count} intersections from WFS data.") ) # 5. Update child equipment where geometry is null self.stdout.write("Updating equipment with null geometry based on their parent intersections...") # Get all intersections that now have a valid geometry valid_intersections = TrafficLightIntersection.objects.filter(geom__isnull=False) equipment_stats = { "poles": 0, "controllers": 0, "hardwares": 0, "detectors": 0, "network_hardwares": 0, "voltage_hardwares": 0, "cabinets": 0, "cabinet_contents": 0, "accessories": 0, } with transaction.atomic(): for intersection in valid_intersections: # Cache geometry fields geom = intersection.geom lon = intersection.lon lat = intersection.lat geojson = intersection.geojson # Update TrafficLightPole poles = TrafficLightPole.objects.filter(intersection=intersection, geom__isnull=True) if poles.exists(): p_count = poles.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) equipment_stats["poles"] += p_count # Update TrafficLightController controllers = TrafficLightController.objects.filter(intersection=intersection, geom__isnull=True) if controllers.exists(): c_count = controllers.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) equipment_stats["controllers"] += c_count # Update TrafficLightControllerHardware hardwares = TrafficLightControllerHardware.objects.filter(controller__intersection=intersection, geom__isnull=True) if hardwares.exists(): h_count = hardwares.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) equipment_stats["hardwares"] += h_count # Update TrafficLightDetector detectors = TrafficLightDetector.objects.filter(intersection=intersection, geom__isnull=True) if detectors.exists(): d_count = detectors.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) equipment_stats["detectors"] += d_count # Update TrafficLightNetworkHardware network_hardwares = TrafficLightNetworkHardware.objects.filter(intersection=intersection, geom__isnull=True) if network_hardwares.exists(): nh_count = network_hardwares.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) equipment_stats["network_hardwares"] += nh_count # Update TrafficLightVoltageHardware voltage_hardwares = TrafficLightVoltageHardware.objects.filter(intersection=intersection, geom__isnull=True) if voltage_hardwares.exists(): vh_count = voltage_hardwares.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) equipment_stats["voltage_hardwares"] += vh_count # Update TrafficLightElectricalCabinet cabinets = TrafficLightElectricalCabinet.objects.filter(intersection=intersection, geom__isnull=True) if cabinets.exists(): cab_count = cabinets.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) equipment_stats["cabinets"] += cab_count # Update TrafficLightElectricalCabinetContent cabinet_contents = TrafficLightElectricalCabinetContent.objects.filter(cabinet__intersection=intersection, geom__isnull=True) if cabinet_contents.exists(): cc_count = cabinet_contents.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) equipment_stats["cabinet_contents"] += cc_count # Update TrafficLightAccessory accessories = TrafficLightAccessory.objects.filter( Q(intersection=intersection) | Q(pole__intersection=intersection), geom__isnull=True ) if accessories.exists(): acc_count = accessories.update(geom=geom, lon=lon, lat=lat, geojson=geojson, updated_at=now) equipment_stats["accessories"] += acc_count self.stdout.write(self.style.SUCCESS("Equipment update complete. Summary of updated equipment:")) for eq_type, count in equipment_stats.items(): self.stdout.write(f" - {eq_type}: {count}")