Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .ai/memory/build/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ dependencies, and GoogleTest is supplied by the `tests` feature. The optional
`full-drivers` feature adds a larger set of GDAL drivers. The manifest baseline
and GDAL minimum version must be considered together when updating GDAL.

GDAL 3.12 renamed the OGR geometry accessors to snake_case (`get_Area()`,
`get_Length()`); the old camelCase names such as `getArea()` are removed.
`get_Area()` is not on the `OGRGeometry` base at all (only on `OGRCurve` and
`OGRSurface`), and a disjoint polygon `Intersection()` returns `POLYGON EMPTY`,
which still reports dimension 2 — check `IsEmpty()` before using `getDimension()`
or a surface cast on intersection results.

MSBuild resolves vcpkg from the `MwgVcpkgRoot` property, `MWG_VCPKG_ROOT`,
the sibling `..\vcpkg` directory, then `VCPKG_ROOT`. It installs to a
platform-specific tree because vcpkg can remove another triplet from a shared
Expand Down
7 changes: 7 additions & 0 deletions .ai/memory/testing/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ The C# host calls ActiveX methods late-bound. An invalid layer ID surfaces as
The host assertion accepts this mapping as well as `COMException`; see
`samples/CSharp/ActiveXTestHost/MainForm.cs`.

GeoPackage fixture layers need an explicit geometry type: a layer created with
`wkbUnknown` maps to `ShapeType::NullShape` and is rejected as non-polygonal by
scene creation, while `wkbMultiPolygon` layers map to `ShapeType::Polygon` and
accept multipart WKT. The GPKG driver also writes self-intersecting polygon WKT
without validation, so invalid-geometry behavior is testable through a generated
GeoPackage.

## References

- [Testing guide](../../../docs/TESTING.md)
Expand Down
6 changes: 5 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,11 @@ Rendering consumes backend-neutral input prepared from core data; neither the
coordinator nor a backend loads datasets or implements GIS rules. For each
visible map layer, Core sequentially reads Polygon, MultiPolygon,
CurvePolygon, and MultiSurface features into an owned vector scene,
linearizing curved geometries. `ViewTransform` captures the `Viewport` once
linearizing curved geometries. Scene creation also triangulates every polygon
into an indexed, backend-neutral triangle mesh with the constrained Delaunay
triangulation of GDAL/OGR and GEOS, retained per polygon so a backend never
triangulates per frame; a polygon that cannot be triangulated keeps its rings
and has no mesh. `ViewTransform` captures the `Viewport` once
per frame so both backends transform every vertex of every layer with the
same arithmetic as `Viewport::MapToScreen`; GDI draws a screen position in
the pixel that contains it (`ToPixel`). One backend draws the whole frame.
Expand Down
2 changes: 2 additions & 0 deletions src/MapWinGIS.Core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ set(MWG_CORE_PUBLIC_HEADERS
include/mapwingis/map/MapView.h
include/mapwingis/map/Viewport.h
include/mapwingis/rendering/MapRenderScene.h
include/mapwingis/rendering/PolygonTriangulation.h
include/mapwingis/rendering/VectorRenderScene.h
include/mapwingis/vector/FieldInfo.h
include/mapwingis/vector/VectorDataset.h
Expand All @@ -48,6 +49,7 @@ set(MWG_CORE_SOURCES
src/map/MapView.cpp
src/map/Viewport.cpp
src/rendering/MapRenderScene.cpp
src/rendering/PolygonTriangulation.cpp
src/rendering/VectorRenderScene.cpp
src/vector/VectorDataset.cpp
)
Expand Down
2 changes: 2 additions & 0 deletions src/MapWinGIS.Core/MapWinGIS.Core.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<ClInclude Include="include\mapwingis\vector\FieldInfo.h" />
<ClInclude Include="include\mapwingis\vector\VectorDataset.h" />
<ClInclude Include="include\mapwingis\rendering\MapRenderScene.h" />
<ClInclude Include="include\mapwingis\rendering\PolygonTriangulation.h" />
<ClInclude Include="include\mapwingis\rendering\VectorRenderScene.h" />
<ClInclude Include="src\internal\GdalExecution.h" />
<ClInclude Include="src\internal\GdalProgressAdapterAccess.h" />
Expand All @@ -88,6 +89,7 @@
<ClCompile Include="src\map\Viewport.cpp" />
<ClCompile Include="src\vector\VectorDataset.cpp" />
<ClCompile Include="src\rendering\MapRenderScene.cpp" />
<ClCompile Include="src\rendering\PolygonTriangulation.cpp" />
<ClCompile Include="src\rendering\VectorRenderScene.cpp" />
</ItemGroup>

Expand Down
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);
}
22 changes: 22 additions & 0 deletions src/MapWinGIS.Core/include/mapwingis/rendering/VectorRenderScene.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#pragma once

#include <cstdint>
#include <optional>
#include <vector>

#include "mapwingis/core/Api.h"
Expand All @@ -35,13 +36,34 @@ namespace mwg
/// A polygon and its rings. The first ring is the exterior boundary.
using Polygon = std::vector<Ring>;

/// A backend-neutral triangulated 2D fill mesh for one polygon.
///
/// Vertices stay in the source layer's coordinate system. Indices
/// occur in groups of three that each describe one triangle; every
/// index references a valid vertex. Triangles produced by
/// TriangulatePolygon share vertices through the index buffer and
/// have consistent counter-clockwise winding. The representation
/// contains no graphics-API, Windows, COM, ATL, or MFC types.
struct MWG_CORE_API TriangleMesh
{
std::vector<Point> vertices;
std::vector<std::uint32_t> indices;
};

/// Owned polygon geometry and its source-coordinate bounds.
struct MWG_CORE_API VectorRenderScene
{
Extent extent;
/// Feature count of the layer used to create this scene.
std::int64_t sourceFeatureCount = 0;
std::vector<Polygon> polygons;
/// Triangulated fill meshes prepared once when the scene is
/// created, aligned with polygons: fillMeshes[i] belongs to
/// polygons[i]. An entry is std::nullopt when that polygon could
/// not be triangulated; its rings in polygons remain available
/// for outline drawing and for backends that draw rings directly,
/// such as GDI.
std::vector<std::optional<TriangleMesh>> fillMeshes;
};

/// Copies polygon geometry from a layer into backend-neutral scene data.
Expand Down
235 changes: 235 additions & 0 deletions src/MapWinGIS.Core/src/rendering/PolygonTriangulation.cpp
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());
Comment thread
pmeems marked this conversation as resolved.
Resolved
_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());
Comment thread
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;
}
}
Loading