import json import re 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.its import ITSGeoAsset def clean_code(code): if not code: return '' c = code.upper().strip() c = c.replace('RAD_PREV_', '').replace('RAD_', '').replace('CAM_', '').replace('CNT_', '').replace('PMV_', '') c = c.replace('GATSO', '').replace('GAT', '').strip() c = re.sub(r'\s+', '', c) c = re.sub(r'_[0-9]+$', '', c) c = re.sub(r'-[0-9]+$', '', c) if c.startswith('Y_'): c = c[2:] if c and c[0] in ('S', 'R', 'C'): c = c[1:] 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 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 speed cameras (ITS radars) objects where geometry is null using Brussels Mobility WFS GeoJSON speedcameras and traffic lights data." def handle(self, *args, **options): # 1. Fetch speedcameras WFS url_speed = ( "https://data.mobility.brussels/geoserver/bm_security/wfs?" "service=wfs&version=1.1.0&request=GetFeature&" "typeName=bm_security:speedcameras&outputFormat=json&srsName=EPSG:4326" ) # 2. Fetch traffic lights WFS url_tl = ( "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" ) context = ssl._create_unverified_context() # Fetch speed cameras self.stdout.write(f"Fetching speed cameras from WFS: {url_speed}") try: req_speed = urllib.request.Request(url_speed, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req_speed, context=context) as response: data_speed = json.loads(response.read().decode('utf-8')) features_speed = data_speed.get('features', []) self.stdout.write(f"Successfully fetched {len(features_speed)} speed cameras.") except Exception as e: self.stdout.write(self.style.ERROR(f"Failed to fetch speed cameras data: {e}")) return # Fetch traffic lights self.stdout.write(f"Fetching traffic lights from WFS: {url_tl}") try: req_tl = urllib.request.Request(url_tl, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req_tl, context=context) as response: data_tl = json.loads(response.read().decode('utf-8')) features_tl = data_tl.get('features', []) self.stdout.write(f"Successfully fetched {len(features_tl)} traffic lights.") except Exception as e: self.stdout.write(self.style.ERROR(f"Failed to fetch traffic lights data: {e}")) return # 3. Map coordinates coords_map = {} # First map from speed cameras for feature in features_speed: props = feature.get('properties', {}) radar_id = props.get('radar_id') or '' fr_key = props.get('fr_key') or '' geom = feature.get('geometry') if geom and geom.get('type') == 'Point': coordinates = geom.get('coordinates') if len(coordinates) == 2: for val in [radar_id, fr_key]: cleaned = clean_code(val) if cleaned: coords_map[cleaned] = coordinates # Second, map from traffic lights if not already mapped for feature in features_tl: props = feature.get('properties', {}) key = props.get('key') or '' geom = feature.get('geometry') if geom and geom.get('type') == 'Point': coordinates = geom.get('coordinates') if len(coordinates) == 2: cleaned = clean_code(key) if cleaned and cleaned not in coords_map: coords_map[cleaned] = coordinates self.stdout.write(f"Mapped coordinates for {len(coords_map)} unique identifiers.") # 4. 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) # 5. Find all ITSGeoAsset (Radars) where geom is null null_radars = ITSGeoAsset.objects.filter( Q(category__name_fr='Radars') | Q(code__startswith='RAD_'), geom__isnull=True ) self.stdout.write(f"Found {null_radars.count()} radars with null geometry in the database.") updated_radars_count = 0 # We perform updates in a transaction block with transaction.atomic(): for radar in null_radars: code = radar.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)]} ) radar.geom = geom_3812 radar.lon = lon radar.lat = lat radar.geojson = geojson_str radar.save() updated_radars_count += 1 except Exception as e: self.stdout.write( self.style.WARNING(f"Error transforming coordinates for radar {code}: {e}") ) self.stdout.write( self.style.SUCCESS(f"Updated {updated_radars_count} radars from WFS speedcameras/traffic-lights data.") )