diff --git a/expression-test/expression_test_parser.cpp b/expression-test/expression_test_parser.cpp index 44f8bebf5e3..0d414cc2c08 100644 --- a/expression-test/expression_test_parser.cpp +++ b/expression-test/expression_test_parser.cpp @@ -14,6 +14,8 @@ #include +#include + using namespace mbgl; using namespace mbgl::style; using namespace mbgl::style::conversion; @@ -254,6 +256,19 @@ bool parseInputs(const JSValue& inputsValue, TestData& data) { heatmapDensity = evaluationContext["heatmapDensity"].GetDouble(); } + // Parse canonicalID + optional canonical; + if (evaluationContext.HasMember("canonicalID")) { + assert(evaluationContext["canonicalID"].IsObject()); + assert( + evaluationContext["canonicalID"].HasMember("z") && evaluationContext["canonicalID"]["z"].IsNumber() && + evaluationContext["canonicalID"].HasMember("x") && evaluationContext["canonicalID"]["x"].IsNumber() && + evaluationContext["canonicalID"].HasMember("y") && evaluationContext["canonicalID"]["y"].IsNumber()); + canonical = CanonicalTileID(evaluationContext["canonicalID"]["z"].GetUint(), + evaluationContext["canonicalID"]["x"].GetUint(), + evaluationContext["canonicalID"]["y"].GetUint()); + } + // Parse availableImages std::set availableImages; if (evaluationContext.HasMember("availableImages")) { @@ -282,8 +297,11 @@ bool parseInputs(const JSValue& inputsValue, TestData& data) { feature.id = mapbox::geojson::convert(featureObject["id"]); } - data.inputs.emplace_back( - std::move(zoom), std::move(heatmapDensity), std::move(availableImages), std::move(feature)); + data.inputs.emplace_back(std::move(zoom), + std::move(heatmapDensity), + std::move(canonical), + std::move(availableImages), + std::move(feature)); } return true; } @@ -294,11 +312,11 @@ std::tuple, bool, uint32_t> pars args::ArgumentParser argumentParser("Mapbox GL Expression Test Runner"); args::HelpFlag helpFlag(argumentParser, "help", "Display this help menu", { 'h', "help" }); - args::Flag shuffleFlag(argumentParser, "shuffle", "Toggle shuffling the tests order", - { 's', "shuffle" }); + args::Flag shuffleFlag(argumentParser, "shuffle", "Toggle shuffling the tests order", {'s', "shuffle"}); args::ValueFlag seedValue(argumentParser, "seed", "Shuffle seed (default: random)", { "seed" }); args::PositionalList testNameValues(argumentParser, "URL", "Test name(s)"); + args::ValueFlag testFilterValue(argumentParser, "filter", "Test filter regex", {'f', "filter"}); try { argumentParser.ParseCLI(argc, argv); @@ -336,6 +354,7 @@ std::tuple, bool, uint32_t> pars paths.emplace_back(rootPath); } + auto testFilter = testFilterValue ? args::get(testFilterValue) : std::string{}; // Recursively traverse through the test paths and collect test directories containing "test.json". std::vector testPaths; testPaths.reserve(paths.size()); @@ -346,6 +365,9 @@ std::tuple, bool, uint32_t> pars } for (auto& testPath : filesystem::recursive_directory_iterator(path)) { + if (!testFilter.empty() && !std::regex_search(testPath.path().string(), std::regex(testFilter))) { + continue; + } if (testPath.path().filename() == "test.json") { testPaths.emplace_back(testPath.path()); } diff --git a/expression-test/expression_test_parser.hpp b/expression-test/expression_test_parser.hpp index 842d8a15634..b73dbf0539e 100644 --- a/expression-test/expression_test_parser.hpp +++ b/expression-test/expression_test_parser.hpp @@ -16,14 +16,17 @@ using namespace mbgl; struct Input { Input(optional zoom_, optional heatmapDensity_, + optional canonical_, std::set availableImages_, Feature feature_) : zoom(std::move(zoom_)), heatmapDensity(std::move(heatmapDensity_)), + canonical(std::move(canonical_)), availableImages(std::move(availableImages_)), feature(std::move(feature_)) {} optional zoom; optional heatmapDensity; + optional canonical; std::set availableImages; Feature feature; }; diff --git a/expression-test/expression_test_runner.cpp b/expression-test/expression_test_runner.cpp index 436e4499218..c0d45116367 100644 --- a/expression-test/expression_test_runner.cpp +++ b/expression-test/expression_test_runner.cpp @@ -104,8 +104,14 @@ TestRunOutput runExpressionTest(TestData& data, const std::string& rootPath, con std::vector outputs; if (!data.inputs.empty()) { for (const auto& input : data.inputs) { - auto evaluationResult = - expression->evaluate(input.zoom, input.feature, input.heatmapDensity, input.availableImages); + mbgl::style::expression::EvaluationResult evaluationResult; + if (input.canonical) { + evaluationResult = expression->evaluate( + input.zoom, input.feature, input.heatmapDensity, input.availableImages, *input.canonical); + } else { + evaluationResult = + expression->evaluate(input.zoom, input.feature, input.heatmapDensity, input.availableImages); + } if (!evaluationResult) { std::unordered_map error{{"error", Value{evaluationResult.error().message}}}; outputs.emplace_back(Value{std::move(error)}); diff --git a/include/mbgl/style/expression/expression.hpp b/include/mbgl/style/expression/expression.hpp index 1e34a8bd387..0fd5c4959ea 100644 --- a/include/mbgl/style/expression/expression.hpp +++ b/include/mbgl/style/expression/expression.hpp @@ -191,6 +191,11 @@ class Expression { const Feature& feature, optional colorRampParameter, const std::set& availableImages) const; + EvaluationResult evaluate(optional zoom, + const Feature& feature, + optional colorRampParameter, + const std::set& availableImages, + const CanonicalTileID& canonical) const; EvaluationResult evaluate(optional accumulated, const Feature& feature) const; /** diff --git a/include/mbgl/style/expression/within.hpp b/include/mbgl/style/expression/within.hpp index 88e9cc56b89..5cc36ab19c2 100644 --- a/include/mbgl/style/expression/within.hpp +++ b/include/mbgl/style/expression/within.hpp @@ -31,10 +31,10 @@ class Within final : public Expression { return false; } - std::vector> possibleOutputs() const override { return {{false}}; } + std::vector> possibleOutputs() const override { return {{true}, {false}}; } mbgl::Value serialize() const override; - std::string getOperator() const override { return "Within"; } + std::string getOperator() const override { return "within"; } private: GeoJSON geoJSONSource; diff --git a/src/mbgl/style/expression/expression.cpp b/src/mbgl/style/expression/expression.cpp index 3252bb632f7..66e2e30b14a 100644 --- a/src/mbgl/style/expression/expression.cpp +++ b/src/mbgl/style/expression/expression.cpp @@ -9,8 +9,16 @@ namespace expression { class GeoJSONFeature : public GeometryTileFeature { public: const Feature& feature; + mutable optional geometry; GeoJSONFeature(const Feature& feature_) : feature(feature_) {} + GeoJSONFeature(const Feature& feature_, const CanonicalTileID& canonical) : feature(feature_) { + geometry = convertGeometry(feature.geometry, canonical); + // https://github.com/mapbox/geojson-vt-cpp/issues/44 + if (getType() == FeatureType::Polygon) { + geometry = fixupPolygons(*geometry); + } + } FeatureType getType() const override { return apply_visitor(ToFeatureType(), feature.geometry); @@ -24,6 +32,11 @@ class GeoJSONFeature : public GeometryTileFeature { } return optional(); } + const GeometryCollection& getGeometries() const override { + if (geometry) return *geometry; + geometry = GeometryCollection(); + return *geometry; + } }; EvaluationResult Expression::evaluate(optional zoom, @@ -41,6 +54,17 @@ EvaluationResult Expression::evaluate(optional zoom, return this->evaluate(EvaluationContext(zoom, &f, colorRampParameter).withAvailableImages(&availableImages)); } +EvaluationResult Expression::evaluate(optional zoom, + const Feature& feature, + optional colorRampParameter, + const std::set& availableImages, + const CanonicalTileID& canonical) const { + GeoJSONFeature f(feature, canonical); + return this->evaluate(EvaluationContext(zoom, &f, colorRampParameter) + .withAvailableImages(&availableImages) + .withCanonicalTileID(&canonical)); +} + EvaluationResult Expression::evaluate(optional accumulated, const Feature& feature) const { GeoJSONFeature f(feature); return this->evaluate(EvaluationContext(accumulated, &f)); diff --git a/src/mbgl/style/expression/within.cpp b/src/mbgl/style/expression/within.cpp index d02ea47cb22..a26f6fb7c88 100644 --- a/src/mbgl/style/expression/within.cpp +++ b/src/mbgl/style/expression/within.cpp @@ -7,41 +7,58 @@ #include #include +#include +#include + namespace mbgl { namespace { -double isLeft(mbgl::Point P0, mbgl::Point P1, mbgl::Point P2) { - return ((P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y)); +class PolygonFeature : public GeometryTileFeature { +public: + const Feature& feature; + mutable optional geometry; + + PolygonFeature(const Feature& feature_, const CanonicalTileID& canonical) : feature(feature_) { + geometry = convertGeometry(feature.geometry, canonical); + assert(geometry); + geometry = fixupPolygons(*geometry); + } + + FeatureType getType() const override { return FeatureType::Polygon; } + const PropertyMap& getProperties() const override { return feature.properties; } + FeatureIdentifier getID() const override { return feature.id; } + optional getValue(const std::string& /*key*/) const override { return optional(); } + const GeometryCollection& getGeometries() const override { + assert(geometry); + return *geometry; + } +}; + +bool rayIntersect(const mbgl::Point& p, const mbgl::Point& p1, const mbgl::Point& p2) { + return ((p1.y > p.y) != (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x); } -// winding number algorithm for checking if point inside a ploygon or not. -// http://geomalgorithms.com/a03-_inclusion.html#wn_PnPoly() -bool pointWithinPolygons(mbgl::Point point, const mapbox::geometry::polygon& polys) { - // wn = the winding number (=0 only when point is outside) - int wn = 0; - for (auto poly : polys) { - auto size = poly.size(); - // loop through every edge of the polygon +// ray casting algorithm for detecting if point is in polygon +bool pointWithinPolygon(const mbgl::Point& point, const mapbox::geometry::polygon& polygon) { + bool within = false; + for (auto ring : polygon) { + auto size = ring.size(); + // loop through every edge of the ring for (decltype(size) i = 0; i < size - 1; ++i) { - if (poly[i].y <= point.y) { - if (poly[i + 1].y > point.y) { // upward horizontal crossing from point to edge E(poly[i], poly[i+1]) - if (isLeft(poly[i], poly[i + 1], point) > 0) { - ++wn; - } - } - } else { - if (poly[i + 1].y <= point.y) { // downward crossing - if (isLeft(poly[i], poly[i + 1], point) < 0) { - --wn; - } - } + if (rayIntersect(point, ring[i], ring[i + 1])) { + within = !within; } } - if (wn != 0) { - return true; - } } - return wn != 0; + return within; +} + +bool pointWithinPolygons(const mbgl::Point& point, const mapbox::geometry::multi_polygon& polygons) { + for (auto polygon : polygons) { + auto within = pointWithinPolygon(point, polygon); + if (within) return true; + } + return false; } bool pointsWithinPolygons(const mbgl::GeometryTileFeature& feature, @@ -49,8 +66,11 @@ bool pointsWithinPolygons(const mbgl::GeometryTileFeature& feature, const mbgl::GeoJSON& geoJson) { return geoJson.match( [&feature, &canonical](const mapbox::geometry::geometry& geometrySet) -> bool { - return geometrySet.match( - [&feature, &canonical](const mapbox::geometry::polygon& polygons) -> bool { + mbgl::Feature f(geometrySet); + PolygonFeature polyFeature(f, canonical); + auto refinedGeoSet = convertGeometry(polyFeature, canonical); + return refinedGeoSet.match( + [&feature, &canonical](const mapbox::geometry::multi_polygon& polygons) -> bool { return convertGeometry(feature, canonical) .match( [&polygons](const mapbox::geometry::point& point) -> bool { @@ -68,6 +88,24 @@ bool pointsWithinPolygons(const mbgl::GeometryTileFeature& feature, }, [](const auto&) -> bool { return false; }); }, + [&feature, &canonical](const mapbox::geometry::polygon& polygon) -> bool { + return convertGeometry(feature, canonical) + .match( + [&polygon](const mapbox::geometry::point& point) -> bool { + return pointWithinPolygon(point, polygon); + }, + [&polygon](const mapbox::geometry::multi_point& points) -> bool { + auto result = false; + for (const auto& p : points) { + result = pointWithinPolygon(p, polygon); + if (!result) { + return result; + } + } + return result; + }, + [](const auto&) -> bool { return false; }); + }, [](const auto&) -> bool { return false; }); }, [](const auto&) -> bool { return false; }); @@ -109,11 +147,11 @@ EvaluationResult Within::evaluate(const EvaluationContext& params) const { return false; } auto geometryType = params.feature->getType(); - // Currently only support Point/Points in polygon + // Currently only support Point/Points in Polygon/Polygons if (geometryType == FeatureType::Point) { return pointsWithinPolygons(*params.feature, *params.canonical, geoJSONSource); } else { - mbgl::Log::Warning(mbgl::Event::General, "Within expression currently only support 'Point' geometry type"); + mbgl::Log::Warning(mbgl::Event::General, "within expression currently only support 'Point' geometry type"); } return false; } @@ -122,7 +160,7 @@ ParseResult Within::parse(const Convertible& value, ParsingContext& ctx) { if (isArray(value)) { // object value, quoted with ["Within", value] if (arrayLength(value) != 2) { - ctx.error("'Within' expression requires exactly one argument, but found " + + ctx.error("'within' expression requires exactly one argument, but found " + util::toString(arrayLength(value) - 1) + " instead."); return ParseResult(); } @@ -136,8 +174,37 @@ ParseResult Within::parse(const Convertible& value, ParsingContext& ctx) { return ParseResult(); } +Value valueConverter(const mapbox::geojson::rapidjson_value& v) { + if (v.IsDouble()) { + return v.GetDouble(); + } + if (v.IsString()) { + return std::string(v.GetString()); + } + if (v.IsArray()) { + std::vector result; + for (const auto& m : v.GetArray()) { + result.push_back(valueConverter(m)); + } + return result; + } + // Ignore other types as valid geojson only contains above types. + return Null; +} + mbgl::Value Within::serialize() const { - return std::vector{{getOperator()}, {mapbox::geojson::stringify(geoJSONSource)}}; + std::unordered_map serialized; + rapidjson::CrtAllocator allocator; + const mapbox::geojson::rapidjson_value value = mapbox::geojson::convert(geoJSONSource, allocator); + if (value.IsObject()) { + for (const auto& m : value.GetObject()) { + serialized.emplace(m.name.GetString(), valueConverter(m.value)); + } + } else { + mbgl::Log::Error(mbgl::Event::General, + "Failed to serialize 'within' expression, converted rapidJSON is not an object"); + } + return std::vector{{getOperator(), *fromExpressionValue(serialized)}}; } } // namespace expression diff --git a/src/mbgl/tile/geometry_tile_data.cpp b/src/mbgl/tile/geometry_tile_data.cpp index 838f37f0dae..56bef66c62f 100644 --- a/src/mbgl/tile/geometry_tile_data.cpp +++ b/src/mbgl/tile/geometry_tile_data.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace mbgl { @@ -173,6 +174,82 @@ Feature::geometry_type convertGeometry(const GeometryTileFeature& geometryTileFe return Point(); } +GeometryCollection convertGeometry(const Feature::geometry_type& geometryTileFeature, const CanonicalTileID& tileID) { + const double size = util::EXTENT * std::pow(2, tileID.z); + const double x0 = util::EXTENT * static_cast(tileID.x); + const double y0 = util::EXTENT * static_cast(tileID.y); + + auto latLonToTileCoodinates = [&](const Point& c) { + Point p; + + auto x = (c.x + 180.0) * size / 360.0 - x0; + p.x = + int16_t(util::clamp(x, std::numeric_limits::min(), std::numeric_limits::max())); + + auto y = (180 - (std::log(std::tan((c.y + 90) * M_PI / 360.0)) * 180 / M_PI)) * size / 360 - y0; + p.y = + int16_t(util::clamp(y, std::numeric_limits::min(), std::numeric_limits::max())); + + return p; + }; + + return geometryTileFeature.match( + [&](const Point& point) -> GeometryCollection { return {{latLonToTileCoodinates(point)}}; }, + [&](const MultiPoint& points) -> GeometryCollection { + GeometryCollection result; + std::vector> temp; + for (const auto p : points) { + temp.emplace_back(latLonToTileCoodinates(p)); + } + result = {temp}; + return result; + }, + [&](const LineString& lineString) -> GeometryCollection { + GeometryCollection result; + std::vector> temp; + for (const auto p : lineString) { + temp.emplace_back(latLonToTileCoodinates(p)); + } + result = {temp}; + return result; + }, + [&](const MultiLineString& lineStrings) -> GeometryCollection { + GeometryCollection result; + for (const auto line : lineStrings) { + std::vector> temp; + for (const auto p : line) { + temp.emplace_back(latLonToTileCoodinates(p)); + } + result.emplace_back(temp); + } + return result; + }, + [&](const Polygon polygon) -> GeometryCollection { + GeometryCollection result; + for (const auto line : polygon) { + std::vector> temp; + for (const auto p : line) { + temp.emplace_back(latLonToTileCoodinates(p)); + } + result.emplace_back(temp); + } + return result; + }, + [&](const MultiPolygon polygons) -> GeometryCollection { + GeometryCollection result; + for (const auto polygon : polygons) + for (const auto line : polygon) { + LineString temp; + for (const auto p : line) { + temp.emplace_back(latLonToTileCoodinates(p)); + } + result.emplace_back(temp); + } + return result; + }, + [](const auto&) -> GeometryCollection { return GeometryCollection(); }); +} + Feature convertFeature(const GeometryTileFeature& geometryTileFeature, const CanonicalTileID& tileID) { Feature feature { convertGeometry(geometryTileFeature, tileID) }; feature.properties = geometryTileFeature.getProperties(); diff --git a/src/mbgl/tile/geometry_tile_data.hpp b/src/mbgl/tile/geometry_tile_data.hpp index a0a069f3d11..fc0790db837 100644 --- a/src/mbgl/tile/geometry_tile_data.hpp +++ b/src/mbgl/tile/geometry_tile_data.hpp @@ -82,6 +82,8 @@ void limitHoles(GeometryCollection&, uint32_t maxHoles); Feature::geometry_type convertGeometry(const GeometryTileFeature& geometryTileFeature, const CanonicalTileID& tileID); +GeometryCollection convertGeometry(const Feature::geometry_type& geometryTileFeature, const CanonicalTileID& tileID); + // convert from GeometryTileFeature to Feature (eventually we should eliminate GeometryTileFeature) Feature convertFeature(const GeometryTileFeature&, const CanonicalTileID&); diff --git a/test/fixtures/expression_equality/within.a.json b/test/fixtures/expression_equality/within.a.json new file mode 100644 index 00000000000..c982681537e --- /dev/null +++ b/test/fixtures/expression_equality/within.a.json @@ -0,0 +1,4 @@ +["within", { + "type": "Polygon", + "coordinates": [[[0, 0], [0, 5], [5, 5], [5, 0], [0, 0]]] + }] \ No newline at end of file diff --git a/test/fixtures/expression_equality/within.b.json b/test/fixtures/expression_equality/within.b.json new file mode 100644 index 00000000000..e5805c93a1f --- /dev/null +++ b/test/fixtures/expression_equality/within.b.json @@ -0,0 +1,4 @@ +["within", { + "type": "Polygon", + "coordinates": [[[0, 0], [0, 6], [5, 5], [5, 0], [0, 0]]] + }] \ No newline at end of file diff --git a/test/src/mbgl/test/stub_geometry_tile_feature.hpp b/test/src/mbgl/test/stub_geometry_tile_feature.hpp index e74988df2e9..024e3c6c405 100644 --- a/test/src/mbgl/test/stub_geometry_tile_feature.hpp +++ b/test/src/mbgl/test/stub_geometry_tile_feature.hpp @@ -16,6 +16,9 @@ class StubGeometryTileFeature : public GeometryTileFeature { geometry(std::move(geometry_)) { } + StubGeometryTileFeature(FeatureType type_, GeometryCollection geometry_) + : type(type_), geometry(std::move(geometry_)) {} + PropertyMap properties; FeatureIdentifier id; FeatureType type = FeatureType::Point; diff --git a/test/style/property_expression.test.cpp b/test/style/property_expression.test.cpp index 6334fcbe390..0624cff385d 100644 --- a/test/style/property_expression.test.cpp +++ b/test/style/property_expression.test.cpp @@ -7,6 +7,8 @@ #include #include +#include + using namespace mbgl; using namespace mbgl::style; using namespace mbgl::style::expression; @@ -218,3 +220,99 @@ TEST(PropertyExpression, ImageExpression) { EXPECT_EQ(evaluatedImage.id(), "bicycle-15"s); } } + +TEST(PropertyExpression, WithinExpression) { + CanonicalTileID canonicalTileID(3, 3, 3); + + // geoJSON source must be Polygon.(Currently only supports Polygon) + static const std::string invalidGeoSource = R"({ + "type": "LineString", + "coordinates": [[0, 0], [0, 5], [5, 5], [5, 0]] + })"; + static const std::string validGeoSource = R"data( + { + "type": "Polygon", + "coordinates": [ + [ + [-11.689453125, -9.79567758282973], + [2.021484375, -10.012129557908128], + [-15.99609375, -17.392579271057766], + [-5.9765625, -5.659718554577273], + [-16.259765625, -3.7327083213358336], + [-17.75390625, -12.897489183755892], + [-17.138671875, -21.002471054356715], + [4.482421875, -16.8886597873816], + [3.076171875, -7.01366792756663], + [ -5.9326171875, 0.6591651462894632], + [-16.1279296875, 1.4939713066293239], + [-11.689453125, -9.79567758282973] + ] + ] + })data"; + + // evaluation test with invalid geojson source + { + std::stringstream ss; + ss << std::string(R"(["within", )") << invalidGeoSource << std::string(R"( ])"); + auto expression = createExpression(ss.str().c_str()); + ASSERT_FALSE(expression); + } + + // evaluation test with valid geojson source + std::stringstream ss; + ss << std::string(R"(["within", )") << validGeoSource << std::string(R"( ])"); + auto expression = createExpression(ss.str().c_str()); + ASSERT_TRUE(expression); + PropertyExpression propExpr(std::move(expression)); + + // evaluation test with valid geojson source but FeatureType is not Point (currently only support + // FeatureType::Point) + { + // testLine is inside polygon, but will return false + LineString testLine{{-9.228515625, -17.560246503294888}, {-2.4609375, -16.04581345375217}}; + auto geoLine = convertGeometry(testLine, canonicalTileID); + StubGeometryTileFeature lineFeature(FeatureType::LineString, geoLine); + + auto evaluatedResult = propExpr.evaluate(EvaluationContext(&lineFeature).withCanonicalTileID(&canonicalTileID)); + EXPECT_FALSE(evaluatedResult); + } + + // evaluation test with valid geojson source and valid point features + { + auto getPointFeature = [&canonicalTileID](const Point& testPoint) -> StubGeometryTileFeature { + auto geoPoint = convertGeometry(testPoint, canonicalTileID); + StubGeometryTileFeature pointFeature(FeatureType::Point, geoPoint); + return pointFeature; + }; + + // check `within` algorithm + auto pointFeature = getPointFeature(Point(-10.72265625, -7.27529233637217)); + auto evaluatedResult = + propExpr.evaluate(EvaluationContext(&pointFeature).withCanonicalTileID(&canonicalTileID)); + EXPECT_FALSE(evaluatedResult); + + pointFeature = getPointFeature(Point(-7.646484374999999, -12.382928338487396)); + evaluatedResult = propExpr.evaluate(EvaluationContext(&pointFeature).withCanonicalTileID(&canonicalTileID)); + EXPECT_FALSE(evaluatedResult); + + pointFeature = getPointFeature(Point(-15.2490234375, -2.986927393334863)); + evaluatedResult = propExpr.evaluate(EvaluationContext(&pointFeature).withCanonicalTileID(&canonicalTileID)); + EXPECT_FALSE(evaluatedResult); + + pointFeature = getPointFeature(Point(-10.590820312499998, 2.4601811810210052)); + evaluatedResult = propExpr.evaluate(EvaluationContext(&pointFeature).withCanonicalTileID(&canonicalTileID)); + EXPECT_FALSE(evaluatedResult); + + pointFeature = getPointFeature(Point(-3.9990234375, -4.915832801313164)); + evaluatedResult = propExpr.evaluate(EvaluationContext(&pointFeature).withCanonicalTileID(&canonicalTileID)); + EXPECT_TRUE(evaluatedResult); + + pointFeature = getPointFeature(Point(-1.1865234375, -16.63619187839765)); + evaluatedResult = propExpr.evaluate(EvaluationContext(&pointFeature).withCanonicalTileID(&canonicalTileID)); + EXPECT_TRUE(evaluatedResult); + + pointFeature = getPointFeature(Point(-15.5126953125, -11.73830237143684)); + evaluatedResult = propExpr.evaluate(EvaluationContext(&pointFeature).withCanonicalTileID(&canonicalTileID)); + EXPECT_TRUE(evaluatedResult); + } +}