From b6c1b594c2146e32967b243951eb17f68e6c5e47 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 17 Jan 2025 14:37:47 -0800 Subject: [PATCH] Fix overlay heuristic for GeometryCollections with empty elements (#1229) --- include/geos/geom/HeuristicOverlay.h | 38 ++-- src/geom/Geometry.cpp | 71 ------- src/geom/HeuristicOverlay.cpp | 181 +++++++++++++++--- src/operation/union/CascadedPolygonUnion.cpp | 6 +- src/operation/valid/MakeValid.cpp | 9 +- tests/unit/geom/HeuristicOverlayTest.cpp | 14 +- tests/xmltester/BufferResultMatcher.cpp | 10 +- tests/xmltester/XMLTester.cpp | 29 ++- .../tests/general/TestOverlayEmpty.xml | 49 +++++ 9 files changed, 273 insertions(+), 134 deletions(-) diff --git a/include/geos/geom/HeuristicOverlay.h b/include/geos/geom/HeuristicOverlay.h index bc1217a785..e4eb8b74ad 100644 --- a/include/geos/geom/HeuristicOverlay.h +++ b/include/geos/geom/HeuristicOverlay.h @@ -46,11 +46,25 @@ class StructuredCollection { public: + static std::unique_ptr overlay(const Geometry* g0, const Geometry* g1, int opCode); + +private: + + const GeometryFactory* factory; + std::vector pts; + std::vector lines; + std::vector polys; + std::unique_ptr pt_union; + std::unique_ptr line_union; + std::unique_ptr poly_union; + Dimension::DimensionType dimension; + StructuredCollection(const Geometry* g) : factory(g->getFactory()) , pt_union(nullptr) , line_union(nullptr) , poly_union(nullptr) + , dimension(Dimension::DONTCARE) { readCollection(g); unionByDimension(); @@ -61,8 +75,19 @@ class StructuredCollection { , pt_union(nullptr) , line_union(nullptr) , poly_union(nullptr) + , dimension(Dimension::DONTCARE) {}; + Dimension::DimensionType getDimension() const + { + return dimension; + }; + + void addDimension(Dimension::DimensionType dim); + std::unique_ptr doUnaryUnion(int resultDim) const; + std::unique_ptr computeResult(StructuredCollection& coll, int opCode, + Dimension::DimensionType dimA, Dimension::DimensionType dimB) const; + void readCollection(const Geometry* g); const Geometry* getPolyUnion() const { return poly_union.get(); } const Geometry* getLineUnion() const { return line_union.get(); } @@ -72,22 +97,9 @@ class StructuredCollection { std::unique_ptr doIntersection(const StructuredCollection& a) const; std::unique_ptr doSymDifference(const StructuredCollection& a) const; std::unique_ptr doDifference(const StructuredCollection& a) const; - std::unique_ptr doUnaryUnion() const; static void toVector(const Geometry* g, std::vector& v); void unionByDimension(void); - - -private: - - const GeometryFactory* factory; - std::vector pts; - std::vector lines; - std::vector polys; - std::unique_ptr pt_union; - std::unique_ptr line_union; - std::unique_ptr poly_union; - }; diff --git a/src/geom/Geometry.cpp b/src/geom/Geometry.cpp index 4339335870..a34c7bf352 100644 --- a/src/geom/Geometry.cpp +++ b/src/geom/Geometry.cpp @@ -619,43 +619,6 @@ Geometry::intersection(const Geometry* other) const std::unique_ptr Geometry::Union(const Geometry* other) const { -#ifdef SHORTCIRCUIT_PREDICATES - // if envelopes are disjoint return a MULTI geom or - // a geometrycollection - if(! getEnvelopeInternal()->intersects(other->getEnvelopeInternal())) { -//cerr<<"SHORTCIRCUITED-UNION engaged"<getNumGeometries(); - - // Allocated for ownership transfer - std::vector> v; - v.reserve(ngeomsThis + ngeomsOther); - - - if(nullptr != (coll = dynamic_cast(this))) { - for(std::size_t i = 0; i < ngeomsThis; ++i) { - v.push_back(coll->getGeometryN(i)->clone()); - } - } - else { - v.push_back(this->clone()); - } - - if(nullptr != (coll = dynamic_cast(other))) { - for(std::size_t i = 0; i < ngeomsOther; ++i) { - v.push_back(coll->getGeometryN(i)->clone()); - } - } - else { - v.push_back(other->clone()); - } - - return _factory->buildGeometry(std::move(v)); - } -#endif - return HeuristicOverlay(this, other, OverlayNG::UNION); } @@ -676,40 +639,6 @@ Geometry::difference(const Geometry* other) const std::unique_ptr Geometry::symDifference(const Geometry* other) const { - // if envelopes are disjoint return a MULTI geom or - // a geometrycollection - if(! getEnvelopeInternal()->intersects(other->getEnvelopeInternal()) && !(isEmpty() && other->isEmpty())) { - const GeometryCollection* coll; - - std::size_t ngeomsThis = getNumGeometries(); - std::size_t ngeomsOther = other->getNumGeometries(); - - // Allocated for ownership transfer - std::vector> v; - v.reserve(ngeomsThis + ngeomsOther); - - - if(nullptr != (coll = dynamic_cast(this))) { - for(std::size_t i = 0; i < ngeomsThis; ++i) { - v.push_back(coll->getGeometryN(i)->clone()); - } - } - else { - v.push_back(this->clone()); - } - - if(nullptr != (coll = dynamic_cast(other))) { - for(std::size_t i = 0; i < ngeomsOther; ++i) { - v.push_back(coll->getGeometryN(i)->clone()); - } - } - else { - v.push_back(other->clone()); - } - - return _factory->buildGeometry(std::move(v)); - } - return HeuristicOverlay(this, other, OverlayNG::SYMDIFFERENCE); } diff --git a/src/geom/HeuristicOverlay.cpp b/src/geom/HeuristicOverlay.cpp index 2d3d51304c..c164e00fb7 100644 --- a/src/geom/HeuristicOverlay.cpp +++ b/src/geom/HeuristicOverlay.cpp @@ -20,11 +20,19 @@ * This file provides a single function, taking two * const Geometry pointers, applying a binary operator to them * and returning a result Geometry in an unique_ptr<>. + * + * It implements a "combine-disjoint" heuristic for optimizing some overlay cases. + * It also implements overlay for GeometryCollections, which is not (yet) + * provided by OverlayNG. + * Internal overlay usage should call OverlayNG directly + * if this behaviour is not needed (i.e. when the inputs are known to be simple + * or when full overlay is desired to be computed.) * **********************************************************************/ #include #include +#include #include #include #include @@ -39,32 +47,109 @@ namespace geom { // geos::geom using operation::overlayng::OverlayNG; using operation::overlayng::OverlayNGRobust; +using operation::overlayng::OverlayUtil; + +bool +hasSingleNonEmptyElement(const Geometry* geom) { + if (geom->getGeometryTypeId() != GEOS_GEOMETRYCOLLECTION) { + //-- is a non-empty element + return ! geom->isEmpty(); + } + //-- iterate over GC elements and determine if a single non-empty is present + //-- buffer num geoms, since can be expensive for nested collections + bool foundNonEmptyElement = false; + std::size_t numGeoms = geom->getNumGeometries(); + for(std::size_t i = 0; i < numGeoms; ++i) { + if (hasSingleNonEmptyElement(geom->getGeometryN(i))) { + //-- already found an element, so more than one present + if (foundNonEmptyElement) { + return false; + } + foundNonEmptyElement = true; + } + } + return foundNonEmptyElement; +} + +bool isCombinable(const Geometry* g0, const Geometry* g1) +{ + //-- if both empty use OverlayNG return-type logic + if (g0->isEmpty() && g1->isEmpty()) + return false; + + //-- not disjoint + if (g0->getEnvelopeInternal()->intersects(g1->getEnvelopeInternal())) + return false; + + return hasSingleNonEmptyElement(g0) + && hasSingleNonEmptyElement(g1); +} + +void +extractElements(const Geometry* g, std::vector>& v) +{ + if (const auto* coll = dynamic_cast(g)) { + //-- buffer num geoms, since can be expensive for nested collections + std::size_t numGeoms = g->getNumGeometries(); + for(std::size_t i = 0; i < numGeoms; ++i) { + //-- recurse to handle nested GCs + extractElements(coll->getGeometryN(i), v); + } + } + else if (g->isEmpty()) { + return; + } + else { + v.push_back(g->clone()); + } +} + +std::unique_ptr +combineReduced(const Geometry* g0, const Geometry* g1) +{ + // Allocated for ownership transfer + std::vector> v; + v.reserve(g0->getNumGeometries() + g1->getNumGeometries()); + extractElements(g0, v); + extractElements(g1, v); + return g0->getFactory()->buildGeometry(std::move(v)); +} + +bool isHandledByOverlayNG(const Geometry* geom) { + if (geom->isMixedDimension() && ! geom->isEmpty()) + return false; + //-- GCs with polygonals must be unioned + if (geom->getGeometryTypeId() == GEOS_GEOMETRYCOLLECTION + && geom->getDimension() == 2) + return false; + return true; +} std::unique_ptr HeuristicOverlay(const Geometry* g0, const Geometry* g1, int opCode) { - std::unique_ptr ret; + /** + * If feasible, do fast combine instead of full overlay + * + * NOTE: does not node LineStrings, or merge elements of arg geoms. + * This is different behaviour to full overlay. + */ + //TODO: could also handle difference and intersection? + if ( ( opCode == OverlayNG::UNION + || opCode == OverlayNG::SYMDIFFERENCE) + && isCombinable(g0, g1)) { + return combineReduced(g0, g1); + } /* * overlayng::OverlayNGRobust does not currently handle - * GeometryCollection (collections of mixed dimension) + * non-empty GeometryCollections * so we handle that case here. */ - if ((g0->isMixedDimension() && !g0->isEmpty()) || - (g1->isMixedDimension() && !g1->isEmpty())) + if (! isHandledByOverlayNG(g0) || + ! isHandledByOverlayNG(g1)) { - StructuredCollection s0(g0); - StructuredCollection s1(g1); - switch (opCode) { - case OverlayNG::UNION: - return s0.doUnion(s1); - case OverlayNG::DIFFERENCE: - return s0.doDifference(s1); - case OverlayNG::SYMDIFFERENCE: - return s0.doSymDifference(s1); - case OverlayNG::INTERSECTION: - return s0.doIntersection(s1); - } + return StructuredCollection::overlay(g0, g1, opCode); } /* @@ -86,6 +171,7 @@ HeuristicOverlay(const Geometry* g0, const Geometry* g1, int opCode) * Running overlayng::OverlayNGRobust at this stage should guarantee * that none of the other heuristics are ever needed. */ + std::unique_ptr ret; if (g0 == nullptr && g1 == nullptr) { return std::unique_ptr(nullptr); } @@ -108,6 +194,34 @@ HeuristicOverlay(const Geometry* g0, const Geometry* g1, int opCode) return ret; } +/* public static */ +std::unique_ptr +StructuredCollection::overlay(const Geometry* g0, const Geometry* g1, int opCode) +{ + StructuredCollection s0(g0); + StructuredCollection s1(g1); + switch (opCode) { + case OverlayNG::UNION: + return s0.doUnion(s1); + case OverlayNG::DIFFERENCE: + return s0.doDifference(s1); + case OverlayNG::SYMDIFFERENCE: + return s0.doSymDifference(s1); + case OverlayNG::INTERSECTION: + return s0.doIntersection(s1); + default: + throw util::IllegalArgumentException("Invalid overlay opcode"); + } +} + +/* private */ +void +StructuredCollection::addDimension(Dimension::DimensionType dim) +{ + if (dimension < dim) + dimension = dim; +} + /* public */ void StructuredCollection::readCollection(const Geometry* g) @@ -123,12 +237,15 @@ StructuredCollection::readCollection(const Geometry* g) switch (g->getGeometryTypeId()) { case GEOS_POINT: pts.push_back(g); + addDimension(Dimension::P); break; case GEOS_LINESTRING: lines.push_back(g); + addDimension(Dimension::L); break; case GEOS_POLYGON: polys.push_back(g); + addDimension(Dimension::A); break; default: throw util::IllegalArgumentException("cannot process unexpected collection"); @@ -181,7 +298,9 @@ StructuredCollection::unionByDimension(void) // io::WKTWriter w; // std::cout << "line_col " << w.write(*line_col) << std::endl; - // std::cout << "line_union " << w.write(*line_union) << std::endl; + // std::cout << "pt_union: " << w.write(*pt_union) << std::endl; + // std::cout << "line_union: " << w.write(*line_union) << std::endl; + // std::cout << "poly_union: " << w.write(*poly_union) << std::endl; if (! pt_union->isPuntal()) throw util::IllegalArgumentException("union of points not puntal"); @@ -193,7 +312,7 @@ StructuredCollection::unionByDimension(void) /* public */ std::unique_ptr -StructuredCollection::doUnaryUnion() const +StructuredCollection::doUnaryUnion(int resultDim) const { /* * Before output, we clean up the components to remove spatial @@ -221,15 +340,27 @@ StructuredCollection::doUnaryUnion() const toVector(lines_less_polys.get(), geoms); toVector(poly_union.get(), geoms); + if (geoms.size() == 0) { + return OverlayUtil::createEmptyResult( + resultDim, factory); + } return factory->buildGeometry(geoms.begin(), geoms.end()); } +/* public */ +std::unique_ptr +StructuredCollection::computeResult(StructuredCollection& coll, int opCode, + Dimension::DimensionType dimA, Dimension::DimensionType dimB) const +{ + coll.unionByDimension(); + int resultDim = OverlayUtil::resultDimension(opCode, dimA, dimB); + return coll.doUnaryUnion(resultDim); +} /* public */ std::unique_ptr StructuredCollection::doUnion(const StructuredCollection& a) const { - auto poly_union_poly = OverlayNGRobust::Overlay( a.getPolyUnion(), poly_union.get(), @@ -249,8 +380,7 @@ StructuredCollection::doUnion(const StructuredCollection& a) const c.readCollection(poly_union_poly.get()); c.readCollection(line_union_line.get()); c.readCollection(pt_union_pt.get()); - c.unionByDimension(); - return c.doUnaryUnion(); + return computeResult(c, OverlayNG::UNION, getDimension(), a.getDimension()); } @@ -322,8 +452,7 @@ StructuredCollection::doIntersection(const StructuredCollection& a) const c.readCollection(pt_inter_poly.get()); c.readCollection(pt_inter_line.get()); c.readCollection(pt_inter_pt.get()); - c.unionByDimension(); - return c.doUnaryUnion(); + return computeResult(c, OverlayNG::INTERSECTION, getDimension(), a.getDimension()); } @@ -364,8 +493,7 @@ StructuredCollection::doDifference(const StructuredCollection& a) const c.readCollection(poly_diff_poly.get()); c.readCollection(line_diff_poly_line.get()); c.readCollection(pt_diff_poly_line_pt.get()); - c.unionByDimension(); - return c.doUnaryUnion(); + return computeResult(c, OverlayNG::DIFFERENCE, getDimension(), a.getDimension()); } std::unique_ptr @@ -390,8 +518,7 @@ StructuredCollection::doSymDifference(const StructuredCollection& a) const c.readCollection(poly_symdiff_poly.get()); c.readCollection(line_symdiff_line.get()); c.readCollection(pt_symdiff_pt.get()); - c.unionByDimension(); - return c.doUnaryUnion(); + return computeResult(c, OverlayNG::SYMDIFFERENCE, getDimension(), a.getDimension()); } diff --git a/src/operation/union/CascadedPolygonUnion.cpp b/src/operation/union/CascadedPolygonUnion.cpp index 2a4e736f42..82cd91bcc7 100644 --- a/src/operation/union/CascadedPolygonUnion.cpp +++ b/src/operation/union/CascadedPolygonUnion.cpp @@ -20,11 +20,11 @@ #include #include -#include #include #include #include #include +#include #include #include #include @@ -201,10 +201,8 @@ CascadedPolygonUnion::restrictToPolygons(std::unique_ptr g) std::unique_ptr ClassicUnionStrategy::Union(const geom::Geometry* g0, const geom::Geometry* g1) { - // TODO make an rvalue overload for this that can consume its inputs. - // At a minimum, a copy in the buffer fallback can be eliminated. try { - return geom::HeuristicOverlay(g0, g1, operation::overlayng::OverlayNG::UNION); + return operation::overlayng::OverlayNGRobust::Overlay(g0, g1, operation::overlayng::OverlayNG::UNION); } catch (const util::TopologyException &ex) { ::geos::ignore_unused_variable_warning(ex); diff --git a/src/operation/valid/MakeValid.cpp b/src/operation/valid/MakeValid.cpp index f544283905..4d00027c71 100644 --- a/src/operation/valid/MakeValid.cpp +++ b/src/operation/valid/MakeValid.cpp @@ -23,9 +23,9 @@ #include #include +#include #include #include -#include #include #include #include @@ -51,6 +51,7 @@ using namespace geos::geom; using geos::operation::overlayng::OverlayNG; +using geos::operation::overlayng::OverlayNGRobust; namespace geos { namespace operation { // geos.operation @@ -60,19 +61,19 @@ namespace valid { // geos.operation.valid static std::unique_ptr makeValidSymDifference(const geom::Geometry* g0, const geom::Geometry* g1) { - return HeuristicOverlay(g0, g1, OverlayNG::SYMDIFFERENCE); + return OverlayNGRobust::Overlay(g0, g1, OverlayNG::SYMDIFFERENCE); } static std::unique_ptr makeValidDifference(const geom::Geometry* g0, const geom::Geometry* g1) { - return HeuristicOverlay(g0, g1, OverlayNG::DIFFERENCE); + return OverlayNGRobust::Overlay(g0, g1, OverlayNG::DIFFERENCE); } static std::unique_ptr makeValidUnion(const geom::Geometry* g0, const geom::Geometry* g1) { - return HeuristicOverlay(g0, g1, OverlayNG::UNION); + return OverlayNGRobust::Overlay(g0, g1, OverlayNG::UNION); } /* diff --git a/tests/unit/geom/HeuristicOverlayTest.cpp b/tests/unit/geom/HeuristicOverlayTest.cpp index c687e8a69b..0f9a197179 100644 --- a/tests/unit/geom/HeuristicOverlayTest.cpp +++ b/tests/unit/geom/HeuristicOverlayTest.cpp @@ -163,7 +163,7 @@ void object::test<8> () "GEOMETRYCOLLECTION(POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)))", "GEOMETRYCOLLECTION(POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)), POINT(20 20))", OverlayNG::DIFFERENCE, - "GEOMETRYCOLLECTION EMPTY" + "POLYGON EMPTY" ); } @@ -204,4 +204,16 @@ void object::test<11> () ); } +template<> +template<> +void object::test<12> () +{ + checkOverlay( + "GEOMETRYCOLLECTION (POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5)), POLYGON ((9 5, 9 1, 5 1, 5 5, 9 5)))", + "POLYGON ((1 5, 9 5, 9 1, 1 1, 1 5))", + OverlayNG::DIFFERENCE, + "POLYGON EMPTY" + ); +} + } // namespace tut diff --git a/tests/xmltester/BufferResultMatcher.cpp b/tests/xmltester/BufferResultMatcher.cpp index dd260ca7c3..7100673d12 100644 --- a/tests/xmltester/BufferResultMatcher.cpp +++ b/tests/xmltester/BufferResultMatcher.cpp @@ -19,7 +19,6 @@ #include "BufferResultMatcher.h" #include -#include #include #include @@ -65,15 +64,8 @@ BufferResultMatcher::isSymDiffAreaInTolerance( const geom::Geometry& actualBuffer, const geom::Geometry& expectedBuffer) { - typedef std::unique_ptr GeomPtr; - - using geos::geom::HeuristicOverlay; - double area = expectedBuffer.getArea(); - GeomPtr diff = HeuristicOverlay(&actualBuffer, &expectedBuffer, - operation::overlayng::OverlayNG::SYMDIFFERENCE); - - double areaDiff = diff->getArea(); + double areaDiff = actualBuffer.symDifference(&expectedBuffer)->getArea(); // can't get closer than difference area = 0 ! // This also handles case when symDiff is empty diff --git a/tests/xmltester/XMLTester.cpp b/tests/xmltester/XMLTester.cpp index 165fa199ba..aa01c88dae 100644 --- a/tests/xmltester/XMLTester.cpp +++ b/tests/xmltester/XMLTester.cpp @@ -178,6 +178,13 @@ tolower(std::string& str) ); } +void toupper(std::string& s) +{ + std::transform(s.begin(), s.end(), s.begin(), + [](char c){ return (char)std::toupper(c); } + ); +} + std::string normalize_filename(const std::string& str) { @@ -875,11 +882,13 @@ XMLTester::parseTest(const tinyxml2::XMLNode* node) tmp = opel->Attribute("arg1"); if(tmp) { opArg1 = tmp; + toupper(opArg1); } tmp = opel->Attribute("arg2"); if(tmp) { opArg2 = tmp; + toupper(opArg2); } tmp = opel->Attribute("arg3"); @@ -988,13 +997,15 @@ XMLTester::parseTest(const tinyxml2::XMLNode* node) } else if(opName == "intersection") { + geom::Geometry* g1 = opArg1 == "B" ? gB : gA; + geom::Geometry* g2 = opArg2 == "B" ? gB : gA; GeomPtr gRes(parseGeometry(opRes, "expected")); gRes->normalize(); profile.start(); - GeomPtr gRealRes(gA->intersection(gB)); + GeomPtr gRealRes(g1->intersection(g2)); profile.stop(); @@ -1312,6 +1323,9 @@ XMLTester::parseTest(const tinyxml2::XMLNode* node) else if(opName == "union") { + geom::Geometry* g1 = opArg1 == "B" ? gB : gA; + geom::Geometry* g2 = opArg2 == "B" ? gB : gA; + GeomPtr gRes(parseGeometry(opRes, "expected")); gRes->normalize(); @@ -1319,10 +1333,10 @@ XMLTester::parseTest(const tinyxml2::XMLNode* node) GeomPtr gRealRes; if(gB) { - gRealRes = gA->Union(gB); + gRealRes = g1->Union(g2); } else { - gRealRes = gA->Union(); + gRealRes = g1->Union(); } profile.stop(); @@ -1339,11 +1353,13 @@ XMLTester::parseTest(const tinyxml2::XMLNode* node) } else if(opName == "difference") { + geom::Geometry* g1 = opArg1 == "B" ? gB : gA; + geom::Geometry* g2 = opArg2 == "B" ? gB : gA; GeomPtr gRes(parseGeometry(opRes, "expected")); gRes->normalize(); - GeomPtr gRealRes(gA->difference(gB)); + GeomPtr gRealRes(g1->difference(g2)); gRealRes->normalize(); @@ -1360,10 +1376,13 @@ XMLTester::parseTest(const tinyxml2::XMLNode* node) } else if(opName == "symdifference") { + geom::Geometry* g1 = opArg1 == "B" ? gB : gA; + geom::Geometry* g2 = opArg2 == "B" ? gB : gA; + GeomPtr gRes(parseGeometry(opRes, "expected")); gRes->normalize(); - GeomPtr gRealRes(gA->symDifference(gB)); + GeomPtr gRealRes(g1->symDifference(g2)); gRealRes->normalize(); diff --git a/tests/xmltester/tests/general/TestOverlayEmpty.xml b/tests/xmltester/tests/general/TestOverlayEmpty.xml index 9dd9896727..2c17375c58 100644 --- a/tests/xmltester/tests/general/TestOverlayEmpty.xml +++ b/tests/xmltester/tests/general/TestOverlayEmpty.xml @@ -1016,6 +1016,55 @@ POLYGON EMPTY + + PC - point and disjoint GC with empty elements + +POINT (1 1) + + +GEOMETRYCOLLECTION (POLYGON EMPTY, POINT EMPTY, LINESTRING (2 2, 3 3)) + + +POINT EMPTY + + +GEOMETRYCOLLECTION (LINESTRING (2 2, 3 3), POINT (1 1)) + + +POINT (1 1) + + +LINESTRING (2 2, 3 3) + + +GEOMETRYCOLLECTION (LINESTRING (2 2, 3 3), POINT (1 1)) + + + + + PC - point and disjoint GC with nested empty elements + +POINT (1 1) + + +GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POLYGON EMPTY, POINT EMPTY), GEOMETRYCOLLECTION(POLYGON EMPTY, LINESTRING (2 2, 3 3))) + + +POINT EMPTY + + +GEOMETRYCOLLECTION (LINESTRING (2 2, 3 3), POINT (1 1)) + + +POINT (1 1) + + +LINESTRING (2 2, 3 3) + + +GEOMETRYCOLLECTION (LINESTRING (2 2, 3 3), POINT (1 1)) + +