184 lines
7.3 KiB
Python
184 lines
7.3 KiB
Python
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.")
|
|
)
|