Additional handling/improvements

This commit is contained in:
Will Bradley 2025-06-29 11:55:45 -07:00
parent 7332ef9748
commit 47516de531
2 changed files with 278 additions and 3 deletions

259
qgis-functions.py Normal file
View File

@ -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()

View File

@ -8,6 +8,10 @@ Compares two GeoJSON files containing road data and identifies:
Only reports differences that are significant (above minimum length threshold). Only reports differences that are significant (above minimum length threshold).
Optimized for performance with parallel processing and spatial indexing. 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 import json
@ -26,9 +30,19 @@ import numpy as np
from concurrent.futures import ProcessPoolExecutor, as_completed from concurrent.futures import ProcessPoolExecutor, as_completed
import gc import gc
import importlib
qgisfunctions = importlib.import_module("qgis-functions")
# Suppress warnings for cleaner output # Suppress warnings for cleaner output
warnings.filterwarnings('ignore') 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: class RoadComparator:
def __init__(self, tolerance_feet: float = 50.0, min_gap_length_feet: float = 100.0, def __init__(self, tolerance_feet: float = 50.0, min_gap_length_feet: float = 100.0,
n_jobs: int = None, chunk_size: int = 1000): n_jobs: int = None, chunk_size: int = 1000):
@ -307,11 +321,13 @@ class RoadComparator:
for key, value in original_properties.items(): for key, value in original_properties.items():
if key == 'NAME': 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': elif key == 'SpeedLimit':
properties['maxspeed'] = f"{value} mph" if value is not None else None properties['maxspeed'] = f"{value} mph" if value is not None else None
elif key == 'RoadClass': elif key == 'RoadClass':
if value.startswith('PRIMARY'): if value is None:
properties['highway'] = 'residential'
elif value.startswith('PRIMARY'):
properties['highway'] = 'trunk' properties['highway'] = 'trunk'
elif value.startswith('MAJOR'): elif value.startswith('MAJOR'):
properties['highway'] = 'primary' properties['highway'] = 'primary'
@ -328,6 +344,7 @@ class RoadComparator:
}) })
except Exception as e: except Exception as e:
print(e)
continue # Skip problematic geometries continue # Skip problematic geometries
return added_roads return added_roads
@ -420,7 +437,6 @@ class RoadComparator:
if field in segment and pd.notna(segment[field]): if field in segment and pd.notna(segment[field]):
road_name = str(segment[field]) road_name = str(segment[field])
break break
if road_name not in removed_by_road: if road_name not in removed_by_road:
removed_by_road[road_name] = [] removed_by_road[road_name] = []
removed_by_road[road_name].append(length_feet) removed_by_road[road_name].append(length_feet)