Skip to content

Polygon clipping package conversion #170

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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
73 changes: 73 additions & 0 deletions lib/src/geojson.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:turf/helpers.dart';

part 'geojson.g.dart';

Expand Down Expand Up @@ -330,6 +331,15 @@ class BBox extends CoordinateType {

factory BBox.fromJson(List<num> list) => BBox.of(list);

factory BBox.fromPositions(Position p1, Position p2) => BBox.named(
lng1: p1.lng,
lat1: p1.lat,
alt1: p1.alt,
lng2: p2.lng,
lat2: p2.lat,
alt2: p2.alt,
);

bool get _is3D => length == 6;

num get lng1 => _items[0];
Expand All @@ -344,6 +354,18 @@ class BBox extends CoordinateType {

num? get alt2 => _is3D ? _items[5] : null;

Position get position1 => Position.named(
lng: lng1,
lat: lat1,
alt: alt1,
);

Position get position2 => Position.named(
lng: lng2,
lat: lat2,
alt: alt2,
);

BBox copyWith({
num? lng1,
num? lat1,
Expand All @@ -361,6 +383,31 @@ class BBox extends CoordinateType {
alt2: alt2 ?? this.alt2,
);

//Adjust the bounds to include the given position
void expandToFitPosition(Position position) {
//If the position is outside the current bounds, expand the bounds
if (position.lng < lng1) {
_items[0] = position.lng;
}
if (position.lat < lat1) {
_items[1] = position.lat;
}
if (position.lng > lng2) {
_items[_is3D ? 3 : 2] = position.lng;
}
if (position.lat > lat2) {
_items[_is3D ? 4 : 3] = position.lat;
}
if (position.alt != null) {
if (alt1 == null || position.alt! < alt1!) {
_items[2] = position.alt!;
}
if (alt2 == null || position.alt! > alt2!) {
_items[5] = position.alt!;
}
}
}

@override
BBox clone() => BBox.of(_items);

Expand All @@ -377,6 +424,28 @@ class BBox extends CoordinateType {
lng2: _untilSigned(lng2, 180),
);

bool isPositionInBBox(Position point) {
return point.lng >= lng1 &&
point.lng <= lng2 &&
point.lat >= lat1 &&
point.lat <= lat2 &&
(point.alt == null ||
(alt1 != null && point.alt! >= alt1!) ||
(alt2 != null && point.alt! <= alt2!));
}

bool isBBoxOverlapping(BBox bbox) {
return bbox.lng1 <= lng2 &&
bbox.lng2 >= lng1 &&
bbox.lat1 <= lat2 &&
bbox.lat2 >= lat1 &&
((alt1 == null && bbox.alt1 == null) ||
(alt1 != null &&
bbox.alt1 != null &&
bbox.alt1! <= alt2! &&
bbox.alt2! >= alt1!));
}

@override
int get hashCode => Object.hashAll(_items);

Expand Down Expand Up @@ -584,6 +653,10 @@ class MultiPolygon extends GeometryType<List<List<List<Position>>>> {
GeoJSONObjectType.multiPolygon,
bbox: bbox);

List<Polygon> toPolygons() {
return coordinates.map((e) => Polygon(coordinates: e)).toList();
}

@override
Map<String, dynamic> toJson() => super.serialize(_$MultiPolygonToJson(this));

Expand Down
29 changes: 29 additions & 0 deletions lib/src/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ const areaFactors = <Unit, num>{
Unit.yards: 1.195990046,
};

const double epsilon =
2.220446049250313e-16; // Equivalent to Number.EPSILON in JavaScript

/// Round number to precision
num round(num value, [num precision = 0]) {
if (!(precision >= 0)) {
Expand Down Expand Up @@ -168,3 +171,29 @@ num convertArea(num area,

return (area / startFactor) * finalFactor;
}

/// Calculate the orientation of three points (a, b, c) in 2D space.
///
/// Parameters:
/// ax (double): X-coordinate of point a.
/// ay (double): Y-coordinate of point a.
/// bx (double): X-coordinate of point b.
/// by (double): Y-coordinate of point b.
/// cx (double): X-coordinate of point c.
/// cy (double): Y-coordinate of point c.
///
/// Returns:
/// double: The orientation value:
/// - Negative if points a, b, c are in counterclockwise order.
/// - Possitive if points a, b, c are in clockwise order.
/// - Zero if points a, b, c are collinear.
///
/// Note:
/// The orientation of three points is determined by the sign of the cross product
/// (bx - ax) * (cy - ay) - (by - ay) * (cx - ax). This value is twice the signed
/// area of the triangle formed by the points (a, b, c). The sign indicates the
/// direction of the rotation formed by the points.
double orient2d(
double ax, double ay, double bx, double by, double cx, double cy) {
return (by - ay) * (cx - bx) - (cy - by) * (bx - ax);
}
30 changes: 30 additions & 0 deletions lib/src/polygon_clipping/flp.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Dart doesn't have integer math; everything is floating point.
// Precision is maintained using double-precision floating-point numbers.

// IE Polyfill (not applicable in Dart)
// If epsilon is undefined, set it to 2^-52 (similar to JavaScript).
// In Dart, this step is unnecessary.

// Calculate the square of epsilon for later use.

import 'package:turf/helpers.dart';

const double epsilonsqrd = epsilon * epsilon;
// FLP (Floating-Position) comparator function
int cmp(double a, double b) {
// Check if both numbers are close to zero.
if (-epsilon < a && a < epsilon) {
if (-epsilon < b && b < epsilon) {
return 0; // Both numbers are effectively zero.
}
}

// Check if the numbers are approximately equal (within epsilon).
final double ab = a - b;
if (ab * ab < epsilonsqrd * a * b) {
return 0; // Numbers are approximately equal.
}

// Normal comparison: return -1 if a < b, 1 if a > b.
return a < b ? -1 : 1;
}
146 changes: 146 additions & 0 deletions lib/src/polygon_clipping/geom_in.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import 'dart:math';

import 'package:turf/helpers.dart';
import 'package:turf/src/polygon_clipping/point_extension.dart';
import 'package:turf/src/polygon_clipping/sweep_event.dart';

import 'segment.dart';

//TODO: mark factory methods to remove late values;
/// Represents a ring in a polygon.
class RingIn {
/// List of segments.
List<Segment> segments = [];

/// Indicates whether the polygon is an exterior polygon.
final bool isExterior;

/// The parent polygon.
final PolyIn? poly;

/// The bounding box of the polygon.
late BBox bbox;

RingIn(List<Position> geomRing, {this.poly, required this.isExterior})
: assert(geomRing.isNotEmpty) {
Position firstPoint =
Position(round(geomRing[0].lng), round(geomRing[0].lat));
bbox = BBox.fromPositions(
Position(firstPoint.lng, firstPoint.lat),
Position(firstPoint.lng, firstPoint.lat),
);

Position prevPoint = firstPoint;
for (var i = 1; i < geomRing.length; i++) {
Position point = Position(round(geomRing[i].lng), round(geomRing[i].lat));
// skip repeated points
if (point.lng == prevPoint.lng && point.lat == prevPoint.lat) continue;
segments.add(Segment.fromRing(
PositionEvents.fromPoint(prevPoint), PositionEvents.fromPoint(point),
ring: this));
bbox.expandToFitPosition(point);

prevPoint = point;
}
// add segment from last to first if last is not the same as first
if (firstPoint.lng != prevPoint.lng || firstPoint.lat != prevPoint.lat) {
segments.add(Segment.fromRing(PositionEvents.fromPoint(prevPoint),
PositionEvents.fromPoint(firstPoint),
ring: this));
}
}

List<SweepEvent> getSweepEvents() {
final List<SweepEvent> sweepEvents = [];
for (var i = 0; i < segments.length; i++) {
final segment = segments[i];
sweepEvents.add(segment.leftSE);
sweepEvents.add(segment.rightSE);
}
return sweepEvents;
}
}

//TODO: mark factory methods to remove late values;
class PolyIn {
late RingIn exteriorRing;
late List<RingIn> interiorRings;
late BBox bbox;
final MultiPolyIn? multiPoly;

PolyIn(
Polygon geomPoly,
this.multiPoly,
) {
exteriorRing =
RingIn(geomPoly.coordinates[0], poly: this, isExterior: true);
// copy by value
bbox = exteriorRing.bbox;

interiorRings = [];
Position lowerLeft = bbox.position1;
Position upperRight = bbox.position2;
for (var i = 1; i < geomPoly.coordinates.length; i++) {
final ring =
RingIn(geomPoly.coordinates[i], poly: this, isExterior: false);
lowerLeft = Position(min(ring.bbox.position1.lng, lowerLeft.lng),
min(ring.bbox.position1.lat, lowerLeft.lat));
upperRight = Position(max(ring.bbox.position2.lng, upperRight.lng),
max(ring.bbox.position2.lat, upperRight.lat));
interiorRings.add(ring);
}

bbox = BBox.fromPositions(lowerLeft, upperRight);
}

List<SweepEvent> getSweepEvents() {
final List<SweepEvent> sweepEvents = exteriorRing.getSweepEvents();
for (var i = 0; i < interiorRings.length; i++) {
final ringSweepEvents = interiorRings[i].getSweepEvents();
for (var j = 0; j < ringSweepEvents.length; j++) {
sweepEvents.add(ringSweepEvents[j]);
}
}
return sweepEvents;
}
}

//TODO: mark factory methods to remove late values;
class MultiPolyIn {
List<PolyIn> polys = [];
final bool isSubject;
late BBox bbox;

MultiPolyIn(MultiPolygon geom, this.isSubject) {
bbox = BBox.fromPositions(
Position(double.infinity, double.infinity),
Position(double.negativeInfinity, double.negativeInfinity),
);

List<Polygon> polygonsIn = geom.toPolygons();

Position lowerLeft = bbox.position1;
Position upperRight = bbox.position2;
for (var i = 0; i < polygonsIn.length; i++) {
final poly = PolyIn(polygonsIn[i], this);
lowerLeft = Position(min(poly.bbox.position1.lng, lowerLeft.lng),
min(poly.bbox.position1.lat, lowerLeft.lat));
upperRight = Position(max(poly.bbox.position2.lng, upperRight.lng),
max(poly.bbox.position2.lat, upperRight.lat));
polys.add(poly);
}

bbox = BBox.fromPositions(lowerLeft, upperRight);
}

List<SweepEvent> getSweepEvents() {
final List<SweepEvent> sweepEvents = [];
for (var i = 0; i < polys.length; i++) {
final polySweepEvents = polys[i].getSweepEvents();
for (var j = 0; j < polySweepEvents.length; j++) {
sweepEvents.add(polySweepEvents[j]);
}
}
return sweepEvents;
}
}
Loading