diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c43e2791 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +mason_packages +node_modules +build +.toolchain +.mason +.git +local.env +lib/binding diff --git a/.travis.yml b/.travis.yml index 59bf2e92..bd0167a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ before_script: # For reference, the default travis behavior which we override comes from https://github.com/travis-ci/travis-build/blob/e5a45cbf49e0d9e27398e76e5f25dd7706feb6aa/lib/travis/build/script/node_js.rb#L62-L69. script: [] -# the matrix allows you to specify different operating systems and environments to +# the matrix allows you to specify different operating systems and environments to # run your tests and build binaries matrix: include: @@ -83,6 +83,7 @@ matrix: before_script: - export LD_PRELOAD=${MASON_LLVM_RT_PRELOAD} - export ASAN_OPTIONS=fast_unwind_on_malloc=0:${ASAN_OPTIONS} + - export LSAN_OPTIONS=verbosity=1:log_threads=1 - npm test - unset LD_PRELOAD # after successful tests, publish binaries if specified in commit message @@ -142,4 +143,4 @@ matrix: # and get in the habit of running these locally before committing) - make tidy # Overrides `script`, no need to run tests - before_script: \ No newline at end of file + before_script: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4f039d60 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM ubuntu:16.04 + +# docker build -t vtquery . +# docker run -it vtquery + +RUN apt-get update -y && \ + apt-get install -y build-essential bash curl git-core ca-certificates software-properties-common vim python-software-properties --no-install-recommends + +RUN add-apt-repository -y ppa:ubuntu-toolchain-r/test && \ + apt-get update -y + +RUN curl https://nodejs.org/dist/v4.8.4/node-v4.8.4-linux-x64.tar.gz | tar zxC /usr/local --strip-components=1 + +ENV PATH=/usr/local/src/mason_packages/.link/bin:${PATH} CXX=clang++ +WORKDIR /usr/local/src +COPY ./ ./ + +RUN ./scripts/setup.sh --config local.env +RUN /bin/bash -c "source local.env && make" diff --git a/README.md b/README.md index 8cdd9e18..5a5623ee 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,33 @@ Get the closest features from a longitude/latitude in a set of vector tile buffe 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 +2. Point in polygon checks (`radius=0`) + +# API + + + +## vtquery + +**Parameters** + +- `tiles` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)>** an array of tile objects with `buffer`, `z`, `x`, and `y` values +- `LngLat` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)>** a query point of longitude and latitude to query, `[lng, lat]` +- `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** + - `options.radius` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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 realstic list of features (optional, default `0`) + - `options.results` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** the number of results/features returned from the query. (optional, default `5`) + - `options.layers` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>?** an array of layer string names to query from. Default is all layers. + - `options.geometry` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)?** only return features of a particular geometry type. Can be `point`, `linestring`, or `polygon`. + Defaults to all geometry types. + +**Examples** + +```javascript +const vtquery = require('@mapbox/vtquery'); + +vtquery(tiles, lnglat, options, callback); +``` # Response object @@ -55,29 +81,13 @@ Here's an example response } ``` -# API - -## vtquery - -**Parameters** +## Point in polygon queries -- `tiles` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array).<[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)>** an array of tile objects with `buffer`, `z`, `x`, and `y` values -- `LngLat` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array).<[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)>** a query point of longitude and latitude to query, `[lng, lat]` -- `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)=** - - `options.radius` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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 realstic list of features (optional, default `0`) - - `options.results` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)=** the number of results/features returned from the query. (optional, default `5`) - - `options.layers` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array).<[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>=** an array of layer string names to query from. Default is all layers. - - `options.geometry` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)=** only return features of a particular geometry type. Can be `point`, `linestring`, or `polygon`. - Defaults to all geometry types. +To perform a "point in polygon" query, set your radius value to `0`. This will only return polygons that your query point is _within_. -**Examples** +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. -```javascript -const vtquery = require('@mapbox/vtquery'); - -vtquery(tiles, lnglat, options, callback); -``` +GOTCHA 2: Any query point that exists _directly_ along an edge of a polygon will _not_ return. # Develop @@ -105,26 +115,39 @@ npm install -g documentation npm run docs ``` +To install and test on a linux instance, you can use the Dockerfile provided. + +```shell +# 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 -``` + 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: 9 tiles, chicago, radius 1000 ... 191 runs/s (5240ms) -2: 9 tiles, chicago, only points ... 3257 runs/s (307ms) -3: mbx streets no radius ... 4049 runs/s (247ms) -4: mbx streets 2000 radius ... 240 runs/s (4159ms) -5: mbx streets only points ... 4673 runs/s (214ms) -6: mbx streets only linestrings ... 286 runs/s (3499ms) -7: mbx streets only polys ... 2801 runs/s (357ms) -8: complex multipolygon ... 1253 runs/s (798ms) -``` + 1: 9 tiles, chicago, radius 1000 ... 191 runs/s (5240ms) + 2: 9 tiles, chicago, only points ... 3257 runs/s (307ms) + 3: mbx streets no radius ... 4049 runs/s (247ms) + 4: mbx streets 2000 radius ... 240 runs/s (4159ms) + 5: mbx streets only points ... 4673 runs/s (214ms) + 6: mbx streets only linestrings ... 286 runs/s (3499ms) + 7: mbx streets only polys ... 2801 runs/s (357ms) + 8: complex multipolygon ... 1253 runs/s (798ms) # Viz diff --git a/scripts/install_deps.sh b/scripts/install_deps.sh index d7c583a2..0053d247 100755 --- a/scripts/install_deps.sh +++ b/scripts/install_deps.sh @@ -12,10 +12,11 @@ function install() { ./scripts/setup.sh --config local.env source local.env -install geometry 0.9.2 +install geometry 96d3505 install variant 1.1.4 -install vtzero 556fac5 -install protozero ccf6c39 -install spatial-algorithms 2904283 +install vtzero 1c38ce7 +install protozero 1.6.0 +install spatial-algorithms cdda174 install boost 1.65.1 install cheap-ruler 2.5.3 +install vector-tile 0390175 diff --git a/scripts/setup.sh b/scripts/setup.sh index b28b9fbe..e2a19e80 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -3,7 +3,7 @@ set -eu set -o pipefail -export MASON_RELEASE="${MASON_RELEASE:-469edc8}" +export MASON_RELEASE="${MASON_RELEASE:-f4c9c09}" export MASON_LLVM_RELEASE="${MASON_LLVM_RELEASE:-5.0.0}" PLATFORM=$(uname | tr A-Z a-z) diff --git a/src/geometry_processors.hpp b/src/geometry_processors.hpp deleted file mode 100644 index 1fc7eff0..00000000 --- a/src/geometry_processors.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#include -#include -#include - -// convert the point geometry from vtzero into mapbox geometry point structure -// https://gist.github.com/artemp/cfe855cbaf0cbc277ce80b5e768e7d0b#file-gistfile1-txt-L24 -struct point_processor { - point_processor(mapbox::geometry::multi_point& mpoint) - : mpoint_(mpoint) {} - - void points_begin(uint32_t count) const { - if (count > 1) mpoint_.reserve(count); - } - - void points_point(vtzero::point const& point) const { - mpoint_.emplace_back(point.x, point.y); - } - - void points_end() const { - // - } - mapbox::geometry::multi_point& mpoint_; -}; - -struct linestring_processor { - linestring_processor(mapbox::geometry::multi_line_string& mline) - : mline_(mline) {} - - void linestring_begin(std::uint32_t count) { - mline_.emplace_back(); - mline_.back().reserve(count); - } - - void linestring_point(vtzero::point const& point) const { - mline_.back().emplace_back(point.x, point.y); - } - - void linestring_end() const noexcept { - } - mapbox::geometry::multi_line_string& mline_; -}; - -struct polygon_processor { - polygon_processor(mapbox::geometry::multi_polygon& mpoly) - : mpoly_(mpoly), - ring_() {} - - void ring_begin(std::uint32_t count) { - ring_.reserve(count); - } - - void ring_point(vtzero::point const& point) { - ring_.emplace_back(point.x, point.y); - } - - void ring_end(bool is_outer) { - if (is_outer) mpoly_.emplace_back(); - mpoly_.back().push_back(std::move(ring_)); - ring_.clear(); - } - - mapbox::geometry::multi_polygon& mpoly_; - mapbox::geometry::linear_ring ring_; -}; diff --git a/src/util.hpp b/src/util.hpp index 54bbb8b4..4eef76de 100644 --- a/src/util.hpp +++ b/src/util.hpp @@ -83,7 +83,7 @@ mapbox::geometry::point convert_vt_to_ll(std::uint32_t extent, std::uint32_t z, std::uint32_t x, std::uint32_t y, - mapbox::geometry::algorithms::closest_point_info cp_info) { + mapbox::geometry::algorithms::closest_point_info cp_info) { double z2 = static_cast(static_cast(1) << z); double ex = static_cast(extent); double size = ex * z2; diff --git a/src/vtquery.cpp b/src/vtquery.cpp index 1ce64585..ac755151 100644 --- a/src/vtquery.cpp +++ b/src/vtquery.cpp @@ -1,5 +1,5 @@ #include "vtquery.hpp" -#include "geometry_processors.hpp" +// #include "geometry_processors.hpp" #include "util.hpp" #include @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -22,17 +24,21 @@ enum GeomType { point, polygon, all, unknown }; -static const char* GeomTypeStrings[] = {"point", "linestring", "polygon", "uknown", "unknown"}; +static const char* GeomTypeStrings[] = {"point", "linestring", "polygon", "unknown"}; const char* getGeomTypeString(int enumVal) { return GeomTypeStrings[enumVal]; } struct ResultObject { - using properties_type = std::vector>; + mapbox::feature::property_map properties; + std::string layer_name; + mapbox::geometry::point coordinates; + double distance; + GeomType original_geometry_type; // custom constructor ResultObject( - std::vector>&& props_map, // specifies an r-value completely, whatever had the memory beforehand, this now controls it + mapbox::feature::property_map&& props_map, // specifies an r-value completely, whatever had the memory beforehand, this now controls it std::string const& name, mapbox::geometry::point&& p, double distance0, @@ -40,8 +46,8 @@ struct ResultObject { : properties(std::move(props_map)), layer_name(name), coordinates(std::move(p)), - distance(distance0), // these are small enough that we can just copy - original_geometry_type(geom_type) {} // these are small enough that we can just copy + distance(distance0), + original_geometry_type(geom_type) {} // default move constructor ResultObject(ResultObject&&) = default; @@ -51,12 +57,6 @@ struct ResultObject { // use the default destructor ~ResultObject() = default; - - std::vector> properties; - std::string layer_name; - mapbox::geometry::point coordinates; - double distance; - GeomType original_geometry_type; }; struct TileObject { @@ -128,34 +128,54 @@ struct QueryData { GeomType geometry_filter_type; }; -v8::Local get_property_value(const vtzero::property_value_view value) { - switch (value.type()) { - case vtzero::property_value_type::string_value: - return Nan::New(std::string(value.string_value())).ToLocalChecked(); - case vtzero::property_value_type::float_value: - return Nan::New(double(value.float_value())); - case vtzero::property_value_type::double_value: - return Nan::New(value.double_value()); - case vtzero::property_value_type::int_value: - return Nan::New(value.int_value()); - case vtzero::property_value_type::uint_value: - return Nan::New(value.uint_value()); - case vtzero::property_value_type::sint_value: - return Nan::New(value.sint_value()); - default: // case vtzero::property_value_type::bool_value: - return Nan::New(value.bool_value()); +struct property_value_visitor { + v8::Local& properties_obj; + std::string const& key; + + template + void operator()(T) {} + + void operator()(bool v) { + properties_obj->Set(Nan::New(key).ToLocalChecked(), Nan::New(v)); + } + void operator()(uint64_t v) { + properties_obj->Set(Nan::New(key).ToLocalChecked(), Nan::New(v)); + } + void operator()(int64_t v) { + properties_obj->Set(Nan::New(key).ToLocalChecked(), Nan::New(v)); + } + void operator()(double v) { + properties_obj->Set(Nan::New(key).ToLocalChecked(), Nan::New(v)); + } + void operator()(std::string const& v) { + properties_obj->Set(Nan::New(key).ToLocalChecked(), Nan::New(v).ToLocalChecked()); } +}; + +void set_property(mapbox::feature::property_map::value_type const& property, + v8::Local& properties_obj) { + mapbox::util::apply_visitor(property_value_visitor{properties_obj, property.first}, property.second); } +struct CompareDistance { + bool operator()(const ResultObject* r1, const ResultObject* r2) const { + return r1->distance < r2->distance; + } +}; + struct Worker : Nan::AsyncWorker { using Base = Nan::AsyncWorker; + std::unique_ptr query_data_; + std::deque results_; + std::priority_queue, CompareDistance> results_queue_; + Worker(std::unique_ptr query_data, Nan::Callback* cb) : Base(cb), query_data_(std::move(query_data)), results_(), - sorted_results_() {} + results_queue_() {} // The Execute() function is getting called when the worker starts to run. // - You only have access to member variables stored in this worker. @@ -207,41 +227,30 @@ struct Worker : Nan::AsyncWorker { mapbox::geometry::point query_point = utils::create_query_point(data.longitude, data.latitude, data.zoom, extent, tile_obj.x, tile_obj.y); GeomType original_geometry_type = GeomType::unknown; // set to unknown but this will get overwritten while (auto feature = layer.next_feature()) { - // create a dummy default geometry structure that will be updated in the switch statement below mapbox::geometry::geometry query_geometry = mapbox::geometry::point(); - // get the geometry type and decode the geometry into mapbox::geometry data structures switch (feature.geometry_type()) { case vtzero::GeomType::POINT: { if (data.geometry_filter_type != GeomType::all && data.geometry_filter_type != GeomType::point) { continue; } - mapbox::geometry::multi_point mpoint; - point_processor proc_point(mpoint); - vtzero::decode_point_geometry(feature.geometry(), false, proc_point); - query_geometry = std::move(mpoint); original_geometry_type = GeomType::point; + query_geometry = mapbox::vector_tile::extract_geometry(feature); break; } case vtzero::GeomType::LINESTRING: { if (data.geometry_filter_type != GeomType::all && data.geometry_filter_type != GeomType::linestring) { continue; } - mapbox::geometry::multi_line_string mline; - linestring_processor proc_line(mline); - vtzero::decode_linestring_geometry(feature.geometry(), false, proc_line); - query_geometry = std::move(mline); original_geometry_type = GeomType::linestring; + query_geometry = mapbox::vector_tile::extract_geometry(feature); break; } case vtzero::GeomType::POLYGON: { if (data.geometry_filter_type != GeomType::all && data.geometry_filter_type != GeomType::polygon) { continue; } - mapbox::geometry::multi_polygon mpoly; - polygon_processor proc_poly(mpoly); - vtzero::decode_polygon_geometry(feature.geometry(), false, proc_poly); - query_geometry = std::move(mpoly); original_geometry_type = GeomType::polygon; + query_geometry = mapbox::vector_tile::extract_geometry(feature); break; } default: { @@ -252,42 +261,63 @@ struct Worker : Nan::AsyncWorker { // implement closest point algorithm on query geometry and the query point auto const cp_info = mapbox::geometry::algorithms::closest_point(query_geometry, query_point); - // convert x/y into lng/lat point - auto feature_lnglat = utils::convert_vt_to_ll(extent, tile_obj.z, tile_obj.x, tile_obj.y, cp_info); - auto meters = utils::distance_in_meters(query_lnglat, feature_lnglat); - - // if the distance is within the threshold, save it - if (meters <= data.radius + 1) { // TODO(sam) https://github.com/mapbox/vtquery/issues/36 + // check if cp_info.distance isn't less than zero, if so, this is an error and we can move on + if (cp_info.distance < 0.0) { + continue; + } - // decode properties (will be libvectortile eventually) - ResultObject::properties_type properties_list; - while (auto prop = feature.next_property()) { - std::string key = std::string{prop.key()}; - vtzero::property_value_view value = prop.value(); - properties_list.emplace_back(std::pair(key, value)); + // if the distance is greater than 0.0, get the distance from the query point + // otherwise if the distance is 0.0, add it to the queue + if (cp_info.distance > 0.0) { + // convert x/y into lng/lat point + auto feature_lnglat = utils::convert_vt_to_ll(extent, tile_obj.z, tile_obj.x, tile_obj.y, cp_info); + auto meters = utils::distance_in_meters(query_lnglat, feature_lnglat); + + // if the distance is within the threshold, save it + if (meters <= data.radius) { + + // if the queue size is still smaller than number of results, just add the hit + // if the queue size is at its max, compare the top() result and only add if this new + // hit is smaller than the top result + if (results_queue_.size() < data.num_results) { + auto props = mapbox::vector_tile::extract_properties(feature); + results_.emplace_back(std::move(props), + layer_name, + std::move(feature_lnglat), + meters, + original_geometry_type); + results_queue_.emplace(&results_.back()); + } else if (meters < results_queue_.top()->distance) { + results_queue_.pop(); + auto props = mapbox::vector_tile::extract_properties(feature); + results_.emplace_back(std::move(props), + layer_name, + std::move(feature_lnglat), + meters, + original_geometry_type); + results_queue_.emplace(&results_.back()); + } + } + } else { + auto qp = mapbox::geometry::point{data.longitude, data.latitude}; // original query lng/lat + auto props = mapbox::vector_tile::extract_properties(feature); + + // if our results list is already full, remove the top() element and add the new one - this way the + // we'll always be removing the largest element by distance and replacing with a 0.0 result + if (results_queue_.size() == data.num_results) { + results_queue_.pop(); } - // emplace_back allows us to put a new object directly into the vector - // wherease push_back we need to create a new object in memory and move it into the vector - results_.emplace_back(std::move(properties_list), + results_.emplace_back(std::move(props), layer_name, - std::move(feature_lnglat), - meters, + std::move(qp), + 0.0, original_geometry_type); + results_queue_.emplace(&results_.back()); } } // end tile.layer.feature loop } // end tile.layer loop } // end tile loop - - // create sort vector - sorted_results_.reserve(results_.size()); - for (auto& r : results_) { - sorted_results_.push_back(&r); // save the pointer of r - } - - // sort based on distance - std::sort(sorted_results_.begin(), sorted_results_.end(), [](ResultObject const* a, ResultObject const* b) { return a->distance < b->distance; }); - } catch (const std::exception& e) { SetErrorMessage(e.what()); } @@ -299,24 +329,20 @@ struct Worker : Nan::AsyncWorker { Nan::HandleScope scope; // number of results to loop through - QueryData const& data = *query_data_; - std::uint32_t num_results = data.num_results; - v8::Local results_object = Nan::New(); - v8::Local features_array = Nan::New(); - std::uint32_t features_size = 0; - + v8::Local features_array = Nan::New(results_queue_.size()); results_object->Set(Nan::New("type").ToLocalChecked(), Nan::New("FeatureCollection").ToLocalChecked()); // for each result object - for (auto feature : sorted_results_) { + while (!results_queue_.empty()) { + auto const& feature = results_queue_.top(); // get reference to top item in results queue v8::Local feature_obj = Nan::New(); feature_obj->Set(Nan::New("type").ToLocalChecked(), Nan::New("Feature").ToLocalChecked()); // create geometry object v8::Local geometry_obj = Nan::New(); geometry_obj->Set(Nan::New("type").ToLocalChecked(), Nan::New("Point").ToLocalChecked()); - v8::Local coordinates_array = Nan::New(); + v8::Local coordinates_array = Nan::New(2); coordinates_array->Set(0, Nan::New(feature->coordinates.x)); // latitude coordinates_array->Set(1, Nan::New(feature->coordinates.y)); // longitude geometry_obj->Set(Nan::New("coordinates").ToLocalChecked(), coordinates_array); @@ -324,12 +350,12 @@ struct Worker : Nan::AsyncWorker { // create properties object v8::Local properties_obj = Nan::New(); - v8::Local tilequery_properties_obj = Nan::New(); for (auto const& prop : feature->properties) { - properties_obj->Set(Nan::New(prop.first).ToLocalChecked(), get_property_value(prop.second)); + set_property(prop, properties_obj); } // set properties.tilquery + v8::Local tilequery_properties_obj = Nan::New(); tilequery_properties_obj->Set(Nan::New("distance").ToLocalChecked(), Nan::New(feature->distance)); std::string og_geom = getGeomTypeString(feature->original_geometry_type); tilequery_properties_obj->Set(Nan::New("geometry").ToLocalChecked(), Nan::New(og_geom).ToLocalChecked()); @@ -340,11 +366,8 @@ struct Worker : Nan::AsyncWorker { feature_obj->Set(Nan::New("properties").ToLocalChecked(), properties_obj); // add feature to features array - features_array->Set(features_size, feature_obj); - features_size++; - if (features_size >= num_results) { - break; - } + features_array->Set(static_cast(results_queue_.size() - 1), feature_obj); + results_queue_.pop(); // remove item from results queue } results_object->Set(Nan::New("features").ToLocalChecked(), features_array); @@ -356,10 +379,6 @@ struct Worker : Nan::AsyncWorker { // Static cast done here to avoid 'cppcoreguidelines-pro-bounds-array-to-pointer-decay' warning with clang-tidy callback->Call(argc, static_cast*>(argv)); } - - std::unique_ptr query_data_; - std::deque results_; - std::vector sorted_results_; }; NAN_METHOD(vtquery) { diff --git a/test/fixtures/expected-order.json b/test/fixtures/expected-order.json new file mode 100644 index 00000000..cbd0c02a --- /dev/null +++ b/test/fixtures/expected-order.json @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-87.7987,41.8451]},"properties":{"type":"school","class":"school","tilequery":{"distance":0,"geometry":"polygon","layer":"landuse"}}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-87.7987,41.8451]},"properties":{"type":"recreation_ground","class":"park","tilequery":{"distance":0,"geometry":"polygon","layer":"landuse"}}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-87.79833505548643,41.84510735740068]},"properties":{"name_fr":"Home Ave","name_zh-Hans":"Home Ave","name_es":"Home Ave","name_pt":"Home Ave","name_ar":"Home Ave","name":"Home Ave","name_zh":"Home Ave","name_de":"Home Ave","localrank":9,"name_ru":"Home Ave","len":4040,"name_en":"Home Ave","iso_3166_2":"US-IL","class":"street","tilequery":{"distance":30.32099616026162,"geometry":"linestring","layer":"road_label"}}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-87.79833505548643,41.84510735740068]},"properties":{"structure":"none","type":"residential","oneway":"false","class":"street","tilequery":{"distance":30.32099616026162,"geometry":"linestring","layer":"road"}}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-87.79832482337952,41.84522846901311]},"properties":{"structure":"none","type":"service:alley","oneway":"false","class":"service","tilequery":{"distance":34.27147583628124,"geometry":"linestring","layer":"road"}}}]} diff --git a/test/vtquery.test.js b/test/vtquery.test.js index 2c946190..dffb6a51 100644 --- a/test/vtquery.test.js +++ b/test/vtquery.test.js @@ -1,3 +1,5 @@ +'use strict'; + const test = require('tape'); const path = require('path'); const vtquery = require('../lib/index.js'); @@ -7,6 +9,10 @@ const fs = require('fs'); const bufferSF = fs.readFileSync(path.resolve(__dirname+'/../node_modules/@mapbox/mvt-fixtures/real-world/sanfrancisco/15-5238-12666.mvt')); +function checkClose(a, b, epsilon) { + return 1*(a-b) < epsilon; +} + test('failure: fails without callback function', assert => { try { vtquery(); @@ -424,6 +430,48 @@ test('options - radius: all results within radius', assert => { }); }); +test('options - radius: all results are in expected order', assert => { + const expected = JSON.parse(fs.readFileSync(__dirname + '/fixtures/expected-order.json')); + const buffer = fs.readFileSync(path.resolve(__dirname+'/../node_modules/@mapbox/mvt-fixtures/real-world/chicago/13-2098-3045.mvt')); + const ll = [-87.7987, 41.8451]; + vtquery([{buffer: buffer, z: 13, x: 2098, y: 3045}], ll, { radius: 50 }, function(err, result) { + assert.ifError(err); + result.features.forEach(function(feature, i) { + let e = expected.features[i].properties; + assert.ok(checkClose(e.tilequery.distance, feature.properties.tilequery.distance, 1e-6), 'expected distance'); + assert.equal(e.tilequery.layer, feature.properties.tilequery.layer, 'same layer'); + assert.equal(e.tilequery.geometry, feature.properties.tilequery.geometry, 'same geometry'); + if (feature.properties.type) { + assert.equal(e.type, feature.properties.type, 'same type'); + } + }); + assert.end(); + }); +}); + +test('options - radius=0: only returns "point in polygon" results (on a building)', assert => { + const buffer = bufferSF; + const ll = [-122.4527, 37.7689]; // direct hit on a building + vtquery([{buffer: buffer, z: 15, x: 5238, y: 12666}], ll, { radius: 0, layers: ['building'] }, function(err, result) { + assert.ifError(err); + assert.equal(result.features.length, 1, 'only one building returned'); + assert.deepEqual(result.features[0].properties.tilequery, { distance: 0.0, layer: 'building', geometry: 'polygon' }, 'expected tilequery info'); + assert.end(); + }); +}); + +test('options - radius=0: returns only radius 0.0 results', assert => { + const buffer = bufferSF; + const ll = [-122.4527, 37.7689]; // direct hit on a building + vtquery([{buffer: buffer, z: 15, x: 5238, y: 12666}], ll, { radius: 0, numResults: 100 }, function(err, result) { + assert.ifError(err); + result.features.forEach(function(feature) { + assert.ok(feature.properties.tilequery.distance == 0.0, 'radius 0.0'); + }); + assert.end(); + }); +}); + test('options - numResults: successfully limits results', assert => { const buffer = bufferSF; const ll = [-122.4477, 37.7665]; // direct hit @@ -446,6 +494,28 @@ test('options - layers: successfully returns only requested layers', assert => { }); }); +test('options - layers: returns zero results for a layer that does not exist - does not error', assert => { + const buffer = bufferSF; + const ll = [-122.4477, 37.7665]; // direct hit + vtquery([{buffer: buffer, z: 15, x: 5238, y: 12666}], ll, {radius: 2000, layers: ['i_am_not_real']}, function(err, result) { + assert.ifError(err); + assert.equal(result.features.length, 0, 'no features'); + assert.end(); + }); +}); + +test('options - radius: all results within radius', assert => { + const buffer = bufferSF; + const ll = [-122.4477, 37.7665]; // direct hit + vtquery([{buffer: buffer, z: 15, x: 5238, y: 12666}], ll, { numResults: 100, radius: 1000 }, function(err, result) { + assert.ifError(err); + result.features.forEach(function(feature) { + assert.ok(feature.properties.tilequery.distance <= 1000, 'less than radius'); + }); + assert.end(); + }); +}); + test('options - geometry: successfully returns only points', assert => { const buffer = bufferSF; const ll = [-122.4477, 37.7665]; // direct hit