Skip to content

Commit 80c5da5

Browse files
Merge pull request #411 from rustprooflabs/fix-routing-docs-for-pgrouting4
Set stage to add docs for pgRouting 4
2 parents 7af15f1 + ce6ae45 commit 80c5da5

File tree

4 files changed

+857
-331
lines changed

4 files changed

+857
-331
lines changed

docs/src/SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
- [Data Files](./data-files.md)
1717
- [Query examples](./query.md)
1818
- [Routing](./routing.md)
19+
- [pgRouting 3](./routing-3.md)
20+
- [pgRouting 4](./routing-4.md)
1921
- [Processing Time](./performance.md)
2022

2123
# Production usages

docs/src/routing-3.md

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
# Routing with PgRouting 3
2+
3+
> If you are using a pgRouting 4.0 or later see [Routing with pgRouting 4](./routing-4.md).
4+
5+
6+
## Clean the data
7+
8+
The following query does the initial cleanup for preparing OpenStreetMap roads
9+
for routing. The following code is converting multi-linestrings to standard
10+
linestrings for subsequent processing steps.
11+
12+
```sql
13+
CREATE TABLE routing.road_line AS
14+
WITH a AS (
15+
-- Remove as many multi-linestrings as possible with ST_LineMerge()
16+
SELECT osm_id, osm_type, maxspeed, oneway, layer,
17+
route_foot, route_cycle, route_motor, access,
18+
ST_LineMerge(geom) AS geom
19+
FROM osm.road_line
20+
), extra_cleanup AS (
21+
-- Pull out those that are still multi, use ST_Dump() to pull out parts
22+
SELECT osm_id, osm_type, maxspeed, oneway, layer,
23+
route_foot, route_cycle, route_motor, access,
24+
(ST_Dump(geom)).geom AS geom
25+
FROM a
26+
WHERE ST_GeometryType(geom) = 'ST_MultiLineString'
27+
), combined AS (
28+
-- Combine two sources
29+
SELECT osm_id, osm_type, maxspeed, oneway, layer,
30+
route_foot, route_cycle, route_motor, access,
31+
geom
32+
FROM a
33+
WHERE ST_GeometryType(geom) != 'ST_MultiLineString'
34+
UNION
35+
SELECT osm_id, osm_type, maxspeed, oneway, layer,
36+
route_foot, route_cycle, route_motor, access,
37+
geom
38+
FROM extra_cleanup
39+
-- Some data may be lost here if multi-linestring somehow
40+
-- persists through the extra_cleanup query
41+
WHERE ST_GeometryType(geom) != 'ST_MultiLineString'
42+
)
43+
-- Calculate a new surrogate ID for key
44+
SELECT ROW_NUMBER() OVER (ORDER BY geom) AS id, *
45+
FROM combined
46+
ORDER BY geom
47+
;
48+
```
49+
50+
The above query creates the `routing.road_line` table. The next step
51+
adds some database best practices to the table:
52+
53+
* Explain why a surrogate ID was added
54+
* Primary key on the `id` column
55+
* Index on `osm_id`
56+
57+
58+
```sql
59+
COMMENT ON COLUMN routing.road_line.id IS 'Surrogate ID, cannot rely on osm_id being unique after converting multi-linestrings to linestrings.';
60+
ALTER TABLE routing.road_line
61+
ADD CONSTRAINT pk_routing_road_line PRIMARY KEY (id)
62+
;
63+
CREATE INDEX ix_routing_road_line_osm_id
64+
ON routing.road_line (osm_id)
65+
;
66+
```
67+
68+
To prepare the OpenStreetMap roads data for routing with the older pgRouting
69+
installation, run the
70+
pgRouting functions `pgr_nodeNetwork()`, `pgr_createTopology()`,
71+
and `pgr_analyzeGraph()`.
72+
73+
74+
```sql
75+
SELECT pgr_nodeNetwork('routing.road_line', 0.1, 'id', 'geom');
76+
SELECT pgr_createTopology('routing.road_line_noded', 0.1, 'geom');
77+
SELECT pgr_analyzeGraph('routing.road_line_noded', 0.1, 'geom');
78+
```
79+
80+
> Note: These functions were all removed in pgRouting 4.0.
81+
82+
Running the functions shown above will create two (2) new tables
83+
usable for routing.
84+
85+
* `routing.road_line_noded`
86+
* `routing.road_line_noded_vertices_pgr`
87+
88+
89+
## Timing note
90+
91+
The pgRouting functions shown in the preceding section can take a
92+
long time to complete on larger regions.
93+
It is often a good idea to run these from `psql` within a screen
94+
emulator, such as `screen` or `tmux` that allow you to disconnect
95+
from the long-running command without cancelling the query.
96+
97+
98+
## Determine Costs
99+
100+
Routing requires a cost in order to determine the best route to
101+
take.
102+
The following query creates a simple `cost_length` column to
103+
the `routing.road_line_noded` table as a generated column.
104+
This is a simple way to get started with costs for routing.
105+
106+
```sql
107+
ALTER TABLE routing.road_line_noded
108+
ADD cost_length DOUBLE PRECISION NOT NULL
109+
GENERATED ALWAYS AS (ST_Length(geom))
110+
STORED;
111+
```
112+
113+
> Note: This is for non-directional routing. See the *Routing `oneway`* section below for more on directional routing.
114+
115+
116+
# Determine route start and end
117+
118+
The following query identifies the vertex IDs for a start and end point
119+
to use for later queries. The query uses an input set of points
120+
created from specific longitude/latitude values.
121+
Use the `start_id` and `end_id` values from this query
122+
in subsequent queries through the `:start_id` and `:end_id` variables
123+
via DBeaver.
124+
125+
126+
```sql
127+
WITH s_point AS (
128+
SELECT v.id AS start_id
129+
FROM routing.road_line_noded_vertices_pgr v
130+
INNER JOIN (SELECT
131+
ST_Transform(ST_SetSRID(ST_MakePoint(-77.0211, 38.92255), 4326), 3857)
132+
AS geom
133+
) p ON v.the_geom <-> geom < 10
134+
ORDER BY v.the_geom <-> geom
135+
LIMIT 1
136+
), e_point AS (
137+
SELECT v.id AS end_id
138+
FROM routing.road_line_noded_vertices_pgr v
139+
INNER JOIN (SELECT
140+
ST_Transform(ST_SetSRID(ST_MakePoint(-77.0183, 38.9227), 4326), 3857)
141+
AS geom
142+
) p ON v.the_geom <-> geom < 10
143+
ORDER BY v.the_geom <-> geom
144+
LIMIT 1
145+
)
146+
SELECT s_point.start_id, e_point.end_id
147+
FROM s_point, e_point
148+
;
149+
```
150+
151+
```bash
152+
┌──────────┬────────┐
153+
│ start_id │ end_id │
154+
╞══════════╪════════╡
155+
│ 14630 │ 14686 │
156+
└──────────┴────────┘
157+
```
158+
159+
160+
> Warning: The vertex IDs returned by the above query will vary. The pgRouting functions that generate this data do not guarantee data will always be generated in precisely the same order, causing these IDs to be different.
161+
162+
163+
The vertex IDs returned were `14630` and `14686`. These points
164+
span a particular segment of road (`osm_id = 6062791`) that is tagged
165+
as `highway=residential` and `access=private`.
166+
This segment is used to illustrate how the calculated access
167+
control columns, `route_motor`, `route_cycle` and `route_foot`,
168+
can influence route selection.
169+
170+
171+
172+
```sql
173+
SELECT *
174+
FROM routing.road_line
175+
WHERE osm_id = 6062791
176+
;
177+
```
178+
179+
![Screenshot from QGIS showing two labeled points, 14630 and 14686. The road between the two points is shown with a light gray dash indicating the access tag indicates non-public travel.](dc-example-route-start-end-vertices.png)
180+
181+
> See `flex-config/helpers.lua` functions (e.g. `routable_motor()`) for logic behind access control columns.
182+
183+
184+
# Simple route
185+
186+
Using `pgr_dijkstra()` and no additional filters will
187+
use all roads from OpenStreetMap without regard to mode of travel
188+
or access rules.
189+
This query picks a route that traverses sidewalks and
190+
a section of road with the
191+
[`access=private` tag from OpenStreetMap](https://wiki.openstreetmap.org/wiki/Tag:access%3Dprivate).
192+
The key details to focus on in the following queries
193+
is the string containing a SQL query passed into the `pgr_dijkstra()`
194+
function. This first example is a simple query from the
195+
`routing.road_line_noded` table.
196+
197+
> Note: These queries are intended to be ran using DBeaver. The `:start_id` and `:end_id` variables work within DBeaver, but not via `psql` or QGIS. Support in other GUIs is unknown at this time (PRs welcome!).
198+
199+
200+
```sql
201+
SELECT d.*, n.the_geom AS node_geom, e.geom AS edge_geom
202+
FROM pgr_dijkstra(
203+
'SELECT id, source, target, cost_length AS cost,
204+
geom
205+
FROM routing.road_line_noded
206+
',
207+
:start_id, :end_id, directed := False
208+
) d
209+
INNER JOIN routing.road_line_noded_vertices_pgr n ON d.node = n.id
210+
LEFT JOIN routing.road_line_noded e ON d.edge = e.id
211+
;
212+
```
213+
214+
![Screenshot from DBeaver showing the route generated with all roads and no access control. The route is direct, traversing the road marked access=private.](dc-example-route-start-no-access-control.png)
215+
216+
217+
# Route motorized
218+
219+
The following query modifies the query passed in to `pgr_dijkstra()`
220+
to join the `routing.road_line_noded` table to the
221+
`routing.road_line` table. This allows using attributes available
222+
in the upstream table for additional routing logic.
223+
The join clause includes a filter on the `route_motor` column.
224+
225+
From the comment on the `osm.road_line.route_motor` column:
226+
227+
> "Best guess if the segment is route-able for motorized traffic. If access is no or private, set to false. WARNING: This does not indicate that this method of travel is safe OR allowed!"
228+
229+
Based on this comment, we can expect that adding `AND r.route_motor`
230+
into the filter will ensure the road type is suitable for motorized
231+
traffic, and it excludes routes marked private.
232+
233+
234+
```sql
235+
SELECT d.*, n.the_geom AS node_geom, e.geom AS edge_geom
236+
FROM pgr_dijkstra(
237+
'SELECT n.id, n.source, n.target, n.cost_length AS cost,
238+
n.geom
239+
FROM routing.road_line_noded n
240+
INNER JOIN routing.road_line r ON n.old_id = r.id
241+
AND r.route_motor
242+
',
243+
:start_id, :end_id, directed := False
244+
) d
245+
INNER JOIN routing.road_line_noded_vertices_pgr n ON d.node = n.id
246+
LEFT JOIN routing.road_line_noded e ON d.edge = e.id
247+
;
248+
```
249+
250+
251+
![Screenshot from DBeaver showing the route generated with all roads and limiting based on route_motor. The route bypasses the road(s) marked access=no and access=private.](dc-example-route-start-motor-access-control.png)
252+
253+
254+
255+
# Route `oneway`
256+
257+
258+
The route shown in the previous example now respects the
259+
access control and limits to routes suitable for motorized traffic.
260+
It, however, **did not** respect the one-way access control.
261+
The very first segment (top-left corner of screenshot) went
262+
the wrong way on a one-way street.
263+
This behavior is a result of the simple length-based cost model.
264+
265+
266+
The `oneway` column in the road tables uses
267+
[osm2pgsql's `direction` data type](https://osm2pgsql.org/doc/manual.html#type-conversions) which resolves to `int2` in Postgres.
268+
Valid values are:
269+
270+
* `0`: Not one way
271+
* `1`: One way, forward travel allowed
272+
* `-1`: One way, reverse travel allowed
273+
* `NULL`: It's complicated. See #172.
274+
275+
276+
Assuming a noded roads table routing table, bring over the `oneway` detail
277+
278+
```sql
279+
ALTER TABLE routing.road_line_noded
280+
ADD oneway INT2 NULL
281+
;
282+
283+
UPDATE routing.road_line_noded rn
284+
SET oneway = r.oneway
285+
FROM routing.road_line r
286+
WHERE rn.old_id = r.id AND rn.oneway IS NULL
287+
;
288+
```
289+
290+
## Forward and reverse costs
291+
292+
Calculate forward cost.
293+
294+
```sql
295+
ALTER TABLE routing.road_line_noded
296+
DROP COLUMN IF EXISTS cost_length
297+
;
298+
299+
-- Cost with oneway considerations
300+
ALTER TABLE routing.road_line_noded
301+
ADD cost_length NUMERIC
302+
GENERATED ALWAYS AS (
303+
CASE WHEN oneway IN (0, 1) OR oneway IS NULL
304+
THEN ST_Length(geom)
305+
WHEN oneway = -1
306+
THEN -1 * ST_Length(geom)
307+
END
308+
)
309+
STORED
310+
;
311+
```
312+
313+
Reverse cost.
314+
315+
```sql
316+
-- Reverse cost with oneway considerations
317+
ALTER TABLE routing.road_line_noded
318+
ADD cost_length_reverse NUMERIC
319+
GENERATED ALWAYS AS (
320+
CASE WHEN oneway IN (0, -1) OR oneway IS NULL
321+
THEN ST_Length(geom)
322+
WHEN oneway = 1
323+
THEN -1 * ST_Length(geom)
324+
END
325+
)
326+
STORED
327+
;
328+
```
329+
330+
331+
This query uses the new reverse cost column, and changes
332+
`directed` from `False` to `True`.
333+
334+
335+
```sql
336+
SELECT d.*, n.the_geom AS node_geom, e.geom AS edge_geom
337+
FROM pgr_dijkstra(
338+
'SELECT n.id, n.source, n.target, n.cost_length AS cost,
339+
n.cost_length_reverse AS reverse_cost,
340+
n.geom
341+
FROM routing.road_line_noded n
342+
INNER JOIN routing.road_line r ON n.old_id = r.id
343+
AND r.route_motor
344+
',
345+
:start_id, :end_id, directed := True
346+
) d
347+
INNER JOIN routing.road_line_noded_vertices_pgr n ON d.node = n.id
348+
LEFT JOIN routing.road_line_noded e ON d.edge = e.id
349+
;
350+
```
351+
352+
![Screenshot from DBeaver showing the route generated with all roads and limiting based on route_motor and using the improved cost model including forward and reverse costs. The route bypasses the road(s) marked access=no and access=private, as well as respects the one-way access controls.](dc-example-route-start-motor-access-control-oneway.png)
353+
354+

0 commit comments

Comments
 (0)