Skip to content
Open
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
9 changes: 9 additions & 0 deletions lib/polygonize.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Implementation of the polygonize algorithm that converts a collection of
/// LineString features to a collection of Polygon features.
///
/// This module follows RFC 7946 (GeoJSON) standards and provides a robust
/// implementation for converting line segments into closed polygons.

library polygonize;

export 'src/polygonize.dart';
51 changes: 51 additions & 0 deletions lib/src/polygonize.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/// Implementation of the polygonize algorithm that converts a collection of
/// LineString features to a collection of Polygon features.
///
/// This implementation follows RFC 7946 (GeoJSON) standards for ring orientation:
/// - Exterior rings are counter-clockwise (CCW)
/// - Interior rings (holes) are clockwise (CW)
///
/// The algorithm includes:
/// 1. Building a planar graph of all line segments
/// 2. Finding rings using the right-hand rule for consistent traversal
/// 3. Classifying rings as exterior or holes based on containment
/// 4. Creating proper polygon geometries with correct orientation

import 'package:turf/helpers.dart';
import 'package:turf/src/invariant.dart';
import 'package:turf/src/booleans/boolean_clockwise.dart';

import 'polygonize/polygonize.dart';
import 'polygonize/config.dart';

/// Converts a collection of LineString features to a collection of Polygon features.
///
/// Takes a [FeatureCollection<LineString>] and returns a [FeatureCollection<Polygon>].
/// The input features must be correctly noded, meaning they should only meet at their endpoints.
///
/// Example:
/// ```dart
/// var lines = FeatureCollection(features: [
/// Feature(geometry: LineString(coordinates: [
/// Position.of([0, 0]),
/// Position.of([10, 0])
/// ])),
/// Feature(geometry: LineString(coordinates: [
/// Position.of([10, 0]),
/// Position.of([10, 10])
/// ])),
/// Feature(geometry: LineString(coordinates: [
/// Position.of([10, 10]),
/// Position.of([0, 10])
/// ])),
/// Feature(geometry: LineString(coordinates: [
/// Position.of([0, 10]),
/// Position.of([0, 0])
/// ]))
/// ]);
///
/// var polygons = polygonize(lines);
/// ```
FeatureCollection<Polygon> polygonize(GeoJSONObject geoJSON, {PolygonizeConfig? config}) {
return Polygonizer.polygonize(geoJSON, config: config);
}
21 changes: 21 additions & 0 deletions lib/src/polygonize/config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// Configuration options for the polygonize algorithm
class PolygonizeConfig {
/// Factor to detect significant gaps in X coordinate clustering
/// A higher value makes the algorithm less likely to split features based on X coordinates
final double gapFactorX;

/// Factor to detect significant gaps in Y coordinate clustering
/// A higher value makes the algorithm less likely to split features based on Y coordinates
final double gapFactorY;

/// Factor to detect significant distance gaps for hole detection
/// A higher value makes the algorithm less likely to detect holes
final double distanceFactorForHoles;

/// Create a configuration for the polygonize algorithm
const PolygonizeConfig({
required this.gapFactorX,
required this.gapFactorY,
required this.distanceFactorForHoles,
});
}
142 changes: 142 additions & 0 deletions lib/src/polygonize/graph.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import 'dart:math';
import 'package:turf/helpers.dart';

/// Edge representation for the graph
class Edge {
final Position from;
final Position to;
bool visited = false;
String? label;

Edge(this.from, this.to);

@override
String toString() => '$from -> $to';

/// Get canonical edge key (ordered by coordinates)
String get key {
final fromKey = '${from[0]},${from[1]}';
final toKey = '${to[0]},${to[1]}';
return fromKey.compareTo(toKey) <= 0
? '$fromKey|$toKey'
: '$toKey|$fromKey';
}

/// Get the key as directed edge
String get directedKey => '${from[0]},${from[1]}|${to[0]},${to[1]}';

/// Create a reversed edge
Edge reversed() => Edge(to, from);
}

/// Helper class to associate an edge with its bearing
class EdgeWithBearing {
final Edge edge;
final num bearing;

EdgeWithBearing(this.edge, this.bearing);
}

/// Node in the graph, representing a vertex with its edges
class Node {
final Position position;
final List<Edge> edges = [];

Node(this.position);

void addEdge(Edge edge) {
edges.add(edge);
}

/// Get string representation for use as a map key
String get key => '${position[0]},${position[1]}';
}

/// Graph representing a planar graph of edges and nodes
class Graph {
final Map<String, Node> nodes = {};
final Map<String, Edge> edges = {};
final Map<String, List<EdgeWithBearing>> edgesByVertex = {};

/// Add an edge to the graph
void addEdge(Position from, Position to) {
// Skip edges with identical start and end points
if (from[0] == to[0] && from[1] == to[1]) {
return;
}

// Create a canonical edge key to avoid duplicates
final edgeKey = _createEdgeKey(from, to);

// Skip duplicate edges
if (edges.containsKey(edgeKey)) {
return;
}

// Create and store the edge
final edge = Edge(from, to);
edges[edgeKey] = edge;

// Add from node if it doesn't exist
final fromKey = '${from[0]},${from[1]}';
if (!nodes.containsKey(fromKey)) {
nodes[fromKey] = Node(from);
}
nodes[fromKey]!.addEdge(edge);

// Add to node if it doesn't exist
final toKey = '${to[0]},${to[1]}';
if (!nodes.containsKey(toKey)) {
nodes[toKey] = Node(to);
}
nodes[toKey]!.addEdge(Edge(to, from));

// Add to edge-by-vertex index for efficient lookup
_addToEdgesByVertex(from, to);
_addToEdgesByVertex(to, from);
}

/// Add edge to the index for efficient lookup by vertex
void _addToEdgesByVertex(Position from, Position to) {
final fromKey = '${from[0]},${from[1]}';
if (!edgesByVertex.containsKey(fromKey)) {
edgesByVertex[fromKey] = [];
}

// Calculate bearing for the edge
final bearing = _calculateBearing(from, to);
edgesByVertex[fromKey]!.add(EdgeWithBearing(Edge(from, to), bearing));
}

/// Calculate bearing between two positions
num _calculateBearing(Position start, Position end) {
num lng1 = _degreesToRadians(start[0]!);
num lng2 = _degreesToRadians(end[0]!);
num lat1 = _degreesToRadians(start[1]!);
num lat2 = _degreesToRadians(end[1]!);
num a = sin(lng2 - lng1) * cos(lat2);
num b = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(lng2 - lng1);

// Convert to azimuth (0-360°, clockwise from north)
num bearing = _radiansToDegrees(atan2(a, b));
return (bearing % 360 + 360) % 360; // Normalize to 0-360
}

/// Create a canonical edge key
String _createEdgeKey(Position from, Position to) {
// Create a key based on the actual coordinate values, not the default toString()
final fromKey = '${from[0]},${from[1]}';
final toKey = '${to[0]},${to[1]}';
return fromKey.compareTo(toKey) < 0 ? '$fromKey|$toKey' : '$toKey|$fromKey';
}

/// Convert degrees to radians
num _degreesToRadians(num degrees) {
return degrees * pi / 180;
}

/// Convert radians to degrees
num _radiansToDegrees(num radians) {
return radians * 180 / pi;
}
}
Loading