Skip to content
Beyond Coordinates
Go back

Generating 3D Road Interchanges from OSM Data

Updated:

Introduction

Building interchanges in Cities: Skylines is both important and tedious. That prompted the idea of automatically generating 3D interchange geometry from existing 2D road-map data.

The Yan’an East Road Interchange illustrates the difference between a flat map and a 3D representation:

Importing road geometry like the second image into the game would be useful. The question is how to derive it from data like the first image.

Formulating the Problem

The planimetric road coordinates are known. Which road attributes can constrain their vertical coordinates?

Anyone who has edited complex OpenStreetMap roads may have encountered the attribute describing their stacking order.

OSM calls this tag layer; in the imported database it appears as z_order. Its most direct purpose is telling a renderer how overlapping roads should be drawn.

The vertical ordering, combined with several engineering assumptions, can determine an elevation or at least a feasible range for each road node.

The additional constraints are:

One more assumption is needed:

Treating every road-node elevation as an unknown yields a system of linear inequalities.

The constraints alone do not select one solution, so introduce an objective:

The task is now a standard linear program: minimize the objective subject to the constraints.

Data Preparation

Acquiring Data

OSM data is easy to download. I selected interchanges in Shanghai, Johannesburg, and Seattle as test cases, although the final experiments only covered Shanghai’s Yan’an East Road and Xinzhuang interchanges.

OSM data for the Yan'an East Road Interchange in Shanghai

Filtering and Processing

Each optimization covers one interchange and excludes unrelated roads, such as ordinary streets passing beneath it. The source data therefore needs to be filtered and extracted.

Filtering must account for differences between motorway and urban-expressway classifications.

Node Categories

The optimization solves elevations at road nodes. For convenience, nodes are divided into four categories:

Only the first three categories are optimization variables. Normal-point elevations can be interpolated afterward.

Implementation

Spatial Relationships

PostGIS performs all spatial relationship calculations. See the PostGIS 2.4 manual for details. Functions used include:

Constraints

The following notation may be imperfect; corrections are welcome.

{Height(Pi)=0,Type(roadi)=BridgeHeight(Pi)>=3,Type(roadi)<>Bridge\begin{cases} Height(P_i) = 0, & Type(road_i) = Bridge \\ Height(P_i) >= 3, & Type(road_i) <> Bridge \end{cases} Height(Pi)Height(Pi+1)LENGTH<=MAX_SLOPE\mid Height(P_i) - Height(P_{i+1})\mid * LENGTH <= MAX\_SLOPE Height(CPhigh)Height(CPlow)>=MIN_ELEVATIONHeight(CP_{high}) - Height(CP_{low}) >= MIN\_ELEVATION

Objective Function

To keep every road as low as possible, minimize the sum of all node elevations:

S=i=1m\Height(CPm)+i=1n\Height(TPn)+i=1p\Height(EPp)+i=1q\Height(NPq)S=\sum_{i=1}^m\Height(CP_m)+\sum_{i=1}^n\Height(TP_n)+\sum_{i=1}^p\Height(EP_p)+\sum_{i=1}^q\Height(NP_q)

Solving the Linear Program

The implementation uses Google’s OR-Tools library. See its Linear Optimization documentation.

Create a linear solver:

from ortools.linear_solver import linear_solver_pb2, pywraplp

# Create the linear solver with the GLOP backend.
solver = pywraplp.Solver('road_3D', pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)

Define variables and their bounds:

for _n in range(0, len(cross_points)):
    cp_list.append(solver.NumVar(0, solver.infinity(), 'cp_{id:02d}'.format(id=_n)))

for _n in range(0, len(touch_points)):
    tp_list.append(solver.NumVar(0, solver.infinity(), 'tp_{id:02d}'.format(id=_n)))

for _n in range(0, len(end_points)):
    ep_list.append(solver.NumVar(0, solver.infinity(), 'ep_{id:02d}'.format(id=_n)))

Express the constraints; for example, bound the grade between two points:

constraint = solver.Constraint(-distance * MAX_SLOPE, distance * MAX_SLOPE)
constraint.SetCoefficient(cp_list[m], 1)
constraint.SetCoefficient(cp_list[n], -1)

Define and minimize the objective:

objective = solver.Objective()
objective.SetMinimization()

for _n in range(0, len(cross_points)):
    objective.SetCoefficient(cp_list[_n], 1)

for _n in range(0, len(touch_points)):
    objective.SetCoefficient(tp_list[_n], 1)

for _n in range(0, len(end_points)):
    objective.SetCoefficient(ep_list[_n], 1)

Solve:

solver.Solve()

# Print one result.
print(cp_list[_n].solution_value())

Artificial Dips

The model has a flaw. Consider three consecutive points A, B, and C. Roads beneath A and C force both to 4 meters, while nothing passes beneath B. What elevation should B receive?

Common sense suggests 4 meters, but the objective pushes B as low as the grade constraint permits, producing an artificial dip in the road profile.

The current workaround is a post-processing pass that detects and corrects these dips after optimization.

Result

After solving, the data is merged, interpolated, and exported. The current workflow stores it in PostgreSQL and writes KML for inspection in Google Earth.

Known Problems

I’m too lazy to fix these issues for now.

Project: BranZhang/intersection-3d-rebuild


Share this post:

Previous Post
Visualizing Train Operations at Shanghai Hongqiao Railway Station
Next Post
Implementing Kriging Interpolation, Part 1: The Mathematics