Skip to content

Commit f1749ea

Browse files
Merge pull request #413 from rustprooflabs/fix-routing-performance
Adjust pgRouting 4 procedures to improve performance of created noded edges
2 parents 80c5da5 + b14d26f commit f1749ea

File tree

4 files changed

+364
-270
lines changed

4 files changed

+364
-270
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2020-2024 Ryan Lambert
3+
Copyright (c) 2020-2025 Ryan Lambert
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

db/deploy/routing_functions.sql

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
2+
CREATE OR REPLACE PROCEDURE {schema_name}.routing_prepare_roads_for_routing()
3+
LANGUAGE plpgsql
4+
AS $$
5+
6+
BEGIN
7+
8+
DROP TABLE IF EXISTS edges_table;
9+
CREATE TEMP TABLE edges_table AS
10+
WITH a AS (
11+
-- Remove as many multi-linestrings as possible with ST_LineMerge()
12+
SELECT r.osm_id, r.osm_type, r.maxspeed, r.oneway, r.layer,
13+
r.route_foot, r.route_cycle, r.route_motor, r.access,
14+
ST_LineMerge(r.geom) AS geom
15+
FROM osm.road_line r
16+
), extra_cleanup AS (
17+
-- Pull out those that are still multi, use ST_Dump() to pull out parts
18+
SELECT osm_id, osm_type, maxspeed, oneway, layer,
19+
route_foot, route_cycle, route_motor, access,
20+
(ST_Dump(geom)).geom AS geom
21+
FROM a
22+
WHERE ST_GeometryType(geom) = 'ST_MultiLineString'
23+
), combined AS (
24+
-- Combine two sources
25+
SELECT osm_id, osm_type, maxspeed, oneway, layer,
26+
route_foot, route_cycle, route_motor, access,
27+
geom
28+
FROM a
29+
WHERE ST_GeometryType(geom) != 'ST_MultiLineString'
30+
UNION
31+
SELECT osm_id, osm_type, maxspeed, oneway, layer,
32+
route_foot, route_cycle, route_motor, access,
33+
geom
34+
FROM extra_cleanup
35+
-- Some data may be lost here if multi-linestring somehow
36+
-- persists through the extra_cleanup query
37+
WHERE ST_GeometryType(geom) != 'ST_MultiLineString'
38+
)
39+
-- Calculate a new surrogate ID for key
40+
SELECT ROW_NUMBER() OVER (ORDER BY geom) AS id
41+
, *
42+
-- Compute start/end points here instead of making this part of an expensive JOIN
43+
-- in the intersection code later.
44+
, ST_StartPoint(geom) AS geom_start
45+
, ST_EndPoint(geom) AS geom_end
46+
FROM combined
47+
ORDER BY geom
48+
;
49+
50+
CREATE INDEX gix_routing_osm_road_intermediate_geom
51+
ON edges_table
52+
USING GIST (geom)
53+
;
54+
CREATE INDEX gix_routing_osm_road_intermediate_geom_start
55+
ON edges_table
56+
USING GIST (geom_start)
57+
;
58+
CREATE INDEX gix_routing_osm_road_intermediate_geom_end
59+
ON edges_table
60+
USING GIST (geom_end)
61+
;
62+
CREATE UNIQUE INDEX gix_tmp_edges_id
63+
ON edges_table (id)
64+
;
65+
ANALYZE edges_table;
66+
67+
RAISE NOTICE 'Edge table table created';
68+
69+
70+
71+
------------------------------------------------------------------
72+
-- Split long lines where there are route-able intersections
73+
-- Based on `pgr_separateTouching()` from pgRouting 4.0
74+
DROP TABLE IF EXISTS initial_intersection;
75+
CREATE TEMP TABLE initial_intersection AS
76+
SELECT e1.id AS id1, e2.id AS id2
77+
, e1.osm_id AS osm_id1, e2.osm_id AS osm_id2
78+
, e1.geom AS geom1
79+
, e2.geom AS geom2
80+
, e1.geom_start AS geom_start1
81+
, e1.geom_end AS geom_end1
82+
, e2.geom_start AS geom_start2
83+
, e2.geom_end AS geom_end2
84+
-- The intersection point is the blade
85+
, ST_Intersection(e1.geom, e2.geom) AS blade
86+
FROM edges_table e1
87+
, edges_table e2
88+
WHERE
89+
-- Find all combinations of mismatches.
90+
e1.id != e2.id
91+
-- This tolerance should be same (??? should it???) as snap tolerance above
92+
AND ST_DWithin(e1.geom, e2.geom, 0.1)
93+
-- They don't share start/end points. If they do, this step doesn't matter.
94+
AND NOT (
95+
e1.geom_start = e2.geom_start OR e1.geom_start = e2.geom_end
96+
OR e1.geom_end = e2.geom_start OR e1.geom_end = e2.geom_end
97+
)
98+
;
99+
100+
CREATE INDEX gix_initial_intersection_geom1 ON initial_intersection USING GIST (geom1);
101+
CREATE INDEX gix_initial_intersection_geom2 ON initial_intersection USING GIST (geom2);
102+
CREATE INDEX gix_initial_intersection_blade ON initial_intersection USING GIST (blade);
103+
104+
RAISE NOTICE 'Intersections table created';
105+
106+
107+
-- Create the combination of lines to be split with all points to use for blades.
108+
-- Looking at both directions for id1/id2 pairs.
109+
DROP TABLE IF EXISTS geom_with_blade;
110+
CREATE TABLE geom_with_blade AS
111+
SELECT id1 AS id, osm_id1 AS osm_id, geom1 AS geom
112+
, ST_UnaryUnion(ST_Collect(ST_PointOnSurface(blade))) AS blades
113+
FROM initial_intersection
114+
WHERE blade NOT IN (geom_start1, geom_end1)
115+
-- AND 17077818 IN (osm_id1)
116+
GROUP BY id1, osm_id1, geom1
117+
UNION
118+
SELECT id2 AS id, osm_id2 AS osm_id, geom2 AS geom
119+
, ST_UnaryUnion(ST_Collect(ST_PointOnSurface(blade))) AS blades
120+
FROM initial_intersection
121+
WHERE blade NOT IN (geom_start2, geom_end2)
122+
-- AND 17077818 IN (osm_id2)
123+
GROUP BY id2, osm_id2, geom2
124+
;
125+
126+
-- Split lines using blades. Assign new `seq` ID (legacy reasons, try to improve this...)
127+
DROP TABLE IF EXISTS split_edges;
128+
CREATE TEMP TABLE split_edges AS
129+
WITH splits AS (
130+
SELECT i.id, i.osm_id
131+
, split.path[1]::BIGINT AS sub_id
132+
, split.geom
133+
FROM geom_with_blade i
134+
CROSS JOIN LATERAL st_dump(st_split(st_snap(i.geom, blades, 0.1), blades)) split
135+
WHERE NOT ST_Relate(st_snap(i.geom, blades, 0.1), blades, '1********')
136+
AND split.geom <> i.geom -- Exclude any unchanged records
137+
AND NOT ST_IsEmpty(blades) -- Exclude any blades that ended up empty
138+
-- AND osm_id1 IN (1171245820, 758283788)
139+
-- AND osm_id2 IN (1171245820, 758283788)
140+
)
141+
SELECT row_number() over()::BIGINT AS seq
142+
, *
143+
FROM splits
144+
;
145+
146+
147+
-------------------------------------------------------
148+
-- Combine the Split edges with the un-split edges
149+
-- This is the production "edge" table for routing.
150+
-------------------------------------------------------
151+
DROP TABLE IF EXISTS {schema_name}.routing_road_edge;
152+
CREATE TABLE {schema_name}.routing_road_edge AS
153+
WITH split_lines AS (
154+
SELECT r.id AS parent_id, spl.sub_id, r.osm_id, r.osm_type, r.maxspeed, r.oneway, r.layer
155+
, route_foot, route_cycle, route_motor
156+
, r.access, spl.geom
157+
FROM edges_table r
158+
INNER JOIN split_edges spl
159+
ON r.id = spl.id
160+
), unsplit_lines AS (
161+
SELECT r.id AS parent_id, 1::INT AS sub_id, r.osm_id, r.osm_type, r.maxspeed, r.oneway, r.layer
162+
, route_foot, route_cycle, route_motor
163+
, r.access, r.geom
164+
FROM edges_table r
165+
LEFT JOIN split_edges spl
166+
ON r.id = spl.id
167+
WHERE spl.id IS NULL
168+
)
169+
SELECT *
170+
FROM split_lines
171+
UNION
172+
SELECT *
173+
FROM unsplit_lines
174+
;
175+
176+
COMMENT ON TABLE {schema_name}.routing_road_edge IS 'OSM road data setup for edges for routing for motorized travel';
177+
ALTER TABLE {schema_name}.routing_road_edge
178+
ADD edge_id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY;
179+
ALTER TABLE {schema_name}.routing_road_edge
180+
ADD source BIGINT;
181+
ALTER TABLE {schema_name}.routing_road_edge
182+
ADD target BIGINT;
183+
184+
/*
185+
ALTER TABLE {schema_name}.routing_road_edge
186+
ADD CONSTRAINT uq_routing_road_edges_parent_id_sub_id
187+
UNIQUE (parent_id, sub_id)
188+
;
189+
*/
190+
RAISE NOTICE 'routing_osm_road_edge table created';
191+
RAISE WARNING 'Not adding a unique constraint that should exist... data cleanup needed.';
192+
193+
194+
DROP TABLE IF EXISTS {schema_name}.routing_road_vertex;
195+
CREATE TABLE {schema_name}.routing_road_vertex AS
196+
SELECT * FROM pgr_extractVertices(
197+
'SELECT edge_id AS id, geom FROM {schema_name}.routing_road_edge')
198+
;
199+
RAISE NOTICE 'routing_osm_road_vertex table created';
200+
201+
-- Update source column from out_edges
202+
WITH outgoing AS (
203+
SELECT id AS source
204+
, unnest(out_edges) AS edge_id
205+
FROM {schema_name}.routing_road_vertex
206+
)
207+
UPDATE {schema_name}.routing_road_edge e
208+
SET source = o.source
209+
FROM outgoing o
210+
WHERE e.edge_id = o.edge_id
211+
AND e.source IS NULL
212+
;
213+
214+
-- Update target column from in_edges
215+
WITH incoming AS (
216+
SELECT id AS target
217+
, unnest(in_edges) AS edge_id
218+
FROM {schema_name}.routing_road_vertex
219+
)
220+
UPDATE {schema_name}.routing_road_edge e
221+
SET target = i.target
222+
FROM incoming i
223+
WHERE e.edge_id = i.edge_id
224+
AND e.target IS NULL
225+
;
226+
227+
228+
ANALYZE {schema_name}.routing_road_vertex;
229+
ANALYZE {schema_name}.routing_road_edge;
230+
231+
END $$;
232+

docker/db.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,11 +404,13 @@ def prepare_osm_schema(db_path: str, skip_qgis_style: bool, schema_name: str):
404404
create_osm_pgosm_flex_file = 'osm_pgosm_flex.sql'
405405
create_pgosm_road_file = 'pgosm_road.sql'
406406
create_replication_functions = 'replication_functions.sql'
407+
create_routing_functions = 'routing_functions.sql'
407408

408409
run_deploy_file(db_path=db_path, sql_filename=create_osm_file, schema_name=schema_name)
409410
run_deploy_file(db_path=db_path, sql_filename=create_osm_pgosm_flex_file, schema_name=schema_name)
410411
run_deploy_file(db_path=db_path, sql_filename=create_pgosm_road_file, schema_name=schema_name)
411412
run_deploy_file(db_path=db_path, sql_filename=create_replication_functions, schema_name=schema_name)
413+
run_deploy_file(db_path=db_path, sql_filename=create_routing_functions, schema_name=schema_name)
412414

413415
if skip_qgis_style:
414416
LOGGER.info('Skipping QGIS styles')

0 commit comments

Comments
 (0)