Repository navigation
Add GEOS-backed polygon triangulation to Core #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+867
−1
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
src/MapWinGIS.Core/include/mapwingis/rendering/PolygonTriangulation.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // SPDX-License-Identifier: MPL-2.0 | ||
|
|
||
| /** | ||
| * @file PolygonTriangulation.h | ||
| * @brief Declares reusable polygon triangulation for rendering. | ||
| * | ||
| * Converts a backend-neutral polygon with optional interior rings into an | ||
| * indexed triangle mesh using the constrained Delaunay triangulation provided | ||
| * by GDAL/OGR and GEOS. The mesh stays in the source coordinate system and | ||
| * contains no graphics-API or platform types. | ||
| */ | ||
| #pragma once | ||
|
|
||
| #include "mapwingis/core/Api.h" | ||
| #include "mapwingis/rendering/VectorRenderScene.h" | ||
|
|
||
| namespace mwg::rendering | ||
| { | ||
| /// Triangulates a polygon with zero or more interior rings into a 2D | ||
| /// fill mesh. | ||
| /// | ||
| /// The first ring of \p polygon is the exterior boundary; any further | ||
| /// rings are interior holes. Rings do not need to repeat their first | ||
| /// point at the end. Interior rings with fewer than three points are | ||
| /// ignored. Duplicate consecutive vertices are tolerated. | ||
| /// | ||
| /// The triangulation is the constrained Delaunay triangulation from | ||
| /// OGRGeometry::ConstrainedDelaunayTriangulation(), which preserves the | ||
| /// polygon boundaries. Vertices are shared through the index buffer and | ||
| /// all triangles have counter-clockwise winding. The result is the 2D | ||
| /// rendering mesh; no Z or M values are computed. | ||
| /// | ||
| /// Invalid input is never repaired: self-intersecting or otherwise | ||
| /// invalid polygons are rejected instead of being reinterpreted. | ||
| /// \throws InvalidArgument when the polygon has no exterior ring of at | ||
| /// least three points or contains non-finite coordinates. | ||
| /// \throws CoreError when the polygon is invalid or the triangulation | ||
| /// fails or produces no triangles. | ||
| [[nodiscard]] MWG_CORE_API TriangleMesh TriangulatePolygon(const Polygon& polygon); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
235 changes: 235 additions & 0 deletions
235
src/MapWinGIS.Core/src/rendering/PolygonTriangulation.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| // SPDX-License-Identifier: MPL-2.0 | ||
|
|
||
| /** | ||
| * @file PolygonTriangulation.cpp | ||
| * @brief Implements polygon triangulation through OGR and GEOS. | ||
| * | ||
| * Builds a temporary OGR polygon from the backend-neutral rings, validates | ||
| * it, and converts the constrained Delaunay triangulation into an indexed | ||
| * triangle mesh. All OGR geometry is owned through RAII handles. | ||
| */ | ||
| #include "mapwingis/rendering/PolygonTriangulation.h" | ||
|
|
||
| #include <cmath> | ||
| #include <cstdint> | ||
| #include <limits> | ||
| #include <map> | ||
| #include <memory> | ||
| #include <new> | ||
| #include <string> | ||
| #include <utility> | ||
|
|
||
| #include <ogrsf_frmts.h> | ||
|
|
||
| #include "mapwingis/core/Error.h" | ||
| #include "mapwingis/gdal/GdalDiagnostics.h" | ||
| #include "mapwingis/gdal/GdalEnvironment.h" | ||
|
|
||
| namespace mwg::rendering | ||
| { | ||
| namespace | ||
| { | ||
| std::unique_ptr<OGRLinearRing> MakeRing(const Ring& ring) | ||
| { | ||
| auto result = std::make_unique<OGRLinearRing>(); | ||
| for (const Point& point : ring) | ||
| { | ||
| result->addPoint(point.x, point.y); | ||
| } | ||
| if (result->getNumPoints() > 1) | ||
| { | ||
| result->closeRings(); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| void ValidateRing(const Ring& ring, const char* role) | ||
| { | ||
| if (ring.size() < 3) | ||
| { | ||
| throw InvalidArgument(std::string("Polygon ") + role + | ||
| " ring has fewer than three points"); | ||
| } | ||
| for (const Point& point : ring) | ||
| { | ||
| if (!std::isfinite(point.x) || !std::isfinite(point.y)) | ||
| { | ||
| throw InvalidArgument(std::string("Polygon ") + role + | ||
| " ring contains a non-finite coordinate"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| std::unique_ptr<OGRPolygon> MakeOgrPolygon(const Polygon& polygon) | ||
| { | ||
| if (polygon.empty()) | ||
| { | ||
| throw InvalidArgument("Polygon has no rings"); | ||
| } | ||
|
|
||
| ValidateRing(polygon.front(), "exterior"); | ||
| auto result = std::make_unique<OGRPolygon>(); | ||
| result->addRingDirectly(MakeRing(polygon.front()).release()); | ||
| for (std::size_t index = 1; index < polygon.size(); ++index) | ||
| { | ||
| const Ring& hole = polygon[index]; | ||
| if (hole.size() < 3) | ||
| { | ||
| continue; | ||
| } | ||
| ValidateRing(hole, "interior"); | ||
| result->addRingDirectly(MakeRing(hole).release()); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| double TwiceSignedArea(const Point& a, const Point& b, const Point& c) | ||
| { | ||
| return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y); | ||
| } | ||
|
|
||
| /// Deduplicates vertices and assembles consistently wound triangles. | ||
| class MeshBuilder | ||
| { | ||
| public: | ||
| void AddTriangle(Point a, Point b, Point c) | ||
| { | ||
| if (TwiceSignedArea(a, b, c) < 0.0) | ||
| { | ||
| std::swap(b, c); | ||
| } | ||
| _mesh.indices.push_back(AddVertex(a)); | ||
| _mesh.indices.push_back(AddVertex(b)); | ||
| _mesh.indices.push_back(AddVertex(c)); | ||
| } | ||
|
|
||
| [[nodiscard]] TriangleMesh TakeResult() | ||
| { | ||
| TriangleMesh result; | ||
| result.vertices = std::move(_mesh.vertices); | ||
| result.indices = std::move(_mesh.indices); | ||
| return result; | ||
| } | ||
|
|
||
| private: | ||
| [[nodiscard]] std::uint32_t AddVertex(const Point& point) | ||
| { | ||
| const auto key = std::make_pair(point.x, point.y); | ||
| const auto found = _vertexLookup.find(key); | ||
| if (found != _vertexLookup.end()) | ||
| { | ||
| return found->second; | ||
| } | ||
| if (_mesh.vertices.size() >= | ||
| static_cast<std::size_t>(std::numeric_limits<std::uint32_t>::max())) | ||
| { | ||
| throw CoreError( | ||
| "Polygon triangulation produced more distinct vertices than the " | ||
| "mesh index type supports"); | ||
| } | ||
| const auto index = static_cast<std::uint32_t>(_mesh.vertices.size()); | ||
| _mesh.vertices.push_back(point); | ||
| _vertexLookup.emplace(key, index); | ||
| return index; | ||
| } | ||
|
|
||
| TriangleMesh _mesh; | ||
| std::map<std::pair<double, double>, std::uint32_t> _vertexLookup; | ||
| }; | ||
|
|
||
| void AppendTriangle(const OGRPolygon& triangle, MeshBuilder& builder) | ||
| { | ||
| const OGRLinearRing* ring = triangle.getExteriorRing(); | ||
| if (ring == nullptr || ring->getNumPoints() < 3) | ||
| { | ||
| return; | ||
| } | ||
| const Point a{ring->getX(0), ring->getY(0)}; | ||
| const Point b{ring->getX(1), ring->getY(1)}; | ||
| const Point c{ring->getX(2), ring->getY(2)}; | ||
| if (TwiceSignedArea(a, b, c) == 0.0) | ||
| { | ||
| return; | ||
| } | ||
| builder.AddTriangle(a, b, c); | ||
| } | ||
|
|
||
| void AppendTriangles(const OGRGeometry& geometry, MeshBuilder& builder) | ||
| { | ||
| switch (wkbFlatten(geometry.getGeometryType())) | ||
| { | ||
| case wkbPolygon: | ||
| AppendTriangle(static_cast<const OGRPolygon&>(geometry), builder); | ||
| return; | ||
|
|
||
| case wkbMultiPolygon: | ||
| case wkbGeometryCollection: | ||
| { | ||
| const auto& collection = static_cast<const OGRGeometryCollection&>(geometry); | ||
| for (int index = 0; index < collection.getNumGeometries(); ++index) | ||
| { | ||
| const OGRGeometry* child = collection.getGeometryRef(index); | ||
| if (child != nullptr) | ||
| { | ||
| AppendTriangles(*child, builder); | ||
| } | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| default: | ||
| throw CoreError("Triangulation returned an unexpected geometry type"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| TriangleMesh TriangulatePolygon(const Polygon& polygon) | ||
| { | ||
| GdalEnvironment::Initialize(); | ||
| const std::unique_ptr<OGRPolygon> source = MakeOgrPolygon(polygon); | ||
|
|
||
| GdalErrorScope scope; | ||
| const bool valid = source->IsValid(); | ||
| scope.RethrowCaptureException(); | ||
| if (!valid) | ||
| { | ||
| throw CoreError( | ||
| "Polygon triangulation requires a valid polygon; the input is " | ||
| "self-intersecting or otherwise invalid"); | ||
| } | ||
|
|
||
| std::unique_ptr<OGRGeometry> triangulation; | ||
| try | ||
| { | ||
| triangulation.reset(source->ConstrainedDelaunayTriangulation()); | ||
| } | ||
| catch (const std::bad_alloc&) | ||
| { | ||
| throw; | ||
| } | ||
| catch (const std::exception& error) | ||
| { | ||
| throw CoreError(std::string("Polygon triangulation failed: ") + error.what()); | ||
|
pmeems marked this conversation as resolved.
Resolved
|
||
| } | ||
| scope.RethrowCaptureException(); | ||
| if (triangulation == nullptr) | ||
| { | ||
| std::string message = "Polygon triangulation failed"; | ||
| const std::string detail = scope.DetailedErrorMessage(); | ||
| if (!detail.empty()) | ||
| { | ||
| message += ": " + detail; | ||
| } | ||
| throw CoreError(message); | ||
| } | ||
|
|
||
| MeshBuilder builder; | ||
| AppendTriangles(*triangulation, builder); | ||
| TriangleMesh mesh = builder.TakeResult(); | ||
| if (mesh.indices.empty()) | ||
| { | ||
| throw CoreError("Polygon triangulation produced no triangles"); | ||
| } | ||
| return mesh; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.