Skip to content

Commit 803bfdb

Browse files
authored
Merge pull request #3 from deanpapas/Polygonize
Polygonize
2 parents 7b5e966 + 9938035 commit 803bfdb

File tree

10 files changed

+1569
-0
lines changed

10 files changed

+1569
-0
lines changed

lib/polygonize.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/// Implementation of the polygonize algorithm that converts a collection of
2+
/// LineString features to a collection of Polygon features.
3+
///
4+
/// This module follows RFC 7946 (GeoJSON) standards and provides a robust
5+
/// implementation for converting line segments into closed polygons.
6+
7+
library polygonize;
8+
9+
export 'src/polygonize.dart';

lib/src/polygonize.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/// Implementation of the polygonize algorithm that converts a collection of
2+
/// LineString features to a collection of Polygon features.
3+
///
4+
/// This implementation follows RFC 7946 (GeoJSON) standards for ring orientation:
5+
/// - Exterior rings are counter-clockwise (CCW)
6+
/// - Interior rings (holes) are clockwise (CW)
7+
///
8+
/// The algorithm includes:
9+
/// 1. Building a planar graph of all line segments
10+
/// 2. Finding rings using the right-hand rule for consistent traversal
11+
/// 3. Classifying rings as exterior or holes based on containment
12+
/// 4. Creating proper polygon geometries with correct orientation
13+
14+
import 'package:turf/helpers.dart';
15+
import 'package:turf/src/invariant.dart';
16+
17+
import 'polygonize/polygonize.dart';
18+
19+
/// Converts a collection of LineString features to a collection of Polygon features.
20+
///
21+
/// Takes a [FeatureCollection<LineString>] and returns a [FeatureCollection<Polygon>].
22+
/// The input features must be correctly noded, meaning they should only meet at their endpoints.
23+
///
24+
/// Example:
25+
/// ```dart
26+
/// var lines = FeatureCollection(features: [
27+
/// Feature(geometry: LineString(coordinates: [
28+
/// Position.of([0, 0]),
29+
/// Position.of([10, 0])
30+
/// ])),
31+
/// Feature(geometry: LineString(coordinates: [
32+
/// Position.of([10, 0]),
33+
/// Position.of([10, 10])
34+
/// ])),
35+
/// Feature(geometry: LineString(coordinates: [
36+
/// Position.of([10, 10]),
37+
/// Position.of([0, 10])
38+
/// ])),
39+
/// Feature(geometry: LineString(coordinates: [
40+
/// Position.of([0, 10]),
41+
/// Position.of([0, 0])
42+
/// ]))
43+
/// ]);
44+
///
45+
/// var polygons = polygonize(lines);
46+
/// ```
47+
FeatureCollection<Polygon> polygonize(GeoJSONObject geoJSON) {
48+
return Polygonizer.polygonize(geoJSON);
49+
}

