Skip to content

Commit 3634bfd

Browse files
committed
Multi-snapping
1 parent ca2f60e commit 3634bfd

7 files changed

Lines changed: 140 additions & 57 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Features:
77
* Multi-cost
88
* Filters
99
* Aggregation of identical sections based on its properties
10+
* Multi-snapping
1011

1112
## How to use ?
1213

@@ -71,6 +72,7 @@ The `conf` object let you configure the connection to the database and some rout
7172
* **Connection parameters:** node-pgrouting use [pg](https://node-postgres.com) as an interface to PostgreSQL database, with the `conf` object as parameters like *host*, *port*, *user*, *password* and *database*. You can use same environment variables as libpq too : see [pg documentation](https://node-postgres.com/features/connecting) for more details.
7273
* **table:** : table that contains the network. You can use the environment variable *PGTABLE* too. *table* can contain a schema. (Default: *edge*)
7374
* **maxSnappingDistance:** when process the routing, *node-pgrouting* needs to connect your start and end point to closest edge of the network within *maxSnappingDistance* meters.
75+
* **snappingRatio:** allow to snap not only the nearest point but also all near points with a distance difference lower than *snappingRatio* (=(distance-min(distance))/min(distance)). (Default: 0 (no ratio)).
7476

7577
## Data structure
7678

dev/js/route.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ module.exports = function(conf){
33
const app = express();
44

55
const routeEngine = require('../../src/index')({
6-
maxSnappingDistance: 10000
6+
maxSnappingDistance: 10000,
7+
snappingRatio: 0.6
78
})
89

910
//compute

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-pgrouting",
3-
"version": "0.1.1",
3+
"version": "0.2.0",
44
"description": "Simple routing API based on pgrouting.",
55
"main": "src/index.js",
66
"scripts": {

src/index.js

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -118,25 +118,36 @@ class RouteEngine {
118118
response.cost[v]=0;
119119
})
120120

121-
let startPoint=await this._findNearestEdge(params.from, params.avoid);
122-
let endPoint=await this._findNearestEdge(params.to, params.avoid);
123-
let path = await this._searchPath(params.type, startPoint, endPoint, params.avoid)
121+
let startPoints=await this._findNearestEdge(params.from, params.avoid);
122+
let endPoints=await this._findNearestEdge(params.to, params.avoid);
123+
124+
let path = await this._searchPath(params.type, startPoints, endPoints, params.avoid)
125+
if (path.length == 0){
126+
throw new routeError("CanNotCompute","")
127+
}
128+
path.forEach((step)=>{step.the_geom = JSON.parse(step.the_geom)})
129+
130+
let startPoint = startPoints[path[0].start_pid-1];
131+
startPoint.edge_point = JSON.parse(startPoint.edge_point)
132+
let endPoint = endPoints[path[0].end_pid-1-startPoints.length];
133+
endPoint.edge_point = JSON.parse(endPoint.edge_point)
134+
124135

125136
// Response building
126137
response.snappingDistance["start"] = startPoint.distance;
127138
response.features.push({
128-
type: "Feature",
129-
properties: {
130-
seq: 0
131-
},
132-
geometry: {
133-
type: "LineString",
134-
coordinates: [
135-
params.from.reverse(),
136-
JSON.parse(startPoint.edge_point).coordinates
137-
]
138-
}
139-
})
139+
type: "Feature",
140+
properties: {
141+
seq: 0
142+
},
143+
geometry: {
144+
type: "LineString",
145+
coordinates: [
146+
params.from.reverse(),
147+
startPoint.edge_point.coordinates
148+
]
149+
}
150+
})
140151
let nbseq=0
141152
path.forEach((step)=>{
142153
nbseq++;
@@ -145,7 +156,7 @@ class RouteEngine {
145156
properties: {
146157
seq: parseInt(step.seq)
147158
},
148-
geometry: JSON.parse(step.the_geom)
159+
geometry: step.the_geom
149160
};
150161
types.forEach((type)=>{
151162
feat.properties[type] = step[type];
@@ -166,13 +177,12 @@ class RouteEngine {
166177
geometry: {
167178
type: "LineString",
168179
coordinates: [
169-
JSON.parse(endPoint.edge_point).coordinates,
180+
endPoint.edge_point.coordinates,
170181
params.to.reverse(),
171182
]
172183
}
173184
})
174185

175-
//
176186

177187
resolve(response);
178188
} catch (e){
@@ -251,28 +261,38 @@ class RouteEngine {
251261

252262
_findNearestEdge(pos, filters){
253263
return new Promise((resolve,reject)=>{
254-
this.pool.query(sqlBuilder.findNearestPoint(this.conf.schema, this.conf.table, this.conf.maxSnappingDistance, filters),pos)
264+
var sql_request;
265+
if (this.conf.snappingRatio == 0){
266+
sql_request = sqlBuilder.findNearestPoint(this.conf.schema, this.conf.table, this.conf.maxSnappingDistance, filters);
267+
} else {
268+
sql_request = sqlBuilder.findNearestPoints(this.conf.schema, this.conf.table, this.conf.maxSnappingDistance, this.conf.snappingRatio, filters);
269+
}
270+
//console.log(sql_request);
271+
this.pool.query(sql_request,pos)
255272
.then((results)=>{
256273
if (results.rows.length == 0){
257274
reject(new routeError("SnappingError","can not link ("+pos+") to the network (max. distance: "+this.conf.maxSnappingDistance+"m)"))
258275
return;
259276
}
260-
if (results.rows[0].fraction == 0){results.rows[0].fraction=0.00001;}
261-
if (results.rows[0].fraction == 1){results.rows[0].fraction=0.99999;}
262-
resolve(results.rows[0]);
277+
results.rows.forEach((r)=>{
278+
if (r.fraction == 0){r.fraction=0.00001;}
279+
if (r.fraction == 1){r.fraction=0.99999;}
280+
});
281+
resolve(results.rows);
263282
})
264283
.catch((e)=>{
265284
reject(e)
266285
});
267286
})
268287
}
269288

270-
_searchPath(type, startPoint, endPoint, filters){
289+
_searchPath(type, startPoints, endPoints, filters){
271290
return new Promise(async (resolve,reject)=>{
272291
let types = await this.getTypes();
273292
let properties = await this.getProperties();
274-
//console.log(sqlBuilder.searchPath(this.conf.schema, this.conf.table, type, startPoint, endPoint, types, filters,properties))
275-
this.pool.query(sqlBuilder.searchPath(this.conf.schema, this.conf.table, type, startPoint, endPoint, types, filters, properties))
293+
let sql_request=sqlBuilder.searchPath(this.conf.schema, this.conf.table, type, startPoints, endPoints, types, filters, properties);
294+
//console.log(sql_request);
295+
this.pool.query(sql_request)
276296
.then((results)=>{
277297
resolve(results.rows);
278298
})

src/sqlBuilder.js

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,75 @@ module.exports = {
3030
ORDER BY st_distance(edge_table.the_geom,st_setsrid(st_makepoint($2,$1),4326),true)
3131
LIMIT 1`
3232
},
33-
searchPath: function(schema, table, type, startPoint, endPoint, types, filters, properties){
34-
function costSection(type, startPoint, endPoint){
33+
findNearestPoints: function(schema, table, maxSnappingDistance, snappingRatio, filters){
34+
let filters_where = filterToWhereClause(filters,"AND");
35+
return `
36+
SELECT edge_id,
37+
fraction,
38+
distance,
39+
edge_point
40+
FROM (
41+
SELECT *,
42+
(case when (min(distance) over (order by distance)) = 0 then (case when distance = 0 then 0 else 100 end) else (distance-(min(distance) over (order by distance)))/(min(distance) over (order by distance)) end) ratio,
43+
( select edge_table.id
44+
from ${schema}.${table} as edge_table
45+
where st_dwithin(edge_table.the_geom,st_setsrid(st_makepoint($2,$1),4326),${maxSnappingDistance},true) and
46+
st_intersects(edge_table.the_geom, new_geom) and
47+
edge_table.id <> edge_id
48+
limit 1
49+
) intersectsother
50+
FROM (
51+
SELECT edge_table.id as edge_id,
52+
st_LineLocatePoint(edge_table.the_geom,st_setsrid(st_makepoint($2,$1),4326)) as fraction,
53+
st_distance(edge_table.the_geom,st_setsrid(st_makepoint($2,$1),4326),true) as distance,
54+
ST_AsGeoJSON(st_LineInterpolatePoint(edge_table.the_geom,st_LineLocatePoint(edge_table.the_geom,st_setsrid(st_makepoint($2,$1),4326)))) as edge_point,
55+
st_linesubstring(
56+
ST_MakeLine(
57+
st_setsrid(st_makepoint($2,$1),4326),
58+
ST_ClosestPoint(
59+
edge_table.the_geom,
60+
st_setsrid(st_makepoint($2,$1),4326)
61+
)
62+
),0.0001,0.9999) new_geom
63+
FROM ${schema}.${table} as edge_table
64+
WHERE st_dwithin(edge_table.the_geom,st_setsrid(st_makepoint($2,$1),4326),${maxSnappingDistance},true) ${filters_where}
65+
ORDER BY st_distance(edge_table.the_geom,st_setsrid(st_makepoint($2,$1),4326),true)
66+
) tmp
67+
) tmp
68+
WHERE ratio < ${snappingRatio} AND intersectsother is null
69+
`
70+
},
71+
searchPath: function(schema, table, type, startPoints, endPoints, types, filters, properties){
72+
const getStartId="select -start_pid from startandstop";
73+
const getStartFraction="select fraction from points where pid = ("+getStartId+")";
74+
const getEndId="select -end_pid from startandstop";
75+
const getEndFraction="select fraction from points where pid = ("+getEndId+")";
76+
function costSection(type, nbStartPoint){
3577
return `
3678
((case
37-
when tmp.source_node=-1 and tmp.target_node=-2 OR tmp.source_node=-2 and tmp.target_node=-1 then (case when ${startPoint.fraction} < ${endPoint.fraction} then (${endPoint.fraction}-${startPoint.fraction})*edge_table.cost_${type} else (${startPoint.fraction}-${endPoint.fraction})*edge_table.reverse_cost_${type} end)
38-
when tmp.source_node=-1 then (case when tmp.target_node = edge_table.target then (1-${startPoint.fraction})*edge_table.cost_${type} else (${startPoint.fraction})*edge_table.reverse_cost_${type} END)
39-
when tmp.target_node=-2 then (case when tmp.source_node = edge_table.source then ${endPoint.fraction}*edge_table.cost_${type} else (1-${endPoint.fraction})*edge_table.reverse_cost_${type} end)
79+
when tmp.source_node<0 and tmp.source_node>=${-nbStartPoint} and tmp.target_node<${-nbStartPoint} OR tmp.source_node<${-nbStartPoint} and tmp.target_node<0 and tmp.target_node>=${-nbStartPoint} then (case when (${getStartFraction}) < ((${getEndFraction})) then (((${getEndFraction}))-(${getStartFraction}))*edge_table.cost_${type} else ((${getStartFraction})-((${getEndFraction})))*edge_table.reverse_cost_${type} end)
80+
when tmp.source_node<0 and tmp.source_node>=${-nbStartPoint} then (case when tmp.target_node = edge_table.target then (1-(${getStartFraction}))*edge_table.cost_${type} else ((${getStartFraction}))*edge_table.reverse_cost_${type} END)
81+
when tmp.target_node<${-nbStartPoint} then (case when tmp.source_node = edge_table.source then ((${getEndFraction}))*edge_table.cost_${type} else (1-((${getEndFraction})))*edge_table.reverse_cost_${type} end)
4082
else (case when tmp.source_node = edge_table.source then edge_table.cost_${type} else edge_table.reverse_cost_${type} END)
4183
end)) as ${type},
4284
`
4385
}
86+
let table_tmpPoints = [];
87+
let startPtsIds=[];
88+
let endPtsIds=[];
89+
startPoints.forEach((pt)=>{
90+
startPtsIds.push(-startPtsIds.length-1)
91+
table_tmpPoints.push("("+(startPtsIds.length)+","+pt.edge_id+","+pt.fraction+")");
92+
});
93+
endPoints.forEach((pt)=>{
94+
endPtsIds.push(-startPtsIds.length-endPtsIds.length-1)
95+
table_tmpPoints.push("("+(startPtsIds.length+endPtsIds.length)+","+pt.edge_id+","+pt.fraction+")");
96+
});
97+
table_tmpPoints = "SELECT pid::integer, edge_id::integer, fraction::float FROM (values "+table_tmpPoints.join(",")+") as t (pid, edge_id, fraction)";
4498
let types_sections ="";
4599
let types_aggregate = ""
46100
types.forEach((type)=>{
47-
types_sections=types_sections+costSection(type,startPoint,endPoint)
101+
types_sections=types_sections+costSection(type,startPtsIds.length)
48102
types_aggregate = types_aggregate+"sum(tmp."+type+") as "+type+", "
49103
})
50104

@@ -59,28 +113,34 @@ module.exports = {
59113
properties_select=properties_select+"edge_table."+v+" as "+v+",";
60114
})
61115

116+
62117
return `
118+
63119
with results as (select *
64120
FROM pgr_withPoints(
65121
'SELECT edge_table.id, edge_table.source, edge_table.target, edge_table.cost_${type} as cost, edge_table.reverse_cost_${type} as reverse_cost FROM ${schema}.${table} as edge_table ${filters_where}',
66-
'SELECT 1 as pid, ${startPoint.edge_id} as edge_id, ${startPoint.fraction}::float as fraction
67-
UNION ALL
68-
SELECT 2, ${endPoint.edge_id}, ${endPoint.fraction}',
69-
-1, -2))
122+
'${table_tmpPoints}',
123+
Array[${startPtsIds.join(',')}], Array[${endPtsIds.join(',')}])),
124+
startandstop as (select start_pid, end_pid from results where edge=-1 order by agg_cost limit 1),
125+
bestresult as (select * from results where start_pid = (select start_pid from startandstop) and end_pid = (select end_pid from startandstop)),
126+
points as (${table_tmpPoints})
70127
select ${types_aggregate}
71128
${properties_list},
72129
flag_groupid as seq,
73-
st_asgeojson(ST_LineMerge(St_union(the_geom))) the_geom from (
130+
st_asgeojson(ST_LineMerge(St_union(the_geom))) the_geom,
131+
(${getStartId}) as start_pid,
132+
(${getEndId}) as end_pid
133+
from (
74134
select *, sum(flag_newgroup) over (order by seq) flag_groupid from (
75135
select
76136
${types_sections}
77137
tmp.seq seq,
78138
(case when lag(${properties_agg}) OVER (order by seq)=${properties_agg} then 0 else 1 end) flag_newgroup,
79139
${properties_select}
80140
((((case
81-
when tmp.source_node=-1 and tmp.target_node=-2 OR tmp.source_node=-2 and tmp.target_node=-1 then (case when ${startPoint.fraction} < ${endPoint.fraction} then ST_LineSubstring(edge_table.the_geom,${startPoint.fraction}, ${endPoint.fraction}) else st_reverse(ST_LineSubstring(edge_table.the_geom, ${endPoint.fraction}, ${startPoint.fraction})) end)
82-
when tmp.source_node=-1 then (case when tmp.target_node = edge_table.target then ST_LineSubstring(edge_table.the_geom,${startPoint.fraction} ,1) else st_reverse(ST_LineSubstring(edge_table.the_geom,0,${startPoint.fraction})) END)
83-
when tmp.target_node=-2 then (case when tmp.source_node = edge_table.source then ST_LineSubstring(edge_table.the_geom,0,${endPoint.fraction}) else st_reverse(ST_LineSubstring(edge_table.the_geom,${endPoint.fraction},1)) end)
141+
when tmp.source_node<0 and tmp.source_node>=${-startPtsIds.length} and tmp.target_node<${-startPtsIds.length} OR tmp.source_node<${-startPtsIds.length} and tmp.target_node<0 and tmp.target_node>=${-startPtsIds.length} then (case when (${getStartFraction}) < ((${getEndFraction})) then ST_LineSubstring(edge_table.the_geom,(${getStartFraction}), ((${getEndFraction}))) else st_reverse(ST_LineSubstring(edge_table.the_geom, ((${getEndFraction})), (${getStartFraction}))) end)
142+
when tmp.source_node<0 and tmp.source_node>=${-startPtsIds.length} then (case when tmp.target_node = edge_table.target then ST_LineSubstring(edge_table.the_geom,(${getStartFraction}) ,1) else st_reverse(ST_LineSubstring(edge_table.the_geom,0,(${getStartFraction}))) END)
143+
when tmp.target_node<${-startPtsIds.length} then (case when tmp.source_node = edge_table.source then ST_LineSubstring(edge_table.the_geom,0,((${getEndFraction}))) else st_reverse(ST_LineSubstring(edge_table.the_geom,((${getEndFraction})),1)) end)
84144
else (case when tmp.source_node = edge_table.source then edge_table.the_geom else st_reverse(edge_table.the_geom) END)
85145
end))))the_geom
86146
from
@@ -90,7 +150,7 @@ module.exports = {
90150
rs.node as source_node,
91151
rt.node as target_node,
92152
rs.edge as edge
93-
from results as rs, results as rt
153+
from bestresult as rs, bestresult as rt
94154
where rs.seq = rt.seq-1) tmp
95155
inner join ${schema}.${table} as edge_table
96156
on edge_table.id=tmp.edge ) tmp ) tmp group by flag_groupid, ${properties_list} order by seq

test/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ describe("Table dependent parameter",()=>{
132132
});
133133

134134
describe("Routing function",()=>{
135-
let re = routeEngine({maxSnappingDistance:10000});
135+
let re = routeEngine({maxSnappingDistance:10000, snappingRatio: 0.6});
136136
let validParam = {
137137
from: '46,1',
138138
to: '47,2',

0 commit comments

Comments
 (0)