Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added "streets within" query to API #288

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions api/within.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const Database = require('better-sqlite3');
const polyline = require('@mapbox/polyline');
const query = { within: require('../query/within') };
const project = require('../lib/project');
const proximity = require('../lib/proximity');

// polyline precision
const PRECISION = 6;

// export setup method
function setup( streetDbPath ){

// connect to db
// @todo: this is required as the query uses the 'street.' prefix for tables
const db = new Database(':memory:');

// attach street database
db.exec(`ATTACH DATABASE '${streetDbPath}' as 'street'`);

// query method
var q = function( topLeftCoord, bottomRightCoord, cb ){

var topLeft = {
lat: parseFloat( topLeftCoord.lat ),
lon: parseFloat( topLeftCoord.lon )
};

var bottomRight = {
lat: parseFloat( bottomRightCoord.lat ),
lon: parseFloat( bottomRightCoord.lon )
};

// error checking
if( isNaN( topLeft.lat ) ){ return cb( 'invalid latitude' ); }
if( isNaN( topLeft.lon ) ){ return cb( 'invalid longitude' ); }

if( isNaN( bottomRight.lat ) ){ return cb( 'invalid latitude' ); }
if( isNaN( bottomRight.lon ) ){ return cb( 'invalid longitude' ); }

try {
// perform a db lookup for nearby streets
const res = query.within( db, topLeft, bottomRight );

// no results were found
if( !res || !res.length ){ return cb( null, null ); }

// decode polylines
res.forEach( function( street, i ){
res[i].coordinates = project.dedupe( polyline.toGeoJSON(street.line, PRECISION).coordinates );
});

var center = {
lat: (topLeft.lat + bottomRight.lat) / 2,
lon: (topLeft.lon + bottomRight.lon) / 2
};

// order streets by proximity from point (by projecting it on to each line string)
var ordered = proximity.nearest.street( res, [ center.lon, center.lat ] );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need to sort the results by distance from the center? I could see users not wanting this or wanting it to be tunable.


// return streets ordered ASC by distance from point
cb( null, ordered );
} catch (err) {
// an error occurred
return cb(err, null);
}
};

// return methods
return {
query: q,
close: db.close.bind( db ),
};
}

module.exports = setup;
39 changes: 38 additions & 1 deletion cmd/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const search = require('../api/search');
const extract = require('../api/extract');
const street = require('../api/street');
const near = require('../api/near');
const within= require('../api/within');
const pretty = require('../lib/pretty');
const analyze = require('../lib/analyze');

Expand Down Expand Up @@ -32,7 +33,8 @@ const conn = {
search: search( process.argv[2], process.argv[3] ),
extract: extract( process.argv[2], process.argv[3] ),
street: street( process.argv[3] ),
near: near( process.argv[3] )
near: near( process.argv[3] ),
within: within( process.argv[3] )
};

function log() {
Expand Down Expand Up @@ -172,6 +174,41 @@ app.get('/street/near/geojson', function( req, res ){
});
});

// get streets within bounding box
// eg: http://localhost:3000/street/within/geojson
app.get('/street/within/geojson', function( req, res ){

var topLeft = { lat: req.query.topLeftLat, lon: req.query.topLeftLon };
var bottomRight = { lat: req.query.bottomRightLat, lon: req.query.bottomRightLon };
Comment on lines +181 to +182
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of copying these variables around giving them slightly different names, can we simplify this?


conn.within.query( topLeft, bottomRight, function( err, ordered ){
if( err ){ return res.status(400).json( formatError( err ) ); }
if( !ordered || !ordered.length ){ return res.status(200).json({}); }

var geojson = {
'type': 'FeatureCollection',
'features': ordered.map( function( o ){
return {
'type': 'Feature',
'properties': {
'id': o.street.id,
'name': Array.isArray( o.street.name ) ? o.street.name[0] : o.street.name,
'polyline': o.street.line,
'distance': ( Math.floor(( o.proj.dist || 0 ) * 1000000 ) / 1000000 ),
'projection': o.proj.point
},
'geometry': {
'type': 'LineString',
'coordinates': o.street.coordinates
}
};
})
};

res.json( geojson );
});
});

// get street geometry as geojson
// eg: http://localhost:3000/street/1/geojson
app.get('/street/:id/geojson', function( req, res ){
Expand Down
33 changes: 33 additions & 0 deletions query/within.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const DynamicQueryCache = require('./DynamicQueryCache');

// Remove before PR.
const logger = require('pelias-logger').get('interpolation');

/**
find all streets which have a bbox which envelops the specified point; regardless of their names.
**/

const SQL = `
SELECT street.polyline.id, street.polyline.line, street.names.name FROM street.polyline
JOIN street.rtree ON street.rtree.id = street.polyline.id
JOIN street.names ON street.names.id = street.polyline.id
WHERE ((street.rtree.minX + street.rtree.maxX) / 2 BETWEEN $topLeftLon AND $bottomRightLon
AND (street.rtree.minY + street.rtree.maxY) / 2 BETWEEN $bottomRightLat AND $topLeftLat )
Comment on lines +14 to +15
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some questions about the math here:

  • Given the values minX=-1, maxX=+1, what happens when dividing 0.0 by 2?
  • Can the brackets be simplified? maybe (math) BETWEEN x AND y, the WHERE and AND condition can be independent.
  • The method is named WITHIN, but this seems to be an INTERSECTS? ie. the text says find all streets which have a bbox which envelops the specified point but the input is a box, not a point, the result will contain geometries which 'leak' out of the bounding box? maybe rename the API for clarity?

GROUP BY street.polyline.id;
`;

const cache = new DynamicQueryCache(SQL);

module.exports = ( db, topLeft, bottomRight) => {

// use a prepared statement from cache (or generate one if not yet cached)
const stmt = cache.getStatement(db);

// execute statement
return stmt.all({
topLeftLat: topLeft.lat,
topLeftLon: topLeft.lon,
bottomRightLat: bottomRight.lat,
bottomRightLon: bottomRight.lon,
});
};
5 changes: 5 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ The API supports additional environment variables that affect its operation:

`geojson`: `/street/near/geojson?lat=-41.288788&lon=174.766843`

### GET /street/within/geojson
> Get all streets within a lat/lon bounding box, ordered by distance to the center, ASC. The top/left pair should contain the minimum longitude and the maximum latitude, and the bottom/left pair should contain the maximum longitude and the minimum latitude.

`geojson`: `/street/within/geojson?topLeftLat=-41.28&topLeftLon=174.76&bottomRightLat=-41.38&bottomRightLon=174.86`

### GET /street/{id}/geojson
> return the geometry for a specific street id

Expand Down