[core] Port "collision group" plumbing to gl-native.

[node] Hook up map-wide "crossSourceCollisions" option, defaulting to true.
[test] Pass "crossSourceCollisions" test option through test harness; enable cross-source-collisions tests on native.
This commit is contained in:
Chris Loer
2018-09-06 14:58:37 -07:00
committed by Chris Loer
parent 079ba0209e
commit d735d89835
17 changed files with 149 additions and 50 deletions

View File

@@ -37,7 +37,8 @@ public:
Scheduler&,
MapMode mapMode = MapMode::Continuous,
ConstrainMode constrainMode = ConstrainMode::HeightOnly,
ViewportMode viewportMode = ViewportMode::Default);
ViewportMode viewportMode = ViewportMode::Default,
bool crossSourceCollisions = true);
~Map();
// Register a callback that will get called (on the render thread) when all resources have

View File

@@ -6,6 +6,7 @@
- Remove unnecessary memory use when collision debug mode is not enabled ([#12294](https://github.com/mapbox/mapbox-gl-native/issues/12294))
- Added support for rendering `symbol-placement: line-center` ([#12337](https://github.com/mapbox/mapbox-gl-native/pull/12337))
- Add support for feature expressions in `line-pattern`, `fill-pattern`, and `fill-extrusion-pattern` properties. [#12284](https://github.com/mapbox/mapbox-gl-native/pull/12284)
- Add `crossSourceCollisions` map option, with default of `true`. When set to `false`, cross-source collision detection is disabled. ([#12820] (https://github.com/mapbox/mapbox-gl-native/issues/12820))
# 3.5.8 - October 19, 2017
- Fixes an issue that causes memory leaks when not deleting the frontend object

View File

@@ -621,7 +621,10 @@ void NodeMap::cancel() {
frontend = std::make_unique<mbgl::HeadlessFrontend>(mbgl::Size{ 256, 256 }, pixelRatio, *this, threadpool);
map = std::make_unique<mbgl::Map>(*frontend, mapObserver, frontend->getSize(), pixelRatio,
*this, threadpool, mode);
*this, threadpool, mode,
mbgl::ConstrainMode::HeightOnly,
mbgl::ViewportMode::Default,
crossSourceCollisions);
// FIXME: Reload the style after recreating the map. We need to find
// a better way of canceling an ongoing rendering on the core level
@@ -1212,6 +1215,14 @@ NodeMap::NodeMap(v8::Local<v8::Object> options)
return mbgl::MapMode::Static;
}
}())
, crossSourceCollisions([&] {
Nan::HandleScope scope;
return Nan::Has(options, Nan::New("crossSourceCollisions").ToLocalChecked()).FromJust()
? Nan::Get(options, Nan::New("crossSourceCollisions").ToLocalChecked())
.ToLocalChecked()
->BooleanValue()
: true;
}())
, mapObserver(NodeMapObserver())
, frontend(std::make_unique<mbgl::HeadlessFrontend>(mbgl::Size { 256, 256 }, pixelRatio, *this, threadpool))
, map(std::make_unique<mbgl::Map>(*frontend,
@@ -1220,7 +1231,10 @@ NodeMap::NodeMap(v8::Local<v8::Object> options)
pixelRatio,
*this,
threadpool,
mode)),
mode,
mbgl::ConstrainMode::HeightOnly,
mbgl::ViewportMode::Default,
crossSourceCollisions)),
async(new uv_async_t) {
async->data = this;

View File

@@ -82,6 +82,7 @@ public:
const float pixelRatio;
mbgl::MapMode mode;
bool crossSourceCollisions;
NodeThreadPool threadpool;
NodeMapObserver mapObserver;
std::unique_ptr<mbgl::HeadlessFrontend> frontend;

View File

@@ -95,7 +95,6 @@
"render-tests/geojson/inline-linestring-fill": "current behavior is arbitrary",
"render-tests/geojson/inline-polygon-symbol": "behavior needs reconciliation with gl-js",
"render-tests/icon-rotate/with-offset": "https://github.com/mapbox/mapbox-gl-native/issues/11872",
"render-tests/icon-no-cross-source-collision/default": "skip - gl-js only",
"render-tests/mixed-zoom/z10-z11": "https://github.com/mapbox/mapbox-gl-native/issues/10397",
"render-tests/raster-masking/overlapping-zoom": "https://github.com/mapbox/mapbox-gl-native/issues/10195",
"render-tests/real-world/bangkok": "https://github.com/mapbox/mapbox-gl-native/issues/10412",
@@ -122,7 +121,6 @@
"render-tests/text-field/formatted-arabic": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624",
"render-tests/text-field/formatted-line": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624",
"render-tests/text-field/formatted": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624",
"render-tests/text-no-cross-source-collision/default": "skip - gl-js only",
"render-tests/text-pitch-alignment/auto-text-rotation-alignment-map": "https://github.com/mapbox/mapbox-gl-native/issues/9732",
"render-tests/text-pitch-alignment/map-text-rotation-alignment-map": "https://github.com/mapbox/mapbox-gl-native/issues/9732",
"render-tests/text-pitch-alignment/viewport-text-rotation-alignment-map": "https://github.com/mapbox/mapbox-gl-native/issues/9732",

View File

@@ -16,7 +16,7 @@ export default function (style, options, callback) {
let map;
if (options.recycleMap) {
const key = options.pixelRatio + '/' + tileMode;
const key = options.pixelRatio + '/' + tileMode + '/' + options.crossSourceCollisions;
if (maps.has(key)) {
map = maps.get(key);
map.request = mapRequest;
@@ -24,7 +24,8 @@ export default function (style, options, callback) {
maps.set(key, new mbgl.Map({
ratio: options.pixelRatio,
request: mapRequest,
mode: options.mapMode
mode: options.mapMode,
crossSourceCollisions: typeof options.crossSourceCollisions === "undefined" ? true : options.crossSourceCollisions
}));
map = maps.get(key);
}
@@ -32,7 +33,8 @@ export default function (style, options, callback) {
map = new mbgl.Map({
ratio: options.pixelRatio,
request: mapRequest,
mode: options.mapMode
mode: options.mapMode,
crossSourceCollisions: typeof options.crossSourceCollisions === "undefined" ? true : options.crossSourceCollisions
});
}

View File

@@ -28,15 +28,17 @@ public:
, bucketLeaderID(std::move(bucketName_))
, sortIndex(sortIndex_)
, bucketInstanceId(0)
, collisionGroupId(0)
{}
IndexedSubfeature(const IndexedSubfeature& other, uint32_t bucketInstanceId_)
IndexedSubfeature(const IndexedSubfeature& other, uint32_t bucketInstanceId_, uint16_t collisionGroupId_)
: index(other.index)
, sourceLayerName(other.sourceLayerName)
, bucketLeaderID(other.bucketLeaderID)
, sortIndex(other.sortIndex)
, bucketInstanceId(bucketInstanceId_)
, collisionGroupId(collisionGroupId_)
{}
size_t index;
std::string sourceLayerName;
@@ -45,6 +47,7 @@ public:
// Only used for symbol features
uint32_t bucketInstanceId;
uint16_t collisionGroupId;
};
class FeatureIndex {

View File

@@ -44,7 +44,8 @@ public:
Scheduler&,
MapMode,
ConstrainMode,
ViewportMode);
ViewportMode,
bool);
~Impl();
@@ -73,6 +74,7 @@ public:
const MapMode mode;
const float pixelRatio;
const bool crossSourceCollisions;
MapDebugOptions debugOptions { MapDebugOptions::NoDebug };
@@ -96,7 +98,8 @@ Map::Map(RendererFrontend& rendererFrontend,
Scheduler& scheduler,
MapMode mapMode,
ConstrainMode constrainMode,
ViewportMode viewportMode)
ViewportMode viewportMode,
bool crossSourceCollisions)
: impl(std::make_unique<Impl>(*this,
rendererFrontend,
mapObserver,
@@ -105,7 +108,8 @@ Map::Map(RendererFrontend& rendererFrontend,
scheduler,
mapMode,
constrainMode,
viewportMode)) {
viewportMode,
crossSourceCollisions)) {
impl->transform.resize(size);
}
@@ -117,7 +121,8 @@ Map::Impl::Impl(Map& map_,
Scheduler& scheduler_,
MapMode mode_,
ConstrainMode constrainMode_,
ViewportMode viewportMode_)
ViewportMode viewportMode_,
bool crossSourceCollisions_)
: map(map_),
observer(mapObserver),
rendererFrontend(frontend),
@@ -128,6 +133,7 @@ Map::Impl::Impl(Map& map_,
viewportMode_),
mode(mode_),
pixelRatio(pixelRatio_),
crossSourceCollisions(crossSourceCollisions_),
style(std::make_unique<Style>(scheduler, fileSource, pixelRatio)),
annotationManager(*style) {
@@ -783,7 +789,8 @@ void Map::Impl::onUpdate() {
style->impl->getLayerImpls(),
annotationManager,
prefetchZoomDelta,
bool(stillImageRequest)
bool(stillImageRequest),
crossSourceCollisions
};
rendererFrontend.update(std::make_shared<UpdateParameters>(std::move(params)));

View File

@@ -61,7 +61,7 @@ Renderer::Impl::Impl(RendererBackend& backend_,
, sourceImpls(makeMutable<std::vector<Immutable<style::Source::Impl>>>())
, layerImpls(makeMutable<std::vector<Immutable<style::Layer::Impl>>>())
, renderLight(makeMutable<Light::Impl>())
, placement(std::make_unique<Placement>(TransformState{}, MapMode::Static)) {
, placement(std::make_unique<Placement>(TransformState{}, MapMode::Static, true)) {
glyphManager->setObserver(this);
}
@@ -398,7 +398,7 @@ void Renderer::Impl::render(const UpdateParameters& updateParameters) {
if (!placement->stillRecent(parameters.timePoint)) {
placementChanged = true;
auto newPlacement = std::make_unique<Placement>(parameters.state, parameters.mapMode);
auto newPlacement = std::make_unique<Placement>(parameters.state, parameters.mapMode, updateParameters.crossSourceCollisions);
std::set<std::string> usedSymbolLayers;
for (auto it = order.rbegin(); it != order.rend(); ++it) {
if (it->layer.is<RenderSymbolLayer>()) {

View File

@@ -38,6 +38,8 @@ public:
// For still image requests, render requested
const bool stillImageRequest;
const bool crossSourceCollisions;
};
} // namespace mbgl

View File

@@ -89,7 +89,8 @@ std::pair<bool,bool> CollisionIndex::placeFeature(CollisionFeature& feature,
const bool allowOverlap,
const bool pitchWithMap,
const bool collisionDebug,
const optional<CollisionTileBoundaries>& avoidEdges) {
const optional<CollisionTileBoundaries>& avoidEdges,
const optional<std::function<bool(const IndexedSubfeature&)>> collisionGroupPredicate) {
if (!feature.alongLine) {
CollisionBox& box = feature.boxes.front();
const auto projectedPoint = projectAndGetPerspectiveRatio(posMatrix, box.anchor);
@@ -102,13 +103,13 @@ std::pair<bool,bool> CollisionIndex::placeFeature(CollisionFeature& feature,
if ((avoidEdges && !isInsideTile(box, *avoidEdges)) ||
!isInsideGrid(box) ||
(!allowOverlap && collisionGrid.hitTest({{ box.px1, box.py1 }, { box.px2, box.py2 }}))) {
(!allowOverlap && collisionGrid.hitTest({{ box.px1, box.py1 }, { box.px2, box.py2 }}, collisionGroupPredicate))) {
return { false, false };
}
return {true, isOffscreen(box)};
} else {
return placeLineFeature(feature, posMatrix, labelPlaneMatrix, textPixelRatio, symbol, scale, fontSize, allowOverlap, pitchWithMap, collisionDebug, avoidEdges);
return placeLineFeature(feature, posMatrix, labelPlaneMatrix, textPixelRatio, symbol, scale, fontSize, allowOverlap, pitchWithMap, collisionDebug, avoidEdges, collisionGroupPredicate);
}
}
@@ -122,7 +123,8 @@ std::pair<bool,bool> CollisionIndex::placeLineFeature(CollisionFeature& feature,
const bool allowOverlap,
const bool pitchWithMap,
const bool collisionDebug,
const optional<CollisionTileBoundaries>& avoidEdges) {
const optional<CollisionTileBoundaries>& avoidEdges,
const optional<std::function<bool(const IndexedSubfeature&)>> collisionGroupPredicate) {
const auto tileUnitAnchorPoint = symbol.anchorPoint;
const auto projectedAnchor = projectAnchor(posMatrix, tileUnitAnchorPoint);
@@ -222,7 +224,7 @@ std::pair<bool,bool> CollisionIndex::placeLineFeature(CollisionFeature& feature,
inGrid |= isInsideGrid(circle);
if ((avoidEdges && !isInsideTile(circle, *avoidEdges)) ||
(!allowOverlap && collisionGrid.hitTest({{circle.px, circle.py}, circle.radius}))) {
(!allowOverlap && collisionGrid.hitTest({{circle.px, circle.py}, circle.radius}, collisionGroupPredicate))) {
if (!collisionDebug) {
return {false, false};
} else {
@@ -237,7 +239,7 @@ std::pair<bool,bool> CollisionIndex::placeLineFeature(CollisionFeature& feature,
}
void CollisionIndex::insertFeature(CollisionFeature& feature, bool ignorePlacement, uint32_t bucketInstanceId) {
void CollisionIndex::insertFeature(CollisionFeature& feature, bool ignorePlacement, uint32_t bucketInstanceId, uint16_t collisionGroupId) {
if (feature.alongLine) {
for (auto& circle : feature.boxes) {
if (!circle.used) {
@@ -245,18 +247,30 @@ void CollisionIndex::insertFeature(CollisionFeature& feature, bool ignorePlaceme
}
if (ignorePlacement) {
ignoredGrid.insert(IndexedSubfeature(feature.indexedFeature, bucketInstanceId), {{ circle.px, circle.py }, circle.radius});
ignoredGrid.insert(
IndexedSubfeature(feature.indexedFeature, bucketInstanceId, collisionGroupId),
{{ circle.px, circle.py }, circle.radius}
);
} else {
collisionGrid.insert(IndexedSubfeature(feature.indexedFeature, bucketInstanceId), {{ circle.px, circle.py }, circle.radius});
collisionGrid.insert(
IndexedSubfeature(feature.indexedFeature, bucketInstanceId, collisionGroupId),
{{ circle.px, circle.py }, circle.radius}
);
}
}
} else {
assert(feature.boxes.size() == 1);
auto& box = feature.boxes[0];
if (ignorePlacement) {
ignoredGrid.insert(IndexedSubfeature(feature.indexedFeature, bucketInstanceId), {{ box.px1, box.py1 }, { box.px2, box.py2 }});
ignoredGrid.insert(
IndexedSubfeature(feature.indexedFeature, bucketInstanceId, collisionGroupId),
{{ box.px1, box.py1 }, { box.px2, box.py2 }}
);
} else {
collisionGrid.insert(IndexedSubfeature(feature.indexedFeature, bucketInstanceId), {{ box.px1, box.py1 }, { box.px2, box.py2 }});
collisionGrid.insert(
IndexedSubfeature(feature.indexedFeature, bucketInstanceId, collisionGroupId),
{{ box.px1, box.py1 }, { box.px2, box.py2 }}
);
}
}
}

View File

@@ -32,9 +32,10 @@ public:
const bool allowOverlap,
const bool pitchWithMap,
const bool collisionDebug,
const optional<CollisionTileBoundaries>& avoidEdges);
const optional<CollisionTileBoundaries>& avoidEdges,
const optional<std::function<bool(const IndexedSubfeature&)>> collisionGroupPredicate);
void insertFeature(CollisionFeature& feature, bool ignorePlacement, uint32_t bucketInstanceId);
void insertFeature(CollisionFeature& feature, bool ignorePlacement, uint32_t bucketInstanceId, uint16_t collisionGroupId);
std::unordered_map<uint32_t, std::vector<IndexedSubfeature>> queryRenderedSymbols(const ScreenLineString&) const;
@@ -55,7 +56,8 @@ private:
const bool allowOverlap,
const bool pitchWithMap,
const bool collisionDebug,
const optional<CollisionTileBoundaries>& avoidEdges);
const optional<CollisionTileBoundaries>& avoidEdges,
const optional<std::function<bool(const IndexedSubfeature&)>> collisionGroupPredicate);
float approximateTileDistance(const TileDistance& tileDistance, const float lastSegmentAngle, const float pixelsToTileUnits, const float cameraToAnchorDistance, const bool pitchWithMap);

View File

@@ -33,11 +33,33 @@ JointOpacityState::JointOpacityState(const JointOpacityState& prevOpacityState,
bool JointOpacityState::isHidden() const {
return icon.isHidden() && text.isHidden();
}
const CollisionGroups::CollisionGroup& CollisionGroups::get(const std::string& sourceID) {
// The predicate/groupID mechanism allows for arbitrary grouping,
// but the current interface defines one source == one group when
// crossSourceCollisions == true.
if (!crossSourceCollisions) {
if (collisionGroups.find(sourceID) == collisionGroups.end()) {
uint16_t nextGroupID = ++maxGroupID;
collisionGroups.emplace(sourceID, CollisionGroup(
nextGroupID,
optional<Predicate>([nextGroupID](const IndexedSubfeature& feature) -> bool {
return feature.collisionGroupId == nextGroupID;
})
));
}
return collisionGroups[sourceID];
} else {
static CollisionGroup nullGroup{0, nullopt};
return nullGroup;
}
}
Placement::Placement(const TransformState& state_, MapMode mapMode_)
Placement::Placement(const TransformState& state_, MapMode mapMode_, const bool crossSourceCollisions)
: collisionIndex(state_)
, state(state_)
, mapMode(mapMode_)
, collisionGroups(crossSourceCollisions)
{}
void Placement::placeLayer(RenderSymbolLayer& symbolLayer, const mat4& projMatrix, bool showCollisionBoxes) {
@@ -92,7 +114,9 @@ void Placement::placeLayer(RenderSymbolLayer& symbolLayer, const mat4& projMatri
std::forward_as_tuple(symbolBucket.bucketInstanceId),
std::forward_as_tuple(symbolBucket.bucketInstanceId, geometryTile.getFeatureIndex(), geometryTile.id));
placeLayerBucket(symbolBucket, posMatrix, textLabelPlaneMatrix, iconLabelPlaneMatrix, scale, textPixelRatio, showCollisionBoxes, seenCrossTileIDs, renderTile.tile.holdForFade());
const auto collisionGroup = collisionGroups.get(geometryTile.sourceID);
placeLayerBucket(symbolBucket, posMatrix, textLabelPlaneMatrix, iconLabelPlaneMatrix, scale, textPixelRatio, showCollisionBoxes, seenCrossTileIDs, renderTile.tile.holdForFade(), collisionGroup);
}
}
@@ -105,7 +129,8 @@ void Placement::placeLayerBucket(
const float textPixelRatio,
const bool showCollisionBoxes,
std::unordered_set<uint32_t>& seenCrossTileIDs,
const bool holdingForFade) {
const bool holdingForFade,
const CollisionGroups::CollisionGroup& collisionGroup) {
auto partiallyEvaluatedTextSize = bucket.textSizeBinder->evaluateForZoom(state.getZoom());
auto partiallyEvaluatedIconSize = bucket.iconSizeBinder->evaluateForZoom(state.getZoom());
@@ -159,7 +184,7 @@ void Placement::placeLayerBucket(
placedSymbol, scale, fontSize,
bucket.layout.get<style::TextAllowOverlap>(),
bucket.layout.get<style::TextPitchAlignment>() == style::AlignmentType::Map,
showCollisionBoxes, avoidEdges);
showCollisionBoxes, avoidEdges, collisionGroup.second);
placeText = placed.first;
offscreen &= placed.second;
}
@@ -173,7 +198,7 @@ void Placement::placeLayerBucket(
placedSymbol, scale, fontSize,
bucket.layout.get<style::IconAllowOverlap>(),
bucket.layout.get<style::IconPitchAlignment>() == style::AlignmentType::Map,
showCollisionBoxes, avoidEdges);
showCollisionBoxes, avoidEdges, collisionGroup.second);
placeIcon = placed.first;
offscreen &= placed.second;
}
@@ -191,11 +216,11 @@ void Placement::placeLayerBucket(
}
if (placeText) {
collisionIndex.insertFeature(symbolInstance.textCollisionFeature, bucket.layout.get<style::TextIgnorePlacement>(), bucket.bucketInstanceId);
collisionIndex.insertFeature(symbolInstance.textCollisionFeature, bucket.layout.get<style::TextIgnorePlacement>(), bucket.bucketInstanceId, collisionGroup.first);
}
if (placeIcon) {
collisionIndex.insertFeature(symbolInstance.iconCollisionFeature, bucket.layout.get<style::IconIgnorePlacement>(), bucket.bucketInstanceId);
collisionIndex.insertFeature(symbolInstance.iconCollisionFeature, bucket.layout.get<style::IconIgnorePlacement>(), bucket.bucketInstanceId, collisionGroup.first);
}
assert(symbolInstance.crossTileID != 0);

View File

@@ -59,9 +59,27 @@ struct RetainedQueryData {
, tileID(std::move(tileID_)) {}
};
class CollisionGroups {
public:
using Predicate = std::function<bool(const IndexedSubfeature&)>;
using CollisionGroup = std::pair<uint16_t, optional<Predicate>>;
CollisionGroups(const bool crossSourceCollisions_)
: maxGroupID(0)
, crossSourceCollisions(crossSourceCollisions_)
{}
const CollisionGroup& get(const std::string& sourceID);
private:
std::map<std::string, CollisionGroup> collisionGroups;
uint16_t maxGroupID;
bool crossSourceCollisions;
};
class Placement {
public:
Placement(const TransformState&, MapMode mapMode);
Placement(const TransformState&, MapMode mapMode, const bool crossSourceCollisions);
void placeLayer(RenderSymbolLayer&, const mat4&, bool showCollisionBoxes);
void commit(const Placement& prevPlacement, TimePoint);
void updateLayerOpacities(RenderSymbolLayer&);
@@ -86,7 +104,8 @@ private:
const float pixelRatio,
const bool showCollisionBoxes,
std::unordered_set<uint32_t>& seenCrossTileIDs,
const bool holdingForFade);
const bool holdingForFade,
const CollisionGroups::CollisionGroup& collisionGroup);
void updateBucketOpacities(SymbolBucket&, std::set<uint32_t>&);
@@ -103,6 +122,7 @@ private:
bool stale = false;
std::unordered_map<uint32_t, RetainedQueryData> retainedQueryData;
CollisionGroups collisionGroups;
};
} // namespace mbgl

View File

@@ -91,6 +91,8 @@ public:
const optional<ImagePosition> getPattern(const std::string& pattern);
const std::shared_ptr<FeatureIndex> getFeatureIndex() const { return latestFeatureIndex; }
const std::string sourceID;
protected:
const GeometryTileData* getData() {
return latestFeatureIndex ? latestFeatureIndex->getData() : nullptr;
@@ -99,8 +101,6 @@ protected:
private:
void markObsolete();
const std::string sourceID;
// Used to signal the worker that it should abandon parsing this tile as soon as possible.
std::atomic<bool> obsolete { false };

View File

@@ -82,21 +82,29 @@ std::vector<std::pair<T, typename GridIndex<T>::BBox>> GridIndex<T>::queryWithBo
}
template <class T>
bool GridIndex<T>::hitTest(const BBox& queryBBox) const {
bool GridIndex<T>::hitTest(const BBox& queryBBox, optional<std::function<bool(const T&)>> predicate) const {
bool hit = false;
query(queryBBox, [&](const T&, const BBox&) -> bool {
hit = true;
return true;
query(queryBBox, [&](const T& t, const BBox&) -> bool {
if (!predicate || (*predicate)(t)) {
hit = true;
return true;
} else {
return false;
}
});
return hit;
}
template <class T>
bool GridIndex<T>::hitTest(const BCircle& queryBCircle) const {
bool GridIndex<T>::hitTest(const BCircle& queryBCircle, optional<std::function<bool(const T&)>> predicate) const {
bool hit = false;
query(queryBCircle, [&](const T&, const BBox&) -> bool {
hit = true;
return true;
query(queryBCircle, [&](const T& t, const BBox&) -> bool {
if (!predicate || (*predicate)(t)) {
hit = true;
return true;
} else {
return false;
}
});
return hit;
}

View File

@@ -2,6 +2,7 @@
#include <mapbox/geometry/point.hpp>
#include <mapbox/geometry/box.hpp>
#include <mbgl/util/optional.hpp>
#include <cstdint>
#include <cstddef>
@@ -67,8 +68,8 @@ public:
std::vector<T> query(const BBox&) const;
std::vector<std::pair<T,BBox>> queryWithBoxes(const BBox&) const;
bool hitTest(const BBox&) const;
bool hitTest(const BCircle&) const;
bool hitTest(const BBox&, optional<std::function<bool(const T&)>> predicate = nullopt) const;
bool hitTest(const BCircle&, optional<std::function<bool(const T&)>> predicate = nullopt) const;
bool empty() const;