lib/src/polygonize/graph.dart

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import 'dart:math';
2+
import 'package:turf/helpers.dart';
3+
4+
/// Edge representation for the graph
5+
class Edge {
6+
final Position from;
7+
final Position to;
8+
bool visited = false;
9+
String? label;
10+
11+
Edge(this.from, this.to);
12+
13+
@override
14+
String toString() => '$from -> $to';
15+
16+
/// Get canonical edge key (ordered by coordinates)
17+
String get key {
18+
return from.toString().compareTo(to.toString()) <= 0
19+
? '${from.toString()}|${to.toString()}'
20+
: '${to.toString()}|${from.toString()}';
21+
}
22+
23+
/// Get the key as directed edge
24+
String get directedKey => '${from.toString()}|${to.toString()}';
25+
26+
/// Create a reversed edge
27+
Edge reversed() => Edge(to, from);
28+
}
29+
30+
/// Helper class to associate an edge with its bearing
31+
class EdgeWithBearing {
32+
final Edge edge;
33+
final num bearing;
34+
35+
EdgeWithBearing(this.edge, this.bearing);
36+
}
37+
38+
/// Node in the graph, representing a vertex with its edges
39+
class Node {
40+
final Position position;
41+
final List<Edge> edges = [];
42+
43+
Node(this.position);
44+
45+
void addEdge(Edge edge) {
46+
edges.add(edge);
47+
}
48+
49+
/// Get string representation for use as a map key
50+
String get key => position.toString();
51+
}
52+
53+
/// Graph representing a planar graph of edges and nodes
54+
class Graph {
55+
final Map<String, Node> nodes = {};
56+
final Map<String, Edge> edges = {};
57+
final Map<String, List<EdgeWithBearing>> edgesByVertex = {};
58+
59+
/// Add an edge to the graph
60+
void addEdge(Position from, Position to) {
61+
// Skip edges with identical start and end points
62+
if (from[0] == to[0] && from[1] == to[1]) {
63+
return;
64+
}
65+
66+
// Create a canonical edge key to avoid duplicates
67+
final edgeKey = _createEdgeKey(from, to);
68+
69+
// Skip duplicate edges
70+
if (edges.containsKey(edgeKey)) {
71+
return;
72+
}
73+
74+
// Create and store the edge
75+
final edge = Edge(from, to);
76+
edges[edgeKey] = edge;
77+
78+
// Add from node if it doesn't exist
79+
final fromKey = from.toString();
80+
if (!nodes.containsKey(fromKey)) {
81+
nodes[fromKey] = Node(from);
82+
}
83+
nodes[fromKey]!.addEdge(edge);
84+
85+
// Add to node if it doesn't exist
86+
final toKey = to.toString();
87+
if (!nodes.containsKey(toKey)) {
88+
nodes[toKey] = Node(to);
89+
}
90+
nodes[toKey]!.addEdge(Edge(to, from));
91+
92+
// Add to edge-by-vertex index for efficient lookup
93+
_addToEdgesByVertex(from, to);
94+
_addToEdgesByVertex(to, from);
95+
}
96+
97+
/// Add edge to the index for efficient lookup by vertex
98+
void _addToEdgesByVertex(Position from, Position to) {
99+
final fromKey = from.toString();
100+
if (!edgesByVertex.containsKey(fromKey)) {
101+
edgesByVertex[fromKey] = [];
102+
}
103+
104+
// Calculate bearing for the edge
105+
final bearing = _calculateBearing(from, to);
106+
edgesByVertex[fromKey]!.add(EdgeWithBearing(Edge(from, to), bearing));
107+
}
108+
109+
/// Calculate bearing between two positions
110+
num _calculateBearing(Position start, Position end) {
111+
num lng1 = _degreesToRadians(start[0]!);
112+
num lng2 = _degreesToRadians(end[0]!);
113+
num lat1 = _degreesToRadians(start[1]!);
114+
num lat2 = _degreesToRadians(end[1]!);
115+
num a = sin(lng2 - lng1) * cos(lat2);
116+
num b = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(lng2 - lng1);
117+
118+
// Convert to azimuth (0-360°, clockwise from north)
119+
num bearing = _radiansToDegrees(atan2(a, b));
120+
return (bearing % 360 + 360) % 360; // Normalize to 0-360
121+
}
122+
123+
/// Create a canonical edge key
124+
String _createEdgeKey(Position from, Position to) {
125+
final fromKey = from.toString();
126+
final toKey = to.toString();
127+
return fromKey.compareTo(toKey) < 0 ? '$fromKey|$toKey' : '$toKey|$fromKey';
128+
}
129+
130+
/// Convert degrees to radians
131+
num _degreesToRadians(num degrees) {
132+
return degrees * pi / 180;
133+
}
134+
135+
/// Convert radians to degrees
136+
num _radiansToDegrees(num radians) {
137+
return radians * 180 / pi;
138+
}
139+
}

0 commit comments

Comments
 (0)