diff --git a/src/clients/meta/MetaClient.cpp b/src/clients/meta/MetaClient.cpp index 0f73cbad02a..6702bae3d82 100644 --- a/src/clients/meta/MetaClient.cpp +++ b/src/clients/meta/MetaClient.cpp @@ -340,6 +340,8 @@ bool MetaClient::loadSchemas(GraphSpaceID spaceId, bool hasDef = col.default_value_ref().has_value(); auto& colType = col.get_type(); size_t len = colType.type_length_ref().has_value() ? *colType.get_type_length() : 0; + cpp2::GeoShape geoShape = + colType.geo_shape_ref().has_value() ? *colType.get_geo_shape() : cpp2::GeoShape::ANY; bool nullable = col.nullable_ref().has_value() ? *col.get_nullable() : false; Expression* defaultValueExpr = nullptr; if (hasDef) { @@ -353,8 +355,12 @@ bool MetaClient::loadSchemas(GraphSpaceID spaceId, } } - schema->addField( - col.get_name(), colType.get_type(), len, nullable, hasDef ? defaultValueExpr : nullptr); + schema->addField(col.get_name(), + colType.get_type(), + len, + nullable, + hasDef ? defaultValueExpr : nullptr, + geoShape); }; for (auto& tagIt : tagItemVec) { diff --git a/src/codec/RowWriterV2.cpp b/src/codec/RowWriterV2.cpp index cb181f08f31..b9ae1d41677 100644 --- a/src/codec/RowWriterV2.cpp +++ b/src/codec/RowWriterV2.cpp @@ -762,6 +762,12 @@ WriteResult RowWriterV2::write(ssize_t index, const DateTime& v) noexcept { } WriteResult RowWriterV2::write(ssize_t index, const Geography& v) noexcept { + auto field = schema_->field(index); + auto geoShape = field->geoShape(); + if (geoShape != meta::cpp2::GeoShape::ANY && + folly::to(geoShape) != folly::to(v.shape())) { + return WriteResult::TYPE_MISMATCH; + } return write(index, folly::StringPiece(v.wkb)); } diff --git a/src/codec/test/ResultSchemaProvider.cpp b/src/codec/test/ResultSchemaProvider.cpp index 359ca2bc80f..80b23ae1b26 100644 --- a/src/codec/test/ResultSchemaProvider.cpp +++ b/src/codec/test/ResultSchemaProvider.cpp @@ -23,14 +23,16 @@ ResultSchemaProvider::ResultSchemaField::ResultSchemaField(std::string name, bool nullable, int32_t offset, size_t nullFlagPos, - Expression* defaultValue) + Expression* defaultValue, + meta::cpp2::GeoShape geoShape) : name_(std::move(name)), type_(type), size_(size), nullable_(nullable), offset_(offset), nullFlagPos_(nullFlagPos), - defaultValue_(defaultValue) {} + defaultValue_(defaultValue), + geoShape_(geoShape) {} const char* ResultSchemaProvider::ResultSchemaField::name() const { return name_.c_str(); } @@ -50,6 +52,8 @@ size_t ResultSchemaProvider::ResultSchemaField::offset() const { return offset_; size_t ResultSchemaProvider::ResultSchemaField::nullFlagPos() const { return nullFlagPos_; } +meta::cpp2::GeoShape ResultSchemaProvider::ResultSchemaField::geoShape() const { return geoShape_; } + /*********************************** * * ResultSchemaProvider diff --git a/src/codec/test/ResultSchemaProvider.h b/src/codec/test/ResultSchemaProvider.h index f0a6bff44ca..4c51288163e 100644 --- a/src/codec/test/ResultSchemaProvider.h +++ b/src/codec/test/ResultSchemaProvider.h @@ -22,7 +22,8 @@ class ResultSchemaProvider : public meta::SchemaProviderIf { bool nullable, int32_t offset, size_t nullFlagPos, - Expression* defaultValue = nullptr); + Expression* defaultValue = nullptr, + meta::cpp2::GeoShape = meta::cpp2::GeoShape::ANY); const char* name() const override; meta::cpp2::PropertyType type() const override; @@ -32,6 +33,7 @@ class ResultSchemaProvider : public meta::SchemaProviderIf { size_t size() const override; size_t offset() const override; size_t nullFlagPos() const override; + meta::cpp2::GeoShape geoShape() const override; private: std::string name_; @@ -41,6 +43,7 @@ class ResultSchemaProvider : public meta::SchemaProviderIf { int32_t offset_; size_t nullFlagPos_; Expression* defaultValue_; + meta::cpp2::GeoShape geoShape_; }; public: diff --git a/src/codec/test/SchemaWriter.cpp b/src/codec/test/SchemaWriter.cpp index 904d3db8bb3..41e56bd22cd 100644 --- a/src/codec/test/SchemaWriter.cpp +++ b/src/codec/test/SchemaWriter.cpp @@ -17,7 +17,8 @@ SchemaWriter& SchemaWriter::appendCol(folly::StringPiece name, PropertyType type, int32_t fixedStrLen, bool nullable, - Expression* defaultValue) noexcept { + Expression* defaultValue, + meta::cpp2::GeoShape geoShape) noexcept { using folly::hash::SpookyHashV2; uint64_t hash = SpookyHashV2::Hash64(name.data(), name.size(), 0); DCHECK(nameIndex_.find(hash) == nameIndex_.end()); @@ -75,6 +76,9 @@ SchemaWriter& SchemaWriter::appendCol(folly::StringPiece name, case PropertyType::DATETIME: size = sizeof(int16_t) + 5 * sizeof(int8_t) + sizeof(int32_t); break; + case PropertyType::GEOGRAPHY: + size = 2 * sizeof(int32_t); // as same as STRING + break; default: LOG(FATAL) << "Unknown column type"; } @@ -84,7 +88,8 @@ SchemaWriter& SchemaWriter::appendCol(folly::StringPiece name, nullFlagPos = numNullableFields_++; } - columns_.emplace_back(name.toString(), type, size, nullable, offset, nullFlagPos, defaultValue); + columns_.emplace_back( + name.toString(), type, size, nullable, offset, nullFlagPos, defaultValue, geoShape); nameIndex_.emplace(std::make_pair(hash, columns_.size() - 1)); return *this; diff --git a/src/codec/test/SchemaWriter.h b/src/codec/test/SchemaWriter.h index 516d120f519..55c5dfb356d 100644 --- a/src/codec/test/SchemaWriter.h +++ b/src/codec/test/SchemaWriter.h @@ -21,7 +21,8 @@ class SchemaWriter : public ResultSchemaProvider { meta::cpp2::PropertyType type, int32_t fixedStrLen = 0, bool nullable = false, - Expression* defaultValue = nullptr) noexcept; + Expression* defaultValue = nullptr, + meta::cpp2::GeoShape geoShape = meta::cpp2::GeoShape::ANY) noexcept; private: }; diff --git a/src/common/datatypes/Geography.cpp b/src/common/datatypes/Geography.cpp index 23def05c158..5a409e44fcb 100644 --- a/src/common/datatypes/Geography.cpp +++ b/src/common/datatypes/Geography.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include @@ -16,10 +18,11 @@ namespace nebula { -ShapeType Geography::shape() const { +GeoShape Geography::shape() const { + // TODO(jie) May store the shapetype as the data member of Geography is ok. std::string wkbCopy = wkb; - uint8_t *beg = reinterpret_cast(wkbCopy.data()); - uint8_t *end = beg + wkbCopy.size(); + uint8_t* beg = reinterpret_cast(wkbCopy.data()); + uint8_t* end = beg + wkbCopy.size(); WKBReader reader; auto byteOrderRet = reader.readByteOrder(beg, end); DCHECK(byteOrderRet.ok()); @@ -38,56 +41,116 @@ std::string Geography::asWKT() const { return wkt; } -S2Region* Geography::asS2() const { - // auto geom = WKBReader().read(wkb); - // return s2RegionFromGeom(&geom); - return nullptr; +std::string Geography::asWKB() const { return folly::hexlify(wkb); } + +std::unique_ptr Geography::asS2() const { + auto geomRet = WKBReader().read(wkb); + DCHECK(geomRet.ok()); + std::unique_ptr geom = std::move(geomRet).value(); + return s2RegionFromGeomtry(geom.get()); +} + +std::unique_ptr Geography::s2RegionFromGeomtry(const Geometry* geom) const { + switch (geom->shape()) { + case GeoShape::POINT: { + const auto* point = static_cast(geom); + auto s2Point = s2PointFromCoordinate(point->coord); + return std::make_unique(s2Point); + } + case GeoShape::LINESTRING: { + const auto* lineString = static_cast(geom); + auto coordList = lineString->coordList; + DCHECK_GE(coordList.size(), 2); + removeAdjacentDuplicateCoordinates(coordList); + if (coordList.size() < 2) { + LOG(INFO) << "Invalid LineString, adjacent coordinates must not be identical or antipodal."; + return nullptr; + } + + auto s2Points = s2PointsFromCoordinateList(coordList); + auto s2Polyline = std::make_unique(s2Points, S2Debug::DISABLE); + if (!s2Polyline->IsValid()) { + return nullptr; + } + return s2Polyline; + } + case GeoShape::POLYGON: { + const auto* polygon = static_cast(geom); + uint32_t numRings = polygon->numRings(); + std::vector> s2Loops; + s2Loops.reserve(numRings); + for (size_t i = 0; i < numRings; ++i) { + auto coordList = polygon->coordListList[i]; + DCHECK_GE(coordList.size(), 4); + DCHECK(isLoopClosed(coordList)); + removeAdjacentDuplicateCoordinates(coordList); + if (coordList.size() < 4) { + LOG(INFO) << "Invalid linearRing in polygon, must have at least 4 distinct coordinates."; + return nullptr; + } + coordList.pop_back(); // Remove redundant last coordinate + auto s2Points = s2PointsFromCoordinateList(coordList); + auto* s2Loop = new S2Loop(std::move(s2Points), S2Debug::DISABLE); + if (!s2Loop->IsValid()) { + return nullptr; + } + s2Loop->Normalize(); // All loops must be oriented CCW(counterclockwise) for S2 + s2Loops.emplace_back(s2Loop); + } + auto s2Polygon = std::make_unique(std::move(s2Loops), S2Debug::DISABLE); + if (!s2Polygon->IsValid()) { // Exterior loop must contain other interior loops + return nullptr; + } + return s2Polygon; + } + default: + LOG(FATAL) + << "Geography shapes other than Point/LineString/Polygon are not currently supported"; + return nullptr; + } +} + +S2Point Geography::s2PointFromCoordinate(const Coordinate& coord) const { + auto latlng = S2LatLng::FromDegrees( + coord.y, coord.x); // S2Point requires latitude to be first, and longitude to be last + DCHECK(latlng.is_valid()); + return latlng.ToPoint(); +} + +std::vector Geography::s2PointsFromCoordinateList( + const std::vector& coordList) const { + std::vector s2Points; + uint32_t numCoords = coordList.size(); + s2Points.reserve(numCoords); + for (size_t i = 0; i < numCoords; ++i) { + auto coord = coordList[i]; + auto s2Point = s2PointFromCoordinate(coord); + s2Points.emplace_back(s2Point); + } + + return s2Points; } -// S2Region* Geography::s2RegionFromGeom(const geos::geom::Geometry* geom) { -// return new S2Region; -// switch (geom->getGeometryTypeId()) { -// case geos::geom::GEOS_POINT: { -// auto *point = static_cast(geom); -// auto latlng = S2LatLng::FromDegrees(point->getX(), point->getY()); -// return new S2PointRegion(latlng.toPoint()); -// } -// case geos::geom::GEOS_LINESTRING: { -// auot *lineString = static_cast(geom); -// std::vector s2Points; -// latlngs.reserve(lineString->numPoints()); -// for (size_t i = 0; i < lineString->numPoints(); ++i) { -// auto latlng = lineString->getCoordinateN(i); -// s2Points.emplace_back(S2LatLng::FromDegrees(latlng.x, latlng.y).ToPoint()); -// } -// return new S2Polyline(s2Points); -// } -// case geos::geom::GEOS_POLYGON: { -// auto *polygon = static_cast(geom); -// size_t ringNum = 1 + polygon->getNumInteriorRing(); -// std::vector> s2Loops; -// s2Loops.reserve(ringNum); - -// std::vector rings; -// rings.reserve(ringNum); - -// std::vector s2Points; -// for (size_t i = 0; i < rings.size(); ++i) { -// const auto *ring = rings[i]; -// s2Points.clear(); -// s2Points.reserve(ring->numPoints()); -// for (size_t j = 0; j < ring->numPoints(); ++j) { -// auto latlng = ring->getCoordinateN(i); -// s2Points.empalce_back(S2LatLng::FromDegrees(latlng.x, latlng.y).ToPoint()); -// } -// auto *s2Loop = new S2Loop(s2Points); -// s2Loop->Normalize(); -// s2Loops.emplace_back(s2Loop); // make loop be CCW -// return new S2Polygon(s2Loops); -// } -// } -// } -// } +bool Geography::isLoopClosed(const std::vector& coordList) const { + DCHECK_GE(coordList.size(), 2); + return coordList.front() == coordList.back(); +} + +void Geography::removeAdjacentDuplicateCoordinates(std::vector& coordList) const { + if (coordList.size() < 2) { + return; + } + + size_t i = 0, j = 1; + for (; j < coordList.size(); ++j) { + if (coordList[i] != coordList[j]) { + ++i; + if (i != j) { // i is always <= j + coordList[i] = coordList[j]; + } + } + } +} } // namespace nebula diff --git a/src/common/datatypes/Geography.h b/src/common/datatypes/Geography.h index 9de48dac5c8..72c4354fdbf 100644 --- a/src/common/datatypes/Geography.h +++ b/src/common/datatypes/Geography.h @@ -16,7 +16,11 @@ #include #include "common/datatypes/Value.h" -#include "common/geo/GeoShape.h" +#include "common/geo/io/Geometry.h" + +// Do not include here, it will indirectly includes a header file which defines a +// enum `BEGIN`(not enum class). While Geography.h is indirectly included by parser.yy, which has a +// macro named `BEGIN`. So they will be conflicted. class S2Polygon; @@ -24,11 +28,11 @@ namespace nebula { // clang-format off /* -static const std::unordered_map kShapeTypeToS2Region = { +static const std::unordered_map kShapeTypeToS2Region = { // S2PointRegion is a wrapper of S2Point, and it inherits from the S2Region class while S2Point doesn't. - {ShapeType::Point, S2PointRegion}, - {ShapeType::LineString, S2Polyline}, - {ShapeType::Polygon, S2Polygon}, + {GeoShape::POINT, S2PointRegion}, + {GeoShape::LINESTRING, S2Polyline}, + {GeoShape::POLYGON, S2Polygon}, }; */ // clang-format on @@ -39,19 +43,19 @@ struct Geography { std::string wkb; // TODO(jie) Maybe store Geometry* here is better Geography() = default; - explicit Geography(const std::string& validWKB) { - // DCHECK(WKB::isValid(wkb)); - wkb = validWKB; + explicit Geography(const std::string& bytes) { + // TODO(jie): Ensure the bytes is valid + wkb = bytes; LOG(INFO) << "Geography.wkb: " << wkb << ", wkb.size(): " << wkb.size(); } - ShapeType shape() const; + GeoShape shape() const; std::string asWKT() const; - std::string asWKB() const { return wkb; } + std::string asWKB() const; - S2Region* asS2() const; + std::unique_ptr asS2() const; std::string toString() const { return asWKT(); } @@ -67,8 +71,16 @@ struct Geography { bool operator<(const Geography& rhs) const { return wkb < rhs.wkb; } - // private: - // S2Region* s2RegionFromGeom(const geos::geom::Geometry* geom); + private: + std::unique_ptr s2RegionFromGeomtry(const Geometry* geom) const; + + S2Point s2PointFromCoordinate(const Coordinate& coord) const; + + std::vector s2PointsFromCoordinateList(const std::vector& coordList) const; + + bool isLoopClosed(const std::vector& coordList) const; + + void removeAdjacentDuplicateCoordinates(std::vector& coordList) const; }; inline std::ostream& operator<<(std::ostream& os, const Geography& g) { return os << g.wkb; } diff --git a/src/common/geo/GeoIndex.cpp b/src/common/geo/GeoIndex.cpp index 2934baa55e6..489e0d859ed 100644 --- a/src/common/geo/GeoIndex.cpp +++ b/src/common/geo/GeoIndex.cpp @@ -53,16 +53,22 @@ std::vector GeographyIndex::indexCells(const Geography& g) const noexc } std::vector GeographyIndex::dWithin(const Geography& g, double distance) const noexcept { - auto* r = g.asS2(); + auto* r = g.asS2().get(); + // TODO(jie): Better to ensure the Geography value is valid to build s2 when constructing. + if (!r) { + LOG(INFO) << "GeographyIndex::dWithin(), asS2() failed."; + return {}; + } + S1Angle radius = S2Earth::ToAngle(util::units::Meters(distance)); switch (g.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { const S2Point& gPoint = static_cast(r)->point(); S2Cap gCap(gPoint, radius); auto cells = coveringCells(gCap); return scanRange(cells); } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { S2Polyline* gLine = static_cast(r); MutableS2ShapeIndex index; index.Add(std::make_unique(gLine)); @@ -70,7 +76,7 @@ std::vector GeographyIndex::dWithin(const Geography& g, double distan auto cells = coveringCells(gBuffer); return scanRange(cells); } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { S2Polygon* gPolygon = static_cast(r); S2ShapeIndexBufferedRegion gBuffer(&gPolygon->index(), radius); auto cells = coveringCells(gBuffer); @@ -84,9 +90,15 @@ std::vector GeographyIndex::dWithin(const Geography& g, double distan } std::vector GeographyIndex::coveringCells(const Geography& g) const noexcept { - auto* r = g.asS2(); + auto* r = g.asS2().get(); + // TODO(jie): Better to ensure the Geography value is valid to build s2 when constructing. + if (!r) { + LOG(INFO) << "GeographyIndex::coveringCells(), asS2() failed."; + return {}; + } + // Currently region coverer options doesn't work for point. Point always use level 30. - if (g.shape() == ShapeType::Point) { + if (g.shape() == GeoShape::POINT) { const S2Point& gPoint = static_cast(r)->point(); return {S2CellId(gPoint)}; } diff --git a/src/common/geo/GeoShape.h b/src/common/geo/GeoShape.h index 07a7edb743b..2cd28700ff5 100644 --- a/src/common/geo/GeoShape.h +++ b/src/common/geo/GeoShape.h @@ -8,10 +8,10 @@ namespace nebula { -enum class ShapeType : uint32_t { - Point = 1, - LineString = 2, - Polygon = 3, +enum class GeoShape : uint32_t { + POINT = 1, + LINESTRING = 2, + POLYGON = 3, }; } // namespace nebula diff --git a/src/common/geo/function/Covers.cpp b/src/common/geo/function/Covers.cpp index cf9341f043c..688062501ef 100644 --- a/src/common/geo/function/Covers.cpp +++ b/src/common/geo/function/Covers.cpp @@ -15,18 +15,23 @@ namespace nebula { // If no point in b lies exterior of b, a covers b. // http://lin-ear-th-inking.blogspot.com/2007/06/subtleties-of-ogc-covers-spatial.html bool covers(const Geography& a, const Geography& b) { - auto* aRegion = a.asS2(); - auto* bRegion = b.asS2(); + auto* aRegion = a.asS2().get(); + auto* bRegion = b.asS2().get(); + // TODO(jie): Better to ensure the Geography value is valid to build s2 when constructing. + if (!aRegion || !bRegion) { + LOG(INFO) << "covers(), asS2() failed."; + return false; + } switch (a.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { switch (b.shape()) { - case ShapeType::Point: + case GeoShape::POINT: return static_cast(aRegion)->Contains( static_cast(bRegion)->point()); - case ShapeType::LineString: + case GeoShape::LINESTRING: return false; - case ShapeType::Polygon: + case GeoShape::POLYGON: return false; default: LOG(FATAL) @@ -34,14 +39,14 @@ bool covers(const Geography& a, const Geography& b) { return -1.0; } } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { S2Polyline* aLine = static_cast(aRegion); switch (b.shape()) { - case ShapeType::Point: + case GeoShape::POINT: return aLine->MayIntersect(S2Cell(static_cast(bRegion)->point())); - case ShapeType::LineString: + case GeoShape::LINESTRING: return aLine->NearlyCovers(*static_cast(bRegion), S1Angle::Radians(1e-15)); - case ShapeType::Polygon: + case GeoShape::POLYGON: return false; default: LOG(FATAL) @@ -49,14 +54,14 @@ bool covers(const Geography& a, const Geography& b) { return -1.0; } } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { S2Polygon* aPolygon = static_cast(aRegion); switch (b.shape()) { - case ShapeType::Point: + case GeoShape::POINT: return aPolygon->Contains(static_cast(bRegion)->point()); - case ShapeType::LineString: + case GeoShape::LINESTRING: return aPolygon->Contains(*static_cast(bRegion)); - case ShapeType::Polygon: + case GeoShape::POLYGON: return aPolygon->Contains(static_cast(bRegion)); default: LOG(FATAL) diff --git a/src/common/geo/function/DWithin.cpp b/src/common/geo/function/DWithin.cpp index 0a173ca882c..129679bdfd1 100644 --- a/src/common/geo/function/DWithin.cpp +++ b/src/common/geo/function/DWithin.cpp @@ -15,23 +15,28 @@ namespace nebula { // We don't need to find the closest points. We just need to find the first point pair whose // distance is less than or less equal than the given distance. (Early quit) bool dWithin(const Geography& a, const Geography& b, double distance, bool inclusive) { - auto aRegion = a.asS2(); - auto bRegion = b.asS2(); + auto aRegion = a.asS2().get(); + auto bRegion = b.asS2().get(); + // TODO(jie): Better to ensure the Geography value is valid to build s2 when constructing. + if (!aRegion || !bRegion) { + LOG(INFO) << "dWithin(), asS2() failed."; + return false; + } switch (a.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { const S2Point& aPoint = static_cast(aRegion)->point(); switch (b.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { const S2Point& bPoint = static_cast(bRegion)->point(); double closestDistance = S2Earth::GetDistanceMeters(aPoint, bPoint); return inclusive ? closestDistance <= distance : closestDistance < distance; } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { S2Polyline* bLine = static_cast(bRegion); return s2PointAndS2PolylineAreWithinDistance(aPoint, bLine, distance, inclusive); } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { S2Polygon* bPolygon = static_cast(bRegion); return s2PointAndS2PolygonAreWithinDistance(aPoint, bPolygon, distance, inclusive); } @@ -41,14 +46,14 @@ bool dWithin(const Geography& a, const Geography& b, double distance, bool inclu return -1.0; } } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { S2Polyline* aLine = static_cast(aRegion); switch (b.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { const S2Point& bPoint = static_cast(bRegion)->point(); return s2PointAndS2PolylineAreWithinDistance(bPoint, aLine, distance, inclusive); } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { S2Polyline* bLine = static_cast(bRegion); MutableS2ShapeIndex aIndex, bIndex; aIndex.Add(std::make_unique(aLine)); @@ -62,7 +67,7 @@ bool dWithin(const Geography& a, const Geography& b, double distance, bool inclu return query.IsDistanceLess(&target, S2Earth::ToChordAngle(util::units::Meters(distance))); } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { S2Polygon* bPolygon = static_cast(bRegion); return s2PolylineAndS2PolygonAreWithinDistance(aLine, bPolygon, distance, inclusive); } @@ -72,18 +77,18 @@ bool dWithin(const Geography& a, const Geography& b, double distance, bool inclu return -1.0; } } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { S2Polygon* aPolygon = static_cast(aRegion); switch (b.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { const S2Point& bPoint = static_cast(bRegion)->point(); return s2PointAndS2PolygonAreWithinDistance(bPoint, aPolygon, distance, inclusive); } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { S2Polyline* bLine = static_cast(bRegion); return s2PolylineAndS2PolygonAreWithinDistance(bLine, aPolygon, distance, inclusive); } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { S2Polygon* bPolygon = static_cast(bRegion); S2ClosestEdgeQuery query(&aPolygon->index()); S2ClosestEdgeQuery::ShapeIndexTarget target(&bPolygon->index()); diff --git a/src/common/geo/function/Distance.cpp b/src/common/geo/function/Distance.cpp index b9cc267635f..344fc0b7c59 100644 --- a/src/common/geo/function/Distance.cpp +++ b/src/common/geo/function/Distance.cpp @@ -15,22 +15,27 @@ namespace nebula { // Find the closest distance of a and b double distance(const Geography& a, const Geography& b) { - auto aRegion = a.asS2(); - auto bRegion = b.asS2(); + auto aRegion = a.asS2().get(); + auto bRegion = b.asS2().get(); + // TODO(jie): Better to ensure the Geography value is valid to build s2 when constructing. + if (!aRegion || !bRegion) { + LOG(INFO) << "distance(), asS2() failed."; + return -1.0; + } switch (a.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { const S2Point& aPoint = static_cast(aRegion)->point(); switch (b.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { const S2Point& bPoint = static_cast(bRegion)->point(); return S2Earth::GetDistanceMeters(aPoint, bPoint); } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { S2Polyline* bLine = static_cast(bRegion); return distanceOfS2PolylineWithS2Point(bLine, aPoint); } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { S2Polygon* bPolygon = static_cast(bRegion); return distanceOfS2PolygonWithS2Point(bPolygon, aPoint); } @@ -40,14 +45,14 @@ double distance(const Geography& a, const Geography& b) { return -1.0; } } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { S2Polyline* aLine = static_cast(aRegion); switch (b.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { const S2Point& bPoint = static_cast(bRegion)->point(); return distanceOfS2PolylineWithS2Point(aLine, bPoint); } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { const S2Polyline* bLine = static_cast(bRegion); MutableS2ShapeIndex aIndex, bIndex; aIndex.Add(std::make_unique(aLine)); @@ -56,7 +61,7 @@ double distance(const Geography& a, const Geography& b) { S2ClosestEdgeQuery::ShapeIndexTarget target(&bIndex); return S2Earth::ToMeters(query.GetDistance(&target)); } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { S2Polygon* bPolygon = static_cast(bRegion); return distanceOfS2PolygonWithS2Polyline(bPolygon, aLine); } @@ -66,18 +71,18 @@ double distance(const Geography& a, const Geography& b) { return -1.0; } } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { S2Polygon* aPolygon = static_cast(aRegion); switch (b.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { const S2Point& bPoint = static_cast(bRegion)->point(); return distanceOfS2PolygonWithS2Point(aPolygon, bPoint); } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { S2Polyline* bLine = static_cast(bRegion); return distanceOfS2PolygonWithS2Polyline(aPolygon, bLine); } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { S2Polygon* bPolygon = static_cast(bRegion); S2ClosestEdgeQuery query(&aPolygon->index()); S2ClosestEdgeQuery::ShapeIndexTarget target(&bPolygon->index()); diff --git a/src/common/geo/function/Intersects.cpp b/src/common/geo/function/Intersects.cpp index 7e6cec71a9f..0e617bc669d 100644 --- a/src/common/geo/function/Intersects.cpp +++ b/src/common/geo/function/Intersects.cpp @@ -15,19 +15,24 @@ namespace nebula { // If any point in the set that comprises A is also a member of the set of points that make up B, // they intersects; bool intersects(const Geography& a, const Geography& b) { - auto aRegion = a.asS2(); - auto bRegion = b.asS2(); + auto aRegion = a.asS2().get(); + auto bRegion = b.asS2().get(); + // TODO(jie): Better to ensure the Geography value is valid to build s2 when constructing. + if (!aRegion || !bRegion) { + LOG(INFO) << "intersects(), asS2() failed."; + return false; + } switch (a.shape()) { - case ShapeType::Point: { + case GeoShape::POINT: { switch (b.shape()) { - case ShapeType::Point: + case GeoShape::POINT: return static_cast(aRegion)->MayIntersect( S2Cell(static_cast(bRegion)->point())); - case ShapeType::LineString: + case GeoShape::LINESTRING: return static_cast(bRegion)->MayIntersect( S2Cell(static_cast(aRegion)->point())); - case ShapeType::Polygon: + case GeoShape::POLYGON: return static_cast(bRegion)->MayIntersect( S2Cell(static_cast(aRegion)->point())); default: @@ -36,14 +41,14 @@ bool intersects(const Geography& a, const Geography& b) { return -1.0; } } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { switch (b.shape()) { - case ShapeType::Point: + case GeoShape::POINT: return static_cast(aRegion)->MayIntersect( S2Cell(static_cast(bRegion)->point())); - case ShapeType::LineString: + case GeoShape::LINESTRING: return static_cast(aRegion)->Intersects(static_cast(bRegion)); - case ShapeType::Polygon: + case GeoShape::POLYGON: return static_cast(bRegion)->Intersects(*static_cast(aRegion)); default: LOG(FATAL) @@ -51,14 +56,14 @@ bool intersects(const Geography& a, const Geography& b) { return -1.0; } } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { switch (b.shape()) { - case ShapeType::Point: + case GeoShape::POINT: return static_cast(aRegion)->MayIntersect( S2Cell(static_cast(bRegion)->point())); - case ShapeType::LineString: + case GeoShape::LINESTRING: return static_cast(aRegion)->Intersects(*static_cast(bRegion)); - case ShapeType::Polygon: + case GeoShape::POLYGON: return static_cast(aRegion)->Intersects(static_cast(bRegion)); default: LOG(FATAL) diff --git a/src/common/geo/io/Geometry.h b/src/common/geo/io/Geometry.h index a6e8ae49b5c..51772bdba7b 100644 --- a/src/common/geo/io/Geometry.h +++ b/src/common/geo/io/Geometry.h @@ -17,36 +17,54 @@ namespace nebula { // These Geometry structs are just for parsing wkt/wkb struct Coordinate { double x, y; + Coordinate(double lng, double lat) : x(lng), y(lat) {} + + // TODO(jie) compare double correctly + bool operator==(const Coordinate &rhs) const { return x == rhs.x && y == rhs.y; } + bool operator!=(const Coordinate &rhs) const { return !(*this == rhs); } + + static bool isValidLng(double lng) { return std::abs(lng) <= 180; } + + static bool isValidLat(double lat) { return std::abs(lat) <= 90; } }; struct Geometry { - virtual ShapeType shape() const = 0; virtual ~Geometry() {} + virtual GeoShape shape() const = 0; }; struct Point : public Geometry { Coordinate coord; - ShapeType shape() const override { return ShapeType::Point; } + explicit Point(const Coordinate &v) : coord(v) {} explicit Point(Coordinate &&v) : coord(std::move(v)) {} ~Point() override = default; + + GeoShape shape() const override { return GeoShape::POINT; } }; struct LineString : public Geometry { std::vector coordList; - ShapeType shape() const override { return ShapeType::LineString; } + explicit LineString(const std::vector &v) : coordList(v) {} explicit LineString(std::vector &&v) : coordList(std::move(v)) {} ~LineString() override = default; + + GeoShape shape() const override { return GeoShape::LINESTRING; } + uint32_t numCoords() const { return coordList.size(); } }; struct Polygon : public Geometry { std::vector> coordListList; - ShapeType shape() const override { return ShapeType::Polygon; } + explicit Polygon(const std::vector> &v) : coordListList(v) {} explicit Polygon(std::vector> &&v) : coordListList(std::move(v)) {} ~Polygon() override = default; + + GeoShape shape() const override { return GeoShape::POLYGON; } + uint32_t numRings() const { return coordListList.size(); } + uint32_t numInteriorRing() const { return numRings() - 1; } }; } // namespace nebula diff --git a/src/common/geo/io/wkb/WKBReader.h b/src/common/geo/io/wkb/WKBReader.h index fa740b94c39..7966816d627 100644 --- a/src/common/geo/io/wkb/WKBReader.h +++ b/src/common/geo/io/wkb/WKBReader.h @@ -31,16 +31,16 @@ class WKBReader { auto shapeTypeRet = readShapeType(beg, end, byteOrder); NG_RETURN_IF_ERROR(shapeTypeRet); - ShapeType shapeType = shapeTypeRet.value(); + GeoShape shapeType = shapeTypeRet.value(); switch (shapeType) { - case ShapeType::Point: { + case GeoShape::POINT: { auto coordRet = readCoordinate(beg, end, byteOrder); NG_RETURN_IF_ERROR(coordRet); Coordinate coord = coordRet.value(); return std::make_unique(coord); } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { auto numPointsRet = readUint32(beg, end, byteOrder); NG_RETURN_IF_ERROR(numPointsRet); uint32_t numPoints = numPointsRet.value(); @@ -49,7 +49,7 @@ class WKBReader { std::vector coordList = coordListRet.value(); return std::make_unique(coordList); } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { auto numRingsRet = readUint32(beg, end, byteOrder); NG_RETURN_IF_ERROR(numRingsRet); uint32_t numRings = numRingsRet.value(); @@ -77,14 +77,14 @@ class WKBReader { return byteOrder; } - StatusOr readShapeType(uint8_t *&beg, uint8_t *end, ByteOrder byteOrder) const { + StatusOr readShapeType(uint8_t *&beg, uint8_t *end, ByteOrder byteOrder) const { auto vRet = readUint32(beg, end, byteOrder); NG_RETURN_IF_ERROR(vRet); uint32_t v = vRet.value(); if (v != 1 && v != 2 && v != 3) { return Status::Error("Unknown shape type"); } - ShapeType shapeType = static_cast(v); + GeoShape shapeType = static_cast(v); return shapeType; } diff --git a/src/common/geo/io/wkb/WKBWriter.h b/src/common/geo/io/wkb/WKBWriter.h index 705c4601744..7077453b6f7 100644 --- a/src/common/geo/io/wkb/WKBWriter.h +++ b/src/common/geo/io/wkb/WKBWriter.h @@ -25,15 +25,15 @@ class WKBWriter { writeUint8(wkb, byteOrder); auto shape = geom->shape(); - uint32_t shapeType = static_cast>(shape); + uint32_t shapeType = folly::to(shape); writeUint32(wkb, shapeType); switch (shape) { - case ShapeType::Point: { + case GeoShape::POINT: { const Point* point = static_cast(geom); writeCoordinate(wkb, point->coord); return wkb; } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { const LineString* line = static_cast(geom); auto coordList = line->coordList; uint32_t numPoints = coordList.size(); @@ -41,7 +41,7 @@ class WKBWriter { writeCoordinateList(wkb, coordList); return wkb; } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { const Polygon* polygon = static_cast(geom); auto coordListList = polygon->coordListList; uint32_t numRings = coordListList.size(); diff --git a/src/common/geo/io/wkt/WKTScanner.h b/src/common/geo/io/wkt/WKTScanner.h index adbd50a119c..7e9c88a9cd8 100644 --- a/src/common/geo/io/wkt/WKTScanner.h +++ b/src/common/geo/io/wkt/WKTScanner.h @@ -24,6 +24,7 @@ namespace nebula { +// TODO(jie) Try to reuse the class GraphScanner class WKTScanner : public yyFlexLexer { public: int yylex(nebula::WKTParser::semantic_type *lval, nebula::WKTParser::location_type *loc) { diff --git a/src/common/geo/io/wkt/WKTWriter.h b/src/common/geo/io/wkt/WKTWriter.h index bdfbbc7c4a9..4e7764acbbb 100644 --- a/src/common/geo/io/wkt/WKTWriter.h +++ b/src/common/geo/io/wkt/WKTWriter.h @@ -21,7 +21,7 @@ class WKTWriter { auto shape = geom->shape(); switch (shape) { - case ShapeType::Point: { + case GeoShape::POINT: { wkt.append("POINT"); const Point* point = static_cast(geom); wkt.append("("); @@ -29,7 +29,7 @@ class WKTWriter { wkt.append(")"); return wkt; } - case ShapeType::LineString: { + case GeoShape::LINESTRING: { wkt.append("LINESTRING"); const LineString* line = static_cast(geom); auto coordList = line->coordList; @@ -40,7 +40,7 @@ class WKTWriter { wkt.append(")"); return wkt; } - case ShapeType::Polygon: { + case GeoShape::POLYGON: { wkt.append("POLYGON"); const Polygon* polygon = static_cast(geom); auto coordListList = polygon->coordListList; diff --git a/src/common/geo/io/wkt/wkt_parser.yy b/src/common/geo/io/wkt/wkt_parser.yy index 66571f638ec..4af77374ac9 100644 --- a/src/common/geo/io/wkt/wkt_parser.yy +++ b/src/common/geo/io/wkt/wkt_parser.yy @@ -94,18 +94,36 @@ point linestring : KW_LINESTRING L_PAREN coordinate_list R_PAREN { + if ($3->size() < 2) { + throw nebula::WKTParser::syntax_error(@3, "LineString must have at least 2 coordinates"); + } $$ = new LineString(*$3); } ; polygon : KW_POLYGON L_PAREN coordinate_list_list R_PAREN { + for (size_t i = 0; i < $3->size(); ++i) { + auto coordList = (*$3)[i]; + if (coordList.size() < 4) { + throw nebula::WKTParser::syntax_error(@3, "Polygon's LinearRing must have at least 4 coordinates"); + } + if (coordList.front() != coordList.back()) { + throw nebula::WKTParser::syntax_error(@3, "Polygon's LinearRing must be closed"); + } + } $$ = new Polygon(*$3); } ; coordinate : DOUBLE DOUBLE { + if (!Coordinate::isValidLng($1)) { + throw nebula::WKTParser::syntax_error(@1, "Longitude must be between -180 and 180 degrees"); + } + if (!Coordinate::isValidLat($2)) { + throw nebula::WKTParser::syntax_error(@2, "Latitude must be between -90 and 90 degrees"); + } $$ = new Coordinate($1, $2); } ; diff --git a/src/common/meta/NebulaSchemaProvider.cpp b/src/common/meta/NebulaSchemaProvider.cpp index dc5aab4a2c5..a9bf2f5e66f 100644 --- a/src/common/meta/NebulaSchemaProvider.cpp +++ b/src/common/meta/NebulaSchemaProvider.cpp @@ -91,7 +91,8 @@ void NebulaSchemaProvider::addField(folly::StringPiece name, cpp2::PropertyType type, size_t fixedStrLen, bool nullable, - Expression* defaultValue) { + Expression* defaultValue, + cpp2::GeoShape geoShape) { size_t size = fieldSize(type, fixedStrLen); size_t offset = 0; @@ -112,7 +113,8 @@ void NebulaSchemaProvider::addField(folly::StringPiece name, defaultValue, size, offset, - nullFlagPos); + nullFlagPos, + geoShape); fieldNameIndex_.emplace(name.toString(), static_cast(fields_.size() - 1)); } diff --git a/src/common/meta/NebulaSchemaProvider.h b/src/common/meta/NebulaSchemaProvider.h index 0b185c7ed3a..31a381a4a83 100644 --- a/src/common/meta/NebulaSchemaProvider.h +++ b/src/common/meta/NebulaSchemaProvider.h @@ -29,7 +29,8 @@ class NebulaSchemaProvider : public SchemaProviderIf { Expression* defaultValue, size_t size, size_t offset, - size_t nullFlagPos) + size_t nullFlagPos, + cpp2::GeoShape geoShape) : name_(std::move(name)), type_(std::move(type)), nullable_(nullable), @@ -37,7 +38,8 @@ class NebulaSchemaProvider : public SchemaProviderIf { defaultValue_(defaultValue), size_(size), offset_(offset), - nullFlagPos_(nullFlagPos) {} + nullFlagPos_(nullFlagPos), + geoShape_(geoShape) {} const char* name() const override { return name_.c_str(); } @@ -58,6 +60,8 @@ class NebulaSchemaProvider : public SchemaProviderIf { return nullFlagPos_; } + cpp2::GeoShape geoShape() const override { return geoShape_; } + private: std::string name_; cpp2::PropertyType type_; @@ -67,6 +71,7 @@ class NebulaSchemaProvider : public SchemaProviderIf { size_t size_; size_t offset_; size_t nullFlagPos_; + cpp2::GeoShape geoShape_; }; public: @@ -93,7 +98,8 @@ class NebulaSchemaProvider : public SchemaProviderIf { cpp2::PropertyType type, size_t fixedStrLen = 0, bool nullable = false, - Expression* defaultValue = nullptr); + Expression* defaultValue = nullptr, + cpp2::GeoShape geoShape = cpp2::GeoShape::ANY); static std::size_t fieldSize(cpp2::PropertyType type, std::size_t fixedStrLimit); diff --git a/src/common/meta/SchemaProviderIf.h b/src/common/meta/SchemaProviderIf.h index b880c7f01cd..f643aab28cb 100644 --- a/src/common/meta/SchemaProviderIf.h +++ b/src/common/meta/SchemaProviderIf.h @@ -40,6 +40,10 @@ class SchemaProviderIf { // In v2, if the field is nullable, it returns the position of // the null flag bit, otherwise, it returns 0 virtual size_t nullFlagPos() const = 0; + // In v1, this always returns cpp2::GeoShape::ANY + // In v2, if the field type is cpp2::PropertyType::Geography, + // it returns the specified geo shape type + virtual cpp2::GeoShape geoShape() const = 0; }; // Inherited classes do not need to implement the Iterator diff --git a/src/common/utils/IndexKeyUtils.h b/src/common/utils/IndexKeyUtils.h index 82947da3f97..27143bdfb2a 100644 --- a/src/common/utils/IndexKeyUtils.h +++ b/src/common/utils/IndexKeyUtils.h @@ -175,7 +175,8 @@ class IndexKeyUtils final { } static std::string encodeUint64(uint64_t v) { - return {reinterpret_cast(&v), sizeof(uint64_t)}; + auto val = folly::Endian::big(v); + return {reinterpret_cast(&val), sizeof(uint64_t)}; } static std::string encodeRank(EdgeRanking rank) { return IndexKeyUtils::encodeInt64(rank); } diff --git a/src/graph/util/SchemaUtil.cpp b/src/graph/util/SchemaUtil.cpp index ccbcb9ca028..c638efa5365 100644 --- a/src/graph/util/SchemaUtil.cpp +++ b/src/graph/util/SchemaUtil.cpp @@ -67,7 +67,8 @@ std::shared_ptr SchemaUtil::generateSchemaProv col.get_type().get_type(), col.type.type_length_ref().value_or(0), col.nullable_ref().value_or(false), - hasDef ? defaultValueExpr : nullptr); + hasDef ? defaultValueExpr : nullptr, + col.type.geo_shape_ref().value_or(meta::cpp2::GeoShape::ANY)); } return schemaPtr; } @@ -259,6 +260,13 @@ std::string SchemaUtil::typeToString(const meta::cpp2::ColumnTypeDef &col) { auto type = apache::thrift::util::enumNameSafe(col.get_type()); if (col.get_type() == meta::cpp2::PropertyType::FIXED_STRING) { return folly::stringPrintf("%s(%d)", type.c_str(), *col.get_type_length()); + } else if (col.get_type() == meta::cpp2::PropertyType::GEOGRAPHY) { + auto geoShape = *col.get_geo_shape(); + if (geoShape == meta::cpp2::GeoShape::ANY) { + return folly::stringPrintf("%s", type.c_str()); + } + return folly::stringPrintf( + "%s(%s)", type.c_str(), apache::thrift::util::enumNameSafe(geoShape).c_str()); } return type; } diff --git a/src/graph/validator/MaintainValidator.cpp b/src/graph/validator/MaintainValidator.cpp index ca04be664ee..345b31d1039 100644 --- a/src/graph/validator/MaintainValidator.cpp +++ b/src/graph/validator/MaintainValidator.cpp @@ -37,6 +37,8 @@ Status SchemaValidator::validateColumns(const std::vector column.type.set_type(type); if (meta::cpp2::PropertyType::FIXED_STRING == type) { column.type.set_type_length(spec->typeLen()); + } else if (meta::cpp2::PropertyType::GEOGRAPHY == type) { + column.type.set_geo_shape(spec->geoShape()); } for (const auto &property : spec->properties()->properties()) { if (property->isNullable()) { diff --git a/src/meta/processors/schema/SchemaUtil.cpp b/src/meta/processors/schema/SchemaUtil.cpp index 5d1b45f80fd..a135ad4df27 100644 --- a/src/meta/processors/schema/SchemaUtil.cpp +++ b/src/meta/processors/schema/SchemaUtil.cpp @@ -158,6 +158,13 @@ bool SchemaUtil::checkType(std::vector& columns) { return false; } break; + case cpp2::PropertyType::GEOGRAPHY: + if (!value.isGeography()) { // TODO(jie) + LOG(ERROR) << "Invalid default value for ` " << name << "', value type is " + << value.type(); + return false; + } + break; default: LOG(ERROR) << "Unsupported type"; return false; diff --git a/src/parser/MaintainSentences.h b/src/parser/MaintainSentences.h index ee2e0393b1c..603e52cba30 100644 --- a/src/parser/MaintainSentences.h +++ b/src/parser/MaintainSentences.h @@ -83,9 +83,14 @@ class ColumnSpecification final { public: ColumnSpecification(std::string *name, meta::cpp2::PropertyType type, - ColumnProperties *properties, - int16_t typeLen = 0) - : name_(name), type_(type), properties_(DCHECK_NOTNULL(properties)), typeLen_(typeLen) {} + ColumnProperties *properties = nullptr, + int16_t typeLen = 0, + meta::cpp2::GeoShape geoShape = meta::cpp2::GeoShape::ANY) + : name_(name), + type_(type), + properties_(DCHECK_NOTNULL(properties)), + typeLen_(typeLen), + geoShape_(geoShape) {} meta::cpp2::PropertyType type() const { return type_; } @@ -93,6 +98,8 @@ class ColumnSpecification final { int16_t typeLen() const { return typeLen_; } + meta::cpp2::GeoShape geoShape() const { return geoShape_; } + auto &properties() const { return properties_; } std::string toString() const; @@ -100,8 +107,9 @@ class ColumnSpecification final { private: std::unique_ptr name_; meta::cpp2::PropertyType type_; - std::unique_ptr properties_{nullptr}; - int16_t typeLen_{0}; + std::unique_ptr properties_; + int16_t typeLen_; + meta::cpp2::GeoShape geoShape_; }; class ColumnSpecificationList final { diff --git a/src/parser/parser.yy b/src/parser/parser.yy index e0badaec70a..6c1d0a19388 100644 --- a/src/parser/parser.yy +++ b/src/parser/parser.yy @@ -2306,11 +2306,11 @@ column_spec_list column_spec : name_label type_spec { - $$ = new ColumnSpecification($1, $2->type, new ColumnProperties(), $2->type_length_ref().value_or(0)); + $$ = new ColumnSpecification($1, $2->type, new ColumnProperties(), $2->type_length_ref().value_or(0), $2->geo_shape_ref().value_or(meta::cpp2::GeoShape::ANY)); delete $2; } | name_label type_spec column_properties { - $$ = new ColumnSpecification($1, $2->type, $3, $2->type_length_ref().value_or(0)); + $$ = new ColumnSpecification($1, $2->type, $3, $2->type_length_ref().value_or(0), $2->geo_shape_ref().value_or(meta::cpp2::GeoShape::ANY)); delete $2; } ;