James Marca, Activimetrics LLC
October 21, 2020
PostgreSQL/PostGIS ⇔ R spatial analysis
PostgreSQL/PostGIS ⇔ mod_perl activity survey website
PostgreSQL/PostGIS ⇔ Java accident risk website
PostgreSQL/PostGIS ⇔ node.js truck activity estimator
PostgreSQL/PostGIS ⇔ … lots of things …
Find the best path
Find the best path
for one or more vehicles
Find the best path
for one or more vehicles
to visit every node in a network
Find the best path
for one or more vehicles
to visit every node in a network
but the vehicles have a fixed capacity
Find the best path
for one or more vehicles
to visit every node in a network
but the vehicles have a fixed capacity
and need to head back to the depot to unload
Find the best path
for one or more vehicles
to visit every node in a network
but the vehicles have a fixed capacity
and need to head back to the depot to unload
and make sure that every vehicle has the about the same work
Find the best path
for one or more vehicles
to visit every node in a network
but the vehicles have a fixed capacity
and need to head back to the depot to unload
and make sure that every vehicle has the about the same work
oh and can you try to minimize the number of vehicles that are needed too?
Find the best path
for one or more vehicles
to visit every node in a network
but the vehicles have a fixed capacity
and need to head back to the depot to unload
and make sure that every vehicle has the about the same work
oh and can you try to minimize the number of vehicles that are needed too?
also the drivers have an eight hour shift, or they cost time and a half, so factor that in, as well as their contractual working hours and break times
(15 minutes in the morning and afternoon,
plus one hour lunch between 11 and 2)
Find the best path
for one or more vehicles
to visit every node in a network
but the vehicles have a fixed capacity
and need to head back to the depot to unload
and make sure that every vehicle has the about the same work
oh and can you try to minimize the number of vehicles that are needed too?
also the drivers have an eight hour shift, or they cost time and a half, so factor that in, as well as their contractual working hours and break times
(15 minutes in the morning and afternoon,
plus one hour lunch between 11 and 2)
Nodes are points in space
Ways are lines
Nodes are points in space
Ways are lines
Relations group nodes and ways
Each tag describes a geographic attribute of the feature being shown by that specific node, way or relation.
Node: 7097377424
Version #1
added service roads.
Edited 9 months ago by pandypr · Changeset #79039579
Location: 33.6756646, -117.9193706
Part of
2 ways
Harbor Boulevard (401292457)
759766208
Way: 759766208
Version #1
added service roads.
Edited 9 months ago by pandypr · Changeset #79039579
Tags
highway service
Nodes
8 nodes
7097377424 (part of way Harbor Boulevard (401292457))
7097377425
7097377426
7097377427
7097377428
7097377429
7097377430
7097377431 (part of way Ponderosa Street (158607554))
Relation: Harbor Boulevard (2484073)
Version #147
ref
Edited about 1 month ago by Fluffy89502 · Changeset #90212661
Tags
name Harbor Boulevard
route road
type route
Members
▶ 700 members
a spatial database extender for PostgreSQL. It adds support for geographic objects allowing location queries to be run in SQL
extends the PostGIS / PostgreSQL geospatial database to provide geospatial routing functionality
https://download.geofabrik.de/north-america/us/california/socal-latest.osm.pbf
osmium extract \
-p glendale-poly.osm \
-o glendale-latest.osm \
socal-latest.osm.pbf
osm2pgrouting \
--f data/glendale-latest.osm \
--conf data/map_config_streets.xml \
--dbname glendale \
--prefix 'glendale_' \
--username dbuser \
--clean
osm2pgrouting
config file
<tag_value name="trunk" id="104" priority="1.05" maxspeed="50" />
<tag_value name="trunk_link" id="105" priority="1.05" maxspeed="50" />
<tag_value name="primary" id="106" priority="1.15" maxspeed="50" />
<tag_value name="primary_link" id="107" priority="1.15" maxspeed="50" />
<tag_value name="secondary" id="108" priority="1.5" maxspeed="50" />
<tag_value name="secondary_link" id="109" priority="1.5" maxspeed="50" />
<tag_value name="tertiary" id="110" priority="1.75" maxspeed="50" />
<tag_value name="tertiary_link" id="111" priority="1.75" maxspeed="50" />
<tag_value name="residential" id="112" priority="2.5" maxspeed="50" />
<tag_value name="living_street" id="113" priority="3" maxspeed="50" />
<tag_value name="unclassified" id="117" priority="3" maxspeed="50"/>
<tag_value name="road" id="100" priority="5" maxspeed="50" />
mydb=> \d glendale_ways
Table "public.glendale_ways"
Column | Type
-------------------+---------------------------
gid | bigint
osm_id | bigint
tag_id | integer
length | double precision
length_m | double precision
name | text
source | bigint
target | bigint
source_osm | bigint
target_osm | bigint
cost | double precision
reverse_cost | double precision
cost_s | double precision
reverse_cost_s | double precision
rule | text
one_way | integer
oneway | text
x1 | double precision
y1 | double precision
x2 | double precision
y2 | double precision
maxspeed_forward | double precision
maxspeed_backward | double precision
priority | double precision
the_geom | geometry(LineString,4326)
Indexes:
"glendale_ways_pkey" PRIMARY KEY, btree (gid)
"glendale_ways_the_geom_idx" gist (the_geom)
Foreign-key constraints:
"glendale_ways_source_fkey" FOREIGN KEY (source) REFERENCES glendale_ways_vertices_pgr(id)
"glendale_ways_source_osm_fkey" FOREIGN KEY (source_osm) REFERENCES glendale_ways_vertices_pgr(osm_id)
"glendale_ways_tag_id_fkey" FOREIGN KEY (tag_id) REFERENCES configuration(tag_id)
"glendale_ways_target_fkey" FOREIGN KEY (target) REFERENCES glendale_ways_vertices_pgr(id)
"glendale_ways_target_osm_fkey" FOREIGN KEY (target_osm) REFERENCES glendale_ways_vertices_pgr(osm_id)
length_m | source | target | cost_s | reverse_cost_s | oneway
----------+--------+--------+--------+----------------+---------
39.63 | 7194 | 1191 | 2.85 | -2.85 | YES
48.42 | 15404 | 1 | 3.49 | 3.49 | UNKNOWN
124.38 | 1195 | 46 | 8.96 | -8.96 | YES
211.32 | 6794 | 2 | 15.21 | -15.21 | YES
59.75 | 5898 | 3 | 4.30 | -4.30 | YES
267.77 | 7762 | 5 | 19.28 | -19.28 | YES
147.70 | 11969 | 6 | 10.63 | 10.63 | UNKNOWN
11.95 | 9845 | 8 | 0.86 | 0.86 | NO
61.69 | 11183 | 9 | 4.44 | 4.44 | NO
63.39 | 9838 | 10 | 4.56 | 4.56 | NO
(10 rows)
39.63m at 50km/hr takes 2.85s ✔
WITH
statements AKA Common Table Expressions
WITH RECURSIVE
statements
WITH RECURSIVE blahquery(arg1, arg2) AS (
-- non-recursive term --
select foo, bar from baz
UNION -- (or UNION ALL) --
-- recursive term --
select ... from blahquery ...
WITH RECURSIVE t(n) AS (
VALUES (1)
UNION ALL
SELECT n+1 FROM t WHERE n < 10
)
SELECT sum(n) FROM t;
WITH RECURSIVE t(n) AS (
VALUES (1)
UNION ALL
SELECT n+1 FROM t WHERE n < 10
)
SELECT sum(n) FROM t;
sum
-----
55
(1 row)
WITH RECURSIVE t(n) AS (
VALUES (1)
UNION ALL
SELECT n+1 FROM t WHERE n < 10
)
SELECT n FROM t;
n
----
1
2
3
4
5
6
7
8
9
10
(10 rows)
WITH RECURSIVE t(n) AS (
VALUES (1)
UNION ALL
SELECT n+1 FROM t WHERE n < 10
)
SELECT sum(n) FROM t;
sum
-----
55
(1 row)
id | name | source | target | one_way | cost_s | rev_cost_s
------+-----------------------+--------+--------+---------+--------+------------
433 | Western Avenue | 1 | 303 | 0 | 1.30 | 1.30
4725 | Glenoaks Boulevard | 1 | 4061 | 1 | 2.58 | -2.58
299 | Geneva Street | 2 | 216 | 0 | 26.01 | 26.01
1735 | Glenoaks Boulevard | 2 | 1267 | 0 | 8.40 | 8.40
WITH RECURSIVE
sources(source,count) as (
select source,count(*) as count
from glendale_ways group by source
),
targets(target,count) as (
select target,count(*) as count
from glendale_ways group by target
)
Any record with target count=1 and source count=1
possible_interiors as (
select w.*,s.count as scount, t.count as tcount
from glendale_ways w
join targets t on (w.target=t.target)
join sources s on (w.source=s.source)
where t.count=1 and s.count=1
),
interiors as (
select pi.*
from possible_interiors pi
join sources s on (pi.target=s.source)
join targets t on (pi.source=t.target)
where s.count=1 and t.count=1
)
possible_starts as (
select w.*, s.count as scount, t.count as tcount
from glendale_ways w
join targets t on (w.target=t.target)
join sources s on (w.source=s.source)
where t.count = 1 -- link is only one touching target
)
starts as (
select ps.*
from possible_starts ps
where ps.scount > 1
)
union
select ps.*
from possible_starts ps
join targets t on (ps.source=t.target)
where ps.scount=1 and t.count>1
-- more than one link target == ps.source
Look at possible interiors to identify a name change
union
select ps.*
from possible_starts ps
join possible_interiors pi on (ps.source=pi.target)
where ps.name != pi.name and ps.scount=1 and pi.tcount=1
-- name change of road
Like name change, but not in possible_interiors
set
union
select ps.*
from possible_starts ps
join possible_starts psi on (ps.source = psi.target
and ps.name != psi.name)
-- singletons
possible_starts as (
select w.*, s.count as scount, t.count as tcount
from glendale_ways w
join targets t on (w.target=t.target)
join sources s on (w.source=s.source)
where t.count = 1 -- link is only one touching target
),
starts as (
select ps.*
from possible_starts ps
where ps.scount >1
union -- more than one link target == ps.source
select ps.*
from possible_starts ps
join targets t on (ps.source=t.target)
where ps.scount =1 and t.count>1
union -- name change of road
select ps.*
from possible_starts ps
join possible_interiors pi on (ps.source=pi.target)
where ps.name != pi.name and ps.scount=1 and pi.tcount=1
union -- singletons
select ps.*
from possible_starts ps
join possible_starts psi on (ps.source=psi.target and ps.name != psi.name)
)
Nothing new, as starts and ends are basically the same
A quick overview of important spatial processing basics
PostGIS offers types, functions, and indices for spatial data
geom
is probably in WGS84 projectionst_length()
on degrees is uselessst_length(st_transform(geom,32611))
st_length()
call gives meters!
select srid,proj4text
from spatial_ref_sys where srid between 32600 and 32661
order by srid;
srid | proj4text
-------+---------------------------------------------------
32601 | +proj=utm +zone=1 +datum=WGS84 +units=m +no_defs
32602 | +proj=utm +zone=2 +datum=WGS84 +units=m +no_defs
32603 | +proj=utm +zone=3 +datum=WGS84 +units=m +no_defs
32604 | +proj=utm +zone=4 +datum=WGS84 +units=m +no_defs
32605 | +proj=utm +zone=5 +datum=WGS84 +units=m +no_defs
…
-- with recursive ...
search_graph(gid, length_m, name, source, target, depth,
path, segments, cycle, ...all_the_things...) as (
select
g.gid, g.length_m,
g.name, g.source, g.target,
1 as depth,
array[g.gid] as path,
st_asewkt(g.the_geom) as segments,
false as cycle,
... -- other stuff
from ends g
-- initialize with ends, connect interiors to source
gid
is unique identifier for each segmentpath
is an array of gid
’sgid
’s to the beginning of the array
st_asewkt(g.the_geom) as segments
union all
select
g.gid, g.length_m + sg.length_m,
sg.name, g.source, sg.target,
sg.depth+1 as depth,
g.gid || sg.path as path,
st_asewkt( st_makeline( g.the_geom, sg.segments )),
g.gid = ANY(sg.path) as cycle,
... -- other stuff
from interiors g -- recurse on interiors
join search_graph sg on
(g.target=sg.source -- interior target -> chain source
and g.name=sg.name)-- but same street name too please
where sg.depth < 100 and not sg.cycle -- stop guards
st_asewkt( st_makeline( g.the_geom, sg.segments ))
st_makeline()
used to avoid array type error
ARRAY[g.the_geom] as segments
…
array_prepend(g.the_geom, sg.segments)
::geometry(LineString,4326)[],
EXPLAIN ANALYZE
says they’re the same speed:
st_asewkt
117s vs ARRAY
119s
WITH RECURSIVE …
select gid,name,source,target,depth
from search_graph order by depth desc,name;
gid | name | source | target | depth
------+-----------------------+--------+--------+-------
6344 | North Louise Street | 5686 | 234 | 19
6179 | North Louise Street | 5685 | 234 | 18
5311 | Emerald Isle Drive | 4635 | 149 | 17
6326 | North Louise Street | 5520 | 234 | 17
5309 | Emerald Isle Drive | 4650 | 149 | 16
5280 | Flintridge Drive | 4620 | 147 | 16
6327 | North Louise Street | 5667 | 234 | 16
5310 | Emerald Isle Drive | 4648 | 149 | 15
gid_paths as (
select unnest(sg.path) as node,depth
from search_graph sg ),
gid_max_depth as (
select node,max(depth) as depth
from gid_paths group by node ),
distinct_paths as (
select distinct path
from search_graph sg
join gid_max_depth gm
on (gm.depth=sg.depth and
gm.node in (select unnest(sg.path))))
distinct_paths
starts
to add starting nodesegments as (
select
c.name, g.source, c.target, c.depth+1 as depth, g.gid || c.path as path,
ST_SimplifyPreserveTopology(
ST_GeomFromEWKT(
ST_AsEWKT(ST_MakeLine(g.the_geom,
c.segments))),
0.0000001) as the_geom,
… other_columns …
from search_graph c
join distinct_paths dp on (c.path=dp.path)
join starts g -- add start nodes to chain
on (g.target=c.source --start.target == source
and g.name=c.name)) -- same name please
grouped as (
select * from keep_ways
union
select * from new_ways
)
insert into new_glendale_ways ( … )
select … from grouped;
Making a routable, directed network
drop sequence if exists curbgraph_v2_serial;
create sequence curbgraph_v2_serial;
drop table if exists curbs_v2_graph cascade;
with
tform as (
select id, st_transform(the_geom,32611) as geom,reverse_cost
from new_glendale_ways),
rhs as (
select ST_Reverse(ST_Transform (
ST_OffsetCurve(
geom,
-2) -- 2 meters offset, 6 feet-ish, reverse of orig direction
,4326)) as geom, -- back to source geometry, (need reverse)
id
from tform),
lhs_twoway as (
select ST_Reverse(ST_Transform (
ST_OffsetCurve(
geom,
2) -- 2 meters offset, 6 feet-ish, same sense
,4326)) as geom, -- back to source geometry, (need reverse because lhs)
id
from tform
where reverse_cost > 0),
lhs_oneway as (
select ST_Transform (
ST_OffsetCurve(
geom,
2) -- 2 meters offset, 6 feet-ish, same sense
,4326) as geom, -- back to source geometry, no rev. for 1-way str
id
from tform
where reverse_cost < 0),
lhs as (
select * from lhs_twoway
union
select * from lhs_oneway),
lhscurbs as (
select
nextval('curbgraph_v2_serial') as curbid,
'lhs' as curbside,
a.id,
a.osm_ids,
a.tag_ids,
a.length,
a.length_m,
a.name,
case when a.one_way=1 then a.source else a.target end as source,
case when a.one_way=1 then a.target else a.source end as target,
a.source_osm,
a.target_osm,
case when a.one_way=1 then a.cost else a.reverse_cost end as cost,
-1 as reverse_cost,
case when a.one_way=1 then a.cost_s else a.reverse_cost_s end as cost_s,
-1 as reverse_cost_s,
a.rule,
a.one_way,
a.oneway,
a.maxspeed_forward,
a.maxspeed_backward,
a.priority,
a.the_geom,
cg.geom as curb_geom
from new_glendale_ways a
left outer join lhs cg on (a.id=cg.id)
where one_way=1 or reverse_cost>0),
rhscurbs as (
select
nextval('curbgraph_v2_serial') as curbid,
'rhs' as curbside,
a.id,
a.osm_ids,
a.tag_ids,
a.length,
a.length_m,
a.name,
a.source,
a.target,
a.source_osm,
a.target_osm,
a.cost,
-1 as reverse_cost,
a.cost_s,
-1 as reverse_cost_s,
a.rule,
a.one_way,
a.oneway,
a.maxspeed_forward,
a.maxspeed_backward,
a.priority,
a.the_geom,
cg.geom as curb_geom
from new_glendale_ways a
left outer join rhs cg on (a.id=cg.id)),
curbs as (
select * from rhscurbs
union
select * from lhscurbs)
select * into curbs_v2_graph from curbs;
drop table if exists curbs_v2_linegraph;
SELECT * into curbs_v2_linegraph FROM pgr_lineGraph(
'SELECT curbid as id, source, target, cost_s as cost, reverse_cost_s as reverse_cost FROM curbs_v2_graph'
);
pgr_dijkstraCostMatrix()
pgr_dijkstraCostMatrix(edges_sql, start_vids)
SELECT * FROM pgr_dijkstraCostMatrix(
'SELECT id, source, target, cost, reverse_cost FROM edge_table',
(SELECT array_agg(id) FROM edge_table_vertices_pgr WHERE id < 5)
);
select * FROM pgr_dijkstraCostMatrix(
'SELECT id, source, target, target_length_m as cost, reverse_cost FROM new_curbs_v2_linegraph',
(select array_agg(source) from (select distinct source from new_curbs_v2_linegraph))
);
create or replace function fleshout_2000_curb_linegraph_matrix(starting int)
returns integer as
$BODY$
DECLARE
i text;
subsel text := 'SELECT id, source, target, target_length_m as cost, reverse_cost FROM new_curbs_v2_linegraph';
insert_sql text := '';
startid int := 0;
BEGIN
insert_sql := '
with
id_block (sid) as (
select distinct source
from new_curbs_v2_linegraph nl
where source > $1
order by source
limit 3000
),
nextbatch as (
select * FROM pgr_dijkstraCostMatrix('
|| quote_literal(subsel)
||',
(select array_agg(sid) from id_block)))
insert into new_curbs_linegraph_matrix
select * from nextbatch
on conflict do nothing';
FOR startid IN starting..7000 by 1000 LOOP
RAISE NOTICE 'populate db starting with %', startid;
EXECUTE insert_sql using startid;
END LOOP;
return count(*) from new_curbs_linegraph_matrix;
END;
$BODY$
LANGUAGE plpgsql;
percentile_cont
: Computes the continuous percentile, a value corresponding to the specified fraction within the ordered set of aggregated argument values. This will interpolate between adjacent input items if needed.with
sid_count (sid,cnt) as (
select start_vid, count(*)
from new_curbs_linegraph_matrix
group by start_vid
order by count),
lo_block (sid) as (
select sid from sid_count
limit 500),
pctl (hicount) as (
SELECT percentile_cont(0.07) WITHIN GROUP (ORDER BY cnt) FROM sid_count ),
hi_block (sid) as (
select sid
from sid_count
cross join pctl
where cnt > hicount
order by random()
limit 2500),
id_block (sid) as (
select sid from lo_block
union
select sid from hi_block),
with counts as (
select start_vid,count(*) as cnt
from new_curbs_linegraph_matrix group by start_vid)
select count(*),floor(cnt) from counts group by floor(cnt);
count | floor
-------+-------
201 | 9190
2374 | 9191
6607 | 9192
11 | 9193
(4 rows)
def sequence_to_table(self,vsequence,table_name):
sequence = 0
insert_query_string = """insert into {} (veh,sweep,linkid,geom)
select %s,%s,%s,c.curb_geom as the_geom
from curbs_v2_graph c
where c.curbid =%s"""
insert_query = sql.SQL(insert_query_string).format(sql.Identifier(table_name))
with self.conn.cursor() as cur:
cur.execute(
sql.SQL("drop table if exists {}").format(sql.Identifier(table_name)))
cur.execute(
sql.SQL("CREATE TABLE {} (id serial, linkid integer, veh integer, sweep boolean default TRUE, geom geometry(LINESTRING,4326) )").format(sql.Identifier(table_name)))
for (veh,sequence) in vsequence.items():
#print(sequence)
for row in sequence:
# basic idea. Save a table. Each row is
# shape,sequence number
sweep = row[1]
linkid = row[0]
cur.execute(insert_query,(veh,sweep,linkid,linkid))
self.conn.commit()
sql.SQL("drop table {}").format(sql.Identifier(table_name)))
...
cur.execute(insert_query,(veh,sweep,linkid,linkid))
ST_LineSubstring(geom, start_frac, end_frac)
geom
: the linestring to processstart_frac
: starting fraction for snipping the line (0 to 1)end_frac
: ending fraction for snipping the line (0 to 1)ST_LineSubstring
to break line into N partsgenerate_series
to generate the i valuesSELECT field1, field2,
ST_LineSubstring(the_geom, 100.00*n/len,
CASE
WHEN 100.00*(n+1) < len THEN 100.00*(n+1)/len
ELSE 1
END) AS the_geom
FROM
(SELECT sometable.field1, sometable.field2,
ST_LineMerge(sometable.the_geom) AS the_geom,
ST_Length(sometable.the_geom) As len
FROM sometable ) AS t
CROSS JOIN generate_series(0,10000) AS n
WHERE n*100.00/len < 1;
with
lengthshare as (
select id,linkid,veh,sweep,geom,
st_length(st_transform(geom,32611)) as len
from solver_output
order by id),
maxlen as (
select max(len) as len from lengthshare ),
maxiter as (
select (ceil(len/25.00)+1)::int as maxiter
from maxlen
)
Divide the longest length by 25, and round
series as (
select maxiter, generate_series(1,maxiter) - 1 as n
from maxiter
)
More flexible than the example code to determine maximum of series from data, rather than hard coding some big number.
snipped as (
select id,
id+(n/maxiter::numeric) as frame,
linkid,veh,sweep,
st_linesubstring(geom,
25.00*n/len,
case
when 25.00*(n+1) < len then 25.00*(n+1)/len
else 1
end) as geom
from lengthshare l
cross join series s
where s.n*25.00/len < 1
order by frame )
insert into
solver_output_snipped (id,frame,linkid,veh,sweep,geom)
select id,frame,linkid,veh,sweep,geom from snipped;
with
idgeom (uid, geom) as (
select uid,
st_collect(geom)
over (order by uid
rows between 2 preceding
and 10 following)
from curbtable_globalspan_snipped ),
centroids (uid, geom) as (
select uid,st_centroid(geom) as geom
from idgeom )
insert into pov (uid,geom)
select uid as uid,geom from centroids;
ffmpeg -framerate 4 -i 'Maps/output_%04d.png' -crf 25 -y Maps/out1.webm
-framerate 4
output_%04d.png