diff --git a/lib/src/geojson.dart b/lib/src/geojson.dart index 619fe127..b70aaa2f 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, + lng2: p2.lng, + lat2: p2.lat, + alt2: p2.alt, + ); + 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,31 @@ class BBox extends CoordinateType { alt2: alt2 ?? this.alt2, ); + //Adjust the bounds to include the given position + void expandToFitPosition(Position position) { + //If the position is outside the current bounds, expand the bounds + if (position.lng < lng1) { + _items[0] = position.lng; + } + if (position.lat < lat1) { + _items[1] = position.lat; + } + if (position.lng > lng2) { + _items[_is3D ? 3 : 2] = position.lng; + } + if (position.lat > lat2) { + _items[_is3D ? 4 : 3] = position.lat; + } + if (position.alt != null) { + if (alt1 == null || position.alt! < alt1!) { + _items[2] = position.alt!; + } + if (alt2 == null || position.alt! > alt2!) { + _items[5] = position.alt!; + } + } + } + @override BBox clone() => BBox.of(_items); @@ -377,6 +424,28 @@ class BBox extends CoordinateType { lng2: _untilSigned(lng2, 180), ); + bool isPositionInBBox(Position point) { + return point.lng >= lng1 && + point.lng <= lng2 && + point.lat >= lat1 && + point.lat <= lat2 && + (point.alt == null || + (alt1 != null && point.alt! >= alt1!) || + (alt2 != null && point.alt! <= alt2!)); + } + + bool isBBoxOverlapping(BBox bbox) { + return bbox.lng1 <= lng2 && + bbox.lng2 >= lng1 && + bbox.lat1 <= lat2 && + bbox.lat2 >= lat1 && + ((alt1 == null && bbox.alt1 == null) || + (alt1 != null && + bbox.alt1 != null && + bbox.alt1! <= alt2! && + bbox.alt2! >= alt1!)); + } + @override int get hashCode => Object.hashAll(_items); @@ -584,6 +653,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 0e15b96e..244e0d55 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/flp.dart b/lib/src/polygon_clipping/flp.dart new file mode 100644 index 00000000..72fbb11a --- /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/helpers.dart'; + +const double epsilonsqrd = epsilon * epsilon; +// FLP (Floating-Position) comparator function +int cmp(double a, double b) { + // Check if both numbers are close to zero. + if (-epsilon < a && a < epsilon) { + if (-epsilon < b && b < epsilon) { + return 0; // Both numbers are effectively zero. + } + } + + // Check if the numbers are approximately equal (within epsilon). + final double ab = a - b; + if (ab * ab < epsilonsqrd * a * b) { + return 0; // Numbers are approximately equal. + } + + // Normal comparison: return -1 if a < b, 1 if a > b. + return a < b ? -1 : 1; +} diff --git a/lib/src/polygon_clipping/geom_in.dart b/lib/src/polygon_clipping/geom_in.dart new file mode 100644 index 00000000..d506bfde --- /dev/null +++ b/lib/src/polygon_clipping/geom_in.dart @@ -0,0 +1,146 @@ +import 'dart:math'; + +import 'package:turf/helpers.dart'; +import 'package:turf/src/polygon_clipping/point_extension.dart'; +import 'package:turf/src/polygon_clipping/sweep_event.dart'; + +import 'segment.dart'; + +//TODO: mark factory methods to remove late values; +/// Represents a ring in a polygon. +class RingIn { + /// List of segments. + List segments = []; + + /// Indicates whether the polygon is an exterior polygon. + final bool isExterior; + + /// The parent polygon. + final PolyIn? poly; + + /// The bounding box of the polygon. + late BBox bbox; + + RingIn(List geomRing, {this.poly, required this.isExterior}) + : assert(geomRing.isNotEmpty) { + Position firstPoint = + Position(round(geomRing[0].lng), round(geomRing[0].lat)); + bbox = BBox.fromPositions( + Position(firstPoint.lng, firstPoint.lat), + Position(firstPoint.lng, firstPoint.lat), + ); + + Position prevPoint = firstPoint; + for (var i = 1; i < geomRing.length; i++) { + Position point = Position(round(geomRing[i].lng), round(geomRing[i].lat)); + // skip repeated points + if (point.lng == prevPoint.lng && point.lat == prevPoint.lat) continue; + segments.add(Segment.fromRing( + PositionEvents.fromPoint(prevPoint), PositionEvents.fromPoint(point), + ring: this)); + bbox.expandToFitPosition(point); + + prevPoint = point; + } + // add segment from last to first if last is not the same as first + if (firstPoint.lng != prevPoint.lng || firstPoint.lat != prevPoint.lat) { + segments.add(Segment.fromRing(PositionEvents.fromPoint(prevPoint), + PositionEvents.fromPoint(firstPoint), + ring: this)); + } + } + + List getSweepEvents() { + final List 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; + late BBox bbox; + final MultiPolyIn? multiPoly; + + PolyIn( + Polygon geomPoly, + this.multiPoly, + ) { + exteriorRing = + RingIn(geomPoly.coordinates[0], poly: this, isExterior: true); + // copy by value + bbox = exteriorRing.bbox; + + interiorRings = []; + Position lowerLeft = bbox.position1; + Position upperRight = bbox.position2; + for (var i = 1; i < geomPoly.coordinates.length; i++) { + final ring = + RingIn(geomPoly.coordinates[i], poly: this, isExterior: false); + lowerLeft = Position(min(ring.bbox.position1.lng, lowerLeft.lng), + min(ring.bbox.position1.lat, lowerLeft.lat)); + upperRight = Position(max(ring.bbox.position2.lng, upperRight.lng), + max(ring.bbox.position2.lat, upperRight.lat)); + interiorRings.add(ring); + } + + bbox = BBox.fromPositions(lowerLeft, upperRight); + } + + List 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++) { + sweepEvents.add(ringSweepEvents[j]); + } + } + return sweepEvents; + } +} + +//TODO: mark factory methods to remove late values; +class MultiPolyIn { + List polys = []; + final bool isSubject; + late BBox bbox; + + MultiPolyIn(MultiPolygon geom, this.isSubject) { + bbox = BBox.fromPositions( + Position(double.infinity, double.infinity), + Position(double.negativeInfinity, double.negativeInfinity), + ); + + List polygonsIn = geom.toPolygons(); + + Position lowerLeft = bbox.position1; + Position upperRight = bbox.position2; + for (var i = 0; i < polygonsIn.length; i++) { + final poly = PolyIn(polygonsIn[i], this); + lowerLeft = Position(min(poly.bbox.position1.lng, lowerLeft.lng), + min(poly.bbox.position1.lat, lowerLeft.lat)); + upperRight = Position(max(poly.bbox.position2.lng, upperRight.lng), + max(poly.bbox.position2.lat, upperRight.lat)); + polys.add(poly); + } + + bbox = BBox.fromPositions(lowerLeft, upperRight); + } + + List 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++) { + 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 00000000..73283670 --- /dev/null +++ b/lib/src/polygon_clipping/geom_out.dart @@ -0,0 +1,258 @@ +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_extension.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 Position 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) { + Position firstPt = events[0].point; + Position lastPt = events[events.length - 1].point; + throw Exception( + '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) { + 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() { + 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; + } + + if (points.length == 1) return null; + + Position pt = points[0]; + Position 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(Position(points[i].lng, points[i].lat)); + } + + return orderedPoints; + } + + RingOut? _enclosingRing; + RingOut enclosingRing() { + _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?.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; + } else { + return prevSeg.ringOut!.enclosingRing(); + } + } + + // 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; + } + } +} + +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); + } + + GeometryObject 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); + } + + 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) { + 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 00000000..9aa8cf21 --- /dev/null +++ b/lib/src/polygon_clipping/index.dart @@ -0,0 +1,25 @@ +import 'package:turf/src/geojson.dart'; + +import 'operation.dart'; + +//?Should these just be methods of operations? or factory constructors or something else? +GeometryObject? union(GeometryObject geom, List moreGeoms) => + operation.run("union", geom, moreGeoms); + +GeometryObject? intersection( + GeometryObject geom, List moreGeoms) => + operation.run("intersection", geom, moreGeoms); + +GeometryObject? xor(GeometryObject geom, List moreGeoms) => + operation.run("xor", geom, moreGeoms); + +GeometryObject? difference( + GeometryObject 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 00000000..8f794901 --- /dev/null +++ b/lib/src/polygon_clipping/intersection.dart @@ -0,0 +1,8 @@ +import 'package:turf/src/geojson.dart'; + +class Intersection { + final int id; + final Position 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 00000000..24ea020b --- /dev/null +++ b/lib/src/polygon_clipping/operation.dart @@ -0,0 +1,152 @@ +import 'dart:collection'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/polygon_clipping/utils.dart'; + +import 'geom_in.dart'; +import 'geom_out.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; + + 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 */ + //TODO: handle multipolygons + final List multipolys = [ + MultiPolyIn(geom as MultiPolygon, true) + ]; + for (var i = 0; i < moreGeoms.length; i++) { + 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; + + /* 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) { + // todo ensure not a list if needed + // return []; + return null; + } + } + } + } + + /* 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.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) { + // 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); + } + + /* Collect and compile segments we're keeping into a multipolygon */ + final ringsOut = RingOut.factory(sweepLine.segments); + final result = 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 00000000..e6832953 --- /dev/null +++ b/lib/src/polygon_clipping/point_extension.dart @@ -0,0 +1,24 @@ +import 'package:turf/src/geojson.dart'; +import 'package:turf/src/polygon_clipping/sweep_event.dart'; + +class PositionEvents extends Position { + List? events; + + PositionEvents( + double super.lng, + double super.lat, { + this.events, + }); + + factory PositionEvents.fromPoint(Position point) { + 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/polygon_clipping.dart b/lib/src/polygon_clipping/polygon_clipping.dart new file mode 100644 index 00000000..64ed99d0 --- /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/segment.dart b/lib/src/polygon_clipping/segment.dart new file mode 100644 index 00000000..5c555d9e --- /dev/null +++ b/lib/src/polygon_clipping/segment.dart @@ -0,0 +1,696 @@ +// Give segments unique ID's to get consistent sorting of +// 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'; +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'; + +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 + //Directional windings + List? windings; + //Testing parameter: should only be used in testing + bool? forceIsInResult; + + ///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, + this.forceIsInResult, + }) + //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 + 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 + * 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.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.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) { + // 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? + int comparePoint(Position point) { + if (isAnEndpoint(point)) return 0; + + final Position lPt = leftSE.point; + final Position rPt = rightSE.point; + final Position v = vector; + + // Exactly vertical segments. + 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.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.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 */ + replaceRightSE(newRightSE) { + rightSE = newRightSE; + rightSE.segment = this; + rightSE.otherSE = leftSE; + leftSE.otherSE = rightSE; + } + + /* Create Bounding Box for segment */ + 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), + ); + } + + /* + * 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. + */ + + Position? 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.lng == orp.lng && tlp.lat == orp.lat) 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.lng == olp.lng && trp.lat == olp.lat) 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 + 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 + 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 Position(round(pt.lng), round(pt.lat)); + } + + /* + * 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(PositionEvents 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: rings != null ? List.from(rings!) : null, + windings: 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 */ + 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 */ + 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; + } + 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; + 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; + } + + factory Segment.fromRing(PositionEvents pt1, PositionEvents pt2, + {RingIn? ring, bool? forceIsInResult}) { + PositionEvents leftPt; + PositionEvents 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.lng}, ${pt1.lat}]"); + } + + final leftSE = SweepEvent(leftPt, true); + final rightSE = SweepEvent(rightPt, false); + return Segment( + leftSE, + rightSE, + rings: ring != null ? [ring] : null, + windings: [winding], + forceIsInResult: forceIsInResult, + ); + } + + Segment? _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; + + SegmentState? 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; + } + + 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; + + 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 bool noBefores = mpsBefore!.isEmpty; + final bool noAfters = mpsAfter!.isEmpty; + _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(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 */ + Position get vector { + 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({ + 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 00000000..8f6f291f --- /dev/null +++ b/lib/src/polygon_clipping/sweep_event.dart @@ -0,0 +1,182 @@ +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 + +/// 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 + SweepEvent? otherSE; + SweepEvent? consumedBy; + + // Warning: 'point' input will be modified and re-used (for performance + + SweepEvent(this.point, this.isLeft) : id = _nextId++ { + print(point); + if (point.events == null) { + point.events = [this]; + } else { + point.events!.add(this); + } + point = point; + // 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 + 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(Position aPt, Position bPt) { + if (aPt.lng < bPt.lng) return -1; + if (aPt.lng > bPt.lng) 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 Position comparison + if (other.point == point) { + throw Exception('Tried to link already linked events'); + } + if (other.point.events == null) { + throw Exception('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 Exception( + '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!) { + print(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).toDouble(), + 'cosine': + cosineOfAngle(point, baseEvent.point, nextEvent.point).toDouble(), + }; + } + } + + 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; + }; + } + + @override + String toString() { + return 'SweepEvent(id:$id, point=$point, segment=${segment?.id})'; + } +} + + +// class Position { +// double x; +// double y; +// List events; + +// Position(this.lng, this.lat); +// } diff --git a/lib/src/polygon_clipping/sweep_line.dart b/lib/src/polygon_clipping/sweep_line.dart new file mode 100644 index 00000000..9294c2c3 --- /dev/null +++ b/lib/src/polygon_clipping/sweep_line.dart @@ -0,0 +1,167 @@ +import 'dart:collection'; +import 'package:turf/src/geojson.dart'; +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; + 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] = null; + 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.lng}, ${segment.leftSE.point.lat}] -> ' + '[${segment.rightSE.point.lng}, ${segment.rightSE.point.lat}] ' + '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 + Position? 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 + Position? 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(). + Position? 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(PositionEvents.fromPoint(mySplitter!))); + } + + if (newEvents.isNotEmpty) { + tree.remove(segment); + tree[segment] = null; + 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] = null; + } + return newEvents; + } +} diff --git a/lib/src/polygon_clipping/utils.dart b/lib/src/polygon_clipping/utils.dart new file mode 100644 index 00000000..4a510284 --- /dev/null +++ b/lib/src/polygon_clipping/utils.dart @@ -0,0 +1,34 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/src/geojson.dart'; + +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; + + // 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_extension.dart b/lib/src/polygon_clipping/vector_extension.dart new file mode 100644 index 00000000..1d3e86af --- /dev/null +++ b/lib/src/polygon_clipping/vector_extension.dart @@ -0,0 +1,100 @@ +import 'dart:math'; + +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. */ +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.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 = crossProductMagnitude(v1, v2); + if (v1CrossV2 == 0) return null; + + 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.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 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. */ +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 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. */ +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 */ +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) / + vectorLength(vAngle) / + vectorLength(vBase); +} + +/* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */ +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) / + vectorLength(vAngle) / + vectorLength(vBase); +} + +/* 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; +} + +num vectorLength(Position vector) { + return sqrt(vector.dotProduct(vector)); +} diff --git a/pubspec.yaml b/pubspec.yaml index 26a6587b..e25be771 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 diff --git a/test/components/bbox_test.dart b/test/components/bbox_test.dart index e9efefcf..86f0388e 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)); + }); + }); + }); + }); } diff --git a/test/polygon_clipping/flp_test.dart b/test/polygon_clipping/flp_test.dart new file mode 100644 index 00000000..66f489ae --- /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/geom_in_test.dart b/test/polygon_clipping/geom_in_test.dart new file mode 100644 index 00000000..f0746afe --- /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 00000000..1ab3ee19 --- /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/orient_2d_test.dart b/test/polygon_clipping/orient_2d_test.dart new file mode 100644 index 00000000..aa23f35f --- /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/segment_test.dart b/test/polygon_clipping/segment_test.dart new file mode 100644 index 00000000..08882eb3 --- /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 00000000..a45dfff4 --- /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 00000000..5243a017 --- /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); + }); + }); +} diff --git a/test/polygon_clipping/vector_test.dart b/test/polygon_clipping/vector_test.dart new file mode 100644 index 00000000..e6bda25d --- /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); + }); + }); +}