//////////////////////////////////////////////////////////////////////////////// /// DISCLAIMER /// /// Copyright 2017-2018 ArangoDB GmbH, Cologne, Germany /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// /// Copyright holder is ArangoDB GmbH, Cologne, Germany /// /// @author Simon Grätzer //////////////////////////////////////////////////////////////////////////////// #include "Index.h" #include #include #include #include #include #include "Aql/Ast.h" #include "Aql/AstNode.h" #include "Aql/Function.h" #include "Aql/Variable.h" #include "Geo/GeoJson.h" #include "Geo/GeoParams.h" #include "Geo/Utils.h" #include "Geo/ShapeContainer.h" namespace arangodb { namespace geo_index { Index::Index(VPackSlice const& info, std::vector> const& fields) : _variant(Variant::NONE) { _coverParams.fromVelocyPack(info); if (fields.size() == 1) { bool geoJson = basics::VelocyPackHelper::getBooleanValue(info, "geoJson", false); // geojson means [, ] or // json object {type:", coordinates:[]}. _variant = geoJson ? Variant::GEOJSON : Variant::COMBINED_LAT_LON; auto& loc = fields[0]; _location.reserve(loc.size()); std::transform(loc.begin(), loc.end(), std::back_inserter(_location), [](auto const& a) { return a.name; } ); } else if (fields.size() == 2) { _variant = Variant::INDIVIDUAL_LAT_LON; auto& lat = fields[0]; _latitude.reserve(lat.size()); std::transform(lat.begin(), lat.end(), std::back_inserter(_latitude), [](auto const& a) { return a.name; } ); auto& lon = fields[1]; _longitude.reserve(lon.size()); std::transform(lon.begin(), lon.end(), std::back_inserter(_longitude), [](auto const& a) { return a.name; } ); } else { THROW_ARANGO_EXCEPTION_MESSAGE( TRI_ERROR_BAD_PARAMETER, "geo index can only be created with one or two fields."); } } /// @brief Parse document and return cells for indexing Result Index::indexCells(VPackSlice const& doc, std::vector& cells, S2Point& centroid) const { if (_variant == Variant::GEOJSON) { VPackSlice loc = doc.get(_location); if (loc.isArray()) { return geo::utils::indexCellsLatLng(loc, /*geojson*/ true, cells, centroid); } geo::ShapeContainer shape; Result r = geo::geojson::parseRegion(loc, shape); if (r.ok()) { S2RegionCoverer coverer(_coverParams.regionCovererOpts()); cells = shape.covering(&coverer); centroid = shape.centroid(); if (!S2LatLng(centroid).is_valid()) { return TRI_ERROR_BAD_PARAMETER; } } else if (r.is(TRI_ERROR_NOT_IMPLEMENTED)) { // ignore not-implemented error on inserts, because index is sparse return TRI_ERROR_BAD_PARAMETER; } return r; } else if (_variant == Variant::COMBINED_LAT_LON) { VPackSlice loc = doc.get(_location); return geo::utils::indexCellsLatLng(loc, /*geojson*/ false, cells, centroid); } else if (_variant == Variant::INDIVIDUAL_LAT_LON) { VPackSlice lat = doc.get(_latitude); VPackSlice lon = doc.get(_longitude); if (!lat.isNumber() || !lon.isNumber()) { return TRI_ERROR_BAD_PARAMETER; } S2LatLng ll = S2LatLng::FromDegrees(lat.getNumericValue(), lon.getNumericValue()); if (!ll.is_valid()) { LOG_TOPIC("8173c", DEBUG, arangodb::Logger::FIXME) << "illegal geo-coordinates, ignoring entry"; return TRI_ERROR_NO_ERROR; } centroid = ll.ToPoint(); cells.emplace_back(centroid); return TRI_ERROR_NO_ERROR; } return TRI_ERROR_INTERNAL; } Result Index::shape(velocypack::Slice const& doc, geo::ShapeContainer& shape) const { if (_variant == Variant::GEOJSON) { VPackSlice loc = doc.get(_location); if (loc.isArray() && loc.length() >= 2) { return shape.parseCoordinates(loc, /*geoJson*/ true); } else if (loc.isObject()) { return geo::geojson::parseRegion(loc, shape); } return TRI_ERROR_BAD_PARAMETER; } else if (_variant == Variant::COMBINED_LAT_LON) { VPackSlice loc = doc.get(_location); return shape.parseCoordinates(loc, /*geoJson*/ false); } else if (_variant == Variant::INDIVIDUAL_LAT_LON) { VPackSlice lon = doc.get(_longitude); VPackSlice lat = doc.get(_latitude); if (!lon.isNumber() || !lat.isNumber()) { return TRI_ERROR_BAD_PARAMETER; } shape.resetCoordinates(lat.getNumericValue(), lon.getNumericValue()); return TRI_ERROR_NO_ERROR; } return TRI_ERROR_INTERNAL; } // Handle GEO_DISTANCE(, doc.field) S2LatLng Index::parseGeoDistance(aql::AstNode const* args, aql::Variable const* ref) { // aql::AstNode* dist = node->getMemberUnchecked(0); TRI_ASSERT(args->numMembers() == 2); if (args->numMembers() != 2) { THROW_ARANGO_EXCEPTION(TRI_ERROR_QUERY_FUNCTION_ARGUMENT_NUMBER_MISMATCH); } // either doc.geo or [doc.lng, doc.lat] aql::AstNode const* var = args->getMember(1); TRI_ASSERT(var->isAttributeAccessForVariable(ref, true) || (var->isArray() && var->getMember(0)->isAttributeAccessForVariable(ref, true) && var->getMember(1)->isAttributeAccessForVariable(ref, true))); aql::AstNode* cc = args->getMemberUnchecked(0); TRI_ASSERT(cc->type != aql::NODE_TYPE_ATTRIBUTE_ACCESS); if (cc->type == aql::NODE_TYPE_ATTRIBUTE_ACCESS) { THROW_ARANGO_EXCEPTION(TRI_ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH); } if (cc->type == aql::NODE_TYPE_ARRAY) { // [lng, lat] is valid input TRI_ASSERT(cc->numMembers() == 2); return S2LatLng::FromDegrees(/*lat*/ cc->getMember(1)->getDoubleValue(), /*lon*/ cc->getMember(0)->getDoubleValue()); } else { VPackBuilder jsonB; cc->toVelocyPackValue(jsonB); VPackSlice json = jsonB.slice(); geo::ShapeContainer shape; Result res; if (json.isArray() && json.length() >= 2) { res = shape.parseCoordinates(json, /*GeoJson*/ true); } else { res = geo::geojson::parseRegion(json, shape); } if (res.fail()) { THROW_ARANGO_EXCEPTION(res); } return S2LatLng(shape.centroid()); } } // either parses GEO_DISTANCE call argument values S2LatLng Index::parseDistFCall(aql::AstNode const* node, aql::Variable const* ref) { TRI_ASSERT(node->type == aql::NODE_TYPE_FCALL); aql::AstNode* args = node->getMemberUnchecked(0); aql::Function const* func = static_cast(node->getData()); TRI_ASSERT(func != nullptr); if (func->name == "GEO_DISTANCE") { return Index::parseGeoDistance(args, ref); } // we should not get here for any other functions, not even DISTANCE THROW_ARANGO_EXCEPTION_MESSAGE( TRI_ERROR_INTERNAL, std::string("parseDistFCall called for unexpected function '") + func->name + "'"); } void Index::handleNode(aql::AstNode const* node, aql::Variable const* ref, geo::QueryParams& qp) { switch (node->type) { // Handle GEO_CONTAINS(, doc.field) // or GEO_INTERSECTS(, doc.field) case aql::NODE_TYPE_FCALL: { // TODO handle GEO_CONTAINS / INTERSECT aql::AstNode const* args = node->getMemberUnchecked(0); TRI_ASSERT(args->numMembers() == 2); if (args->numMembers() != 2) { THROW_ARANGO_EXCEPTION(TRI_ERROR_QUERY_FUNCTION_ARGUMENT_NUMBER_MISMATCH); } aql::AstNode const* geoJson = args->getMemberUnchecked(0); aql::AstNode const* symbol = args->getMemberUnchecked(1); // geojson or array with variables TRI_ASSERT(symbol->isAttributeAccessForVariable(ref, true) || (symbol->isArray() && symbol->numMembers() == 2 && symbol->getMember(0)->getAttributeAccessForVariable(true) != nullptr && symbol->getMember(1)->getAttributeAccessForVariable(true) != nullptr)); TRI_ASSERT(geoJson->type != aql::NODE_TYPE_REFERENCE); // arrays can't occur only handle real GeoJSON VPackBuilder bb; geoJson->toVelocyPackValue(bb); Result res = geo::geojson::parseRegion(bb.slice(), qp.filterShape); if (res.fail()) { THROW_ARANGO_EXCEPTION(res); } aql::Function* func = static_cast(node->getData()); TRI_ASSERT(func != nullptr); if (func->name == "GEO_CONTAINS") { qp.filterType = geo::FilterType::CONTAINS; } else if (func->name == "GEO_INTERSECTS") { qp.filterType = geo::FilterType::INTERSECTS; } else { TRI_ASSERT(false); } break; } // Handle GEO_DISTANCE(, doc.field) [<|<=|=>|>] case aql::NODE_TYPE_OPERATOR_BINARY_LE: qp.maxInclusive = true; // intentional fallthrough case aql::NODE_TYPE_OPERATOR_BINARY_LT: { TRI_ASSERT(node->numMembers() == 2); qp.origin = Index::parseDistFCall(node->getMemberUnchecked(0), ref); if (!qp.origin.is_valid()) { THROW_ARANGO_EXCEPTION(TRI_ERROR_QUERY_INVALID_GEO_VALUE); } aql::AstNode const* max = node->getMemberUnchecked(1); TRI_ASSERT(max->type == aql::NODE_TYPE_VALUE); if (!max->isValueType(aql::VALUE_TYPE_STRING)) { qp.maxDistance = max->getDoubleValue(); } // else assert(max->getStringValue() == "unlimited") break; } case aql::NODE_TYPE_OPERATOR_BINARY_GE: qp.minInclusive = true; // intentional fallthrough case aql::NODE_TYPE_OPERATOR_BINARY_GT: { TRI_ASSERT(node->numMembers() == 2); qp.origin = Index::parseDistFCall(node->getMemberUnchecked(0), ref); if (!qp.origin.is_valid()) { THROW_ARANGO_EXCEPTION(TRI_ERROR_QUERY_INVALID_GEO_VALUE); } // LOG_TOPIC("a9633", ERR, Logger::FIXME) << "Found center: " << c.toString(); aql::AstNode const* min = node->getMemberUnchecked(1); TRI_ASSERT(min->type == aql::NODE_TYPE_VALUE); qp.minDistance = min->getDoubleValue(); break; } default: TRI_ASSERT(false); break; } } void Index::parseCondition(aql::AstNode const* node, aql::Variable const* reference, geo::QueryParams& params) { if (aql::Ast::IsAndOperatorType(node->type)) { for (size_t i = 0; i < node->numMembers(); i++) { handleNode(node->getMemberUnchecked(i), reference, params); } } else { handleNode(node, reference, params); } } } // namespace geo_index } // namespace arangodb