From bcb21ac4f24212983c8bc012cf2bdcbd6b9613d3 Mon Sep 17 00:00:00 2001 From: Aaron S Kennedy <36516690+aaronsamkennedy@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:59:07 -0700 Subject: [PATCH 1/6] Start draft for polygon clipping package concertion --- lib/src/polygon_clipping/bbox.dart | 36 ++ lib/src/polygon_clipping/flp.dart | 30 + lib/src/polygon_clipping/geom_in.dart | 142 +++++ lib/src/polygon_clipping/geom_out.dart | 242 ++++++++ lib/src/polygon_clipping/index.dart | 21 + lib/src/polygon_clipping/intersection.dart | 8 + lib/src/polygon_clipping/operation.dart | 139 +++++ lib/src/polygon_clipping/point_extension.dart | 17 + .../polygon_clipping/polygon_clipping.dart | 16 + lib/src/polygon_clipping/rounder.dart | 83 +++ lib/src/polygon_clipping/segment.dart | 573 ++++++++++++++++++ lib/src/polygon_clipping/sweep_event.dart | 147 +++++ lib/src/polygon_clipping/sweep_line.dart | 165 +++++ lib/src/polygon_clipping/utils.dart | 30 + lib/src/polygon_clipping/vector.dart | 26 + .../polygon_clipping/vector_extension.dart | 73 +++ pubspec.yaml | 1 + 17 files changed, 1749 insertions(+) create mode 100644 lib/src/polygon_clipping/bbox.dart create mode 100644 lib/src/polygon_clipping/flp.dart create mode 100644 lib/src/polygon_clipping/geom_in.dart create mode 100644 lib/src/polygon_clipping/geom_out.dart create mode 100644 lib/src/polygon_clipping/index.dart create mode 100644 lib/src/polygon_clipping/intersection.dart create mode 100644 lib/src/polygon_clipping/operation.dart create mode 100644 lib/src/polygon_clipping/point_extension.dart create mode 100644 lib/src/polygon_clipping/polygon_clipping.dart create mode 100644 lib/src/polygon_clipping/rounder.dart create mode 100644 lib/src/polygon_clipping/segment.dart create mode 100644 lib/src/polygon_clipping/sweep_event.dart create mode 100644 lib/src/polygon_clipping/sweep_line.dart create mode 100644 lib/src/polygon_clipping/utils.dart create mode 100644 lib/src/polygon_clipping/vector.dart create mode 100644 lib/src/polygon_clipping/vector_extension.dart diff --git a/lib/src/polygon_clipping/bbox.dart b/lib/src/polygon_clipping/bbox.dart new file mode 100644 index 0000000..2f74836 --- /dev/null +++ b/lib/src/polygon_clipping/bbox.dart @@ -0,0 +1,36 @@ +import 'dart:math'; + +class BoundingBox { + Point ll; // Lower left point + Point ur; // Upper right point + + BoundingBox(this.ll, this.ur); +} + +bool isInBbox(BoundingBox bbox, Point point) { + return (bbox.ll.x <= point.x && + point.x <= bbox.ur.x && + bbox.ll.y <= point.y && + point.y <= bbox.ur.y); +} + +BoundingBox? getBboxOverlap(BoundingBox b1, BoundingBox b2) { + // Check if the bboxes overlap at all + if (b2.ur.x < b1.ll.x || + b1.ur.x < b2.ll.x || + b2.ur.y < b1.ll.y || + b1.ur.y < b2.ll.y) { + return null; + } + + // Find the middle two X values + final lowerX = b1.ll.x < b2.ll.x ? b2.ll.x : b1.ll.x; + final upperX = b1.ur.x < b2.ur.x ? b1.ur.x : b2.ur.x; + + // Find the middle two Y values + final lowerY = b1.ll.y < b2.ll.y ? b2.ll.y : b1.ll.y; + final upperY = b1.ur.y < b2.ur.y ? b1.ur.y : b2.ur.y; + + // Create a new bounding box with the overlap + return BoundingBox(Point(lowerX, lowerY), Point(upperX, upperY)); +} diff --git a/lib/src/polygon_clipping/flp.dart b/lib/src/polygon_clipping/flp.dart new file mode 100644 index 0000000..0e31062 --- /dev/null +++ b/lib/src/polygon_clipping/flp.dart @@ -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/src/polygon_clipping/utils.dart'; + +const double epsilonsqrd = epsilon * epsilon; +// FLP (Floating-Point) 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; +} diff --git a/lib/src/polygon_clipping/geom_in.dart b/lib/src/polygon_clipping/geom_in.dart new file mode 100644 index 0000000..359b99b --- /dev/null +++ b/lib/src/polygon_clipping/geom_in.dart @@ -0,0 +1,142 @@ +import 'dart:math'; + +import 'package:turf/src/polygon_clipping/bbox.dart'; +import 'package:turf/src/polygon_clipping/point_extension.dart'; + +import 'rounder.dart'; +import 'segment.dart'; + +//TODO: mark factory methods to remove late values; +class RingIn { + List segments = []; + final bool isExterior; + final PolyIn poly; + late BoundingBox bbox; + + RingIn(List geomRing, this.poly, this.isExterior) { + if (!(geomRing is List && geomRing.isNotEmpty)) { + throw ArgumentError( + "Input geometry is not a valid Polygon or MultiPolygon"); + } + + final firstPoint = rounder.round(geomRing[0].x, geomRing[0].y); + bbox = BoundingBox( + Point(firstPoint.x, firstPoint.y), + Point(firstPoint.x, firstPoint.y), + ); + + var prevPoint = firstPoint; + for (var i = 1; i < geomRing.length; i++) { + var point = rounder.round(geomRing[i].x, geomRing[i].y); + // skip repeated points + if (point.x == prevPoint.x && point.y == prevPoint.y) continue; + segments.add(Segment.fromRing(PointEvents.fromPoint(prevPoint), + PointEvents.fromPoint(point), this)); + + bbox.ll = Point(min(point.x, bbox.ll.x), min(point.y, bbox.ll.y)); + bbox.ur = Point(max(point.x, bbox.ur.x), max(point.y, bbox.ur.y)); + + prevPoint = point; + } + // add segment from last to first if last is not the same as first + if (firstPoint.x != prevPoint.x || firstPoint.y != prevPoint.y) { + segments.add(Segment.fromRing(PointEvents.fromPoint(prevPoint), + PointEvents.fromPoint(firstPoint), this)); + } + } + + List getSweepEvents() { + final 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 interiorRings; + final MultiPolyIn multiPoly; + late BoundingBox bbox; + + PolyIn(List geomPoly, this.multiPoly) { + if (!(geomPoly is List)) { + throw ArgumentError( + "Input geometry is not a valid Polygon or MultiPolygon"); + } + exteriorRing = RingIn(geomPoly[0], this, true); + // copy by value + bbox = exteriorRing.bbox; + + interiorRings = []; + for (var i = 1; i < geomPoly.length; i++) { + final ring = RingIn(geomPoly[i], this, false); + bbox.ll = + Point(min(ring.bbox.ll.x, bbox.ll.x), min(ring.bbox.ll.y, bbox.ll.y)); + bbox.ur = + Point(max(ring.bbox.ur.x, bbox.ur.x), max(ring.bbox.ur.y, bbox.ur.y)); + interiorRings.add(ring); + } + } + + List getSweepEvents() { + final 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 { + late List polys; + final bool isSubject; + late BoundingBox bbox; + + MultiPolyIn(List geom, this.isSubject) { + if (!(geom is List)) { + throw ArgumentError( + "Input geometry is not a valid Polygon or MultiPolygon"); + } + + try { + // if the input looks like a polygon, convert it to a multipolygon + if (geom[0][0][0] is num) geom = [geom]; + } catch (ex) { + // The input is either malformed or has empty arrays. + // In either case, it will be handled later on. + } + + polys = []; + bbox = BoundingBox( + Point(double.infinity, double.infinity), + Point(double.negativeInfinity, double.negativeInfinity), + ); + for (var i = 0; i < geom.length; i++) { + final poly = PolyIn(geom[i], this); + bbox.ll = + Point(min(poly.bbox.ll.x, bbox.ll.x), min(poly.bbox.ll.y, bbox.ll.y)); + bbox.ur = + Point(max(poly.bbox.ur.x, bbox.ur.x), max(poly.bbox.ur.y, bbox.ur.y)); + } + } + + List getSweepEvents() { + final 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; + } +} diff --git a/lib/src/polygon_clipping/geom_out.dart b/lib/src/polygon_clipping/geom_out.dart new file mode 100644 index 0000000..c1d8f6f --- /dev/null +++ b/lib/src/polygon_clipping/geom_out.dart @@ -0,0 +1,242 @@ +import 'dart:math'; + +import 'package:turf/src/polygon_clipping/intersection.dart'; +import 'package:turf/src/polygon_clipping/sweep_event.dart'; +import 'package:turf/src/polygon_clipping/segment.dart'; +import 'package:turf/src/polygon_clipping/vector.dart'; + +class RingOut { + List events; + PolyOut? poly; + + RingOut(this.events) { + for (int i = 0, iMax = events.length; i < iMax; i++) { + events[i].segment!.ringOut = this; + } + poly = null; + } + /* Given the segments from the sweep line pass, compute & return a series + * of closed rings from all the segments marked to be part of the result */ + static List factory(List allSegments) { + List ringsOut = []; + + for (int i = 0, iMax = allSegments.length; i < iMax; i++) { + final Segment segment = allSegments[i]; + if (!segment.isInResult() || segment.ringOut != null) continue; + + SweepEvent prevEvent; + SweepEvent event = segment.leftSE; + SweepEvent nextEvent = segment.rightSE; + final List events = [event]; + + final Point startingPoint = event.point; + final List intersectionLEs = []; + + while (true) { + prevEvent = event; + event = nextEvent; + events.add(event); + + if (event.point == startingPoint) break; + + while (true) { + List availableLEs = event.getAvailableLinkedEvents(); + + if (availableLEs.isEmpty) { + Point firstPt = events[0].point; + Point lastPt = events[events.length - 1].point; + throw Exception( + 'Unable to complete output ring starting at [${firstPt.x}, ${firstPt.y}]. Last matching segment found ends at [${lastPt.x}, ${lastPt.y}].'); + } + + if (availableLEs.length == 1) { + nextEvent = availableLEs[0].otherSE!; + break; + } + + ///Index of the intersection + int? indexLE; + for (int j = 0, jMax = intersectionLEs.length; j < jMax; j++) { + if (intersectionLEs[j].point == event.point) { + indexLE = j; + break; + } + } + + if (indexLE != null) { + Intersection intersectionLE = intersectionLEs.removeAt(indexLE); + List ringEvents = events.sublist(intersectionLE.id); + ringEvents.insert(0, ringEvents[0].otherSE!); + ringsOut.add(RingOut(ringEvents.reversed.toList())); + continue; + } + + intersectionLEs.add(Intersection( + events.length, + event.point, + )); + + Comparator comparator = + event.getLeftmostComparator(prevEvent); + availableLEs.sort(comparator); + nextEvent = availableLEs[0].otherSE!; + break; + } + } + + ringsOut.add(RingOut(events)); + } + + return ringsOut; + } + + bool? _isExteriorRing; + + bool get isExteriorRing { + if (_isExteriorRing == null) { + RingOut enclosing = enclosingRing(); + _isExteriorRing = (enclosing != null) ? !enclosing.isExteriorRing : true; + } + return _isExteriorRing!; + } + + //TODO: Convert type to List? + List>? getGeom() { + Point prevPt = events[0].point; + List points = [prevPt]; + + for (int i = 1, iMax = events.length - 1; i < iMax; i++) { + Point pt = events[i].point; + Point nextPt = events[i + 1].point; + if (compareVectorAngles(pt, prevPt, nextPt) == 0) continue; + points.add(pt); + prevPt = pt; + } + + if (points.length == 1) return null; + + Point pt = points[0]; + Point nextPt = points[1]; + if (compareVectorAngles(pt, prevPt, nextPt) == 0) points.removeAt(0); + + points.add(points[0]); + int step = isExteriorRing ? 1 : -1; + int iStart = isExteriorRing ? 0 : points.length - 1; + int iEnd = isExteriorRing ? points.length : -1; + List> orderedPoints = []; + + for (int i = iStart; i != iEnd; i += step) { + orderedPoints.add([points[i].x.toDouble(), points[i].y.toDouble()]); + } + + return orderedPoints; + } + + RingOut? _enclosingRing; + RingOut enclosingRing() { + if (_enclosingRing == null) { + _enclosingRing = _calcEnclosingRing(); + } + return _enclosingRing!; + } + + RingOut? _calcEnclosingRing() { + SweepEvent leftMostEvt = events[0]; + + for (int i = 1, iMax = events.length; i < iMax; i++) { + SweepEvent evt = events[i]; + if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt; + } + + Segment? prevSeg = leftMostEvt.segment!.prevInResult(); + Segment? prevPrevSeg = prevSeg != null ? prevSeg.prevInResult() : null; + + while (true) { + if (prevSeg == null) return null; + + if (prevPrevSeg == null) return prevSeg.ringOut; + + if (prevPrevSeg.ringOut != prevSeg.ringOut) { + if (prevPrevSeg.ringOut!.enclosingRing() != prevSeg.ringOut) { + return prevSeg.ringOut; + } else { + return prevSeg.ringOut!.enclosingRing(); + } + } + + prevSeg = prevPrevSeg.prevInResult(); + prevPrevSeg = prevSeg != null ? prevSeg.prevInResult() : null; + } + } +} + +class PolyOut { + RingOut exteriorRing; + List interiorRings = []; + + PolyOut(this.exteriorRing) { + exteriorRing.poly = this; + } + + void addInterior(RingOut ring) { + interiorRings.add(ring); + ring.poly = this; + } + + List>>? getGeom() { + List>? exteriorGeom = exteriorRing.getGeom(); + List>>? geom = + exteriorGeom != null ? [exteriorGeom] : null; + + if (geom == null) return null; + + for (int i = 0, iMax = interiorRings.length; i < iMax; i++) { + List>? ringGeom = interiorRings[i].getGeom(); + if (ringGeom == null) continue; + geom.add(ringGeom); + } + + return geom; + } +} + +class MultiPolyOut { + List rings; + late List polys; + + MultiPolyOut(this.rings) { + polys = _composePolys(rings); + } + + List>>> getGeom() { + List>>> geom = []; + + for (int i = 0, iMax = polys.length; i < iMax; i++) { + List>>? polyGeom = polys[i].getGeom(); + if (polyGeom == null) continue; + geom.add(polyGeom); + } + + return geom; + } + + List _composePolys(List rings) { + List polys = []; + + for (int i = 0, iMax = rings.length; i < iMax; i++) { + RingOut ring = rings[i]; + if (ring.poly != null) continue; + if (ring.isExteriorRing) { + polys.add(PolyOut(ring)); + } else { + RingOut enclosingRing = ring.enclosingRing(); + if (enclosingRing.poly == null) { + polys.add(PolyOut(enclosingRing)); + } + enclosingRing.poly!.addInterior(ring); + } + } + + return polys; + } +} diff --git a/lib/src/polygon_clipping/index.dart b/lib/src/polygon_clipping/index.dart new file mode 100644 index 0000000..546a7c9 --- /dev/null +++ b/lib/src/polygon_clipping/index.dart @@ -0,0 +1,21 @@ +import 'operation.dart'; + +//?Should these just be methods of operations? or factory constructors or something else? +dynamic union(dynamic geom, List moreGeoms) => + operation.run("union", geom, moreGeoms); + +dynamic intersection(dynamic geom, List moreGeoms) => + operation.run("intersection", geom, moreGeoms); + +dynamic xor(dynamic geom, List moreGeoms) => + operation.run("xor", geom, moreGeoms); + +dynamic difference(dynamic subjectGeom, List clippingGeoms) => + operation.run("difference", subjectGeom, clippingGeoms); + +Map operations = { + 'union': union, + 'intersection': intersection, + 'xor': xor, + 'difference': difference, +}; diff --git a/lib/src/polygon_clipping/intersection.dart b/lib/src/polygon_clipping/intersection.dart new file mode 100644 index 0000000..4ffd805 --- /dev/null +++ b/lib/src/polygon_clipping/intersection.dart @@ -0,0 +1,8 @@ +import 'dart:math'; + +class Intersection { + final int id; + final Point point; + + Intersection(this.id, this.point); +} diff --git a/lib/src/polygon_clipping/operation.dart b/lib/src/polygon_clipping/operation.dart new file mode 100644 index 0000000..b4aae43 --- /dev/null +++ b/lib/src/polygon_clipping/operation.dart @@ -0,0 +1,139 @@ +import 'dart:collection'; +import 'dart:math' as math; +import 'bbox.dart'; +import 'geom_in.dart' as geomIn; +import 'geom_out.dart' as geomOut; +import 'rounder.dart'; +import 'sweep_event.dart'; +import 'sweep_line.dart'; + +// Limits on iterative processes to prevent infinite loops - usually caused by floating-point math round-off errors. +const int POLYGON_CLIPPING_MAX_QUEUE_SIZE = + (bool.fromEnvironment('dart.library.io') + ? int.fromEnvironment('POLYGON_CLIPPING_MAX_QUEUE_SIZE') + : 1000000) ?? + 1000000; +const int POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS = + (bool.fromEnvironment('dart.library.io') + ? int.fromEnvironment('POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS') + : 1000000) ?? + 1000000; + +class Operation { + late String type; + int numMultiPolys = 0; + + List run(String type, dynamic geom, List moreGeoms) { + this.type = type; + rounder.reset(); + + /* Convert inputs to MultiPoly objects */ + final List multipolys = [ + geomIn.MultiPolyIn(geom, true) + ]; + for (var i = 0; i < moreGeoms.length; i++) { + multipolys.add(geomIn.MultiPolyIn(moreGeoms[i], false)); + } + numMultiPolys = multipolys.length; + + /* BBox optimization for difference operation + * If the bbox of a multipolygon that's part of the clipping doesn't + * intersect the bbox of the subject at all, we can just drop that + * multiploygon. */ + if (this.type == 'difference') { + // in place removal + final subject = multipolys[0]; + var i = 1; + while (i < multipolys.length) { + if (getBboxOverlap(multipolys[i].bbox, subject.bbox) != null) { + i++; + } else { + multipolys.removeAt(i); + } + } + } + + /* BBox optimization for intersection operation + * If we can find any pair of multipolygons whose bbox does not overlap, + * then the result will be empty. */ + if (this.type == 'intersection') { + // TODO: this is O(n^2) in number of polygons. By sorting the bboxes, + // it could be optimized to O(n * ln(n)) + for (var i = 0; i < multipolys.length; i++) { + final mpA = multipolys[i]; + for (var j = i + 1; j < multipolys.length; j++) { + if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) == null) { + return []; + } + } + } + } + + /* Put segment endpoints in a priority queue */ + final queue = SplayTreeSet(SweepEvent.compare); + for (var i = 0; i < multipolys.length; i++) { + final sweepEvents = multipolys[i].getSweepEvents(); + for (var j = 0; j < sweepEvents.length; j++) { + queue.add(sweepEvents[j]); + + if (queue.length > POLYGON_CLIPPING_MAX_QUEUE_SIZE) { + // prevents an infinite loop, an otherwise common manifestation of bugs + throw StateError( + 'Infinite loop when putting segment endpoints in a priority queue ' + '(queue size too big).'); + } + } + } + + /* Pass the sweep line over those endpoints */ + final sweepLine = SweepLine(queue.toList()); + var prevQueueSize = queue.length; + var node = queue.last; + queue.remove(node); + while (node != null) { + final evt = node; + if (queue.length == prevQueueSize) { + // prevents an infinite loop, an otherwise common manifestation of bugs + final seg = evt.segment; + throw StateError('Unable to pop() ${evt.isLeft ? 'left' : 'right'} ' + 'SweepEvent [${evt.point.x}, ${evt.point.y}] from segment #${seg?.id} ' + '[${seg?.leftSE.point.x}, ${seg?.leftSE.point.y}] -> ' + '[${seg?.rightSE.point.x}, ${seg?.rightSE.point.y}] from queue.'); + } + + if (queue.length > POLYGON_CLIPPING_MAX_QUEUE_SIZE) { + // prevents an infinite loop, an otherwise common manifestation of bugs + throw StateError('Infinite loop when passing sweep line over endpoints ' + '(queue size too big).'); + } + + if (sweepLine.segments.length > POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS) { + // prevents an infinite loop, an otherwise common manifestation of bugs + throw StateError('Infinite loop when passing sweep line over endpoints ' + '(too many sweep line segments).'); + } + + final newEvents = sweepLine.process(evt); + for (var i = 0; i < newEvents.length; i++) { + final evt = newEvents[i]; + if (evt.consumedBy == null) { + queue.add(evt); + } + } + prevQueueSize = queue.length; + node = queue.last; + queue.remove(node); + } + + // free some memory we don't need anymore + rounder.reset(); + + /* Collect and compile segments we're keeping into a multipolygon */ + final ringsOut = geomOut.RingOut.factory(sweepLine.segments); + final result = geomOut.MultiPolyOut(ringsOut); + return result.getGeom(); + } +} + +// singleton available by import +final operation = Operation(); diff --git a/lib/src/polygon_clipping/point_extension.dart b/lib/src/polygon_clipping/point_extension.dart new file mode 100644 index 0000000..4d67daf --- /dev/null +++ b/lib/src/polygon_clipping/point_extension.dart @@ -0,0 +1,17 @@ +import 'dart:math'; + +import 'package:turf/src/polygon_clipping/sweep_event.dart'; + +class PointEvents extends Point { + List? events; + + PointEvents( + double super.x, + double super.y, + this.events, + ); + + factory PointEvents.fromPoint(Point point) { + return PointEvents(point.x.toDouble(), point.y.toDouble(), []); + } +} diff --git a/lib/src/polygon_clipping/polygon_clipping.dart b/lib/src/polygon_clipping/polygon_clipping.dart new file mode 100644 index 0000000..64ed99d --- /dev/null +++ b/lib/src/polygon_clipping/polygon_clipping.dart @@ -0,0 +1,16 @@ +//? how do we want to express this? + +//* Here's the code from the JS package +// export type Pair = [number, number] +// export type Ring = Pair[] +// export type Polygon = Ring[] +// export type MultiPolygon = Polygon[] +// type Geom = Polygon | MultiPolygon +// export function intersection(geom: Geom, ...geoms: Geom[]): MultiPolygon +// export function xor(geom: Geom, ...geoms: Geom[]): MultiPolygon +// export function union(geom: Geom, ...geoms: Geom[]): MultiPolygon +// export function difference( +// subjectGeom: Geom, +// ...clipGeoms: Geom[] +// ): MultiPolygon +//* } \ No newline at end of file diff --git a/lib/src/polygon_clipping/rounder.dart b/lib/src/polygon_clipping/rounder.dart new file mode 100644 index 0000000..7bcdc86 --- /dev/null +++ b/lib/src/polygon_clipping/rounder.dart @@ -0,0 +1,83 @@ +import 'dart:collection'; +import 'dart:math'; + +/// A class for rounding floating-point coordinates to avoid floating-point problems. +class PtRounder { + late CoordRounder xRounder; + late CoordRounder yRounder; + + /// Constructor for PtRounder. + PtRounder() { + reset(); + } + + /// Resets the PtRounder by creating new instances of CoordRounder. + void reset() { + xRounder = CoordRounder(); + yRounder = CoordRounder(); + } + + /// Rounds the input x and y coordinates using CoordRounder instances. + Point round(num x, num y) { + return Point( + xRounder.round(x), + xRounder.round(y), + ); + } +} + +/// A class for rounding individual coordinates. +class CoordRounder { + late SplayTreeMap tree; + + /// Constructor for CoordRounder. + CoordRounder() { + tree = SplayTreeMap(); + // Preseed with 0 so we don't end up with values < epsilon. + round(0); + } + + /// Rounds the input coordinate and adds it to the tree. + /// + /// Returns the rounded value of the coordinate. + num round(num coord) { + final node = tree.putIfAbsent(coord, () => coord); + + final prevKey = nodeKeyBefore(coord); + final prevNode = prevKey != null ? tree[prevKey] : null; + + if (prevNode != null && node == prevNode) { + tree.remove(coord); + return prevNode; + } + + final nextKey = nodeKeyAfter(coord); + final nextNode = nextKey != null ? tree[nextKey] : null; + + if (nextNode != null && node == nextNode) { + tree.remove(coord); + return nextNode; + } + + return coord; + } + + /// Finds the key of the node before the given key in the tree. + /// + /// Returns the key of the previous node. + num? nodeKeyBefore(num key) { + final lowerKey = tree.keys.firstWhere((k) => k < key, orElse: () => key); + return lowerKey != key ? lowerKey : null; + } + + /// Finds the key of the node after the given key in the tree. + /// + /// Returns the key of the next node. + num? nodeKeyAfter(num key) { + final upperKey = tree.keys.firstWhere((k) => k > key, orElse: () => key); + return upperKey != key ? upperKey : null; + } +} + +/// Global instance of PtRounder available for use. +final rounder = PtRounder(); diff --git a/lib/src/polygon_clipping/segment.dart b/lib/src/polygon_clipping/segment.dart new file mode 100644 index 0000000..0829acd --- /dev/null +++ b/lib/src/polygon_clipping/segment.dart @@ -0,0 +1,573 @@ +// Give segments unique ID's to get consistent sorting of +// segments and sweep events when all else is identical +import 'dart:math'; + +import 'package:turf/src/polygon_clipping/bbox.dart'; +import 'package:turf/src/polygon_clipping/geom_out.dart'; +import 'package:turf/src/polygon_clipping/operation.dart'; +import 'package:turf/src/polygon_clipping/point_extension.dart'; +import 'package:turf/src/polygon_clipping/rounder.dart'; +import 'package:turf/src/polygon_clipping/sweep_event.dart'; +import 'package:turf/src/polygon_clipping/vector_extension.dart'; +import 'package:vector_math/vector_math.dart'; + +class Segment { + static int _nextId = 1; + int id; + SweepEvent leftSE; + SweepEvent rightSE; + //TODO: can we make these empty lists instead of being nullable? + List? rings; + // TODO: add concrete typing for winding, should this be a nullable boolean? true, clockwise, false counter clockwhise, null unknown + List? windings; + + ///These set later in algorithm + Segment? consumedBy; + Segment? prev; + RingOut? ringOut; + + /* Warning: a reference to ringWindings input will be stored, + * and possibly will be later modified */ + Segment(this.leftSE, this.rightSE, this.rings, this.windings) + //Auto increment id + : id = _nextId++ { + //Set intertwined relationships between segment and sweep events + leftSE.segment = this; + leftSE.otherSE = rightSE; + + rightSE.segment = this; + rightSE.otherSE = leftSE; + // left unset for performance, set later in algorithm + // this.ringOut, this.consumedBy, this.prev + } + + /* This compare() function is for ordering segments in the sweep + * line tree, and does so according to the following criteria: + * + * Consider the vertical line that lies an infinestimal step to the + * right of the right-more of the two left endpoints of the input + * segments. Imagine slowly moving a point up from negative infinity + * in the increasing y direction. Which of the two segments will that + * point intersect first? That segment comes 'before' the other one. + * + * If neither segment would be intersected by such a line, (if one + * or more of the segments are vertical) then the line to be considered + * is directly on the right-more of the two left inputs. + */ + + //TODO: Implement compare type, should return bool? + static int compare(Segment a, Segment b) { + final alx = a.leftSE.point.x; + final blx = b.leftSE.point.x; + final arx = a.rightSE.point.x; + final brx = b.rightSE.point.x; + + // check if they're even in the same vertical plane + if (brx < alx) return 1; + if (arx < blx) return -1; + + final aly = a.leftSE.point.y; + final bly = b.leftSE.point.y; + final ary = a.rightSE.point.y; + final bry = b.rightSE.point.y; + + // is left endpoint of segment B the right-more? + if (alx < blx) { + // are the two segments in the same horizontal plane? + if (bly < aly && bly < ary) return 1; + if (bly > aly && bly > ary) return -1; + + // is the B left endpoint colinear to segment A? + final aCmpBLeft = a.comparePoint(b.leftSE.point); + if (aCmpBLeft < 0) return 1; + if (aCmpBLeft > 0) return -1; + + // is the A right endpoint colinear to segment B ? + final bCmpARight = b.comparePoint(a.rightSE.point); + if (bCmpARight != 0) return bCmpARight; + + // colinear segments, consider the one with left-more + // left endpoint to be first (arbitrary?) + return -1; + } + + // is left endpoint of segment A the right-more? + if (alx > blx) { + if (aly < bly && aly < bry) return -1; + if (aly > bly && aly > bry) return 1; + + // is the A left endpoint colinear to segment B? + final bCmpALeft = b.comparePoint(a.leftSE.point); + if (bCmpALeft != 0) return bCmpALeft; + + // is the B right endpoint colinear to segment A? + final aCmpBRight = a.comparePoint(b.rightSE.point); + if (aCmpBRight < 0) return 1; + if (aCmpBRight > 0) return -1; + + // colinear segments, consider the one with left-more + // left endpoint to be first (arbitrary?) + return 1; + } + + // if we get here, the two left endpoints are in the same + // vertical plane, ie alx === blx + + // consider the lower left-endpoint to come first + if (aly < bly) return -1; + if (aly > bly) return 1; + + // left endpoints are identical + // check for colinearity by using the left-more right endpoint + + // is the A right endpoint more left-more? + if (arx < brx) { + final bCmpARight = b.comparePoint(a.rightSE.point); + if (bCmpARight != 0) return bCmpARight; + } + + // is the B right endpoint more left-more? + if (arx > brx) { + final aCmpBRight = a.comparePoint(b.rightSE.point); + if (aCmpBRight < 0) return 1; + if (aCmpBRight > 0) return -1; + } + + if (arx != brx) { + // are these two [almost] vertical segments with opposite orientation? + // if so, the one with the lower right endpoint comes first + final ay = ary - aly; + final ax = arx - alx; + final by = bry - bly; + final bx = brx - blx; + if (ay > ax && by < bx) return 1; + if (ay < ax && by > bx) return -1; + } + + // we have colinear segments with matching orientation + // consider the one with more left-more right endpoint to be first + if (arx > brx) return 1; + if (arx < brx) return -1; + + // if we get here, two two right endpoints are in the same + // vertical plane, ie arx === brx + + // consider the lower right-endpoint to come first + if (ary < bry) return -1; + if (ary > bry) return 1; + + // right endpoints identical as well, so the segments are idential + // fall back on creation order as consistent tie-breaker + if (a.id < b.id) return -1; + if (a.id > b.id) return 1; + + // identical segment, ie a === b + return 0; + } + + /* Compare this segment with a point. + * + * A point P is considered to be colinear to a segment if there + * exists a distance D such that if we travel along the segment + * from one * endpoint towards the other a distance D, we find + * ourselves at point P. + * + * Return value indicates: + * + * 1: point lies above the segment (to the left of vertical) + * 0: point is colinear to segment + * -1: point lies below the segment (to the right of vertical) + */ + + //TODO: return bool? + comparePoint(Point point) { + if (isAnEndpoint(point)) return 0; + + final Point lPt = leftSE.point; + final Point rPt = rightSE.point; + final Vector2 v = vector; + + // Exactly vertical segments. + if (lPt.x == rPt.x) { + if (point.x == lPt.x) return 0; + return point.x < lPt.x ? 1 : -1; + } + + // Nearly vertical segments with an intersection. + // Check to see where a point on the line with matching Y coordinate is. + final yDist = (point.y - lPt.y) / v.y; + final xFromYDist = lPt.x + yDist * v.x; + if (point.x == xFromYDist) return 0; + + // General case. + // Check to see where a point on the line with matching X coordinate is. + final xDist = (point.x - lPt.x) / v.x; + final yFromXDist = lPt.y + xDist * v.y; + if (point.y == yFromXDist) return 0; + return point.y < yFromXDist ? -1 : 1; + } + + /* When a segment is split, the rightSE is replaced with a new sweep event */ + replaceRightSE(newRightSE) { + rightSE = newRightSE; + rightSE.segment = this; + rightSE.otherSE = leftSE; + leftSE.otherSE = rightSE; + } + + /* Create Bounding Box for segment */ + BoundingBox get bbox { + final y1 = leftSE.point.y; + final y2 = rightSE.point.y; + return BoundingBox(Point(leftSE.point.x, y1 < y2 ? y1 : y2), + Point(rightSE.point.x, y1 > y2 ? y1 : y2)); + } + + /* + * Given another segment, returns the first non-trivial intersection + * between the two segments (in terms of sweep line ordering), if it exists. + * + * A 'non-trivial' intersection is one that will cause one or both of the + * segments to be split(). As such, 'trivial' vs. 'non-trivial' intersection: + * + * * endpoint of segA with endpoint of segB --> trivial + * * endpoint of segA with point along segB --> non-trivial + * * endpoint of segB with point along segA --> non-trivial + * * point along segA with point along segB --> non-trivial + * + * If no non-trivial intersection exists, return null + * Else, return null. + */ + + Point? getIntersection(Segment other) { + // If bboxes don't overlap, there can't be any intersections + final tBbox = bbox; + final oBbox = other.bbox; + final bboxOverlap = getBboxOverlap(tBbox, oBbox); + if (bboxOverlap == null) return null; + + // We first check to see if the endpoints can be considered intersections. + // This will 'snap' intersections to endpoints if possible, and will + // handle cases of colinearity. + + final tlp = leftSE.point; + final trp = rightSE.point; + final olp = other.leftSE.point; + final orp = other.rightSE.point; + + // does each endpoint touch the other segment? + // note that we restrict the 'touching' definition to only allow segments + // to touch endpoints that lie forward from where we are in the sweep line pass + final touchesOtherLSE = isInBbox(tBbox, olp) && comparePoint(olp) == 0; + final touchesThisLSE = isInBbox(oBbox, tlp) && other.comparePoint(tlp) == 0; + final touchesOtherRSE = isInBbox(tBbox, orp) && comparePoint(orp) == 0; + final touchesThisRSE = isInBbox(oBbox, trp) && other.comparePoint(trp) == 0; + + // do left endpoints match? + if (touchesThisLSE && touchesOtherLSE) { + // these two cases are for colinear segments with matching left + // endpoints, and one segment being longer than the other + if (touchesThisRSE && !touchesOtherRSE) return trp; + if (!touchesThisRSE && touchesOtherRSE) return orp; + // either the two segments match exactly (two trival intersections) + // or just on their left endpoint (one trivial intersection + return null; + } + + // does this left endpoint matches (other doesn't) + if (touchesThisLSE) { + // check for segments that just intersect on opposing endpoints + if (touchesOtherRSE) { + if (tlp.x == orp.x && tlp.y == orp.y) return null; + } + // t-intersection on left endpoint + return tlp; + } + + // does other left endpoint matches (this doesn't) + if (touchesOtherLSE) { + // check for segments that just intersect on opposing endpoints + if (touchesThisRSE) { + if (trp.x == olp.x && trp.y == olp.y) return null; + } + // t-intersection on left endpoint + return olp; + } + + // trivial intersection on right endpoints + if (touchesThisRSE && touchesOtherRSE) return null; + + // t-intersections on just one right endpoint + if (touchesThisRSE) return trp; + if (touchesOtherRSE) return orp; + + // None of our endpoints intersect. Look for a general intersection between + // infinite lines laid over the segments + Point? pt = intersection(tlp, vector, olp, other.vector); + + // are the segments parrallel? Note that if they were colinear with overlap, + // they would have an endpoint intersection and that case was already handled above + if (pt == null) return null; + + // is the intersection found between the lines not on the segments? + if (!isInBbox(bboxOverlap, pt)) return null; + + // round the the computed point if needed + return rounder.round(pt.x, pt.y); + } + + /* + * Split the given segment into multiple segments on the given points. + * * Each existing segment will retain its leftSE and a new rightSE will be + * generated for it. + * * A new segment will be generated which will adopt the original segment's + * rightSE, and a new leftSE will be generated for it. + * * If there are more than two points given to split on, new segments + * in the middle will be generated with new leftSE and rightSE's. + * * An array of the newly generated SweepEvents will be returned. + * + * Warning: input array of points is modified + */ + //TODO: point events + List split(PointEvents point) { + final List newEvents = []; + final alreadyLinked = point.events != null; + + final newLeftSE = SweepEvent(point, true); + final newRightSE = SweepEvent(point, false); + final oldRightSE = rightSE; + replaceRightSE(newRightSE); + newEvents.add(newRightSE); + newEvents.add(newLeftSE); + final newSeg = Segment( + newLeftSE, + oldRightSE, + //TODO: Can rings and windings be null here? + rings != null ? List.from(rings!) : null, + windings != null ? List.from(windings!) : null, + ); + + // when splitting a nearly vertical downward-facing segment, + // sometimes one of the resulting new segments is vertical, in which + // case its left and right events may need to be swapped + if (SweepEvent.comparePoints(newSeg.leftSE.point, newSeg.rightSE.point) > + 0) { + newSeg.swapEvents(); + } + if (SweepEvent.comparePoints(leftSE.point, rightSE.point) > 0) { + swapEvents(); + } + + // in the point we just used to create new sweep events with was already + // linked to other events, we need to check if either of the affected + // segments should be consumed + if (alreadyLinked) { + newLeftSE.checkForConsuming(); + newRightSE.checkForConsuming(); + } + + return newEvents; + } + + /* Swap which event is left and right */ + swapEvents() {} + + /* Consume another segment. We take their rings under our wing + * and mark them as consumed. Use for perfectly overlapping segments */ + consume(other) { + Segment consumer = this; + Segment consumee = other; + while (consumer.consumedBy != null) { + consumer = consumer.consumedBy!; + } + while (consumee.consumedBy != null) { + consumee = consumee.consumedBy!; + } + ; + final cmp = Segment.compare(consumer, consumee); + if (cmp == 0) return; // already consumed + // the winner of the consumption is the earlier segment + // according to sweep line ordering + if (cmp > 0) { + final tmp = consumer; + consumer = consumee; + consumee = tmp; + } + + // make sure a segment doesn't consume it's prev + if (consumer.prev == consumee) { + final tmp = consumer; + consumer = consumee; + consumee = tmp; + } + + for (var i = 0, iMax = consumee.rings!.length; i < iMax; i++) { + final ring = consumee.rings![i]; + final winding = consumee.windings![i]; + final index = consumer.rings!.indexOf(ring); + if (index == -1) { + consumer.rings!.add(ring); + consumer.windings!.add(winding); + } else { + consumer.windings![index] += winding; + } + } + consumee.rings = null; + consumee.windings = null; + consumee.consumedBy = consumer; + + // mark sweep events consumed as to maintain ordering in sweep event queue + consumee.leftSE.consumedBy = consumer.leftSE; + consumee.rightSE.consumedBy = consumer.rightSE; + } + + static Segment fromRing(PointEvents pt1, PointEvents pt2, ring) { + PointEvents leftPt; + PointEvents rightPt; + var winding; + + // ordering the two points according to sweep line ordering + final cmpPts = SweepEvent.comparePoints(pt1, pt2); + if (cmpPts < 0) { + leftPt = pt1; + rightPt = pt2; + winding = 1; + } else if (cmpPts > 0) { + leftPt = pt2; + rightPt = pt1; + winding = -1; + } else { + throw Exception( + "Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]"); + } + + final leftSE = SweepEvent(leftPt, true); + final rightSE = SweepEvent(rightPt, false); + return Segment(leftSE, rightSE, [ring], [winding]); + } + + var _prevInResult; + + /* The first segment previous segment chain that is in the result */ + Segment? prevInResult() { + if (_prevInResult != null) return _prevInResult; + if (prev == null) { + _prevInResult = null; + } else if (prev!.isInResult()) { + _prevInResult = prev; + } else { + _prevInResult = prev!.prevInResult(); + } + return _prevInResult; + } + + _SegmentState? _beforeState; + + beforeState() { + if (_beforeState != null) return _beforeState; + if (prev == null) { + _beforeState = _SegmentState( + rings: [], + windings: [], + multiPolys: [], + ); + } else { + final Segment seg = prev!.consumedBy ?? prev!; + _beforeState = seg.afterState(); + } + return _beforeState; + } + + afterState() {} + + bool? _isInResult; + + /* Is this segment part of the final result? */ + bool isInResult() { + // if we've been consumed, we're not in the result + if (consumedBy != null) return false; + + if (_isInResult != null) return _isInResult!; + + final mpsBefore = beforeState().multiPolys; + final mpsAfter = afterState().multiPolys; + + switch (operation.type) { + case "union": + { + // UNION - included iff: + // * On one side of us there is 0 poly interiors AND + // * On the other side there is 1 or more. + final noBefores = mpsBefore.length == 0; + final noAfters = mpsAfter.length == 0; + _isInResult = noBefores != noAfters; + break; + } + + case "intersection": + { + // INTERSECTION - included iff: + // * on one side of us all multipolys are rep. with poly interiors AND + // * on the other side of us, not all multipolys are repsented + // with poly interiors + int least; + int most; + if (mpsBefore.length < mpsAfter.length) { + least = mpsBefore.length; + most = mpsAfter.length; + } else { + least = mpsAfter.length; + most = mpsBefore.length; + } + _isInResult = most == operation.numMultiPolys && least < most; + break; + } + + case "xor": + { + // XOR - included iff: + // * the difference between the number of multipolys represented + // with poly interiors on our two sides is an odd number + final diff = (mpsBefore.length - mpsAfter.length).abs(); + _isInResult = diff % 2 == 1; + break; + } + + case "difference": + { + // DIFFERENCE included iff: + // * on exactly one side, we have just the subject + bool isJustSubject(List mps) => mps.length == 1 && mps[0].isSubject; + _isInResult = isJustSubject(mpsBefore) != isJustSubject(mpsAfter); + break; + } + + default: + throw Exception('Unrecognized operation type found ${operation.type}'); + } + + return _isInResult!; + } + + isAnEndpoint(Point pt) { + return ((pt.x == leftSE.point.x && pt.y == leftSE.point.y) || + (pt.x == rightSE.point.x && pt.y == rightSE.point.y)); + } + + /* A vector from the left point to the right */ + Vector2 get vector { + return Vector2((rightSE.point.x - leftSE.point.x).toDouble(), + (rightSE.point.y - leftSE.point.y).toDouble()); + } +} + +class _SegmentState { + List rings; + List windings; + List multiPolys; + _SegmentState({ + required this.rings, + required this.windings, + required this.multiPolys, + }); +} diff --git a/lib/src/polygon_clipping/sweep_event.dart b/lib/src/polygon_clipping/sweep_event.dart new file mode 100644 index 0000000..03b85f8 --- /dev/null +++ b/lib/src/polygon_clipping/sweep_event.dart @@ -0,0 +1,147 @@ +import 'dart:math'; + +import 'package:turf/src/polygon_clipping/point_extension.dart'; +import 'package:turf/src/polygon_clipping/vector_extension.dart'; + +import 'segment.dart'; // Assuming this is the Dart equivalent of your Segment class // Assuming this contains cosineOfAngle and sineOfAngle functions + +class SweepEvent { + PointEvents point; + bool isLeft; + Segment? segment; // Assuming these are defined in your environment + SweepEvent? otherSE; + SweepEvent? consumedBy; + + // Warning: 'point' input will be modified and re-used (for performance + + SweepEvent(this.point, this.isLeft) { + if (point.events == null) { + point.events = [this]; + } else { + point.events!.add(this); + } + point = point; + isLeft = isLeft; + // this.segment, this.otherSE set by factory + } + + // for ordering sweep events in the sweep event queue + static int compare(SweepEvent a, SweepEvent b) { + // favor event with a point that the sweep line hits first + final int ptCmp = SweepEvent.comparePoints(a.point, b.point); + if (ptCmp != 0) return ptCmp; + + // the points are the same, so link them if needed + if (a.point != b.point) a.link(b); + + // favor right events over left + if (a.isLeft != b.isLeft) return a.isLeft ? 1 : -1; + + // we have two matching left or right endpoints + // ordering of this case is the same as for their segments + return Segment.compare(a.segment!, b.segment!); + } + + static int comparePoints(Point aPt, Point bPt) { + if (aPt.x < bPt.x) return -1; + if (aPt.x > bPt.x) return 1; + + if (aPt.y < bPt.y) return -1; + if (aPt.y > bPt.y) return 1; + + return 0; + } + + void link(SweepEvent other) { + //TODO: write test for Point comparison + if (other.point == point) { + throw 'Tried to link already linked events'; + } + if (other.point.events == null) { + throw 'PointEventsError: events called on null point.events'; + } + for (var evt in other.point.events!) { + point.events!.add(evt); + evt.point = point; + } + checkForConsuming(); + } + + void checkForConsuming() { + if (point.events == null) { + throw 'PointEventsError: events called on null point.events, method requires events'; + } + var numEvents = point.events!.length; + for (int i = 0; i < numEvents; i++) { + var evt1 = point.events![i]; + if (evt1.segment == null) throw Exception("evt1.segment is null"); + if (evt1.segment!.consumedBy != null) continue; + for (int j = i + 1; j < numEvents; j++) { + var evt2 = point.events![j]; + if (evt2.consumedBy != null) continue; + if (evt1.otherSE!.point.events != evt2.otherSE!.point.events) continue; + evt1.segment!.consume(evt2.segment); + } + } + } + + List getAvailableLinkedEvents() { + List events = []; + for (var evt in point.events!) { + //TODO: !evt.segment!.ringOut was written first but th + if (evt != this && + evt.segment!.ringOut == null && + evt.segment!.isInResult()) { + events.add(evt); + } + } + return events; + } + + Comparator getLeftmostComparator(SweepEvent baseEvent) { + var cache = >{}; + + void fillCache(SweepEvent linkedEvent) { + var nextEvent = linkedEvent.otherSE; + if (nextEvent != null) { + cache[linkedEvent] = { + 'sine': sineOfAngle(point, baseEvent.point, nextEvent!.point), + 'cosine': cosineOfAngle(point, baseEvent.point, nextEvent.point), + }; + } + } + + return (SweepEvent a, SweepEvent b) { + if (!cache.containsKey(a)) fillCache(a); + if (!cache.containsKey(b)) fillCache(b); + + var aValues = cache[a]!; + var bValues = cache[b]!; + + if (aValues['sine']! >= 0 && bValues['sine']! >= 0) { + if (aValues['cosine']! < bValues['cosine']!) return 1; + if (aValues['cosine']! > bValues['cosine']!) return -1; + return 0; + } + + if (aValues['sine']! < 0 && bValues['sine']! < 0) { + if (aValues['cosine']! < bValues['cosine']!) return -1; + if (aValues['cosine']! > bValues['cosine']!) return 1; + return 0; + } + + if (bValues['sine']! < aValues['sine']!) return -1; + if (bValues['sine']! > aValues['sine']!) return 1; + return 0; + }; + } +} + + +// class Point { +// double x; +// double y; +// List events; + +// Point(this.x, this.y); +// } diff --git a/lib/src/polygon_clipping/sweep_line.dart b/lib/src/polygon_clipping/sweep_line.dart new file mode 100644 index 0000000..7a682ea --- /dev/null +++ b/lib/src/polygon_clipping/sweep_line.dart @@ -0,0 +1,165 @@ +import 'dart:collection'; +import 'dart:math'; +import 'package:turf/src/polygon_clipping/point_extension.dart'; + +import 'segment.dart'; +import 'sweep_event.dart'; + +class SweepLine { + late SplayTreeMap tree; + final List segments = []; + final List queue; + + SweepLine(this.queue, {int Function(Segment a, Segment b)? comparator}) { + tree = SplayTreeMap(comparator ?? Segment.compare); + } + + List process(SweepEvent event) { + Segment segment = event.segment!; + List newEvents = []; + + // if we've already been consumed by another segment, + // clean up our body parts and get out + if (event.consumedBy != null) { + if (event.isLeft) { + queue.remove(event.otherSE!); + } else { + tree.remove(segment); + } + return newEvents; + } + + Segment? node; + + if (event.isLeft) { + tree[segment]; + node = null; + //? Can you use SplayTreeSet lookup here? looks for internal of segment. + } else if (tree.containsKey(segment)) { + node = segment; + } else { + node = null; + } + + if (node == null) { + throw ArgumentError( + 'Unable to find segment #${segment.id} ' + '[${segment.leftSE.point.x}, ${segment.leftSE.point.y}] -> ' + '[${segment.rightSE.point.x}, ${segment.rightSE.point.y}] ' + 'in SweepLine tree.', + ); + } + + Segment? prevNode = node; + Segment? nextNode = node; + Segment? prevSeg; + Segment? nextSeg; + + // skip consumed segments still in tree + while (prevSeg == null) { + prevNode = tree.lastKeyBefore(prevNode!); + if (prevNode == null) { + prevSeg = null; + } else if (prevNode.consumedBy == null) { + prevSeg = prevNode; + } + } + + // skip consumed segments still in tree + while (nextSeg == null) { + nextNode = tree.firstKeyAfter(nextNode!); + if (nextNode == null) { + nextSeg = null; + } else if (nextNode.consumedBy == null) { + nextSeg = nextNode; + } + } + + if (event.isLeft) { + // Check for intersections against the previous segment in the sweep line + Point? prevMySplitter; + if (prevSeg != null) { + var prevInter = prevSeg.getIntersection(segment); + if (prevInter != null) { + if (!segment.isAnEndpoint(prevInter)) prevMySplitter = prevInter; + if (!prevSeg.isAnEndpoint(prevInter)) { + var newEventsFromSplit = _splitSafely(prevSeg, prevInter); + newEvents.addAll(newEventsFromSplit); + } + } + } + // Check for intersections against the next segment in the sweep line + Point? nextMySplitter; + if (nextSeg != null) { + var nextInter = nextSeg.getIntersection(segment); + if (nextInter != null) { + if (!segment.isAnEndpoint(nextInter)) nextMySplitter = nextInter; + if (!nextSeg.isAnEndpoint(nextInter)) { + var newEventsFromSplit = _splitSafely(nextSeg, nextInter); + newEvents.addAll(newEventsFromSplit); + } + } + } + + // For simplicity, even if we find more than one intersection we only + // spilt on the 'earliest' (sweep-line style) of the intersections. + // The other intersection will be handled in a future process(). + Point? mySplitter; + if (prevMySplitter == null) { + mySplitter = nextMySplitter; + } else if (nextMySplitter == null) { + mySplitter = prevMySplitter; + } else { + var cmpSplitters = SweepEvent.comparePoints( + prevMySplitter, + nextMySplitter, + ); + mySplitter = cmpSplitters <= 0 ? prevMySplitter : nextMySplitter; + } + //TODO: check if mySplitter is null? do we need that check? + if (prevMySplitter != null || nextMySplitter != null) { + queue.remove(segment.rightSE); + newEvents.addAll(segment.split(PointEvents.fromPoint(mySplitter!))); + } + + if (newEvents.isNotEmpty) { + tree.remove(segment); + tree[segment]; + newEvents.add(event); + } else { + segments.add(segment); + segment.prev = prevSeg; + } + } else { + if (prevSeg != null && nextSeg != null) { + var inter = prevSeg.getIntersection(nextSeg); + if (inter != null) { + if (!prevSeg.isAnEndpoint(inter)) { + var newEventsFromSplit = _splitSafely(prevSeg, inter); + newEvents.addAll(newEventsFromSplit); + } + if (!nextSeg.isAnEndpoint(inter)) { + var newEventsFromSplit = _splitSafely(nextSeg, inter); + newEvents.addAll(newEventsFromSplit); + } + } + } + + tree.remove(segment); + } + + return newEvents; + } + + List _splitSafely(Segment seg, dynamic pt) { + tree.remove(seg); + var rightSE = seg.rightSE; + queue.remove(rightSE); + var newEvents = seg.split(pt); + newEvents.add(rightSE); + if (seg.consumedBy == null) { + tree[seg]; + } + return newEvents; + } +} diff --git a/lib/src/polygon_clipping/utils.dart b/lib/src/polygon_clipping/utils.dart new file mode 100644 index 0000000..d0d6e51 --- /dev/null +++ b/lib/src/polygon_clipping/utils.dart @@ -0,0 +1,30 @@ +import 'dart:collection'; + +const double epsilon = + 2.220446049250313e-16; // Equivalent to Number.EPSILON in JavaScript + +/// 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); +} diff --git a/lib/src/polygon_clipping/vector.dart b/lib/src/polygon_clipping/vector.dart new file mode 100644 index 0000000..5902dee --- /dev/null +++ b/lib/src/polygon_clipping/vector.dart @@ -0,0 +1,26 @@ +import 'dart:math'; + +import 'package:turf/src/polygon_clipping/utils.dart'; + +/* Cross Product of two vectors with first point at origin */ +num crossProduct(Point a, Point b) => a.x * b.y - a.y * b.x; + +/* Dot Product of two vectors with first point at origin */ +num dotProduct(Point a, Point b) => a.x * b.x + a.y * b.y; + +/* Comparator for two vectors with same starting point */ +num compareVectorAngles(Point basePt, Point endPt1, Point endPt2) { + double res = orient2d( + endPt1.x.toDouble(), + endPt1.y.toDouble(), + basePt.x.toDouble(), + basePt.y.toDouble(), + endPt2.x.toDouble(), + endPt2.y.toDouble(), + ); + return res > 0 + ? -1 + : res < 0 + ? 1 + : 0; +} diff --git a/lib/src/polygon_clipping/vector_extension.dart b/lib/src/polygon_clipping/vector_extension.dart new file mode 100644 index 0000000..d4f6d6b --- /dev/null +++ b/lib/src/polygon_clipping/vector_extension.dart @@ -0,0 +1,73 @@ +import 'dart:math'; + +import 'package:vector_math/vector_math.dart'; + +extension Vector2Extension on Vector2 { + /* Given a vector, return one that is perpendicular */ + Vector2 get perpendicularVector { + return Vector2(-y, x); + } +} + +/* Get the intersection of two lines, each defined by a base point and a vector. + * In the case of parrallel lines (including overlapping ones) returns null. */ +Point? intersection(Point pt1, Vector2 v1, Point pt2, Vector2 v2) { + // take some shortcuts for vertical and horizontal lines + // this also ensures we don't calculate an intersection and then discover + // it's actually outside the bounding box of the line + if (v1.x == 0) return verticalIntersection(pt2, v2, pt1.x); + if (v2.x == 0) return verticalIntersection(pt1, v1, pt2.x); + if (v1.y == 0) return horizontalIntersection(pt2, v2, pt1.y); + if (v2.y == 0) return horizontalIntersection(pt1, v1, pt2.y); + + // General case for non-overlapping segments. + // This algorithm is based on Schneider and Eberly. + // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244 + final v1CrossV2 = v1.cross(v2); + if (v1CrossV2 == 0) return null; + + final ve = Vector2((pt2.x - pt1.x).toDouble(), (pt2.y - pt1.y).toDouble()); + final d1 = ve.cross(v1) / v1CrossV2; + final d2 = ve.cross(v2) / v1CrossV2; + + // take the average of the two calculations to minimize rounding error + final x1 = pt1.x + d2 * v1.x, x2 = pt2.x + d1 * v2.x; + final y1 = pt1.y + d2 * v1.y, y2 = pt2.y + d1 * v2.y; + final x = (x1 + x2) / 2; + final y = (y1 + y2) / 2; + return Point(x, y); +} + +/* Get the x coordinate where the given line (defined by a point and vector) + * crosses the horizontal line with the given y coordiante. + * In the case of parrallel lines (including overlapping ones) returns null. */ +Point? horizontalIntersection(Point pt, Vector2 v, num y) { + if (v.y == 0) return null; + return Point(pt.x + (v.x / v.y) * (y - pt.y), y); +} + +/* Get the y coordinate where the given line (defined by a point and vector) + * crosses the vertical line with the given x coordiante. + * In the case of parrallel lines (including overlapping ones) returns null. */ +Point? verticalIntersection(Point pt, Vector2 v, num x) { + if (v.x == 0) return null; + return Point(x, pt.y + (v.y / v.x) * (x - pt.x)); +} + +/* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */ +sineOfAngle(Point pShared, Point pBase, Point pAngle) { + final Vector2 vBase = Vector2( + (pBase.x - pShared.x).toDouble(), (pBase.y - pShared.y).toDouble()); + final Vector2 vAngle = Vector2( + (pAngle.x - pShared.x).toDouble(), (pAngle.y - pShared.y).toDouble()); + return vAngle.cross(vBase) / vAngle.length / vBase.length; +} + +/* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */ +cosineOfAngle(Point pShared, Point pBase, Point pAngle) { + final Vector2 vBase = Vector2( + (pBase.x - pShared.x).toDouble(), (pBase.y - pShared.y).toDouble()); + final Vector2 vAngle = Vector2( + (pAngle.x - pShared.x).toDouble(), (pAngle.y - pShared.y).toDouble()); + return vAngle.dot(vBase) / vAngle.length / vBase.length; +} diff --git a/pubspec.yaml b/pubspec.yaml index 26a6587..e25be77 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: turf_pip: ^0.0.2 rbush: ^1.1.0 sweepline_intersections: ^0.0.4 + vector_math: ^2.1.4 dev_dependencies: lints: ^3.0.0 From 1a9edbe0714c902b4c6ef59626ced87be4b33f85 Mon Sep 17 00:00:00 2001 From: Aaron S Kennedy <36516690+aaronsamkennedy@users.noreply.github.com> Date: Mon, 19 Feb 2024 15:59:18 -0700 Subject: [PATCH 2/6] Starting adapt polygon-clipping to Position and BBox --- lib/src/geojson.dart | 72 +++++++++++ lib/src/helpers.dart | 29 +++++ lib/src/polygon_clipping/bbox.dart | 36 ------ lib/src/polygon_clipping/flp.dart | 4 +- lib/src/polygon_clipping/geom_in.dart | 117 ++++++++---------- lib/src/polygon_clipping/geom_out.dart | 29 +++-- lib/src/polygon_clipping/intersection.dart | 4 +- lib/src/polygon_clipping/operation.dart | 10 +- lib/src/polygon_clipping/point_extension.dart | 15 ++- lib/src/polygon_clipping/rounder.dart | 7 +- lib/src/polygon_clipping/segment.dart | 93 +++++++------- lib/src/polygon_clipping/sweep_event.dart | 27 ++-- lib/src/polygon_clipping/sweep_line.dart | 14 +-- lib/src/polygon_clipping/utils.dart | 58 +++++---- lib/src/polygon_clipping/vector.dart | 26 ---- .../polygon_clipping/vector_extension.dart | 109 +++++++++------- 16 files changed, 349 insertions(+), 301 deletions(-) delete mode 100644 lib/src/polygon_clipping/bbox.dart delete mode 100644 lib/src/polygon_clipping/vector.dart diff --git a/lib/src/geojson.dart b/lib/src/geojson.dart index 619fe12..0fe3f8b 100644 --- a/lib/src/geojson.dart +++ b/lib/src/geojson.dart @@ -1,4 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:turf/helpers.dart'; part 'geojson.g.dart'; @@ -330,6 +331,15 @@ class BBox extends CoordinateType { factory BBox.fromJson(List list) => BBox.of(list); + factory BBox.fromPositions(Position p1, Position p2) => BBox.named( + lng1: p1.lng, + lat1: p1.lat, + alt1: p1.alt ?? 0, + lng2: p2.lng, + lat2: p2.lat, + alt2: p2.alt ?? 0, + ); + bool get _is3D => length == 6; num get lng1 => _items[0]; @@ -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, @@ -361,6 +383,30 @@ class BBox extends CoordinateType { alt2: alt2 ?? this.alt2, ); + //Adjust the bounds to include the given position + void expandToFitPosition(Position position) { + if (position.lng < lng1) { + _items[0] = position.lng; + } + if (position.lat < lat1) { + _items[1] = position.lat; + } + if (position.lng > lng2) { + _items[3] = position.lng; + } + if (position.lat > lat2) { + _items[4] = 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); @@ -377,6 +423,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); @@ -584,6 +652,10 @@ class MultiPolygon extends GeometryType>>> { GeoJSONObjectType.multiPolygon, bbox: bbox); + List toPolygons() { + return coordinates.map((e) => Polygon(coordinates: e)).toList(); + } + @override Map toJson() => super.serialize(_$MultiPolygonToJson(this)); diff --git a/lib/src/helpers.dart b/lib/src/helpers.dart index 0e15b96..244e0d5 100644 --- a/lib/src/helpers.dart +++ b/lib/src/helpers.dart @@ -77,6 +77,9 @@ const areaFactors = { 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)) { @@ -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); +} diff --git a/lib/src/polygon_clipping/bbox.dart b/lib/src/polygon_clipping/bbox.dart deleted file mode 100644 index 2f74836..0000000 --- a/lib/src/polygon_clipping/bbox.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:math'; - -class BoundingBox { - Point ll; // Lower left point - Point ur; // Upper right point - - BoundingBox(this.ll, this.ur); -} - -bool isInBbox(BoundingBox bbox, Point point) { - return (bbox.ll.x <= point.x && - point.x <= bbox.ur.x && - bbox.ll.y <= point.y && - point.y <= bbox.ur.y); -} - -BoundingBox? getBboxOverlap(BoundingBox b1, BoundingBox b2) { - // Check if the bboxes overlap at all - if (b2.ur.x < b1.ll.x || - b1.ur.x < b2.ll.x || - b2.ur.y < b1.ll.y || - b1.ur.y < b2.ll.y) { - return null; - } - - // Find the middle two X values - final lowerX = b1.ll.x < b2.ll.x ? b2.ll.x : b1.ll.x; - final upperX = b1.ur.x < b2.ur.x ? b1.ur.x : b2.ur.x; - - // Find the middle two Y values - final lowerY = b1.ll.y < b2.ll.y ? b2.ll.y : b1.ll.y; - final upperY = b1.ur.y < b2.ur.y ? b1.ur.y : b2.ur.y; - - // Create a new bounding box with the overlap - return BoundingBox(Point(lowerX, lowerY), Point(upperX, upperY)); -} diff --git a/lib/src/polygon_clipping/flp.dart b/lib/src/polygon_clipping/flp.dart index 0e31062..72fbb11 100644 --- a/lib/src/polygon_clipping/flp.dart +++ b/lib/src/polygon_clipping/flp.dart @@ -7,10 +7,10 @@ // Calculate the square of epsilon for later use. -import 'package:turf/src/polygon_clipping/utils.dart'; +import 'package:turf/helpers.dart'; const double epsilonsqrd = epsilon * epsilon; -// FLP (Floating-Point) comparator function +// FLP (Floating-Position) comparator function int cmp(double a, double b) { // Check if both numbers are close to zero. if (-epsilon < a && a < epsilon) { diff --git a/lib/src/polygon_clipping/geom_in.dart b/lib/src/polygon_clipping/geom_in.dart index 359b99b..12c3160 100644 --- a/lib/src/polygon_clipping/geom_in.dart +++ b/lib/src/polygon_clipping/geom_in.dart @@ -1,7 +1,8 @@ import 'dart:math'; -import 'package:turf/src/polygon_clipping/bbox.dart'; +import 'package:turf/helpers.dart'; import 'package:turf/src/polygon_clipping/point_extension.dart'; +import 'package:turf/src/polygon_clipping/sweep_event.dart'; import 'rounder.dart'; import 'segment.dart'; @@ -11,42 +12,35 @@ class RingIn { List segments = []; final bool isExterior; final PolyIn poly; - late BoundingBox bbox; + late BBox bbox; - RingIn(List geomRing, this.poly, this.isExterior) { - if (!(geomRing is List && geomRing.isNotEmpty)) { - throw ArgumentError( - "Input geometry is not a valid Polygon or MultiPolygon"); - } - - final firstPoint = rounder.round(geomRing[0].x, geomRing[0].y); - bbox = BoundingBox( - Point(firstPoint.x, firstPoint.y), - Point(firstPoint.x, firstPoint.y), + RingIn(List geomRing, this.poly, this.isExterior) { + final firstPoint = rounder.round(geomRing[0].lng, geomRing[0].lat); + bbox = BBox.fromPositions( + Position(firstPoint.lng, firstPoint.lat), + Position(firstPoint.lng, firstPoint.lat), ); var prevPoint = firstPoint; for (var i = 1; i < geomRing.length; i++) { - var point = rounder.round(geomRing[i].x, geomRing[i].y); + var point = rounder.round(geomRing[i].lng, geomRing[i].lat); // skip repeated points - if (point.x == prevPoint.x && point.y == prevPoint.y) continue; - segments.add(Segment.fromRing(PointEvents.fromPoint(prevPoint), - PointEvents.fromPoint(point), this)); - - bbox.ll = Point(min(point.x, bbox.ll.x), min(point.y, bbox.ll.y)); - bbox.ur = Point(max(point.x, bbox.ur.x), max(point.y, bbox.ur.y)); + if (point.lng == prevPoint.lng && point.lat == prevPoint.lat) continue; + segments.add(Segment.fromRing(PositionEvents.fromPoint(prevPoint), + PositionEvents.fromPoint(point), this)); + bbox.expandToFitPosition(point); prevPoint = point; } // add segment from last to first if last is not the same as first - if (firstPoint.x != prevPoint.x || firstPoint.y != prevPoint.y) { - segments.add(Segment.fromRing(PointEvents.fromPoint(prevPoint), - PointEvents.fromPoint(firstPoint), this)); + if (firstPoint.lng != prevPoint.lng || firstPoint.lat != prevPoint.lat) { + segments.add(Segment.fromRing(PositionEvents.fromPoint(prevPoint), + PositionEvents.fromPoint(firstPoint), this)); } } - List getSweepEvents() { - final sweepEvents = []; + List getSweepEvents() { + final List sweepEvents = []; for (var i = 0; i < segments.length; i++) { final segment = segments[i]; sweepEvents.add(segment.leftSE); @@ -61,30 +55,30 @@ class PolyIn { late RingIn exteriorRing; late List interiorRings; final MultiPolyIn multiPoly; - late BoundingBox bbox; + late BBox bbox; - PolyIn(List geomPoly, this.multiPoly) { - if (!(geomPoly is List)) { - throw ArgumentError( - "Input geometry is not a valid Polygon or MultiPolygon"); - } - exteriorRing = RingIn(geomPoly[0], this, true); + PolyIn(Polygon geomPoly, this.multiPoly) { + exteriorRing = RingIn(geomPoly.coordinates[0], this, true); // copy by value bbox = exteriorRing.bbox; interiorRings = []; - for (var i = 1; i < geomPoly.length; i++) { - final ring = RingIn(geomPoly[i], this, false); - bbox.ll = - Point(min(ring.bbox.ll.x, bbox.ll.x), min(ring.bbox.ll.y, bbox.ll.y)); - bbox.ur = - Point(max(ring.bbox.ur.x, bbox.ur.x), max(ring.bbox.ur.y, bbox.ur.y)); + Position lowerLeft = bbox.position1; + Position upperRight = bbox.position2; + for (var i = 1; i < geomPoly.coordinates.length; i++) { + final ring = RingIn(geomPoly.coordinates[i], this, 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 getSweepEvents() { - final sweepEvents = exteriorRing.getSweepEvents(); + List getSweepEvents() { + final List sweepEvents = exteriorRing.getSweepEvents(); for (var i = 0; i < interiorRings.length; i++) { final ringSweepEvents = interiorRings[i].getSweepEvents(); for (var j = 0; j < ringSweepEvents.length; j++) { @@ -99,38 +93,31 @@ class PolyIn { class MultiPolyIn { late List polys; final bool isSubject; - late BoundingBox bbox; + late BBox bbox; - MultiPolyIn(List geom, this.isSubject) { - if (!(geom is List)) { - throw ArgumentError( - "Input geometry is not a valid Polygon or MultiPolygon"); - } + MultiPolyIn(MultiPolygon geom, this.isSubject) { + bbox = BBox.fromPositions( + Position(double.infinity, double.infinity), + Position(double.negativeInfinity, double.negativeInfinity), + ); - try { - // if the input looks like a polygon, convert it to a multipolygon - if (geom[0][0][0] is num) geom = [geom]; - } catch (ex) { - // The input is either malformed or has empty arrays. - // In either case, it will be handled later on. - } + List polygonsIn = geom.toPolygons(); - polys = []; - bbox = BoundingBox( - Point(double.infinity, double.infinity), - Point(double.negativeInfinity, double.negativeInfinity), - ); - for (var i = 0; i < geom.length; i++) { - final poly = PolyIn(geom[i], this); - bbox.ll = - Point(min(poly.bbox.ll.x, bbox.ll.x), min(poly.bbox.ll.y, bbox.ll.y)); - bbox.ur = - Point(max(poly.bbox.ur.x, bbox.ur.x), max(poly.bbox.ur.y, bbox.ur.y)); + 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)); } + + bbox = BBox.fromPositions(lowerLeft, upperRight); } - List getSweepEvents() { - final sweepEvents = []; + List getSweepEvents() { + final List sweepEvents = []; for (var i = 0; i < polys.length; i++) { final polySweepEvents = polys[i].getSweepEvents(); for (var j = 0; j < polySweepEvents.length; j++) { diff --git a/lib/src/polygon_clipping/geom_out.dart b/lib/src/polygon_clipping/geom_out.dart index c1d8f6f..c18076d 100644 --- a/lib/src/polygon_clipping/geom_out.dart +++ b/lib/src/polygon_clipping/geom_out.dart @@ -1,9 +1,8 @@ -import 'dart:math'; - +import 'package:turf/src/geojson.dart'; import 'package:turf/src/polygon_clipping/intersection.dart'; import 'package:turf/src/polygon_clipping/sweep_event.dart'; import 'package:turf/src/polygon_clipping/segment.dart'; -import 'package:turf/src/polygon_clipping/vector.dart'; +import 'package:turf/src/polygon_clipping/vector_extension.dart'; class RingOut { List events; @@ -29,7 +28,7 @@ class RingOut { SweepEvent nextEvent = segment.rightSE; final List events = [event]; - final Point startingPoint = event.point; + final Position startingPoint = event.point; final List intersectionLEs = []; while (true) { @@ -43,10 +42,10 @@ class RingOut { List availableLEs = event.getAvailableLinkedEvents(); if (availableLEs.isEmpty) { - Point firstPt = events[0].point; - Point lastPt = events[events.length - 1].point; + Position firstPt = events[0].point; + Position lastPt = events[events.length - 1].point; throw Exception( - 'Unable to complete output ring starting at [${firstPt.x}, ${firstPt.y}]. Last matching segment found ends at [${lastPt.x}, ${lastPt.y}].'); + 'Unable to complete output ring starting at [${firstPt.lng}, ${firstPt.lat}]. Last matching segment found ends at [${lastPt.lng}, ${lastPt.lat}].'); } if (availableLEs.length == 1) { @@ -100,14 +99,14 @@ class RingOut { return _isExteriorRing!; } - //TODO: Convert type to List? + //TODO: Convert type to List? List>? getGeom() { - Point prevPt = events[0].point; - List points = [prevPt]; + Position prevPt = events[0].point; + List points = [prevPt]; for (int i = 1, iMax = events.length - 1; i < iMax; i++) { - Point pt = events[i].point; - Point nextPt = events[i + 1].point; + Position pt = events[i].point; + Position nextPt = events[i + 1].point; if (compareVectorAngles(pt, prevPt, nextPt) == 0) continue; points.add(pt); prevPt = pt; @@ -115,8 +114,8 @@ class RingOut { if (points.length == 1) return null; - Point pt = points[0]; - Point nextPt = points[1]; + Position pt = points[0]; + Position nextPt = points[1]; if (compareVectorAngles(pt, prevPt, nextPt) == 0) points.removeAt(0); points.add(points[0]); @@ -126,7 +125,7 @@ class RingOut { List> orderedPoints = []; for (int i = iStart; i != iEnd; i += step) { - orderedPoints.add([points[i].x.toDouble(), points[i].y.toDouble()]); + orderedPoints.add([points[i].lng.toDouble(), points[i].lat.toDouble()]); } return orderedPoints; diff --git a/lib/src/polygon_clipping/intersection.dart b/lib/src/polygon_clipping/intersection.dart index 4ffd805..8f79490 100644 --- a/lib/src/polygon_clipping/intersection.dart +++ b/lib/src/polygon_clipping/intersection.dart @@ -1,8 +1,8 @@ -import 'dart:math'; +import 'package:turf/src/geojson.dart'; class Intersection { final int id; - final Point point; + final Position point; Intersection(this.id, this.point); } diff --git a/lib/src/polygon_clipping/operation.dart b/lib/src/polygon_clipping/operation.dart index b4aae43..19c6b2c 100644 --- a/lib/src/polygon_clipping/operation.dart +++ b/lib/src/polygon_clipping/operation.dart @@ -1,6 +1,6 @@ import 'dart:collection'; -import 'dart:math' as math; -import 'bbox.dart'; +import 'package:turf/src/polygon_clipping/utils.dart'; + import 'geom_in.dart' as geomIn; import 'geom_out.dart' as geomOut; import 'rounder.dart'; @@ -96,9 +96,9 @@ class Operation { // prevents an infinite loop, an otherwise common manifestation of bugs final seg = evt.segment; throw StateError('Unable to pop() ${evt.isLeft ? 'left' : 'right'} ' - 'SweepEvent [${evt.point.x}, ${evt.point.y}] from segment #${seg?.id} ' - '[${seg?.leftSE.point.x}, ${seg?.leftSE.point.y}] -> ' - '[${seg?.rightSE.point.x}, ${seg?.rightSE.point.y}] from queue.'); + 'SweepEvent [${evt.point.lng}, ${evt.point.lat}] from segment #${seg?.id} ' + '[${seg?.leftSE.point.lng}, ${seg?.leftSE.point.lat}] -> ' + '[${seg?.rightSE.point.lng}, ${seg?.rightSE.point.lat}] from queue.'); } if (queue.length > POLYGON_CLIPPING_MAX_QUEUE_SIZE) { diff --git a/lib/src/polygon_clipping/point_extension.dart b/lib/src/polygon_clipping/point_extension.dart index 4d67daf..ca43d26 100644 --- a/lib/src/polygon_clipping/point_extension.dart +++ b/lib/src/polygon_clipping/point_extension.dart @@ -1,17 +1,16 @@ -import 'dart:math'; - +import 'package:turf/src/geojson.dart'; import 'package:turf/src/polygon_clipping/sweep_event.dart'; -class PointEvents extends Point { +class PositionEvents extends Position { List? events; - PointEvents( - double super.x, - double super.y, + PositionEvents( + double super.lng, + double super.lat, this.events, ); - factory PointEvents.fromPoint(Point point) { - return PointEvents(point.x.toDouble(), point.y.toDouble(), []); + factory PositionEvents.fromPoint(Position point) { + return PositionEvents(point.lng.toDouble(), point.lat.toDouble(), []); } } diff --git a/lib/src/polygon_clipping/rounder.dart b/lib/src/polygon_clipping/rounder.dart index 7bcdc86..f55b5f3 100644 --- a/lib/src/polygon_clipping/rounder.dart +++ b/lib/src/polygon_clipping/rounder.dart @@ -1,5 +1,6 @@ import 'dart:collection'; -import 'dart:math'; + +import 'package:turf/helpers.dart'; /// A class for rounding floating-point coordinates to avoid floating-point problems. class PtRounder { @@ -18,8 +19,8 @@ class PtRounder { } /// Rounds the input x and y coordinates using CoordRounder instances. - Point round(num x, num y) { - return Point( + Position round(num x, num y) { + return Position( xRounder.round(x), xRounder.round(y), ); diff --git a/lib/src/polygon_clipping/segment.dart b/lib/src/polygon_clipping/segment.dart index 0829acd..0ba2253 100644 --- a/lib/src/polygon_clipping/segment.dart +++ b/lib/src/polygon_clipping/segment.dart @@ -1,15 +1,14 @@ // Give segments unique ID's to get consistent sorting of // segments and sweep events when all else is identical -import 'dart:math'; -import 'package:turf/src/polygon_clipping/bbox.dart'; +import 'package:turf/src/geojson.dart'; import 'package:turf/src/polygon_clipping/geom_out.dart'; import 'package:turf/src/polygon_clipping/operation.dart'; import 'package:turf/src/polygon_clipping/point_extension.dart'; import 'package:turf/src/polygon_clipping/rounder.dart'; import 'package:turf/src/polygon_clipping/sweep_event.dart'; +import 'package:turf/src/polygon_clipping/utils.dart'; import 'package:turf/src/polygon_clipping/vector_extension.dart'; -import 'package:vector_math/vector_math.dart'; class Segment { static int _nextId = 1; @@ -57,19 +56,19 @@ class Segment { //TODO: Implement compare type, should return bool? static int compare(Segment a, Segment b) { - final alx = a.leftSE.point.x; - final blx = b.leftSE.point.x; - final arx = a.rightSE.point.x; - final brx = b.rightSE.point.x; + final alx = a.leftSE.point.lng; + final blx = b.leftSE.point.lng; + final arx = a.rightSE.point.lng; + final brx = b.rightSE.point.lng; // check if they're even in the same vertical plane if (brx < alx) return 1; if (arx < blx) return -1; - final aly = a.leftSE.point.y; - final bly = b.leftSE.point.y; - final ary = a.rightSE.point.y; - final bry = b.rightSE.point.y; + final aly = a.leftSE.point.lat; + final bly = b.leftSE.point.lat; + final ary = a.rightSE.point.lat; + final bry = b.rightSE.point.lat; // is left endpoint of segment B the right-more? if (alx < blx) { @@ -180,31 +179,31 @@ class Segment { */ //TODO: return bool? - comparePoint(Point point) { + comparePoint(Position point) { if (isAnEndpoint(point)) return 0; - final Point lPt = leftSE.point; - final Point rPt = rightSE.point; - final Vector2 v = vector; + final Position lPt = leftSE.point; + final Position rPt = rightSE.point; + final Position v = vector; // Exactly vertical segments. - if (lPt.x == rPt.x) { - if (point.x == lPt.x) return 0; - return point.x < lPt.x ? 1 : -1; + if (lPt.lng == rPt.lng) { + if (point.lng == lPt.lng) return 0; + return point.lng < lPt.lng ? 1 : -1; } // Nearly vertical segments with an intersection. // Check to see where a point on the line with matching Y coordinate is. - final yDist = (point.y - lPt.y) / v.y; - final xFromYDist = lPt.x + yDist * v.x; - if (point.x == xFromYDist) return 0; + final yDist = (point.lat - lPt.lat) / v.lat; + final xFromYDist = lPt.lng + yDist * v.lng; + if (point.lng == xFromYDist) return 0; // General case. // Check to see where a point on the line with matching X coordinate is. - final xDist = (point.x - lPt.x) / v.x; - final yFromXDist = lPt.y + xDist * v.y; - if (point.y == yFromXDist) return 0; - return point.y < yFromXDist ? -1 : 1; + final xDist = (point.lng - lPt.lng) / v.lng; + final yFromXDist = lPt.lat + xDist * v.lat; + if (point.lat == yFromXDist) return 0; + return point.lat < yFromXDist ? -1 : 1; } /* When a segment is split, the rightSE is replaced with a new sweep event */ @@ -216,11 +215,13 @@ class Segment { } /* Create Bounding Box for segment */ - BoundingBox get bbox { - final y1 = leftSE.point.y; - final y2 = rightSE.point.y; - return BoundingBox(Point(leftSE.point.x, y1 < y2 ? y1 : y2), - Point(rightSE.point.x, y1 > y2 ? y1 : y2)); + BBox get bbox { + final y1 = leftSE.point.lat; + final y2 = rightSE.point.lat; + return BBox.fromPositions( + Position(leftSE.point.lng, y1 < y2 ? y1 : y2), + Position(rightSE.point.lng, y1 > y2 ? y1 : y2), + ); } /* @@ -239,7 +240,7 @@ class Segment { * Else, return null. */ - Point? getIntersection(Segment other) { + Position? getIntersection(Segment other) { // If bboxes don't overlap, there can't be any intersections final tBbox = bbox; final oBbox = other.bbox; @@ -278,7 +279,7 @@ class Segment { if (touchesThisLSE) { // check for segments that just intersect on opposing endpoints if (touchesOtherRSE) { - if (tlp.x == orp.x && tlp.y == orp.y) return null; + if (tlp.lng == orp.lng && tlp.lat == orp.lat) return null; } // t-intersection on left endpoint return tlp; @@ -288,7 +289,7 @@ class Segment { if (touchesOtherLSE) { // check for segments that just intersect on opposing endpoints if (touchesThisRSE) { - if (trp.x == olp.x && trp.y == olp.y) return null; + if (trp.lng == olp.lng && trp.lat == olp.lat) return null; } // t-intersection on left endpoint return olp; @@ -303,7 +304,7 @@ class Segment { // None of our endpoints intersect. Look for a general intersection between // infinite lines laid over the segments - Point? pt = intersection(tlp, vector, olp, other.vector); + Position? pt = intersection(tlp, vector, olp, other.vector); // are the segments parrallel? Note that if they were colinear with overlap, // they would have an endpoint intersection and that case was already handled above @@ -313,7 +314,7 @@ class Segment { if (!isInBbox(bboxOverlap, pt)) return null; // round the the computed point if needed - return rounder.round(pt.x, pt.y); + return rounder.round(pt.lng, pt.lat); } /* @@ -329,7 +330,7 @@ class Segment { * Warning: input array of points is modified */ //TODO: point events - List split(PointEvents point) { + List split(PositionEvents point) { final List newEvents = []; final alreadyLinked = point.events != null; @@ -421,9 +422,9 @@ class Segment { consumee.rightSE.consumedBy = consumer.rightSE; } - static Segment fromRing(PointEvents pt1, PointEvents pt2, ring) { - PointEvents leftPt; - PointEvents rightPt; + static Segment fromRing(PositionEvents pt1, PositionEvents pt2, ring) { + PositionEvents leftPt; + PositionEvents rightPt; var winding; // ordering the two points according to sweep line ordering @@ -438,7 +439,7 @@ class Segment { winding = -1; } else { throw Exception( - "Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]"); + "Tried to create degenerate segment at [${pt1.lng}, ${pt1.lat}]"); } final leftSE = SweepEvent(leftPt, true); @@ -549,15 +550,15 @@ class Segment { return _isInResult!; } - isAnEndpoint(Point pt) { - return ((pt.x == leftSE.point.x && pt.y == leftSE.point.y) || - (pt.x == rightSE.point.x && pt.y == rightSE.point.y)); + isAnEndpoint(Position pt) { + return ((pt.lng == leftSE.point.lng && pt.lat == leftSE.point.lat) || + (pt.lng == rightSE.point.lng && pt.lat == rightSE.point.lat)); } /* A vector from the left point to the right */ - Vector2 get vector { - return Vector2((rightSE.point.x - leftSE.point.x).toDouble(), - (rightSE.point.y - leftSE.point.y).toDouble()); + Position get vector { + return Position((rightSE.point.lng - leftSE.point.lng).toDouble(), + (rightSE.point.lat - leftSE.point.lat).toDouble()); } } diff --git a/lib/src/polygon_clipping/sweep_event.dart b/lib/src/polygon_clipping/sweep_event.dart index 03b85f8..ae703f6 100644 --- a/lib/src/polygon_clipping/sweep_event.dart +++ b/lib/src/polygon_clipping/sweep_event.dart @@ -1,12 +1,11 @@ -import 'dart:math'; - +import 'package:turf/src/geojson.dart'; import 'package:turf/src/polygon_clipping/point_extension.dart'; import 'package:turf/src/polygon_clipping/vector_extension.dart'; import 'segment.dart'; // Assuming this is the Dart equivalent of your Segment class // Assuming this contains cosineOfAngle and sineOfAngle functions class SweepEvent { - PointEvents point; + PositionEvents point; bool isLeft; Segment? segment; // Assuming these are defined in your environment SweepEvent? otherSE; @@ -42,18 +41,18 @@ class SweepEvent { return Segment.compare(a.segment!, b.segment!); } - static int comparePoints(Point aPt, Point bPt) { - if (aPt.x < bPt.x) return -1; - if (aPt.x > bPt.x) return 1; + static int comparePoints(Position aPt, Position bPt) { + if (aPt.lng < bPt.lng) return -1; + if (aPt.lng > bPt.lng) return 1; - if (aPt.y < bPt.y) return -1; - if (aPt.y > bPt.y) return 1; + if (aPt.lat < bPt.lat) return -1; + if (aPt.lat > bPt.lat) return 1; return 0; } void link(SweepEvent other) { - //TODO: write test for Point comparison + //TODO: write test for Position comparison if (other.point == point) { throw 'Tried to link already linked events'; } @@ -105,8 +104,10 @@ class SweepEvent { var nextEvent = linkedEvent.otherSE; if (nextEvent != null) { cache[linkedEvent] = { - 'sine': sineOfAngle(point, baseEvent.point, nextEvent!.point), - 'cosine': cosineOfAngle(point, baseEvent.point, nextEvent.point), + 'sine': + sineOfAngle(point, baseEvent.point, nextEvent!.point).toDouble(), + 'cosine': + cosineOfAngle(point, baseEvent.point, nextEvent.point).toDouble(), }; } } @@ -138,10 +139,10 @@ class SweepEvent { } -// class Point { +// class Position { // double x; // double y; // List events; -// Point(this.x, this.y); +// Position(this.lng, this.lat); // } diff --git a/lib/src/polygon_clipping/sweep_line.dart b/lib/src/polygon_clipping/sweep_line.dart index 7a682ea..57bfbed 100644 --- a/lib/src/polygon_clipping/sweep_line.dart +++ b/lib/src/polygon_clipping/sweep_line.dart @@ -1,5 +1,5 @@ import 'dart:collection'; -import 'dart:math'; +import 'package:turf/src/geojson.dart'; import 'package:turf/src/polygon_clipping/point_extension.dart'; import 'segment.dart'; @@ -44,8 +44,8 @@ class SweepLine { if (node == null) { throw ArgumentError( 'Unable to find segment #${segment.id} ' - '[${segment.leftSE.point.x}, ${segment.leftSE.point.y}] -> ' - '[${segment.rightSE.point.x}, ${segment.rightSE.point.y}] ' + '[${segment.leftSE.point.lng}, ${segment.leftSE.point.lat}] -> ' + '[${segment.rightSE.point.lng}, ${segment.rightSE.point.lat}] ' 'in SweepLine tree.', ); } @@ -77,7 +77,7 @@ class SweepLine { if (event.isLeft) { // Check for intersections against the previous segment in the sweep line - Point? prevMySplitter; + Position? prevMySplitter; if (prevSeg != null) { var prevInter = prevSeg.getIntersection(segment); if (prevInter != null) { @@ -89,7 +89,7 @@ class SweepLine { } } // Check for intersections against the next segment in the sweep line - Point? nextMySplitter; + Position? nextMySplitter; if (nextSeg != null) { var nextInter = nextSeg.getIntersection(segment); if (nextInter != null) { @@ -104,7 +104,7 @@ class SweepLine { // For simplicity, even if we find more than one intersection we only // spilt on the 'earliest' (sweep-line style) of the intersections. // The other intersection will be handled in a future process(). - Point? mySplitter; + Position? mySplitter; if (prevMySplitter == null) { mySplitter = nextMySplitter; } else if (nextMySplitter == null) { @@ -119,7 +119,7 @@ class SweepLine { //TODO: check if mySplitter is null? do we need that check? if (prevMySplitter != null || nextMySplitter != null) { queue.remove(segment.rightSE); - newEvents.addAll(segment.split(PointEvents.fromPoint(mySplitter!))); + newEvents.addAll(segment.split(PositionEvents.fromPoint(mySplitter!))); } if (newEvents.isNotEmpty) { diff --git a/lib/src/polygon_clipping/utils.dart b/lib/src/polygon_clipping/utils.dart index d0d6e51..4a51028 100644 --- a/lib/src/polygon_clipping/utils.dart +++ b/lib/src/polygon_clipping/utils.dart @@ -1,30 +1,34 @@ -import 'dart:collection'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/geojson.dart'; -const double epsilon = - 2.220446049250313e-16; // Equivalent to Number.EPSILON in JavaScript +bool isInBbox(BBox bbox, Position point) { + return (bbox.position1.lng <= point.lng && + point.lng <= bbox.position2.lng && + bbox.position1.lat <= point.lat && + point.lat <= bbox.position2.lat); +} + +BBox? getBboxOverlap(BBox b1, BBox b2) { + // Check if the bboxes overlap at all + if (b2.position2.lng < b1.position1.lng || + b1.position2.lng < b2.position1.lng || + b2.position2.lat < b1.position1.lat || + b1.position2.lat < b2.position1.lat) { + return null; + } + + // Find the middle two lng values + final num lowerX = + b1.position1.lng < b2.position1.lng ? b2.position1.lng : b1.position1.lng; + final num upperX = + b1.position2.lng < b2.position2.lng ? b1.position2.lng : b2.position2.lng; + + // Find the middle two lat values + final num lowerY = + b1.position1.lat < b2.position1.lat ? b2.position1.lat : b1.position1.lat; + final num upperY = + b1.position2.lat < b2.position2.lat ? b1.position2.lat : b2.position2.lat; -/// 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); + // Create a new bounding box with the overlap + return BBox.fromPositions(Position(lowerX, lowerY), Position(upperX, upperY)); } diff --git a/lib/src/polygon_clipping/vector.dart b/lib/src/polygon_clipping/vector.dart deleted file mode 100644 index 5902dee..0000000 --- a/lib/src/polygon_clipping/vector.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:math'; - -import 'package:turf/src/polygon_clipping/utils.dart'; - -/* Cross Product of two vectors with first point at origin */ -num crossProduct(Point a, Point b) => a.x * b.y - a.y * b.x; - -/* Dot Product of two vectors with first point at origin */ -num dotProduct(Point a, Point b) => a.x * b.x + a.y * b.y; - -/* Comparator for two vectors with same starting point */ -num compareVectorAngles(Point basePt, Point endPt1, Point endPt2) { - double res = orient2d( - endPt1.x.toDouble(), - endPt1.y.toDouble(), - basePt.x.toDouble(), - basePt.y.toDouble(), - endPt2.x.toDouble(), - endPt2.y.toDouble(), - ); - return res > 0 - ? -1 - : res < 0 - ? 1 - : 0; -} diff --git a/lib/src/polygon_clipping/vector_extension.dart b/lib/src/polygon_clipping/vector_extension.dart index d4f6d6b..23efd5e 100644 --- a/lib/src/polygon_clipping/vector_extension.dart +++ b/lib/src/polygon_clipping/vector_extension.dart @@ -1,73 +1,90 @@ -import 'dart:math'; - -import 'package:vector_math/vector_math.dart'; - -extension Vector2Extension on Vector2 { - /* Given a vector, return one that is perpendicular */ - Vector2 get perpendicularVector { - return Vector2(-y, x); - } -} +import 'package:turf/helpers.dart'; /* Get the intersection of two lines, each defined by a base point and a vector. * In the case of parrallel lines (including overlapping ones) returns null. */ -Point? intersection(Point pt1, Vector2 v1, Point pt2, Vector2 v2) { +Position? intersection(Position pt1, Position v1, Position pt2, Position v2) { // take some shortcuts for vertical and horizontal lines // this also ensures we don't calculate an intersection and then discover // it's actually outside the bounding box of the line - if (v1.x == 0) return verticalIntersection(pt2, v2, pt1.x); - if (v2.x == 0) return verticalIntersection(pt1, v1, pt2.x); - if (v1.y == 0) return horizontalIntersection(pt2, v2, pt1.y); - if (v2.y == 0) return horizontalIntersection(pt1, v1, pt2.y); + if (v1.lng == 0) return verticalIntersection(pt2, v2, pt1.lng); + if (v2.lng == 0) return verticalIntersection(pt1, v1, pt2.lng); + if (v1.lat == 0) return horizontalIntersection(pt2, v2, pt1.lat); + if (v2.lat == 0) return horizontalIntersection(pt1, v1, pt2.lat); // General case for non-overlapping segments. // This algorithm is based on Schneider and Eberly. // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244 - final v1CrossV2 = v1.cross(v2); + final v1CrossV2 = crossProductMagnitude(v1, v2); if (v1CrossV2 == 0) return null; - final ve = Vector2((pt2.x - pt1.x).toDouble(), (pt2.y - pt1.y).toDouble()); - final d1 = ve.cross(v1) / v1CrossV2; - final d2 = ve.cross(v2) / v1CrossV2; + final ve = + Position((pt2.lng - pt1.lng).toDouble(), (pt2.lat - pt1.lat).toDouble()); + final d1 = crossProductMagnitude(ve, v1) / v1CrossV2; + final d2 = crossProductMagnitude(ve, v2) / v1CrossV2; // take the average of the two calculations to minimize rounding error - final x1 = pt1.x + d2 * v1.x, x2 = pt2.x + d1 * v2.x; - final y1 = pt1.y + d2 * v1.y, y2 = pt2.y + d1 * v2.y; - final x = (x1 + x2) / 2; - final y = (y1 + y2) / 2; - return Point(x, y); + final x1 = pt1.lng + d2 * v1.lng, x2 = pt2.lng + d1 * v2.lng; + final y1 = pt1.lat + d2 * v1.lat, y2 = pt2.lat + d1 * v2.lat; + final lng = (x1 + x2) / 2; + final lat = (y1 + y2) / 2; + return Position(lng, lat); } -/* Get the x coordinate where the given line (defined by a point and vector) - * crosses the horizontal line with the given y coordiante. +/* Get the lng coordinate where the given line (defined by a point and vector) + * crosses the horizontal line with the given lat coordiante. * In the case of parrallel lines (including overlapping ones) returns null. */ -Point? horizontalIntersection(Point pt, Vector2 v, num y) { - if (v.y == 0) return null; - return Point(pt.x + (v.x / v.y) * (y - pt.y), y); +Position? horizontalIntersection(Position pt, Position v, num lat) { + if (v.lat == 0) return null; + return Position(pt.lng + (v.lng / v.lat) * (lat - pt.lat), lat); } -/* Get the y coordinate where the given line (defined by a point and vector) - * crosses the vertical line with the given x coordiante. +/* Get the lat coordinate where the given line (defined by a point and vector) + * crosses the vertical line with the given lng coordiante. * In the case of parrallel lines (including overlapping ones) returns null. */ -Point? verticalIntersection(Point pt, Vector2 v, num x) { - if (v.x == 0) return null; - return Point(x, pt.y + (v.y / v.x) * (x - pt.x)); +Position? verticalIntersection(Position pt, Position v, num lng) { + if (v.lng == 0) return null; + return Position(lng, pt.lat + (v.lat / v.lng) * (lng - pt.lng)); } /* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */ -sineOfAngle(Point pShared, Point pBase, Point pAngle) { - final Vector2 vBase = Vector2( - (pBase.x - pShared.x).toDouble(), (pBase.y - pShared.y).toDouble()); - final Vector2 vAngle = Vector2( - (pAngle.x - pShared.x).toDouble(), (pAngle.y - pShared.y).toDouble()); - return vAngle.cross(vBase) / vAngle.length / vBase.length; +num sineOfAngle(Position pShared, Position pBase, Position pAngle) { + final Position vBase = Position((pBase.lng - pShared.lng).toDouble(), + (pBase.lat - pShared.lat).toDouble()); + final Position vAngle = Position((pAngle.lng - pShared.lng).toDouble(), + (pAngle.lat - pShared.lat).toDouble()); + return crossProductMagnitude(vAngle, vBase) / vAngle.length / vBase.length; } /* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */ -cosineOfAngle(Point pShared, Point pBase, Point pAngle) { - final Vector2 vBase = Vector2( - (pBase.x - pShared.x).toDouble(), (pBase.y - pShared.y).toDouble()); - final Vector2 vAngle = Vector2( - (pAngle.x - pShared.x).toDouble(), (pAngle.y - pShared.y).toDouble()); - return vAngle.dot(vBase) / vAngle.length / vBase.length; +num cosineOfAngle(Position pShared, Position pBase, Position pAngle) { + final Position vBase = Position((pBase.lng - pShared.lng).toDouble(), + (pBase.lat - pShared.lat).toDouble()); + final Position vAngle = Position((pAngle.lng - pShared.lng).toDouble(), + (pAngle.lat - pShared.lat).toDouble()); + return dotProductMagnitude(vAngle, vBase) / vAngle.length / vBase.length; +} + +/* Cross Product of two vectors with first point at origin */ +num crossProductMagnitude(Position a, Position b) => + a.lng * b.lat - a.lat * b.lng; + +/* Dot Product of two vectors with first point at origin */ +num dotProductMagnitude(Position a, Position b) => + a.lng * b.lng + a.lat * b.lat; + +/* Comparator for two vectors with same starting point */ +num compareVectorAngles(Position basePt, Position endPt1, Position endPt2) { + double res = orient2d( + endPt1.lng.toDouble(), + endPt1.lat.toDouble(), + basePt.lng.toDouble(), + basePt.lat.toDouble(), + endPt2.lng.toDouble(), + endPt2.lat.toDouble(), + ); + return res > 0 + ? -1 + : res < 0 + ? 1 + : 0; } From c5fe12320bd03654ade920720da0a54936953869 Mon Sep 17 00:00:00 2001 From: Aaron S Kennedy <36516690+aaronsamkennedy@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:52:38 -0700 Subject: [PATCH 3/6] bounding box method testing for is in bbox and for isOverlapping --- test/components/bbox_test.dart | 280 +++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) diff --git a/test/components/bbox_test.dart b/test/components/bbox_test.dart index e9efefc..86f0388 100644 --- a/test/components/bbox_test.dart +++ b/test/components/bbox_test.dart @@ -1,6 +1,7 @@ import 'package:test/test.dart'; import 'package:turf/bbox.dart'; import 'package:turf/helpers.dart'; +import 'package:turf/src/polygon_clipping/utils.dart'; void main() { final pt = Feature( @@ -99,4 +100,283 @@ void main() { expect(bbox(pt2, recompute: true), [0.5, 102, 0.5, 102], reason: "recomputes bbox with recompute option"); }); + + group('is in bbox', () { + test('outside', () { + final bbox = BBox.fromPositions(Position(1, 2), Position(5, 6)); + expect(isInBbox(bbox, Position(0, 3)), isFalse); + expect(isInBbox(bbox, Position(3, 30)), isFalse); + expect(isInBbox(bbox, Position(3, -30)), isFalse); + expect(isInBbox(bbox, Position(9, 3)), isFalse); + }); + + test('inside', () { + final bbox = BBox.fromPositions(Position(1, 2), Position(5, 6)); + expect(isInBbox(bbox, Position(1, 2)), isTrue); + expect(isInBbox(bbox, Position(5, 6)), isTrue); + expect(isInBbox(bbox, Position(1, 6)), isTrue); + expect(isInBbox(bbox, Position(5, 2)), isTrue); + expect(isInBbox(bbox, Position(3, 4)), isTrue); + }); + + test('barely inside & outside', () { + final bbox = BBox.fromPositions(Position(1, 0.8), Position(1.2, 6)); + expect(isInBbox(bbox, Position(1.2 - epsilon, 6)), isTrue); + expect(isInBbox(bbox, Position(1.2 + epsilon, 6)), isFalse); + expect(isInBbox(bbox, Position(1, 0.8 + epsilon)), isTrue); + expect(isInBbox(bbox, Position(1, 0.8 - epsilon)), isFalse); + }); + }); + + group('bbox overlap', () { + final b1 = BBox.fromPositions(Position(4, 4), Position(6, 6)); + + group('disjoint - none', () { + test('above', () { + final b2 = BBox.fromPositions(Position(7, 7), Position(8, 8)); + expect(getBboxOverlap(b1, b2), isNull); + }); + + test('left', () { + final b2 = BBox.fromPositions(Position(1, 5), Position(3, 8)); + expect(getBboxOverlap(b1, b2), isNull); + }); + + test('down', () { + final b2 = BBox.fromPositions(Position(2, 2), Position(3, 3)); + expect(getBboxOverlap(b1, b2), isNull); + }); + + test('right', () { + final b2 = BBox.fromPositions(Position(12, 1), Position(14, 9)); + expect(getBboxOverlap(b1, b2), isNull); + }); + }); + + group('touching - one point', () { + test('upper right corner of 1', () { + final b2 = BBox.fromPositions(Position(6, 6), Position(7, 8)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(6, 6), Position(6, 6)))); + }); + + test('upper left corner of 1', () { + final b2 = BBox.fromPositions(Position(3, 6), Position(4, 8)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(4, 6), Position(4, 6)))); + }); + + test('lower left corner of 1', () { + final b2 = BBox.fromPositions(Position(0, 0), Position(4, 4)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(4, 4), Position(4, 4)))); + }); + + test('lower right corner of 1', () { + final b2 = BBox.fromPositions(Position(6, 0), Position(12, 4)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(6, 4), Position(6, 4)))); + }); + }); + + group('overlapping - two points', () { + group('full overlap', () { + test('matching bboxes', () { + expect(getBboxOverlap(b1, b1), equals(b1)); + }); + + test('one side & two corners matching', () { + final b2 = BBox.fromPositions(Position(4, 4), Position(5, 6)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(4, 4), Position(5, 6)))); + }); + + test('one corner matching, part of two sides', () { + final b2 = BBox.fromPositions(Position(5, 4), Position(6, 5)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(5, 4), Position(6, 5)))); + }); + + test('part of a side matching, no corners', () { + final b2 = BBox.fromPositions(Position(4.5, 4.5), Position(5.5, 6)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(4.5, 4.5), Position(5.5, 6)))); + }); + + test('completely enclosed - no side or corner matching', () { + final b2 = BBox.fromPositions(Position(4.5, 5), Position(5.5, 5.5)); + expect(getBboxOverlap(b1, b2), equals(b2)); + }); + }); + + group('partial overlap', () { + test('full side overlap', () { + final b2 = BBox.fromPositions(Position(3, 4), Position(5, 6)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(4, 4), Position(5, 6)))); + }); + + test('partial side overlap', () { + final b2 = BBox.fromPositions(Position(5, 4.5), Position(7, 5.5)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(5, 4.5), Position(6, 5.5)))); + }); + + test('corner overlap', () { + final b2 = BBox.fromPositions(Position(5, 5), Position(7, 7)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(5, 5), Position(6, 6)))); + }); + }); + }); + + group('line bboxes', () { + group('vertical line & normal', () { + test('no overlap', () { + final b2 = BBox.fromPositions(Position(7, 3), Position(7, 6)); + expect(getBboxOverlap(b1, b2), isNull); + }); + + test('point overlap', () { + final b2 = BBox.fromPositions(Position(6, 0), Position(6, 4)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(6, 4), Position(6, 4)))); + }); + + test('line overlap', () { + final b2 = BBox.fromPositions(Position(5, 0), Position(5, 9)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(5, 4), Position(5, 6)))); + }); + }); + + group('horizontal line & normal', () { + test('no overlap', () { + final b2 = BBox.fromPositions(Position(3, 7), Position(6, 7)); + expect(getBboxOverlap(b1, b2), isNull); + }); + + test('point overlap', () { + final b2 = BBox.fromPositions(Position(1, 6), Position(4, 6)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(4, 6), Position(4, 6)))); + }); + + test('line overlap', () { + final b2 = BBox.fromPositions(Position(4, 6), Position(6, 6)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(4, 6), Position(6, 6)))); + }); + }); + + group('two vertical lines', () { + final v1 = BBox.fromPositions(Position(4, 4), Position(4, 6)); + + test('no overlap', () { + final v2 = BBox.fromPositions(Position(4, 7), Position(4, 8)); + expect(getBboxOverlap(v1, v2), isNull); + }); + + test('point overlap', () { + final v2 = BBox.fromPositions(Position(4, 3), Position(4, 4)); + expect(getBboxOverlap(v1, v2), + equals(BBox.fromPositions(Position(4, 4), Position(4, 4)))); + }); + + test('line overlap', () { + final v2 = BBox.fromPositions(Position(4, 3), Position(4, 5)); + expect(getBboxOverlap(v1, v2), + equals(BBox.fromPositions(Position(4, 4), Position(4, 5)))); + }); + }); + + group('two horizontal lines', () { + final h1 = BBox.fromPositions(Position(4, 6), Position(7, 6)); + + test('no overlap', () { + final h2 = BBox.fromPositions(Position(4, 5), Position(7, 5)); + expect(getBboxOverlap(h1, h2), isNull); + }); + + test('point overlap', () { + final h2 = BBox.fromPositions(Position(7, 6), Position(8, 6)); + expect(getBboxOverlap(h1, h2), + equals(BBox.fromPositions(Position(7, 6), Position(7, 6)))); + }); + + test('line overlap', () { + final h2 = BBox.fromPositions(Position(4, 6), Position(7, 6)); + expect(getBboxOverlap(h1, h2), + equals(BBox.fromPositions(Position(4, 6), Position(7, 6)))); + }); + }); + + group('horizonal and vertical lines', () { + test('no overlap', () { + final h1 = BBox.fromPositions(Position(4, 6), Position(8, 6)); + final v1 = BBox.fromPositions(Position(5, 7), Position(5, 9)); + expect(getBboxOverlap(h1, v1), isNull); + }); + + test('point overlap', () { + final h1 = BBox.fromPositions(Position(4, 6), Position(8, 6)); + final v1 = BBox.fromPositions(Position(5, 5), Position(5, 9)); + expect(getBboxOverlap(h1, v1), + equals(BBox.fromPositions(Position(5, 6), Position(5, 6)))); + }); + }); + + group('produced line box', () { + test('horizontal', () { + final b2 = BBox.fromPositions(Position(4, 6), Position(8, 8)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(4, 6), Position(6, 6)))); + }); + + test('vertical', () { + final b2 = BBox.fromPositions(Position(6, 2), Position(8, 8)); + expect(getBboxOverlap(b1, b2), + equals(BBox.fromPositions(Position(6, 4), Position(6, 6)))); + }); + }); + }); + + group('point bboxes', () { + group('point & normal', () { + test('no overlap', () { + final p = BBox.fromPositions(Position(2, 2), Position(2, 2)); + expect(getBboxOverlap(b1, p), isNull); + }); + test('point overlap', () { + final p = BBox.fromPositions(Position(5, 5), Position(5, 5)); + expect(getBboxOverlap(b1, p), equals(p)); + }); + }); + + group('point & line', () { + test('no overlap', () { + final p = BBox.fromPositions(Position(2, 2), Position(2, 2)); + final l = BBox.fromPositions(Position(4, 6), Position(4, 8)); + expect(getBboxOverlap(l, p), isNull); + }); + test('point overlap', () { + final p = BBox.fromPositions(Position(5, 5), Position(5, 5)); + final l = BBox.fromPositions(Position(4, 5), Position(6, 5)); + expect(getBboxOverlap(l, p), equals(p)); + }); + }); + + group('point & point', () { + test('no overlap', () { + final p1 = BBox.fromPositions(Position(2, 2), Position(2, 2)); + final p2 = BBox.fromPositions(Position(4, 6), Position(4, 6)); + expect(getBboxOverlap(p1, p2), isNull); + }); + test('point overlap', () { + final p = BBox.fromPositions(Position(5, 5), Position(5, 5)); + expect(getBboxOverlap(p, p), equals(p)); + }); + }); + }); + }); } From 15f8fc30c90f21bed35db0a22e36b3caedbf83aa Mon Sep 17 00:00:00 2001 From: Aaron S Kennedy <36516690+aaronsamkennedy@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:15:11 -0700 Subject: [PATCH 4/6] remove rounder --- lib/src/polygon_clipping/operation.dart | 5 -- lib/src/polygon_clipping/rounder.dart | 84 ------------------- lib/src/polygon_clipping/segment.dart | 5 +- .../polygon_clipping/vector_extension.dart | 18 +++- 4 files changed, 16 insertions(+), 96 deletions(-) delete mode 100644 lib/src/polygon_clipping/rounder.dart diff --git a/lib/src/polygon_clipping/operation.dart b/lib/src/polygon_clipping/operation.dart index 19c6b2c..e795fe7 100644 --- a/lib/src/polygon_clipping/operation.dart +++ b/lib/src/polygon_clipping/operation.dart @@ -3,7 +3,6 @@ import 'package:turf/src/polygon_clipping/utils.dart'; import 'geom_in.dart' as geomIn; import 'geom_out.dart' as geomOut; -import 'rounder.dart'; import 'sweep_event.dart'; import 'sweep_line.dart'; @@ -25,7 +24,6 @@ class Operation { List run(String type, dynamic geom, List moreGeoms) { this.type = type; - rounder.reset(); /* Convert inputs to MultiPoly objects */ final List multipolys = [ @@ -125,9 +123,6 @@ class Operation { queue.remove(node); } - // free some memory we don't need anymore - rounder.reset(); - /* Collect and compile segments we're keeping into a multipolygon */ final ringsOut = geomOut.RingOut.factory(sweepLine.segments); final result = geomOut.MultiPolyOut(ringsOut); diff --git a/lib/src/polygon_clipping/rounder.dart b/lib/src/polygon_clipping/rounder.dart deleted file mode 100644 index f55b5f3..0000000 --- a/lib/src/polygon_clipping/rounder.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'dart:collection'; - -import 'package:turf/helpers.dart'; - -/// A class for rounding floating-point coordinates to avoid floating-point problems. -class PtRounder { - late CoordRounder xRounder; - late CoordRounder yRounder; - - /// Constructor for PtRounder. - PtRounder() { - reset(); - } - - /// Resets the PtRounder by creating new instances of CoordRounder. - void reset() { - xRounder = CoordRounder(); - yRounder = CoordRounder(); - } - - /// Rounds the input x and y coordinates using CoordRounder instances. - Position round(num x, num y) { - return Position( - xRounder.round(x), - xRounder.round(y), - ); - } -} - -/// A class for rounding individual coordinates. -class CoordRounder { - late SplayTreeMap tree; - - /// Constructor for CoordRounder. - CoordRounder() { - tree = SplayTreeMap(); - // Preseed with 0 so we don't end up with values < epsilon. - round(0); - } - - /// Rounds the input coordinate and adds it to the tree. - /// - /// Returns the rounded value of the coordinate. - num round(num coord) { - final node = tree.putIfAbsent(coord, () => coord); - - final prevKey = nodeKeyBefore(coord); - final prevNode = prevKey != null ? tree[prevKey] : null; - - if (prevNode != null && node == prevNode) { - tree.remove(coord); - return prevNode; - } - - final nextKey = nodeKeyAfter(coord); - final nextNode = nextKey != null ? tree[nextKey] : null; - - if (nextNode != null && node == nextNode) { - tree.remove(coord); - return nextNode; - } - - return coord; - } - - /// Finds the key of the node before the given key in the tree. - /// - /// Returns the key of the previous node. - num? nodeKeyBefore(num key) { - final lowerKey = tree.keys.firstWhere((k) => k < key, orElse: () => key); - return lowerKey != key ? lowerKey : null; - } - - /// Finds the key of the node after the given key in the tree. - /// - /// Returns the key of the next node. - num? nodeKeyAfter(num key) { - final upperKey = tree.keys.firstWhere((k) => k > key, orElse: () => key); - return upperKey != key ? upperKey : null; - } -} - -/// Global instance of PtRounder available for use. -final rounder = PtRounder(); diff --git a/lib/src/polygon_clipping/segment.dart b/lib/src/polygon_clipping/segment.dart index 0ba2253..0e3c2a5 100644 --- a/lib/src/polygon_clipping/segment.dart +++ b/lib/src/polygon_clipping/segment.dart @@ -1,11 +1,10 @@ // Give segments unique ID's to get consistent sorting of // segments and sweep events when all else is identical -import 'package:turf/src/geojson.dart'; +import 'package:turf/helpers.dart'; import 'package:turf/src/polygon_clipping/geom_out.dart'; import 'package:turf/src/polygon_clipping/operation.dart'; import 'package:turf/src/polygon_clipping/point_extension.dart'; -import 'package:turf/src/polygon_clipping/rounder.dart'; import 'package:turf/src/polygon_clipping/sweep_event.dart'; import 'package:turf/src/polygon_clipping/utils.dart'; import 'package:turf/src/polygon_clipping/vector_extension.dart'; @@ -314,7 +313,7 @@ class Segment { if (!isInBbox(bboxOverlap, pt)) return null; // round the the computed point if needed - return rounder.round(pt.lng, pt.lat); + return Position(round(pt.lng), round(pt.lat)); } /* diff --git a/lib/src/polygon_clipping/vector_extension.dart b/lib/src/polygon_clipping/vector_extension.dart index 23efd5e..1d3e86a 100644 --- a/lib/src/polygon_clipping/vector_extension.dart +++ b/lib/src/polygon_clipping/vector_extension.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:turf/helpers.dart'; /* Get the intersection of two lines, each defined by a base point and a vector. @@ -52,7 +54,9 @@ num sineOfAngle(Position pShared, Position pBase, Position pAngle) { (pBase.lat - pShared.lat).toDouble()); final Position vAngle = Position((pAngle.lng - pShared.lng).toDouble(), (pAngle.lat - pShared.lat).toDouble()); - return crossProductMagnitude(vAngle, vBase) / vAngle.length / vBase.length; + return crossProductMagnitude(vAngle, vBase) / + vectorLength(vAngle) / + vectorLength(vBase); } /* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */ @@ -61,7 +65,9 @@ num cosineOfAngle(Position pShared, Position pBase, Position pAngle) { (pBase.lat - pShared.lat).toDouble()); final Position vAngle = Position((pAngle.lng - pShared.lng).toDouble(), (pAngle.lat - pShared.lat).toDouble()); - return dotProductMagnitude(vAngle, vBase) / vAngle.length / vBase.length; + return dotProductMagnitude(vAngle, vBase) / + vectorLength(vAngle) / + vectorLength(vBase); } /* Cross Product of two vectors with first point at origin */ @@ -82,9 +88,13 @@ num compareVectorAngles(Position basePt, Position endPt1, Position endPt2) { endPt2.lng.toDouble(), endPt2.lat.toDouble(), ); - return res > 0 + return res < 0 ? -1 - : res < 0 + : res > 0 ? 1 : 0; } + +num vectorLength(Position vector) { + return sqrt(vector.dotProduct(vector)); +} From 5d33eff85b78da6db002d82133cb7c61822ad688 Mon Sep 17 00:00:00 2001 From: Aaron S Kennedy <36516690+aaronsamkennedy@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:15:46 -0700 Subject: [PATCH 5/6] Add tests for flp, orient_2d and vector methods. --- test/polygon_clipping/flp_test.dart | 55 +++ test/polygon_clipping/orient_2d_test.dart | 23 ++ test/polygon_clipping/vector_test.dart | 391 ++++++++++++++++++++++ 3 files changed, 469 insertions(+) create mode 100644 test/polygon_clipping/flp_test.dart create mode 100644 test/polygon_clipping/orient_2d_test.dart create mode 100644 test/polygon_clipping/vector_test.dart diff --git a/test/polygon_clipping/flp_test.dart b/test/polygon_clipping/flp_test.dart new file mode 100644 index 0000000..66f489a --- /dev/null +++ b/test/polygon_clipping/flp_test.dart @@ -0,0 +1,55 @@ +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/polygon_clipping/flp.dart'; + +void main() { + group('compare', () { + test('exactly equal', () { + final double a = 1; + final double b = 1; + expect(cmp(a, b), equals(0)); + }); + + test('flp equal', () { + final double a = 1; + final double b = 1 + epsilon; + expect(cmp(a, b), equals(0)); + }); + + test('barely less than', () { + final double a = 1; + final double b = 1 + epsilon * 2; + expect(cmp(a, b), equals(-1)); + }); + + test('less than', () { + final double a = 1; + final double b = 2; + expect(cmp(a, b), equals(-1)); + }); + + test('barely more than', () { + final double a = 1 + epsilon * 2; + final double b = 1; + expect(cmp(a, b), equals(1)); + }); + + test('more than', () { + final double a = 2; + final double b = 1; + expect(cmp(a, b), equals(1)); + }); + + test('both flp equal to zero', () { + final double a = 0.0; + final double b = epsilon - epsilon * epsilon; + expect(cmp(a, b), equals(0)); + }); + + test('really close to zero', () { + final double a = epsilon; + final double b = epsilon + epsilon * epsilon * 2; + expect(cmp(a, b), equals(-1)); + }); + }); +} diff --git a/test/polygon_clipping/orient_2d_test.dart b/test/polygon_clipping/orient_2d_test.dart new file mode 100644 index 0000000..aa23f35 --- /dev/null +++ b/test/polygon_clipping/orient_2d_test.dart @@ -0,0 +1,23 @@ +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; + +void main() { + group('orient2d', () { + test('return 0.0 for collinear points', () { + // Test collinear points + expect(orient2d(0, 0, 1, 1, 2, 2), equals(0.0)); + }); + + test('return a positive value for clockwise points', () { + // Test clockwise points + expect(orient2d(0, 0, 1, 1, 2, 0), greaterThan(0.0)); + }); + + test('return a negative value for counterclockwise points', () { + // Test counterclockwise points + expect(orient2d(0, 0, 2, 0, 1, 1), lessThan(0.0)); + }); + + // Add more test cases here if needed + }); +} diff --git a/test/polygon_clipping/vector_test.dart b/test/polygon_clipping/vector_test.dart new file mode 100644 index 0000000..e6bda25 --- /dev/null +++ b/test/polygon_clipping/vector_test.dart @@ -0,0 +1,391 @@ +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/polygon_clipping/vector_extension.dart'; + +void main() { + group('cross product', () { + test('general', () { + final Position pt1 = Position(1, 2); + final Position pt2 = Position(3, 4); + expect(crossProductMagnitude(pt1, pt2), -2); + }); + }); + + group('dot product', () { + test('general', () { + final Position pt1 = Position(1, 2); + final Position pt2 = Position(3, 4); + expect(pt1.dotProduct(pt2), 11); + }); + }); + + group('length()', () { + test('horizontal', () { + final Position v = Position(3, 0); + expect(vectorLength(v), 3); + }); + + test('vertical', () { + final Position v = Position(0, -2); + expect(vectorLength(v), 2); + }); + + test('3-4-5', () { + final Position v = Position(3, 4); + expect(vectorLength(v), 5); + }); + }); + + group('compare vector angles', () { + test('colinear', () { + final Position pt1 = Position(1, 1); + final Position pt2 = Position(2, 2); + final Position pt3 = Position(3, 3); + + expect(compareVectorAngles(pt1, pt2, pt3), 0); + expect(compareVectorAngles(pt2, pt1, pt3), 0); + expect(compareVectorAngles(pt2, pt3, pt1), 0); + expect(compareVectorAngles(pt3, pt2, pt1), 0); + }); + + test('offset', () { + final Position pt1 = Position(0, 0); + final Position pt2 = Position(1, 1); + final Position pt3 = Position(1, 0); + + expect(compareVectorAngles(pt1, pt2, pt3), -1); + expect(compareVectorAngles(pt2, pt1, pt3), 1); + expect(compareVectorAngles(pt2, pt3, pt1), -1); + expect(compareVectorAngles(pt3, pt2, pt1), 1); + }); + }); + + group('sine and cosine of angle', () { + group('parallel', () { + final Position shared = Position(0, 0); + final Position base = Position(1, 0); + final Position angle = Position(1, 0); + test('sine', () { + expect(sineOfAngle(shared, base, angle), 0); + }); + test('cosine', () { + expect(cosineOfAngle(shared, base, angle), 1); + }); + }); + + group('45 degrees', () { + final Position shared = Position(0, 0); + final Position base = Position(1, 0); + final Position angle = Position(1, -1); + test('sine', () { + expect(sineOfAngle(shared, base, angle), closeTo(0.707, 0.001)); + }); + test('cosine', () { + expect(cosineOfAngle(shared, base, angle), closeTo(0.707, 0.001)); + }); + }); + + group('90 degrees', () { + final Position shared = Position(0, 0); + final Position base = Position(1, 0); + final Position angle = Position(0, -1); + test('sine', () { + expect(sineOfAngle(shared, base, angle), 1); + }); + test('cosine', () { + expect(cosineOfAngle(shared, base, angle), 0); + }); + }); + + group('135 degrees', () { + final Position shared = Position(0, 0); + final Position base = Position(1, 0); + final Position angle = Position(-1, -1); + test('sine', () { + expect(sineOfAngle(shared, base, angle), closeTo(0.707, 0.001)); + }); + test('cosine', () { + expect(cosineOfAngle(shared, base, angle), closeTo(-0.707, 0.001)); + }); + }); + + group('anti-parallel', () { + final Position shared = Position(0, 0); + final Position base = Position(1, 0); + final Position angle = Position(-1, 0); + test('sine', () { + expect(sineOfAngle(shared, base, angle), -0); + }); + test('cosine', () { + expect(cosineOfAngle(shared, base, angle), -1); + }); + }); + + group('225 degrees', () { + final Position shared = Position(0, 0); + final Position base = Position(1, 0); + final Position angle = Position(-1, 1); + test('sine', () { + expect(sineOfAngle(shared, base, angle), closeTo(-0.707, 0.001)); + }); + test('cosine', () { + expect(cosineOfAngle(shared, base, angle), closeTo(-0.707, 0.001)); + }); + }); + + group('270 degrees', () { + final Position shared = Position(0, 0); + final Position base = Position(1, 0); + final Position angle = Position(0, 1); + test('sine', () { + expect(sineOfAngle(shared, base, angle), -1); + }); + test('cosine', () { + expect(cosineOfAngle(shared, base, angle), 0); + }); + }); + + group('315 degrees', () { + final Position shared = Position(0, 0); + final Position base = Position(1, 0); + final Position angle = Position(1, 1); + test('sine', () { + expect(sineOfAngle(shared, base, angle), closeTo(-0.707, 0.001)); + }); + test('cosine', () { + expect(cosineOfAngle(shared, base, angle), closeTo(0.707, 0.001)); + }); + }); + }); + + // group('perpendicular()', () { + // test('vertical', () { + // final Position v = Position( 0, 1); + // final Position r = perpendicular(v); + // expect(dotProduct(v, r), 0); + // expect(crossProduct(v, r), isNot(0)); + // }); + + // test('horizontal', () { + // final Position v = Position( 1, 0); + // final Position r = perpendicular(v); + // expect(dotProduct(v, r), 0); + // expect(crossProduct(v, r), isNot(0)); + // }); + + // test('45 degrees', () { + // final Position v = Position( 1, 1); + // final Position r = perpendicular(v); + // expect(dotProduct(v, r), 0); + // expect(crossProduct(v, r), isNot(0)); + // }); + + // test('120 degrees', () { + // final Position v = Position( -1, 2); + // final Position r = perpendicular(v); + // expect(dotProduct(v, r), 0); + // expect(crossProduct(v, r), isNot(0)); + // }); + // }); + + // group('closestPoint()', () { + // test('on line', () { + // final Position pA1 = Position( 2, 2); + // final Position pA2 = Position( 3, 3); + // final Position pB = Position( -1, -1); + // final Position cp = closestPoint(pA1, pA2, pB); + // expect(cp, pB); + // }); + + // test('on first point', () { + // final Position pA1 = Position( 2, 2); + // final Position pA2 = Position( 3, 3); + // final Position pB = Position( 2, 2); + // final Position cp = closestPoint(pA1, pA2, pB); + // expect(cp, pB); + // }); + + // test('off line above', () { + // final Position pA1 = Position( 2, 2); + // final Position pA2 = Position( 3, 1); + // final Position pB = Position( 3, 7); + // final Position expected = Position( 0, 4); + // expect(closestPoint(pA1, pA2, pB), expected); + // expect(closestPoint(pA2, pA1, pB), expected); + // }); + + // test('off line below', () { + // final Position pA1 = Position( 2, 2); + // final Position pA2 = Position( 3, 1); + // final Position pB = Position( 0, 2); + // final Position expected = Position( 1, 3); + // expect(closestPoint(pA1, pA2, pB), expected); + // expect(closestPoint(pA2, pA1, pB), expected); + // }); + + // test('off line perpendicular to first point', () { + // final Position pA1 = Position( 2, 2); + // final Position pA2 = Position( 3, 3); + // final Position pB = Position( 1, 3); + // final Position cp = closestPoint(pA1, pA2, pB); + // final Position expected = Position( 2, 2); + // expect(cp, expected); + // }); + + // test('horizontal vector', () { + // final Position pA1 = Position( 2, 2); + // final Position pA2 = Position( 3, 2); + // final Position pB = Position( 1, 3); + // final Position cp = closestPoint(pA1, pA2, pB); + // final Position expected = Position( 1, 2); + // expect(cp, expected); + // }); + + // test('vertical vector', () { + // final Position pA1 = Position( 2, 2); + // final Position pA2 = Position( 2, 3); + // final Position pB = Position( 1, 3); + // final Position cp = closestPoint(pA1, pA2, pB); + // final Position expected = Position( 2, 3); + // expect(cp, expected); + // }); + + // test('on line but dot product does not think so - part of issue 60-2', () { + // final Position pA1 = Position( -45.3269382, -1.4059341); + // final Position pA2 = Position( -45.326737413921656, -1.40635); + // final Position pB = Position( -45.326833968900424, -1.40615); + // final Position cp = closestPoint(pA1, pA2, pB); + // expect(cp, pB); + // }); + // }); + + group('verticalIntersection()', () { + test('horizontal', () { + final Position p = Position(42, 3); + final Position v = Position(-2, 0); + final double x = 37; + final Position? i = verticalIntersection(p, v, x); + expect(i?.lng, 37); + expect(i?.lat, 3); + }); + + test('vertical', () { + final Position p = Position(42, 3); + final Position v = Position(0, 4); + final double x = 37; + expect(verticalIntersection(p, v, x), null); + }); + + test('45 degree', () { + final Position p = Position(1, 1); + final Position v = Position(1, 1); + final double x = -2; + final Position? i = verticalIntersection(p, v, x); + expect(i?.lng, -2); + expect(i?.lat, -2); + }); + + test('upper left quadrant', () { + final Position p = Position(-1, 1); + final Position v = Position(-2, 1); + final double x = -3; + final Position? i = verticalIntersection(p, v, x); + expect(i?.lng, -3); + expect(i?.lat, 2); + }); + }); + + group('horizontalIntersection()', () { + test('horizontal', () { + final Position p = Position(42, 3); + final Position v = Position(-2, 0); + final double y = 37; + expect(horizontalIntersection(p, v, y), null); + }); + + test('vertical', () { + final Position p = Position(42, 3); + final Position v = Position(0, 4); + final double y = 37; + final Position? i = horizontalIntersection(p, v, y); + expect(i?.lng, 42); + expect(i?.lat, 37); + }); + + test('45 degree', () { + final Position p = Position(1, 1); + final Position v = Position(1, 1); + final double y = 4; + final Position? i = horizontalIntersection(p, v, y); + expect(i?.lng, 4); + expect(i?.lat, 4); + }); + + test('bottom left quadrant', () { + final Position p = Position(-1, -1); + final Position v = Position(-2, -1); + final double y = -3; + final Position? i = horizontalIntersection(p, v, y); + expect(i?.lng, -5); + expect(i?.lat, -3); + }); + }); + + group('intersection()', () { + final Position p1 = Position(42, 42); + final Position p2 = Position(-32, 46); + + test('parrallel', () { + final Position v1 = Position(1, 2); + final Position v2 = Position(-1, -2); + final Position? i = intersection(p1, v1, p2, v2); + expect(i, null); + }); + + test('horizontal and vertical', () { + final Position v1 = Position(0, 2); + final Position v2 = Position(-1, 0); + final Position? i = intersection(p1, v1, p2, v2); + expect(i?.lng, 42); + expect(i?.lat, 46); + }); + + test('horizontal', () { + final Position v1 = Position(1, 1); + final Position v2 = Position(-1, 0); + final Position? i = intersection(p1, v1, p2, v2); + expect(i?.lng, 46); + expect(i?.lat, 46); + }); + + test('vertical', () { + final Position v1 = Position(1, 1); + final Position v2 = Position(0, 1); + final Position? i = intersection(p1, v1, p2, v2); + expect(i?.lng, -32); + expect(i?.lat, -32); + }); + + test('45 degree & 135 degree', () { + final Position v1 = Position(1, 1); + final Position v2 = Position(-1, 1); + final Position? i = intersection(p1, v1, p2, v2); + expect(i?.lng, 7); + expect(i?.lat, 7); + }); + + test('consistency', () { + // Taken from https://github.com/mfogel/polygon-clipping/issues/37 + final Position p1 = Position(0.523787, 51.281453); + final Position v1 = + Position(0.0002729999999999677, 0.0002729999999999677); + final Position p2 = Position(0.523985, 51.281651); + final Position v2 = + Position(0.000024999999999941735, 0.000049000000004184585); + final Position? i1 = intersection(p1, v1, p2, v2); + final Position? i2 = intersection(p2, v2, p1, v1); + expect(i1!.lng, i2!.lng); + expect(i1.lat, i2.lat); + }); + }); +} From 36b6cf2b5f8a9f15eab989af92f51a938ecb7aa9 Mon Sep 17 00:00:00 2001 From: Aaron S Kennedy <36516690+aaronsamkennedy@users.noreply.github.com> Date: Fri, 15 Mar 2024 07:21:27 -0600 Subject: [PATCH 6/6] Begin class testing --- lib/src/geojson.dart | 9 +- lib/src/polygon_clipping/geom_in.dart | 47 +- lib/src/polygon_clipping/geom_out.dart | 51 ++- lib/src/polygon_clipping/index.dart | 12 +- lib/src/polygon_clipping/operation.dart | 36 +- lib/src/polygon_clipping/point_extension.dart | 14 +- lib/src/polygon_clipping/segment.dart | 201 +++++++-- lib/src/polygon_clipping/sweep_event.dart | 44 +- lib/src/polygon_clipping/sweep_line.dart | 10 +- test/polygon_clipping/geom_in_test.dart | 280 ++++++++++++ test/polygon_clipping/geom_out_test.dart | 68 +++ test/polygon_clipping/segment_test.dart | 408 ++++++++++++++++++ test/polygon_clipping/sweep_event_test.dart | 321 ++++++++++++++ test/polygon_clipping/sweep_line_test.dart | 134 ++++++ 14 files changed, 1535 insertions(+), 100 deletions(-) create mode 100644 test/polygon_clipping/geom_in_test.dart create mode 100644 test/polygon_clipping/geom_out_test.dart create mode 100644 test/polygon_clipping/segment_test.dart create mode 100644 test/polygon_clipping/sweep_event_test.dart create mode 100644 test/polygon_clipping/sweep_line_test.dart diff --git a/lib/src/geojson.dart b/lib/src/geojson.dart index 0fe3f8b..b70aaa2 100644 --- a/lib/src/geojson.dart +++ b/lib/src/geojson.dart @@ -334,10 +334,10 @@ class BBox extends CoordinateType { factory BBox.fromPositions(Position p1, Position p2) => BBox.named( lng1: p1.lng, lat1: p1.lat, - alt1: p1.alt ?? 0, + alt1: p1.alt, lng2: p2.lng, lat2: p2.lat, - alt2: p2.alt ?? 0, + alt2: p2.alt, ); bool get _is3D => length == 6; @@ -385,6 +385,7 @@ class BBox extends CoordinateType { //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; } @@ -392,10 +393,10 @@ class BBox extends CoordinateType { _items[1] = position.lat; } if (position.lng > lng2) { - _items[3] = position.lng; + _items[_is3D ? 3 : 2] = position.lng; } if (position.lat > lat2) { - _items[4] = position.lat; + _items[_is3D ? 4 : 3] = position.lat; } if (position.alt != null) { if (alt1 == null || position.alt! < alt1!) { diff --git a/lib/src/polygon_clipping/geom_in.dart b/lib/src/polygon_clipping/geom_in.dart index 12c3160..d506bfd 100644 --- a/lib/src/polygon_clipping/geom_in.dart +++ b/lib/src/polygon_clipping/geom_in.dart @@ -4,30 +4,40 @@ import 'package:turf/helpers.dart'; import 'package:turf/src/polygon_clipping/point_extension.dart'; import 'package:turf/src/polygon_clipping/sweep_event.dart'; -import 'rounder.dart'; import 'segment.dart'; //TODO: mark factory methods to remove late values; +/// Represents a ring in a polygon. class RingIn { + /// List of segments. List segments = []; + + /// Indicates whether the polygon is an exterior polygon. final bool isExterior; - final PolyIn poly; + + /// The parent polygon. + final PolyIn? poly; + + /// The bounding box of the polygon. late BBox bbox; - RingIn(List geomRing, this.poly, this.isExterior) { - final firstPoint = rounder.round(geomRing[0].lng, geomRing[0].lat); + RingIn(List 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), ); - var prevPoint = firstPoint; + Position prevPoint = firstPoint; for (var i = 1; i < geomRing.length; i++) { - var point = rounder.round(geomRing[i].lng, geomRing[i].lat); + 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), this)); + segments.add(Segment.fromRing( + PositionEvents.fromPoint(prevPoint), PositionEvents.fromPoint(point), + ring: this)); bbox.expandToFitPosition(point); prevPoint = point; @@ -35,7 +45,8 @@ class RingIn { // 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), this)); + PositionEvents.fromPoint(firstPoint), + ring: this)); } } @@ -54,11 +65,15 @@ class RingIn { class PolyIn { late RingIn exteriorRing; late List interiorRings; - final MultiPolyIn multiPoly; late BBox bbox; - - PolyIn(Polygon geomPoly, this.multiPoly) { - exteriorRing = RingIn(geomPoly.coordinates[0], this, true); + final MultiPolyIn? multiPoly; + + PolyIn( + Polygon geomPoly, + this.multiPoly, + ) { + exteriorRing = + RingIn(geomPoly.coordinates[0], poly: this, isExterior: true); // copy by value bbox = exteriorRing.bbox; @@ -66,7 +81,8 @@ class PolyIn { Position lowerLeft = bbox.position1; Position upperRight = bbox.position2; for (var i = 1; i < geomPoly.coordinates.length; i++) { - final ring = RingIn(geomPoly.coordinates[i], this, false); + 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), @@ -91,7 +107,7 @@ class PolyIn { //TODO: mark factory methods to remove late values; class MultiPolyIn { - late List polys; + List polys = []; final bool isSubject; late BBox bbox; @@ -111,6 +127,7 @@ class MultiPolyIn { 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); diff --git a/lib/src/polygon_clipping/geom_out.dart b/lib/src/polygon_clipping/geom_out.dart index c18076d..7328367 100644 --- a/lib/src/polygon_clipping/geom_out.dart +++ b/lib/src/polygon_clipping/geom_out.dart @@ -100,13 +100,14 @@ class RingOut { } //TODO: Convert type to List? - List>? getGeom() { + List? getGeom() { Position prevPt = events[0].point; List points = [prevPt]; for (int i = 1, iMax = events.length - 1; i < iMax; i++) { Position pt = events[i].point; Position nextPt = events[i + 1].point; + //Check co-linear if (compareVectorAngles(pt, prevPt, nextPt) == 0) continue; points.add(pt); prevPt = pt; @@ -122,10 +123,10 @@ class RingOut { int step = isExteriorRing ? 1 : -1; int iStart = isExteriorRing ? 0 : points.length - 1; int iEnd = isExteriorRing ? points.length : -1; - List> orderedPoints = []; + List orderedPoints = []; for (int i = iStart; i != iEnd; i += step) { - orderedPoints.add([points[i].lng.toDouble(), points[i].lat.toDouble()]); + orderedPoints.add(Position(points[i].lng, points[i].lat)); } return orderedPoints; @@ -133,28 +134,34 @@ class RingOut { RingOut? _enclosingRing; RingOut enclosingRing() { - if (_enclosingRing == null) { - _enclosingRing = _calcEnclosingRing(); - } + _enclosingRing ??= _calcEnclosingRing(); return _enclosingRing!; } + /* Returns the ring that encloses this one, if any */ RingOut? _calcEnclosingRing() { SweepEvent leftMostEvt = events[0]; + // start with the ealier sweep line event so that the prevSeg + // chain doesn't lead us inside of a loop of ours for (int i = 1, iMax = events.length; i < iMax; i++) { SweepEvent evt = events[i]; if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt; } Segment? prevSeg = leftMostEvt.segment!.prevInResult(); - Segment? prevPrevSeg = prevSeg != null ? prevSeg.prevInResult() : null; + Segment? prevPrevSeg = prevSeg?.prevInResult(); while (true) { + // no segment found, thus no ring can enclose us if (prevSeg == null) return null; - + // no segments below prev segment found, thus the ring of the prev + // segment must loop back around and enclose us if (prevPrevSeg == null) return prevSeg.ringOut; + // if the two segments are of different rings, the ring of the prev + // segment must either loop around us or the ring of the prev prev + // seg, which would make us and the ring of the prev peers if (prevPrevSeg.ringOut != prevSeg.ringOut) { if (prevPrevSeg.ringOut!.enclosingRing() != prevSeg.ringOut) { return prevSeg.ringOut; @@ -163,6 +170,8 @@ class RingOut { } } + // two segments are from the same ring, so this was a penisula + // of that ring. iterate downward, keep searching prevSeg = prevPrevSeg.prevInResult(); prevPrevSeg = prevSeg != null ? prevSeg.prevInResult() : null; } @@ -182,15 +191,14 @@ class PolyOut { ring.poly = this; } - List>>? getGeom() { - List>? exteriorGeom = exteriorRing.getGeom(); - List>>? geom = - exteriorGeom != null ? [exteriorGeom] : null; + List>? getGeom() { + List? exteriorGeom = exteriorRing.getGeom(); + List>? geom = exteriorGeom != null ? [exteriorGeom] : null; if (geom == null) return null; for (int i = 0, iMax = interiorRings.length; i < iMax; i++) { - List>? ringGeom = interiorRings[i].getGeom(); + List? ringGeom = interiorRings[i].getGeom(); if (ringGeom == null) continue; geom.add(ringGeom); } @@ -207,16 +215,25 @@ class MultiPolyOut { polys = _composePolys(rings); } - List>>> getGeom() { - List>>> geom = []; + GeometryObject getGeom() { + List>> geom = []; for (int i = 0, iMax = polys.length; i < iMax; i++) { - List>>? polyGeom = polys[i].getGeom(); + List>? polyGeom = polys[i].getGeom(); if (polyGeom == null) continue; geom.add(polyGeom); } - return geom; + if (geom.length > 1) { + //Return MultiPolgyons + return MultiPolygon(coordinates: geom); + } + if (geom.length == 1) { + //Return Polygon + return Polygon(coordinates: geom[0]); + } else { + throw new Exception("geomOut getGeometry empty"); + } } List _composePolys(List rings) { diff --git a/lib/src/polygon_clipping/index.dart b/lib/src/polygon_clipping/index.dart index 546a7c9..9aa8cf2 100644 --- a/lib/src/polygon_clipping/index.dart +++ b/lib/src/polygon_clipping/index.dart @@ -1,16 +1,20 @@ +import 'package:turf/src/geojson.dart'; + import 'operation.dart'; //?Should these just be methods of operations? or factory constructors or something else? -dynamic union(dynamic geom, List moreGeoms) => +GeometryObject? union(GeometryObject geom, List moreGeoms) => operation.run("union", geom, moreGeoms); -dynamic intersection(dynamic geom, List moreGeoms) => +GeometryObject? intersection( + GeometryObject geom, List moreGeoms) => operation.run("intersection", geom, moreGeoms); -dynamic xor(dynamic geom, List moreGeoms) => +GeometryObject? xor(GeometryObject geom, List moreGeoms) => operation.run("xor", geom, moreGeoms); -dynamic difference(dynamic subjectGeom, List clippingGeoms) => +GeometryObject? difference( + GeometryObject subjectGeom, List clippingGeoms) => operation.run("difference", subjectGeom, clippingGeoms); Map operations = { diff --git a/lib/src/polygon_clipping/operation.dart b/lib/src/polygon_clipping/operation.dart index e795fe7..24ea020 100644 --- a/lib/src/polygon_clipping/operation.dart +++ b/lib/src/polygon_clipping/operation.dart @@ -1,8 +1,9 @@ import 'dart:collection'; +import 'package:turf/helpers.dart'; import 'package:turf/src/polygon_clipping/utils.dart'; -import 'geom_in.dart' as geomIn; -import 'geom_out.dart' as geomOut; +import 'geom_in.dart'; +import 'geom_out.dart'; import 'sweep_event.dart'; import 'sweep_line.dart'; @@ -22,15 +23,30 @@ class Operation { late String type; int numMultiPolys = 0; - List run(String type, dynamic geom, List moreGeoms) { + GeometryObject? run( + String type, GeometryObject geom, List moreGeoms) { this.type = type; + if (geom is! Polygon || geom is! MultiPolygon) { + throw Exception( + "Input GeometryTry doesn't match Polygon or MultiPolygon"); + } + + if (geom is! Polygon) { + geom = MultiPolygon(coordinates: [geom.coordinates]); + } + /* Convert inputs to MultiPoly objects */ - final List multipolys = [ - geomIn.MultiPolyIn(geom, true) + //TODO: handle multipolygons + final List multipolys = [ + MultiPolyIn(geom as MultiPolygon, true) ]; for (var i = 0; i < moreGeoms.length; i++) { - multipolys.add(geomIn.MultiPolyIn(moreGeoms[i], false)); + if (moreGeoms[i] is! Polygon && moreGeoms[i] is! MultiPolygon) { + throw Exception( + "Input GeometryTry doesn't match Polygon or MultiPolygon"); + } + multipolys.add(MultiPolyIn(moreGeoms[i] as MultiPolygon, false)); } numMultiPolys = multipolys.length; @@ -61,7 +77,9 @@ class Operation { final mpA = multipolys[i]; for (var j = i + 1; j < multipolys.length; j++) { if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) == null) { - return []; + // todo ensure not a list if needed + // return []; + return null; } } } @@ -124,8 +142,8 @@ class Operation { } /* Collect and compile segments we're keeping into a multipolygon */ - final ringsOut = geomOut.RingOut.factory(sweepLine.segments); - final result = geomOut.MultiPolyOut(ringsOut); + final ringsOut = RingOut.factory(sweepLine.segments); + final result = MultiPolyOut(ringsOut); return result.getGeom(); } } diff --git a/lib/src/polygon_clipping/point_extension.dart b/lib/src/polygon_clipping/point_extension.dart index ca43d26..e683295 100644 --- a/lib/src/polygon_clipping/point_extension.dart +++ b/lib/src/polygon_clipping/point_extension.dart @@ -6,11 +6,19 @@ class PositionEvents extends Position { PositionEvents( double super.lng, - double super.lat, + double super.lat, { this.events, - ); + }); factory PositionEvents.fromPoint(Position point) { - return PositionEvents(point.lng.toDouble(), point.lat.toDouble(), []); + return PositionEvents( + point.lng.toDouble(), + point.lat.toDouble(), + ); + } + + @override + String toString() { + return 'PositionEvents(lng: $lng, lat: $lat, events: $events)'; } } diff --git a/lib/src/polygon_clipping/segment.dart b/lib/src/polygon_clipping/segment.dart index 0e3c2a5..5c555d9 100644 --- a/lib/src/polygon_clipping/segment.dart +++ b/lib/src/polygon_clipping/segment.dart @@ -2,6 +2,7 @@ // segments and sweep events when all else is identical import 'package:turf/helpers.dart'; +import 'package:turf/src/polygon_clipping/geom_in.dart'; import 'package:turf/src/polygon_clipping/geom_out.dart'; import 'package:turf/src/polygon_clipping/operation.dart'; import 'package:turf/src/polygon_clipping/point_extension.dart'; @@ -15,9 +16,12 @@ class Segment { SweepEvent leftSE; SweepEvent rightSE; //TODO: can we make these empty lists instead of being nullable? - List? rings; + List? rings; // TODO: add concrete typing for winding, should this be a nullable boolean? true, clockwise, false counter clockwhise, null unknown - List? windings; + //Directional windings + List? windings; + //Testing parameter: should only be used in testing + bool? forceIsInResult; ///These set later in algorithm Segment? consumedBy; @@ -26,9 +30,15 @@ class Segment { /* Warning: a reference to ringWindings input will be stored, * and possibly will be later modified */ - Segment(this.leftSE, this.rightSE, this.rings, this.windings) - //Auto increment id - : id = _nextId++ { + Segment( + this.leftSE, + this.rightSE, { + this.rings, + this.windings, + this.forceIsInResult, + }) + //Auto increment id + : id = _nextId++ { //Set intertwined relationships between segment and sweep events leftSE.segment = this; leftSE.otherSE = rightSE; @@ -37,6 +47,33 @@ class Segment { rightSE.otherSE = leftSE; // left unset for performance, set later in algorithm // this.ringOut, this.consumedBy, this.prev + if (forceIsInResult != null) { + _isInResult = forceIsInResult; + } + } + + @override + String toString() { + return 'Segment(id: $id, leftSE: $leftSE, rightSE: $rightSE, rings: $rings, windings: $windings, forceIsInResult: $forceIsInResult)'; + } + + @override + bool operator ==(Object other) { + if (other is Segment) { + if (leftSE == other.leftSE && + rightSE == other.rightSE && + rings == other.rings && + windings == other.windings && + consumedBy == other.consumedBy && + prev == other.prev && + ringOut == other.ringOut) { + return true; + } else { + return false; + } + } else { + return false; + } } /* This compare() function is for ordering segments in the sweep @@ -178,7 +215,7 @@ class Segment { */ //TODO: return bool? - comparePoint(Position point) { + int comparePoint(Position point) { if (isAnEndpoint(point)) return 0; final Position lPt = leftSE.point; @@ -343,8 +380,8 @@ class Segment { newLeftSE, oldRightSE, //TODO: Can rings and windings be null here? - rings != null ? List.from(rings!) : null, - windings != null ? List.from(windings!) : null, + rings: rings != null ? List.from(rings!) : null, + windings: windings != null ? List.from(windings!) : null, ); // when splitting a nearly vertical downward-facing segment, @@ -370,7 +407,18 @@ class Segment { } /* Swap which event is left and right */ - swapEvents() {} + void swapEvents() { + final tmpEvt = rightSE; + rightSE = leftSE; + leftSE = tmpEvt; + leftSE.isLeft = true; + rightSE.isLeft = false; + if (windings != null) { + for (var i = 0; i < windings!.length; i++) { + windings![i] *= -1; + } + } + } /* Consume another segment. We take their rings under our wing * and mark them as consumed. Use for perfectly overlapping segments */ @@ -400,16 +448,17 @@ class Segment { consumer = consumee; consumee = tmp; } - - for (var i = 0, iMax = consumee.rings!.length; i < iMax; i++) { - final ring = consumee.rings![i]; - final winding = consumee.windings![i]; - final index = consumer.rings!.indexOf(ring); - if (index == -1) { - consumer.rings!.add(ring); - consumer.windings!.add(winding); - } else { - consumer.windings![index] += winding; + if (consumee.rings != null) { + for (var i = 0, iMax = consumee.rings!.length; i < iMax; i++) { + final ring = consumee.rings![i]; + final winding = consumee.windings![i]; + final index = consumer.rings!.indexOf(ring); + if (index == -1) { + consumer.rings!.add(ring); + consumer.windings!.add(winding); + } else { + consumer.windings![index] += winding; + } } } consumee.rings = null; @@ -421,7 +470,8 @@ class Segment { consumee.rightSE.consumedBy = consumer.rightSE; } - static Segment fromRing(PositionEvents pt1, PositionEvents pt2, ring) { + factory Segment.fromRing(PositionEvents pt1, PositionEvents pt2, + {RingIn? ring, bool? forceIsInResult}) { PositionEvents leftPt; PositionEvents rightPt; var winding; @@ -443,10 +493,16 @@ class Segment { final leftSE = SweepEvent(leftPt, true); final rightSE = SweepEvent(rightPt, false); - return Segment(leftSE, rightSE, [ring], [winding]); + return Segment( + leftSE, + rightSE, + rings: ring != null ? [ring] : null, + windings: [winding], + forceIsInResult: forceIsInResult, + ); } - var _prevInResult; + Segment? _prevInResult; /* The first segment previous segment chain that is in the result */ Segment? prevInResult() { @@ -461,12 +517,12 @@ class Segment { return _prevInResult; } - _SegmentState? _beforeState; + SegmentState? _beforeState; - beforeState() { + SegmentState? beforeState() { if (_beforeState != null) return _beforeState; if (prev == null) { - _beforeState = _SegmentState( + _beforeState = SegmentState( rings: [], windings: [], multiPolys: [], @@ -478,19 +534,82 @@ class Segment { return _beforeState; } - afterState() {} + SegmentState? _afterState; + + SegmentState? afterState() { + if (_afterState != null) return _afterState; + + final beforeState = this.beforeState(); + if (beforeState != null) { + throw Exception("Segment afterState() called with no before state"); + } + _afterState = SegmentState( + rings: List.from(beforeState!.rings), + windings: List.from(beforeState.windings), + multiPolys: [], + ); + + final ringsAfter = _afterState!.rings; + final windingsAfter = _afterState!.windings; + final mpsAfter = _afterState!.multiPolys; + + // calculate ringsAfter, windingsAfter + for (var i = 0; i < rings!.length; i++) { + final ring = rings![i]; + final winding = windings![i]; + final index = ringsAfter.indexOf(ring); + if (index == -1) { + ringsAfter.add(ring); + windingsAfter.add(winding); + } else { + windingsAfter[index] += winding; + } + } + + // calculate polysAfter + final polysAfter = []; + final polysExclude = []; + for (var i = 0; i < ringsAfter.length; i++) { + if (windingsAfter[i] == 0) continue; // non-zero rule + final ring = ringsAfter[i]; + final poly = ring.poly; + if (polysExclude.indexOf(poly) != -1) continue; + if (ring.isExterior) { + polysAfter.add(poly); + } else { + if (polysExclude.indexOf(poly) == -1) { + polysExclude.add(poly); + } + final index = polysAfter.indexOf(ring.poly); + if (index != -1) { + polysAfter.removeAt(index); + } + } + } + + // calculate multiPolysAfter + for (var i = 0; i < polysAfter.length; i++) { + final mp = polysAfter[i].multiPoly; + if (mpsAfter.indexOf(mp) == -1) { + mpsAfter.add(mp); + } + } + + return _afterState; + } bool? _isInResult; /* Is this segment part of the final result? */ bool isInResult() { + if (forceIsInResult != null) return forceIsInResult!; // if we've been consumed, we're not in the result if (consumedBy != null) return false; if (_isInResult != null) return _isInResult!; - final mpsBefore = beforeState().multiPolys; - final mpsAfter = afterState().multiPolys; + final mpsBefore = beforeState()?.multiPolys; + final mpsAfter = afterState()?.multiPolys; switch (operation.type) { case "union": @@ -498,8 +617,8 @@ class Segment { // UNION - included iff: // * On one side of us there is 0 poly interiors AND // * On the other side there is 1 or more. - final noBefores = mpsBefore.length == 0; - final noAfters = mpsAfter.length == 0; + final bool noBefores = mpsBefore!.isEmpty; + final bool noAfters = mpsAfter!.isEmpty; _isInResult = noBefores != noAfters; break; } @@ -512,7 +631,7 @@ class Segment { // with poly interiors int least; int most; - if (mpsBefore.length < mpsAfter.length) { + if (mpsBefore!.length < mpsAfter!.length) { least = mpsBefore.length; most = mpsAfter.length; } else { @@ -528,7 +647,7 @@ class Segment { // XOR - included iff: // * the difference between the number of multipolys represented // with poly interiors on our two sides is an odd number - final diff = (mpsBefore.length - mpsAfter.length).abs(); + final diff = (mpsBefore!.length - mpsAfter!.length).abs(); _isInResult = diff % 2 == 1; break; } @@ -537,8 +656,9 @@ class Segment { { // DIFFERENCE included iff: // * on exactly one side, we have just the subject - bool isJustSubject(List mps) => mps.length == 1 && mps[0].isSubject; - _isInResult = isJustSubject(mpsBefore) != isJustSubject(mpsAfter); + bool isJustSubject(List mps) => + mps.length == 1 && mps[0].isSubject; + _isInResult = isJustSubject(mpsBefore!) != isJustSubject(mpsAfter!); break; } @@ -559,13 +679,16 @@ class Segment { return Position((rightSE.point.lng - leftSE.point.lng).toDouble(), (rightSE.point.lat - leftSE.point.lat).toDouble()); } + + @override + int get hashCode => id.hashCode; } -class _SegmentState { - List rings; - List windings; - List multiPolys; - _SegmentState({ +class SegmentState { + List rings; + List windings; + List multiPolys; + SegmentState({ required this.rings, required this.windings, required this.multiPolys, diff --git a/lib/src/polygon_clipping/sweep_event.dart b/lib/src/polygon_clipping/sweep_event.dart index ae703f6..8f6f291 100644 --- a/lib/src/polygon_clipping/sweep_event.dart +++ b/lib/src/polygon_clipping/sweep_event.dart @@ -4,7 +4,14 @@ import 'package:turf/src/polygon_clipping/vector_extension.dart'; import 'segment.dart'; // Assuming this is the Dart equivalent of your Segment class // Assuming this contains cosineOfAngle and sineOfAngle functions +/// Represents a sweep event in the polygon clipping algorithm. +/// +/// A sweep event is a point where the sweep line intersects an edge of a polygon. +/// It is used in the polygon clipping algorithm to track the state of the sweep line +/// as it moves across the polygon edges. class SweepEvent { + static int _nextId = 1; + int id; PositionEvents point; bool isLeft; Segment? segment; // Assuming these are defined in your environment @@ -13,17 +20,36 @@ class SweepEvent { // Warning: 'point' input will be modified and re-used (for performance - SweepEvent(this.point, this.isLeft) { + SweepEvent(this.point, this.isLeft) : id = _nextId++ { + print(point); if (point.events == null) { point.events = [this]; } else { point.events!.add(this); } point = point; - isLeft = isLeft; // this.segment, this.otherSE set by factory } + @override + bool operator ==(Object other) { + if (other is SweepEvent) { + print("id matching: $id ${other.id}"); + if (isLeft == other.isLeft && + //Becuase segments self reference within the sweet event in their own paramenters it creates a loop that cannot be equivelant. + segment?.id == other.segment?.id && + otherSE?.id == other.otherSE?.id && + consumedBy == other.consumedBy && + point == other.point) { + return true; + } else { + return false; + } + } else { + return false; + } + } + // for ordering sweep events in the sweep event queue static int compare(SweepEvent a, SweepEvent b) { // favor event with a point that the sweep line hits first @@ -54,10 +80,10 @@ class SweepEvent { void link(SweepEvent other) { //TODO: write test for Position comparison if (other.point == point) { - throw 'Tried to link already linked events'; + throw Exception('Tried to link already linked events'); } if (other.point.events == null) { - throw 'PointEventsError: events called on null point.events'; + throw Exception('PointEventsError: events called on null point.events'); } for (var evt in other.point.events!) { point.events!.add(evt); @@ -68,7 +94,8 @@ class SweepEvent { void checkForConsuming() { if (point.events == null) { - throw 'PointEventsError: events called on null point.events, method requires events'; + throw Exception( + 'PointEventsError: events called on null point.events, method requires events'); } var numEvents = point.events!.length; for (int i = 0; i < numEvents; i++) { @@ -87,7 +114,9 @@ class SweepEvent { List getAvailableLinkedEvents() { List events = []; for (var evt in point.events!) { + print(point.events!); //TODO: !evt.segment!.ringOut was written first but th + if (evt != this && evt.segment!.ringOut == null && evt.segment!.isInResult()) { @@ -136,6 +165,11 @@ class SweepEvent { return 0; }; } + + @override + String toString() { + return 'SweepEvent(id:$id, point=$point, segment=${segment?.id})'; + } } diff --git a/lib/src/polygon_clipping/sweep_line.dart b/lib/src/polygon_clipping/sweep_line.dart index 57bfbed..9294c2c 100644 --- a/lib/src/polygon_clipping/sweep_line.dart +++ b/lib/src/polygon_clipping/sweep_line.dart @@ -5,8 +5,10 @@ import 'package:turf/src/polygon_clipping/point_extension.dart'; import 'segment.dart'; import 'sweep_event.dart'; +/// Represents a sweep line used in polygon clipping algorithms. +/// The sweep line is used to efficiently process intersecting edges of polygons. class SweepLine { - late SplayTreeMap tree; + late SplayTreeMap tree; final List segments = []; final List queue; @@ -32,7 +34,7 @@ class SweepLine { Segment? node; if (event.isLeft) { - tree[segment]; + tree[segment] = null; node = null; //? Can you use SplayTreeSet lookup here? looks for internal of segment. } else if (tree.containsKey(segment)) { @@ -124,7 +126,7 @@ class SweepLine { if (newEvents.isNotEmpty) { tree.remove(segment); - tree[segment]; + tree[segment] = null; newEvents.add(event); } else { segments.add(segment); @@ -158,7 +160,7 @@ class SweepLine { var newEvents = seg.split(pt); newEvents.add(rightSE); if (seg.consumedBy == null) { - tree[seg]; + tree[seg] = null; } return newEvents; } diff --git a/test/polygon_clipping/geom_in_test.dart b/test/polygon_clipping/geom_in_test.dart new file mode 100644 index 0000000..f0746af --- /dev/null +++ b/test/polygon_clipping/geom_in_test.dart @@ -0,0 +1,280 @@ +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/polygon_clipping/geom_in.dart'; + +void main() { + group('RingIn', () { + test('create exterior ring', () { + final List ringGeomIn = [ + Position(0, 0), + Position(1, 0), + Position(1, 1), + ]; + final Position expectedPt1 = Position(0, 0); + final Position expectedPt2 = Position(1, 0); + final Position expectedPt3 = Position(1, 1); + final PolyIn poly = PolyIn( + Polygon(coordinates: [ringGeomIn]), + null, + ); + final ring = RingIn( + ringGeomIn, + poly: poly, + isExterior: true, + ); + poly.exteriorRing = ring; + + expect(ring.poly, equals(poly), reason: "ring.poly self reference"); + expect(ring.isExterior, isTrue, reason: "ring.isExterior"); + expect(ring.segments.length, equals(3), reason: "ring.segments.length"); + expect(ring.getSweepEvents().length, equals(6), + reason: "ring.getSweepEvents().length"); + + expect(ring.segments[0].leftSE.point, equals(expectedPt1), + reason: "ring.segments[0].leftSE.point"); + expect(ring.segments[0].rightSE.point, equals(expectedPt2), + reason: "ring.segments[0].rightSE.point"); + expect(ring.segments[1].leftSE.point, equals(expectedPt2), + reason: "ring.segments[1].leftSE.point"); + expect(ring.segments[1].rightSE.point, equals(expectedPt3), + reason: "ring.segments[1].rightSE.point"); + expect(ring.segments[2].leftSE.point, equals(expectedPt1), + reason: "ring.segments[2].leftSE.point"); + expect(ring.segments[2].rightSE.point, equals(expectedPt3), + reason: "ring.segments[2].rightSE.point"); + }); + + test('create an interior ring', () { + final ring = RingIn( + [ + Position(0, 0), + Position(1, 1), + Position(1, 0), + ], + isExterior: false, + ); + expect(ring.isExterior, isFalse); + }); + + test('bounding box construction', () { + final ring = RingIn([ + Position(0, 0), + Position(1, 1), + Position(0, 1), + Position(0, 0), + ], isExterior: true); + + expect(ring.bbox.position1, equals(Position(0, 0))); + expect(ring.bbox.position2, equals(Position(1, 1))); + }); + }); + + group('PolyIn', () { + test('creation', () { + final MultiPolyIn multiPoly = MultiPolyIn( + MultiPolygon(coordinates: [ + [ + [ + Position(0, 0), + Position(10, 0), + Position(10, 10), + Position(0, 10), + ], + [ + Position(0, 0), + Position(1, 1), + Position(1, 0), + ], + [ + Position(2, 2), + Position(2, 3), + Position(3, 3), + Position(3, 2), + ] + ], + [ + [ + Position(0, 0), + Position(1, 1), + Position(0, 1), + Position(0, 0), + ], + [ + Position(0, 0), + Position(4, 0), + Position(4, 9), + ], + [ + Position(2, 2), + Position(3, 3), + Position(3, 2), + ] + ] + ]), + false, + ); + + final poly = PolyIn( + Polygon( + coordinates: [ + [ + Position(0, 0), + Position(10, 0), + Position(10, 10), + Position(0, 10), + ], + [ + Position(0, 0), + Position(1, 1), + Position(1, 0), + ], + [ + Position(2, 2), + Position(2, 3), + Position(3, 3), + Position(3, 2), + ], + ], + ), + multiPoly); + + expect(poly.multiPoly, equals(multiPoly)); + expect(poly.exteriorRing.segments.length, equals(4)); + expect(poly.interiorRings.length, equals(2)); + expect(poly.interiorRings[0].segments.length, equals(3)); + expect(poly.interiorRings[1].segments.length, equals(4)); + expect(poly.getSweepEvents().length, equals(22)); + }); + test('bbox construction', () { + final multiPoly = MultiPolyIn( + MultiPolygon(coordinates: [ + [ + [ + Position(0, 0), + Position(1, 1), + Position(0, 1), + ], + ], + [ + [ + Position(0, 0), + Position(4, 0), + Position(4, 9), + ], + [ + Position(2, 2), + Position(3, 3), + Position(3, 2), + ], + ], + ]), + false, + ); + + final poly = PolyIn( + Polygon( + coordinates: [ + [ + Position(0, 0), + Position(10, 0), + Position(10, 10), + Position(0, 10), + ], + [ + Position(0, 0), + Position(1, 1), + Position(1, 0), + ], + [ + Position(2, 2), + Position(2, 3), + Position(3, 11), + Position(3, 2), + ], + ], + ), + multiPoly, + ); + + expect(poly.bbox.position1, equals(Position(0, 0))); + expect(poly.bbox.position2, equals(Position(10, 11))); + }); + }); + + group('MultiPolyIn', () { + test('creation with multipoly', () { + final multipoly = MultiPolyIn( + MultiPolygon(coordinates: [ + [ + [ + Position(0, 0), + Position(1, 1), + Position(0, 1), + ], + ], + [ + [ + Position(0, 0), + Position(4, 0), + Position(4, 9), + ], + [ + Position(2, 2), + Position(3, 3), + Position(3, 2), + ], + ], + ]), + true, + ); + + expect(multipoly.polys.length, equals(2), + reason: "multipoly.polys.length"); + expect(multipoly.getSweepEvents().length, equals(18), + reason: "multipoly.getSweepEvents().length"); + }); + + test('creation with poly', () { + final multipoly = MultiPolyIn( + MultiPolygon(coordinates: [ + [ + [ + Position(0, 0), + Position(1, 1), + Position(0, 1), + Position(0, 0), + ], + ], + ]), + true, + ); + + expect(multipoly.polys.length, equals(1), + reason: "multipoly.polys.length"); + expect(multipoly.getSweepEvents().length, equals(6), + reason: "multipoly.getSweepEvents().length"); + }); + + ///Clipper lib does not support elevation because it's creating new points at intersections and can not assume the elevation at those generated points. + test('third or more coordinates are ignored', () { + final multipoly = MultiPolyIn( + MultiPolygon(coordinates: [ + [ + [ + Position(0, 0, 42), + Position(1, 1, 128), + Position(0, 1, 84), + Position(0, 0, 42), + ], + ], + ]), + true, + ); + + expect(multipoly.polys.length, equals(1), + reason: "multipoly.polys.length"); + expect(multipoly.getSweepEvents().length, equals(6), + reason: "multipoly.getSweepEvents().length"); + }); + }); +} diff --git a/test/polygon_clipping/geom_out_test.dart b/test/polygon_clipping/geom_out_test.dart new file mode 100644 index 0000000..1ab3ee1 --- /dev/null +++ b/test/polygon_clipping/geom_out_test.dart @@ -0,0 +1,68 @@ +import 'package:test/test.dart'; +import 'package:turf/src/geojson.dart'; +import 'package:turf/src/polygon_clipping/geom_out.dart'; +import 'package:turf/src/polygon_clipping/point_extension.dart'; +import 'package:turf/src/polygon_clipping/segment.dart'; + +void main() { + test('simple triangle', () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(1, 1); + final p3 = PositionEvents(0, 1); + + final seg1 = Segment.fromRing(p1, p2, forceIsInResult: true); + final seg2 = Segment.fromRing(p2, p3, forceIsInResult: true); + final seg3 = Segment.fromRing(p3, p1, forceIsInResult: true); + + final rings = RingOut.factory([seg1, seg2, seg3]); + + expect(rings.length, 1); + expect(rings[0].getGeom(), [ + [0, 0], + [1, 1], + [0, 1], + [0, 0], + ]); + }); + + test('bow tie', () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(1, 1); + final p3 = PositionEvents(0, 2); + + final seg1 = Segment.fromRing(p1, p2); + final seg2 = Segment.fromRing(p2, p3); + final seg3 = Segment.fromRing(p3, p1); + + final p4 = PositionEvents(2, 0); + final p5 = p2; + final p6 = PositionEvents(2, 2); + + final seg4 = Segment.fromRing(p4, p5); + final seg5 = Segment.fromRing(p5, p6); + final seg6 = Segment.fromRing(p6, p4); + + // seg1.isInResult = true; + // seg2.isInResult = true; + // seg3.isInResult = true; + // seg4.isInResult = true; + // seg5.isInResult = true; + // seg6.isInResult = true; + + final rings = RingOut.factory([seg1, seg2, seg3, seg4, seg5, seg6]); + + expect(rings.length, 2); + expect(rings[0].getGeom(), [ + [0, 0], + [1, 1], + [0, 2], + [0, 0], + ]); + expect(rings[1].getGeom(), [ + [1, 1], + [2, 0], + [2, 2], + [1, 1], + ]); + }); +} diff --git a/test/polygon_clipping/segment_test.dart b/test/polygon_clipping/segment_test.dart new file mode 100644 index 0000000..08882eb --- /dev/null +++ b/test/polygon_clipping/segment_test.dart @@ -0,0 +1,408 @@ +import 'package:test/test.dart'; +import 'package:turf/src/polygon_clipping/geom_in.dart'; +import 'package:turf/src/polygon_clipping/point_extension.dart'; +import 'package:turf/src/polygon_clipping/segment.dart'; +import 'package:turf/src/polygon_clipping/sweep_event.dart'; +import 'package:turf/turf.dart'; + +void main() { + group("constructor", () { + test("general", () { + final leftSE = SweepEvent(PositionEvents(0, 0), false); + final rightSE = SweepEvent(PositionEvents(1, 1), false); + final List? rings = []; + final List windings = []; + final seg = Segment(leftSE, rightSE, rings: rings, windings: windings); + expect(seg.rings, rings); + expect(seg.windings, windings); + expect(seg.leftSE, leftSE); + expect(seg.leftSE.otherSE, rightSE); + expect(seg.rightSE, rightSE); + expect(seg.rightSE.otherSE, leftSE); + expect(seg.ringOut, null); + expect(seg.prev, null); + expect(seg.consumedBy, null); + }); + + test("segment Id increments", () { + final leftSE = SweepEvent(PositionEvents(0, 0), false); + final rightSE = SweepEvent(PositionEvents(1, 1), false); + final seg1 = Segment( + leftSE, + rightSE, + ); + final seg2 = Segment( + leftSE, + rightSE, + ); + expect(seg2.id - seg1.id, 1); + }); + }); + + group("fromRing", () { + test("correct point on left and right 1", () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(0, 1); + final seg = Segment.fromRing(p1, p2); + expect(seg.leftSE.point, p1); + expect(seg.rightSE.point, p2); + }); + + test("correct point on left and right 1", () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(-1, 0); + final seg = Segment.fromRing(p1, p2); + expect(seg.leftSE.point, p2); + expect(seg.rightSE.point, p1); + }); + + test("attempt create segment with same points", () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(0, 0); + expect(() => Segment.fromRing(p1, p2), throwsException); + }); + }); + + group("split", () { + test("on interior point", () { + final seg = Segment.fromRing( + PositionEvents(0, 0), + PositionEvents(10, 10), + ); + final pt = PositionEvents(5, 5); + final evts = seg.split(pt); + expect(evts[0].segment, seg); + expect(evts[0].point, pt); + expect(evts[0].isLeft, false); + expect(evts[0].otherSE!.otherSE, evts[0]); + expect(evts[1].segment!.leftSE.segment, evts[1].segment); + expect(evts[1].segment, isNot(seg)); + expect(evts[1].point, pt); + expect(evts[1].isLeft, true); + expect(evts[1].otherSE!.otherSE, evts[1]); + expect(evts[1].segment!.rightSE.segment, evts[1].segment); + }); + + test("on close-to-but-not-exactly interior point", () { + final seg = Segment.fromRing( + PositionEvents(0, 10), + PositionEvents(10, 0), + ); + final pt = PositionEvents(5 + epsilon, 5); + final evts = seg.split(pt); + expect(evts[0].segment, seg); + expect(evts[0].point, pt); + expect(evts[0].isLeft, false); + expect(evts[1].segment, isNot(seg)); + expect(evts[1].point, pt); + expect(evts[1].isLeft, true); + expect(evts[1].segment!.rightSE.segment, evts[1].segment); + }); + + test("on three interior points", () { + final seg = Segment.fromRing( + PositionEvents(0, 0), + PositionEvents(10, 10), + ); + final sPt1 = PositionEvents(2, 2); + final sPt2 = PositionEvents(4, 4); + final sPt3 = PositionEvents(6, 6); + + final orgLeftEvt = seg.leftSE; + final orgRightEvt = seg.rightSE; + final newEvts3 = seg.split(sPt3); + final newEvts2 = seg.split(sPt2); + final newEvts1 = seg.split(sPt1); + final newEvts = [...newEvts1, ...newEvts2, ...newEvts3]; + + expect(newEvts.length, 6); + + expect(seg.leftSE, orgLeftEvt); + var evt = newEvts.firstWhere((e) => e.point == sPt1 && !e.isLeft); + expect(seg.rightSE, evt); + + evt = newEvts.firstWhere((e) => e.point == sPt1 && e.isLeft); + var otherEvt = newEvts.firstWhere((e) => e.point == sPt2 && !e.isLeft); + expect(evt.segment, otherEvt.segment); + + evt = newEvts.firstWhere((e) => e.point == sPt2 && e.isLeft); + otherEvt = newEvts.firstWhere((e) => e.point == sPt3 && !e.isLeft); + expect(evt.segment, otherEvt.segment); + + evt = newEvts.firstWhere((e) => e.point == sPt3 && e.isLeft); + expect(evt.segment, orgRightEvt.segment); + }); + }); + + group("simple properties - bbox, vector", () { + test("general", () { + final seg = Segment.fromRing(PositionEvents(1, 2), PositionEvents(3, 4)); + expect(seg.bbox, + BBox.fromPositions(PositionEvents(1, 2), PositionEvents(3, 4))); + expect(seg.vector, Position(2, 2)); + }); + + test("horizontal", () { + final seg = Segment.fromRing(PositionEvents(1, 4), PositionEvents(3, 4)); + expect( + seg.bbox, equals(BBox.fromPositions(Position(1, 4), Position(3, 4)))); + expect(seg.vector, Position(2, 0)); + }); + + test("vertical", () { + final seg = Segment.fromRing(PositionEvents(3, 2), PositionEvents(3, 4)); + expect(seg.bbox, + BBox.fromPositions(PositionEvents(3, 2), PositionEvents(3, 4))); + expect(seg.vector, Position(0, 2)); + }); + }); + + group("consume()", () { + test("not automatically consumed", () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(1, 0); + final seg1 = Segment.fromRing(p1, p2); + final seg2 = Segment.fromRing(p1, p2); + expect(seg1.consumedBy, null); + expect(seg2.consumedBy, null); + }); + + test("basic case", () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(1, 0); + final seg1 = Segment.fromRing( + p1, + p2, + // {}, + ); + final seg2 = Segment.fromRing( + p1, + p2, + // {}, + ); + seg1.consume(seg2); + expect(seg2.consumedBy, seg1); + expect(seg1.consumedBy, null); + }); + + test("ealier in sweep line sorting consumes later", () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(1, 0); + final seg1 = Segment.fromRing( + p1, + p2, + // {}, + ); + final seg2 = Segment.fromRing( + p1, + p2, + // {}, + ); + seg2.consume(seg1); + expect(seg2.consumedBy, seg1); + expect(seg1.consumedBy, null); + }); + + test("consuming cascades", () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(0, 0); + final p3 = PositionEvents(1, 0); + final p4 = PositionEvents(1, 0); + final seg1 = Segment.fromRing( + p1, + p3, + // {}, + ); + final seg2 = Segment.fromRing( + p1, + p3, + // {}, + ); + final seg3 = Segment.fromRing( + p2, + p4, + // {}, + ); + final seg4 = Segment.fromRing( + p2, + p4, + // {}, + ); + final seg5 = Segment.fromRing( + p2, + p4, + // {}, + ); + seg1.consume(seg2); + seg4.consume(seg2); + seg3.consume(seg2); + seg3.consume(seg5); + expect(seg1.consumedBy, null); + expect(seg2.consumedBy, seg1); + expect(seg3.consumedBy, seg1); + expect(seg4.consumedBy, seg1); + expect(seg5.consumedBy, seg1); + }); + }); + + group("is an endpoint", () { + final p1 = PositionEvents(0, -1); + final p2 = PositionEvents(1, 0); + final seg = Segment.fromRing(p1, p2); + + test("yup", () { + expect(seg.isAnEndpoint(p1), true); + expect(seg.isAnEndpoint(p2), true); + }); + + test("nope", () { + expect(seg.isAnEndpoint(PositionEvents(-34, 46)), false); + expect(seg.isAnEndpoint(PositionEvents(0, 0)), false); + }); + }); + + group("comparison with point", () { + test("general", () { + final s1 = Segment.fromRing(PositionEvents(0, 0), PositionEvents(1, 1)); + final s2 = Segment.fromRing(PositionEvents(0, 1), PositionEvents(0, 0)); + + expect(s1.comparePoint(PositionEvents(0, 1)), 1); + expect(s1.comparePoint(PositionEvents(1, 2)), 1); + expect(s1.comparePoint(PositionEvents(0, 0)), 0); + expect(s1.comparePoint(PositionEvents(5, -1)), -1); + + expect(s2.comparePoint(PositionEvents(0, 1)), 0); + expect(s2.comparePoint(PositionEvents(1, 2)), -1); + expect(s2.comparePoint(PositionEvents(0, 0)), 0); + expect(s2.comparePoint(PositionEvents(5, -1)), -1); + }); + + test("barely above", () { + final s1 = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 1)); + final pt = PositionEvents(2, 1 - epsilon); + expect(s1.comparePoint(pt), -1); + }); + + test("barely below", () { + final s1 = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 1)); + final pt = PositionEvents(2, 1 + (epsilon * 3) / 2); + expect(s1.comparePoint(pt), 1); + }); + + test("vertical before", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(1, 3)); + final pt = PositionEvents(0, 0); + expect(seg.comparePoint(pt), 1); + }); + + test("vertical after", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(1, 3)); + final pt = PositionEvents(2, 0); + expect(seg.comparePoint(pt), -1); + }); + + test("vertical on", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(1, 3)); + final pt = PositionEvents(1, 0); + expect(seg.comparePoint(pt), 0); + }); + + test("horizontal below", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 1)); + final pt = PositionEvents(0, 0); + expect(seg.comparePoint(pt), -1); + }); + + test("horizontal above", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 1)); + final pt = PositionEvents(0, 2); + expect(seg.comparePoint(pt), 1); + }); + + test("horizontal on", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 1)); + final pt = PositionEvents(0, 1); + expect(seg.comparePoint(pt), 0); + }); + + test("in vertical plane below", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 3)); + final pt = PositionEvents(2, 0); + expect(seg.comparePoint(pt), -1); + }); + + test("in vertical plane above", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 3)); + final pt = PositionEvents(2, 4); + expect(seg.comparePoint(pt), 1); + }); + + test("in horizontal plane upward sloping before", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 3)); + final pt = PositionEvents(0, 2); + expect(seg.comparePoint(pt), 1); + }); + + test("in horizontal plane upward sloping after", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 3)); + final pt = PositionEvents(4, 2); + expect(seg.comparePoint(pt), -1); + }); + + test("in horizontal plane downward sloping before", () { + final seg = Segment.fromRing(PositionEvents(1, 3), PositionEvents(3, 1)); + final pt = PositionEvents(0, 2); + expect(seg.comparePoint(pt), -1); + }); + + test("in horizontal plane downward sloping after", () { + final seg = Segment.fromRing(PositionEvents(1, 3), PositionEvents(3, 1)); + final pt = PositionEvents(4, 2); + expect(seg.comparePoint(pt), 1); + }); + + test("upward more vertical before", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 6)); + final pt = PositionEvents(0, 2); + expect(seg.comparePoint(pt), 1); + }); + + test("upward more vertical after", () { + final seg = Segment.fromRing(PositionEvents(1, 1), PositionEvents(3, 6)); + final pt = PositionEvents(4, 2); + expect(seg.comparePoint(pt), -1); + }); + + test("downward more vertical before", () { + final seg = Segment.fromRing(PositionEvents(1, 6), PositionEvents(3, 1)); + final pt = PositionEvents(0, 2); + expect(seg.comparePoint(pt), -1); + }); + + test("downward more vertical after", () { + final seg = Segment.fromRing(PositionEvents(1, 6), PositionEvents(3, 1)); + final pt = PositionEvents(4, 2); + expect(seg.comparePoint(pt), 1); + }); + + test("downward-slopping segment with almost touching point - from issue 37", + () { + final seg = Segment.fromRing( + PositionEvents(0.523985, 51.281651), + PositionEvents(0.5241, 51.281651000100005), + ); + final pt = PositionEvents(0.5239850000000027, 51.281651000000004); + expect(seg.comparePoint(pt), 1); + }); + + test("avoid splitting loops on near vertical segments - from issue 60-2", + () { + final seg = Segment.fromRing( + PositionEvents(-45.3269382, -1.4059341), + PositionEvents(-45.326737413921656, -1.40635), + ); + final pt = PositionEvents(-45.326833968900424, -1.40615); + expect(seg.comparePoint(pt), 0); + }); + }); +} diff --git a/test/polygon_clipping/sweep_event_test.dart b/test/polygon_clipping/sweep_event_test.dart new file mode 100644 index 0000000..a45dfff --- /dev/null +++ b/test/polygon_clipping/sweep_event_test.dart @@ -0,0 +1,321 @@ +import 'dart:developer'; + +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/polygon_clipping/geom_in.dart'; +import 'package:turf/src/polygon_clipping/point_extension.dart'; +import 'package:turf/src/polygon_clipping/segment.dart'; +import 'package:turf/src/polygon_clipping/sweep_event.dart'; + +void main() { + group('sweep event compare', () { + final RingIn placeholderRingIn = RingIn([ + Position(0, 0), + Position(6, 6), + Position(4, 2), + ], isExterior: true); + test('favor earlier x in point', () { + final s1 = SweepEvent(PositionEvents(-5, 4), false); + final s2 = SweepEvent(PositionEvents(5, 1), false); + expect(SweepEvent.compare(s1, s2), -1); + expect(SweepEvent.compare(s2, s1), 1); + }); + + test('then favor earlier y in point', () { + final s1 = SweepEvent(PositionEvents(5, -4), false); + final s2 = SweepEvent(PositionEvents(5, 4), false); + expect(SweepEvent.compare(s1, s2), -1); + expect(SweepEvent.compare(s2, s1), 1); + }); + + test('then favor right events over left', () { + final seg1 = Segment.fromRing( + PositionEvents(5, 4), + PositionEvents(3, 2), + ); + final seg2 = Segment.fromRing( + PositionEvents(5, 4), + PositionEvents(6, 5), + ); + expect(SweepEvent.compare(seg1.rightSE, seg2.leftSE), -1); + expect(SweepEvent.compare(seg2.leftSE, seg1.rightSE), 1); + }); + + test('then favor non-vertical segments for left events', () { + final seg1 = Segment.fromRing( + PositionEvents(3, 2), + PositionEvents(3, 4), + ); + final seg2 = Segment.fromRing( + PositionEvents(3, 2), + PositionEvents(5, 4), + ); + expect(SweepEvent.compare(seg1.leftSE, seg2.rightSE), -1); + expect(SweepEvent.compare(seg2.rightSE, seg1.leftSE), 1); + }); + + test('then favor vertical segments for right events', () { + final seg1 = Segment.fromRing( + PositionEvents(3, 4), + PositionEvents(3, 2), + ); + final seg2 = Segment.fromRing( + PositionEvents(3, 4), + PositionEvents(1, 2), + ); + expect(SweepEvent.compare(seg1.leftSE, seg2.rightSE), -1); + expect(SweepEvent.compare(seg2.rightSE, seg1.leftSE), 1); + }); + + test('then favor lower segment', () { + final seg1 = Segment.fromRing( + PositionEvents(0, 0), + PositionEvents(4, 4), + ); + final seg2 = Segment.fromRing( + PositionEvents(0, 0), + PositionEvents(5, 6), + ); + expect(SweepEvent.compare(seg1.leftSE, seg2.rightSE), -1); + expect(SweepEvent.compare(seg2.rightSE, seg1.leftSE), 1); + }); + + test('and favor barely lower segment', () { + final seg1 = Segment.fromRing( + PositionEvents(-75.725, 45.357), + PositionEvents(-75.72484615384616, 45.35723076923077), + ); + final seg2 = Segment.fromRing( + PositionEvents(-75.725, 45.357), + PositionEvents(-75.723, 45.36), + ); + expect(SweepEvent.compare(seg1.leftSE, seg2.leftSE), 1); + expect(SweepEvent.compare(seg2.leftSE, seg1.leftSE), -1); + }); + + test('then favor lower ring id', () { + final seg1 = Segment.fromRing( + PositionEvents(0, 0), + PositionEvents(4, 4), + ); + final seg2 = Segment.fromRing( + PositionEvents(0, 0), + PositionEvents(5, 5), + ); + expect(SweepEvent.compare(seg1.leftSE, seg2.leftSE), -1); + expect(SweepEvent.compare(seg2.leftSE, seg1.leftSE), 1); + }); + + test('identical equal', () { + final s1 = SweepEvent(PositionEvents(0, 0), false); + final s3 = SweepEvent(PositionEvents(3, 3), false); + Segment(s1, s3); + Segment(s1, s3); + expect(SweepEvent.compare(s1, s1), 0); + }); + + test('totally equal but not identical events are consistent', () { + final s1 = SweepEvent(PositionEvents(0, 0), false); + final s2 = SweepEvent(PositionEvents(0, 0), false); + final s3 = SweepEvent(PositionEvents(3, 3), false); + Segment(s1, s3); + Segment(s2, s3); + final result = SweepEvent.compare(s1, s2); + expect(SweepEvent.compare(s1, s2), result); + expect(SweepEvent.compare(s2, s1), result * -1); + }); + + test('events are linked as side effect', () { + final s1 = SweepEvent(PositionEvents(0, 0), false); + final s2 = SweepEvent(PositionEvents(0, 0), false); + Segment(s1, SweepEvent(PositionEvents(2, 2), false)); + Segment(s2, SweepEvent(PositionEvents(3, 4), false)); + expect(s1.point, equals(s2.point)); + SweepEvent.compare(s1, s2); + expect(s1.point, equals(s2.point)); + }); + + test('consistency edge case', () { + final seg1 = Segment.fromRing( + PositionEvents(-71.0390933353125, 41.504475), + PositionEvents(-71.0389879, 41.5037842), + ); + final seg2 = Segment.fromRing( + PositionEvents(-71.0390933353125, 41.504475), + PositionEvents(-71.03906280974431, 41.504275), + ); + expect(SweepEvent.compare(seg1.leftSE, seg2.leftSE), -1); + expect(SweepEvent.compare(seg2.leftSE, seg1.leftSE), 1); + }); + }); + group('constructor', () { + test('events created from same point are already linked', () { + final p1 = PositionEvents(0, 0); + final s1 = SweepEvent(p1, false); + final s2 = SweepEvent(p1, false); + expect(s1.point, equals(p1)); + expect(s1.point.events, equals(s2.point.events)); + }); + }); + + group('sweep event link', () { + test('no linked events', () { + final s1 = SweepEvent(PositionEvents(0, 0), false); + expect(s1.point.events, [s1]); + expect(s1.getAvailableLinkedEvents(), []); + }); + + test('link events already linked with others', () { + final p1 = PositionEvents(1, 2); + final p2 = PositionEvents(2, 3); + final se1 = SweepEvent(p1, false); + final se2 = SweepEvent(p1, false); + final se3 = SweepEvent(p2, false); + final se4 = SweepEvent(p2, false); + Segment(se1, SweepEvent(PositionEvents(5, 5), false)); + Segment(se2, SweepEvent(PositionEvents(6, 6), false)); + Segment(se3, SweepEvent(PositionEvents(7, 7), false)); + Segment(se4, SweepEvent(PositionEvents(8, 8), false)); + se1.link(se3); + // expect(se1.point.events!.length, 4); + expect(se1.point, se2.point); + expect(se1.point, se3.point); + expect(se1.point, se4.point); + }); + + test('same event twice', () { + final p1 = PositionEvents(0, 0); + final s1 = SweepEvent(p1, false); + final s2 = SweepEvent(p1, false); + expect(() => s2.link(s1), throwsException); + expect(() => s1.link(s2), throwsException); + }); + + test('unavailable linked events do not show up', () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(1, 1); + final p3 = PositionEvents(1, 0); + final se1 = SweepEvent(p1, false); + final se2 = SweepEvent(p2, false); + final se3 = SweepEvent(p3, true); + final seNotInResult = SweepEvent(p1, false); + seNotInResult.segment = Segment(se2, se3, forceIsInResult: false); + print(seNotInResult); + expect(se1.getAvailableLinkedEvents(), []); + }); + + test('available linked events show up', () { + final p1 = PositionEvents(0, 0); + final p2 = PositionEvents(1, 1); + final p3 = PositionEvents(1, 0); + final se1 = SweepEvent(p1, false); + final se2 = SweepEvent(p2, false); + final se3 = SweepEvent(p3, true); + final seOkay = SweepEvent(p1, false); + seOkay.segment = Segment(se2, se3, forceIsInResult: true); + List events = se1.getAvailableLinkedEvents(); + expect(events[0], equals(seOkay)); + }); + + //TODO: verify constructor functioning with reference events + // test('link goes both ways', () { + // // final p2 = PositionEvents(1, 1); + // // final p3 = PositionEvents(1, 0); + // // final se2 = SweepEvent(p2, false); + // // final se3 = SweepEvent(p3, false); + + // final p1 = PositionEvents(0, 0); + // final se1 = SweepEvent(p1, false); + // print(se1); + // final seOkay1 = SweepEvent(p1, false); + // print(seOkay1); + // final seOkay2 = SweepEvent(p1, false); + // print(seOkay2); + // seOkay1.segment = Segment( + // se1, + // seOkay1, + // forceIsInResult: true, + // ); + // seOkay2.segment = Segment( + // se1, + // seOkay2, + // forceIsInResult: true, + // ); + // expect(seOkay1.getAvailableLinkedEvents(), [seOkay2]); + // expect(seOkay2.getAvailableLinkedEvents(), [seOkay1]); + // }); + }); + + group('sweep event get leftmost comparator', () { + test('after a segment straight to the right', () { + final prevEvent = SweepEvent(PositionEvents(0, 0), false); + final event = SweepEvent(PositionEvents(1, 0), false); + final comparator = event.getLeftmostComparator(prevEvent); + + final e1 = SweepEvent(PositionEvents(1, 0), false); + Segment(e1, SweepEvent(PositionEvents(0, 1), false)); + + final e2 = SweepEvent(PositionEvents(1, 0), false); + Segment(e2, SweepEvent(PositionEvents(1, 1), false)); + + final e3 = SweepEvent(PositionEvents(1, 0), false); + Segment(e3, SweepEvent(PositionEvents(2, 0), false)); + + final e4 = SweepEvent(PositionEvents(1, 0), false); + Segment(e4, SweepEvent(PositionEvents(1, -1), false)); + + final e5 = SweepEvent(PositionEvents(1, 0), false); + Segment(e5, SweepEvent(PositionEvents(0, -1), false)); + + expect(comparator(e1, e2), -1); + expect(comparator(e2, e3), -1); + expect(comparator(e3, e4), -1); + expect(comparator(e4, e5), -1); + + expect(comparator(e2, e1), 1); + expect(comparator(e3, e2), 1); + expect(comparator(e4, e3), 1); + expect(comparator(e5, e4), 1); + + expect(comparator(e1, e3), -1); + expect(comparator(e1, e4), -1); + expect(comparator(e1, e5), -1); + + expect(comparator(e1, e1), 0); + }); + + test('after a down and to the left', () { + final prevEvent = SweepEvent(PositionEvents(1, 1), false); + final event = SweepEvent(PositionEvents(0, 0), false); + final comparator = event.getLeftmostComparator(prevEvent); + + final e1 = SweepEvent(PositionEvents(0, 0), false); + Segment(e1, SweepEvent(PositionEvents(0, 1), false)); + + final e2 = SweepEvent(PositionEvents(0, 0), false); + Segment(e2, SweepEvent(PositionEvents(1, 0), false)); + + final e3 = SweepEvent(PositionEvents(0, 0), false); + Segment(e3, SweepEvent(PositionEvents(0, -1), false)); + + final e4 = SweepEvent(PositionEvents(0, 0), false); + Segment(e4, SweepEvent(PositionEvents(-1, 0), false)); + + expect(comparator(e1, e2), 1); + expect(comparator(e1, e3), 1); + expect(comparator(e1, e4), 1); + + expect(comparator(e2, e1), -1); + expect(comparator(e2, e3), -1); + expect(comparator(e2, e4), -1); + + expect(comparator(e3, e1), -1); + expect(comparator(e3, e2), 1); + expect(comparator(e3, e4), -1); + + expect(comparator(e4, e1), -1); + expect(comparator(e4, e2), 1); + expect(comparator(e4, e3), 1); + }); + }); +} diff --git a/test/polygon_clipping/sweep_line_test.dart b/test/polygon_clipping/sweep_line_test.dart new file mode 100644 index 0000000..5243a01 --- /dev/null +++ b/test/polygon_clipping/sweep_line_test.dart @@ -0,0 +1,134 @@ +import 'package:test/test.dart'; +import 'package:turf/src/polygon_clipping/point_extension.dart'; +import 'package:turf/src/polygon_clipping/segment.dart'; +import 'package:turf/src/polygon_clipping/sweep_event.dart'; +import 'package:turf/src/polygon_clipping/sweep_line.dart'; + +void main() { + test('Test tree construction', () { + final sl = SweepLine( + [], + ); + + final leftSE1 = SweepEvent(PositionEvents(0, 0), true); + final rightSE1 = SweepEvent(PositionEvents(10, 10), false); + final segment1 = Segment( + leftSE1, + rightSE1, + ); + + final leftSE2 = SweepEvent(PositionEvents(5, 5), true); + final rightSE2 = SweepEvent(PositionEvents(15, 15), false); + final segment2 = Segment( + leftSE2, + rightSE2, + ); + + sl.tree[segment1] = null; + sl.tree[segment2] = null; + + expect(sl.tree.containsKey(segment1), equals(true)); + expect(sl.tree.containsKey(segment2), equals(true)); + }); + group("Test tree", () { + final sl = SweepLine( + [], + ); + + final leftSE1 = SweepEvent(PositionEvents(0, 0), true); + final rightSE1 = SweepEvent(PositionEvents(10, 10), false); + final segment1 = Segment( + leftSE1, + rightSE1, + ); + + final leftSE2 = SweepEvent(PositionEvents(5, 5), true); + final rightSE2 = SweepEvent(PositionEvents(15, 15), false); + final segment2 = Segment( + leftSE2, + rightSE2, + ); + + final leftSE3 = SweepEvent(PositionEvents(20, 20), true); + final rightSE3 = SweepEvent(PositionEvents(25, 10), false); + final segment3 = Segment( + leftSE3, + rightSE3, + ); + + final leftSE4 = SweepEvent(PositionEvents(5, 5), true); + final rightSE4 = SweepEvent(PositionEvents(10, 10), false); + final segment4 = Segment( + leftSE4, + rightSE4, + ); + + test("test filling up the tree then emptying it out", () { + // var n1 = sl.tree[segment1]; + // var segment2 = sl.tree[segment2]; + // var segment4 = sl.tree[segment4]; + // var segment3 = sl.tree[segment3]; + + sl.tree[segment1] = null; + sl.tree[segment2] = null; + sl.tree[segment3] = null; + sl.tree[segment4] = null; + + expect(sl.tree.containsKey(segment1), equals(true)); + expect(sl.tree.containsKey(segment2), equals(true)); + expect(sl.tree.containsKey(segment3), equals(true)); + expect(sl.tree.containsKey(segment4), equals(true)); + + // expect(sl.tree.lastKeyBefore(segment1), isNull); + // expect(sl.tree.firstKeyAfter(segment1), equals(segment2)); + + // expect(sl.tree.lastKeyBefore(segment2), equals(segment1)); + // expect(sl.tree.firstKeyAfter(segment2), equals(segment3)); + + // expect(sl.tree.lastKeyBefore(segment3), equals(segment2)); + // expect(sl.tree.firstKeyAfter(segment3), equals(segment4)); + + // expect(sl.tree.lastKeyBefore(segment4), equals(segment3)); + // expect(sl.tree.firstKeyAfter(segment4), isNull); + + sl.tree.remove(segment2); + expect(sl.tree.containsKey(segment2), isNull); + + // n1 = sl.tree.containsKey(segment1); + // segment3 = sl.tree.containsKey(segment3); + // segment4 = sl.tree.containsKey(segment4); + + // expect(sl.tree.lastKeyBefore(n1), isNull); + // expect(sl.tree.firstKeyAfter(n1), equals(segment3)); + + // expect(sl.tree.lastKeyBefore(segment3), equals(segment1)); + // expect(sl.tree.firstKeyAfter(segment3), equals(segment4)); + + // expect(sl.tree.lastKeyBefore(segment4), equals(segment3)); + // expect(sl.tree.firstKeyAfter(segment4), isNull); + + // sl.tree.remove(segment4); + // expect(sl.tree.containsKey(segment4), isNull); + + // n1 = sl.tree.containsKey(segment1); + // segment3 = sl.tree.containsKey(segment3); + + // expect(sl.tree.lastKeyBefore(n1), isNull); + // expect(sl.tree.firstKeyAfter(n1), equals(segment3)); + + // expect(sl.tree.lastKeyBefore(segment3), equals(segment1)); + // expect(sl.tree.firstKeyAfter(segment3), isNull); + + // sl.tree.remove(segment1); + // expect(sl.tree.containsKey(segment1), isNull); + + // segment3 = sl.tree.containsKey(segment3); + + // expect(sl.tree.lastKeyBefore(segment3), isNull); + // expect(sl.tree.firstKeyAfter(segment3), isNull); + + // sl.tree.remove(segment3); + // expect(sl.tree.containsKey(segment3), isNull); + }); + }); +}