/vtquery

Query some gosh darn vector tiles

Primary LanguageJavaScriptBSD 2-Clause "Simplified" LicenseBSD-2-Clause

vtquery

Build Status codecov node-cpp-skel

npm install @mapbox/vtquery

Get the closest features from a longitude/latitude in a set of vector tile buffers. Made possible with node-cpp-skel, vtzero, geometry.hpp, spatial-algorithms, and cheap-ruler-cpp.

The two major use cases for this library are:

  1. To get a list of the features closest to a query point
  2. Point in polygon checks (radius=0)

API

Table of Contents

vtquery

Parameters

  • tiles Array<Object> an array of tile objects with buffer, z, x, and y values
  • LngLat Array<Number> a query point of longitude and latitude to query, [lng, lat]
  • options Object?
    • options.radius Number the radius to query for features. If your radius is larger than the extent of an individual tile, include multiple nearby buffers to collect a realistic list of features (optional, default 0)
    • options.limit Number limit the number of results/features returned from the query. Minimum is 1, maximum is 1000 (to avoid pre allocating large amounts of memory) (optional, default 5)
    • options.layers Array<String>? an array of layer string names to query from. Default is all layers.
    • options.geometry String? only return features of a particular geometry type. Can be point, linestring, or polygon. Defaults to all geometry types.
    • options.dedupe String perform deduplication of features based on shared layers, geometry, IDs and matching properties. (optional, default true)
    • options.basic-filters Array<String, Array>? an expression-like filter to include features with Numeric or Boolean properties that match the filters based on the following conditions: =, !=, <, <=, >, >=. The first item must be the value "any" or "all" whether any or all filters must evaluate to true.
    • options.direct_hit_polygon Boolean When true, the query will exlcude any polygons that do not contain the query point regardless of the radius value. (Optional, defaults to false)

Examples

const vtquery = require('@mapbox/vtquery');
const fs = require('fs');

const tiles = [
  { buffer: fs.readFileSync('./path/to/tile.mvt'), z: 15, x: 5238, y: 12666 }
];

const options = {
  radius: 0,
  limit: 5,
  geometry: 'polygon',
  layers: ['building', 'parks'],
  dedupe: true,
  'basic-filters': ['all', [['population', '>', 10], ['population', '<', 1000]]]
};

vtquery(tiles, [-122.4477, 37.7665], options, function(err, result) {
  if (err) throw err;
  console.log(result); // geojson FeatureCollection
});

Response object

The response object is a GeoJSON FeatureCollection with Point features containing the following in formation:

  • Geometry - always a Point. This library does not return full geometries of features. It only returns the closest point (longitude/latitude) of a feature. If the original feature is a point, the result is the actual location of that point. If the original feature is a linestring or polygon, the result is the interpolated closest latitude/longitude point of that feature. This could be along a line of a polygon and not an actual node of a polygon. If the query point is within a polygon, the result will be the query point location.
  • Properties of the feature
  • Extra properties including:
    • tilequery.geometry_type - either "Point", "Linestring", or "Polygon"
    • tilequery.distance in meters - if distance is 0.0, the query point is within the geometry (point in polygon)
    • tilequery.layer which layer the feature was a part of in the vector tile buffer
  • An id if it existed in the vector tile feature

Here's an example response

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          -122.42901,
          37.80633
        ]
      },
      "properties": {
        "property_one": "hello",
        "property_two": 15,
        "tilequery": {
          "distance": 11.3,
          "geometry": "Point",
          "layer": "parks"
        }
      }
    },
    { ... },
    { ... },
  ]
}

Point in polygon queries

To perform a "point in polygon" query, set your radius value to 0. This will only return polygons that your query point is within.

GOTCHA 1: Be aware of the number of results you are returning - there may be overlapping polygons in a tile, especially if you are querying multiple layers. If a query point exists within multiple polygons there is no way to sort them so they come back in the order they were queried. If there are more results than your numResults value specifies, they will just be cut off once the query hits the maximum number of results.

GOTCHA 2: Any query point that exists directly along an edge of a polygon will not return.

Deduplicating results

When querying across multiple tiles (or even within a single tile) it's likely source geometries have been split by the tile boundaries into multiple, seemingly unique geometries. This can result in duplicate results in a response for edges of tile boundaries, rather than actual edges of source data. Vtquery assumes features are duplicates if all of the following are true:

  • The features are from the same layer
  • The features are the same geometry type
  • The features have the same id AND same properties
  • The features' properties are the same (if no ids are present)

Develop

git clone git@github.com:mapbox/vtquery.git
cd vtquery

# Build binaries. This looks to see if there were changes in the C++ code. This does not reinstall deps.
make

# Run tests
make test

# Cleans your current builds and removes potential cache
make clean

# Cleans everything, including the things you download from the network in order to compile (ex: npm packages).
# This is useful if you want to nuke everything and start from scratch.
# For example, it's super useful for making sure everything works for Travis, production, someone else's machine, etc
make distclean

# Generate API docs from index.js
# Requires documentation.js to be installed globally
npm install -g documentation
npm run docs

To install and test on a linux instance, you can use the Dockerfile provided.

# build the image
docker build -t vtquery .

# run it - this will put you inside the image
docker run -it vtquery

# run tests
make test

# edit files - helpful for debugging
vim src/vtquery.cpp

Benchmarks

Benchmarks can be run with the bench/vtquery.bench.js script to test vtquery against common real-world fixtures (provided by mvt-fixtures). When making changes in a pull request, please provide the benchmarks from the master branch and the HEAD of your current branch. You can control the concurrency and iterations of the benchmarks with the following command:

node bench/vtquery.bench.js --iterations 1000 --concurrency 5

And the output will show how many times the library was able to execute per second, per fixture:

1: pip: many building polygons ... 954 runs/s (1048ms)
2: pip: many building polygons, single layer ... 1012 runs/s (988ms)
3: query: many building polygons, single layer ... 773 runs/s (1294ms)
4: query: linestrings, mapbox streets roads ... 1664 runs/s (601ms)
5: query: polygons, mapbox streets buildings ... 745 runs/s (1343ms)
6: query: all things - dense single tile ... 400 runs/s (2497ms)
7: query: all things - dense nine tiles ... 51 runs/s (19544ms)
8: elevation: terrain tile nepal ... 801 runs/s (1249ms)
9: geometry: 2000 points in a single tile, no properties ... 2083 runs/s (480ms)
10: geometry: 2000 points in a single tile, with properties ... 1047 runs/s (955ms)
11: geometry: 2000 linestrings in a single tile, no properties ... 978 runs/s (1022ms)
12: geometry: 2000 linestrings in a single tile, with properties ... 689 runs/s (1452ms)
13: geometry: 2000 polygons in a single tile, no properties ... 661 runs/s (1513ms)
14: geometry: 2000 polygons in a single tile, with properties ... 485 runs/s (2062ms)

Viz

The viz/ directory contains a small node application that is helpful for visual QA of vtquery results. It requests Mapbox Streets tiles and adds results as points to the map. In order to request tiles, you'll need a MapboxAccessToken environment variable.

cd viz
npm install
MapboxAccessToken={token} node app.js
# localhost:5000