loko/streetup/assets/management/commands/update_null_traffic_lights_geom.py
2026-07-22 14:48:40 +02:00

233 lines
10 KiB
Python

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}")