From 47516de53111f49e8c2d51ec66da788f0e3ca7e6 Mon Sep 17 00:00:00 2001 From: Will Bradley Date: Sun, 29 Jun 2025 11:55:45 -0700 Subject: [PATCH] Additional handling/improvements --- qgis-functions.py | 259 ++++++++++++++++++++++++++++++++++++++++++++++ threaded.py | 22 +++- 2 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 qgis-functions.py diff --git a/qgis-functions.py b/qgis-functions.py new file mode 100644 index 0000000..898ef51 --- /dev/null +++ b/qgis-functions.py @@ -0,0 +1,259 @@ +#import qgis.core +#import qgis.gui +import re + +# +# This will keep street names like SR 574A as SR 574A however +# will lowercase other number-digit suffixes with <2 or >4 numbers +# or >1 suffix-letters, like 12th Street or 243rd Ave. +# + +# @qgsfunction(args='auto', group='Custom', referenced_columns=[]) +def getstreetfromaddress(value1, feature, parent): + parts = value1.split() + parts.pop(0) # Ignore the first bit (i.e. "123" in "123 N MAIN ST") + parts = map(formatstreetname, parts) + return " ".join(parts) + +# @qgsfunction(args='auto', group='Custom', referenced_columns=[]) +def formatstreet(value1, feature, parent): + parts = value1.split() + # Handle the special case of a street name starting with "ST" + # which is almost always "Saint __" and not "Street __" + if parts[0].upper() == "ST": + parts[0] = "Saint" + if parts[0].upper() == "ROYAL" and parts[1].upper() == "ST": + parts[0] = "Royal" + parts[1] = "Saint" + # And "CR" as a first part (County Road) vs last part (Circle) + if parts[0].upper() == "CR": + parts[0] = "County Road" + if parts[0].upper() == "SR": + parts[0] = "State Route" + parts = map(formatstreetname, parts) + return " ".join(parts) + + +# @qgsfunction(args='auto', group='Custom', referenced_columns=[]) +def formatname(value1, feature, parent): + parts = value1.split() + parts = map(formatstreetname, parts) + return " ".join(parts) + +# @qgsfunction(args='auto', group='Custom', referenced_columns=[]) +def gethighwaytype(value1, feature, parent): + match value1: + case "ALLEY": + return "alley" + case "LOCAL": + return "residential" + case "MAJOR": + return "trunk" + case "MEDIAN CUT": + return "primary_link" + case "OTHER": + return "unclassified" + case "PRIMARY": + return "primary" + case "PRIVATE": + return "service" + case "RAMP": + return "trunk_link" + case "SECONDARY": + return "secondary" + case "TURN LANE": + return "primary_link" + case "VEHICULAR TRAIL": + return "track" + +# Internal function +def formatstreetname(name): + nameUp = name.upper() + # Acronyms + if nameUp == "SR": + return "SR" # State Route + if nameUp == "NFS": + return "NFS" # National Forest Service? + if nameUp == "US": + return "US" + # Directions + if nameUp == "N": + return "North" + if nameUp == "NE": + return "Northeast" + if nameUp == "E": + return "East" + if nameUp == "SE": + return "Southeast" + if nameUp == "S": + return "South" + if nameUp == "SW": + return "Southwest" + if nameUp == "W": + return "West" + if nameUp == "NW": + return "Northwest" + # Names + if nameUp == "MACLEAY": + return "MacLeay" + if nameUp == "MCCLAINE": + return "McClaine" + if nameUp == "MCAHREN": + return "McAhren" + if nameUp == "MCCAMMON": + return "McCammon" + if nameUp == "MCCLELLAN": + return "McClellan" + if nameUp == "MCCOY": + return "McCoy" + if nameUp == "MCDONALD": + return "McDonald" + if nameUp == "MCGEE": + return "McGee" + if nameUp == "MCGILCHRIST": + return "McGilchrist" + if nameUp == "MCINTOSH": + return "McIntosh" + if nameUp == "MCKAY": + return "McKay" + if nameUp == "MCKEE": + return "McKee" + if nameUp == "MCKENZIE": + return "McKenzie" + if nameUp == "MCKILLOP": + return "McKillop" + if nameUp == "MCKINLEY": + return "McKinley" + if nameUp == "MCKNIGHT": + return "McKnight" + if nameUp == "MCLAUGHLIN": + return "McLaughlin" + if nameUp == "MCLEOD": + return "McLeod" + if nameUp == "MCMASTER": + return "McMaster" + if nameUp == "MCNARY": + return "McNary" + if nameUp == "MCNAUGHT": + return "McNaught" + if nameUp == "O'BRIEN": + return "O'Brien" + if nameUp == "O'CONNOR": + return "O'Connor" + if nameUp == "O'NEIL": + return "O'Neil" + if nameUp == "O'TOOLE": + return "O'Toole" + # Suffixes + if nameUp == "ALY": + return "Alley" + if nameUp == "AV": + return "Avenue" + if nameUp == "AVE": + return "Avenue" + if nameUp == "BAY": + return "Bay" + if nameUp == "BLF": + return "Bluff" + if nameUp == "BLVD": + return "Boulevard" + if nameUp == "BV": + return "Boulevard" + if nameUp == "BND": + return "Bend" + if nameUp == "CIR": + return "Circle" + if nameUp == "CR": + return "Circle" + if nameUp == "CRK": + return "Creek" + if nameUp == "CRST": + return "Crest" + if nameUp == "CT": + return "Court" + if nameUp == "CURV": + return "Curve" + if nameUp == "CV": + return "Curve" + if nameUp == "DR": + return "Drive" + if nameUp == "FLDS": + return "Fields" + if nameUp == "GLN": + return "Glenn" + if nameUp == "GRV": + return "Grove" + if nameUp == "HL": + return "Hill" + if nameUp == "HOLW": + return "Hollow" + if nameUp == "HTS": + return "Heights" + if nameUp == "HW": + return "Highway" + if nameUp == "HWY": + return "Highway" + if nameUp == "HY": + return "Highway" + if nameUp == "LN": + return "Lane" + if nameUp == "LNDG": + return "Landing" + if nameUp == "LOOP": + return "Loop" + if nameUp == "LP": + return "Loop" + if nameUp == "MNR": + return "Manor" + if nameUp == "MT": + return "Mount" + if nameUp == "MTN": + return "Mountain" + if nameUp == "PARK": + return "Park" + if nameUp == "PASS": + return "Pass" + if nameUp == "PATH": + return "Path" + if nameUp == "PKWY": + return "Parkway" + if nameUp == "PL": + return "Place" + if nameUp == "PLZ": + return "Plaza" + if nameUp == "PS": + return "Pass" + if nameUp == "PT": + return "Point" + if nameUp == "RD": + return "Road" + if nameUp == "RDG": + return "Ridge" + if nameUp == "RUN": + return "Run" + if nameUp == "SHRS": + return "Shores" + if nameUp == "SQ": + return "Square" + if nameUp == "ST": + return "Street" + if nameUp == "TER": + return "Terrace" + if nameUp == "TR": + return "Trail" + if nameUp == "TRL": + return "Trail" + if nameUp == "VW": + return "View" + if nameUp == "WALK": + return "Walk" + if nameUp == "WAY": + return "Way" + if nameUp == "WY": + return "Way" + if nameUp == "XING": + return "Crossing" + if re.match('^[0-9]{2,4}[A-Za-z]$', name) != None: + return name + + return name #.capitalize() \ No newline at end of file diff --git a/threaded.py b/threaded.py index bb05b24..ff70cb4 100644 --- a/threaded.py +++ b/threaded.py @@ -8,6 +8,10 @@ Compares two GeoJSON files containing road data and identifies: Only reports differences that are significant (above minimum length threshold). Optimized for performance with parallel processing and spatial indexing. + +TODO: +- put properties properly on removed roads, so they're visible in JOSM +- handle polygons properly (on previous geojson step?) for circular roads """ import json @@ -26,9 +30,19 @@ import numpy as np from concurrent.futures import ProcessPoolExecutor, as_completed import gc +import importlib +qgisfunctions = importlib.import_module("qgis-functions") + # Suppress warnings for cleaner output warnings.filterwarnings('ignore') +import re +def titlecase(s): + return re.sub( + r"[A-Za-z]+('[A-Za-z]+)?", + lambda word: word.group(0).capitalize(), + s) + class RoadComparator: def __init__(self, tolerance_feet: float = 50.0, min_gap_length_feet: float = 100.0, n_jobs: int = None, chunk_size: int = 1000): @@ -307,11 +321,13 @@ class RoadComparator: for key, value in original_properties.items(): if key == 'NAME': - properties['name'] = str(value).title() if value is not None else None + properties['name'] = titlecase(qgisfunctions.formatstreet(value,None,None)) if value is not None else None elif key == 'SpeedLimit': properties['maxspeed'] = f"{value} mph" if value is not None else None elif key == 'RoadClass': - if value.startswith('PRIMARY'): + if value is None: + properties['highway'] = 'residential' + elif value.startswith('PRIMARY'): properties['highway'] = 'trunk' elif value.startswith('MAJOR'): properties['highway'] = 'primary' @@ -328,6 +344,7 @@ class RoadComparator: }) except Exception as e: + print(e) continue # Skip problematic geometries return added_roads @@ -420,7 +437,6 @@ class RoadComparator: if field in segment and pd.notna(segment[field]): road_name = str(segment[field]) break - if road_name not in removed_by_road: removed_by_road[road_name] = [] removed_by_road[road_name].append(length_feet)