diff --git a/lib/lineclip.dart b/lib/lineclip.dart new file mode 100644 index 00000000..17422c70 --- /dev/null +++ b/lib/lineclip.dart @@ -0,0 +1,4 @@ +library turf_lineclip.dart; + +export 'package:geotypes/geotypes.dart'; +export 'src/lineclip.dart'; \ No newline at end of file diff --git a/lib/src/lineclip.dart b/lib/src/lineclip.dart new file mode 100644 index 00000000..d057f9cf --- /dev/null +++ b/lib/src/lineclip.dart @@ -0,0 +1,134 @@ +import 'package:geotypes/geotypes.dart'; +import 'package:turf/bbox.dart'; + +// Bit code reflects the position relative to the bounding box +int bitCode(Position p, BBox bbox) { + int code = 0; + + if (p.lng < bbox.lng1) { + code |= 1; // left + } else if (p.lng > bbox.lng2) { + code |= 2; // right + } + + if (p.lat < bbox.lat1) { + code |= 4; // bottom + } else if (p.lat > bbox.lat2) { + code |= 8; // top + } + + return code; +} + +// Intersection of a segment against one of the bbox edges +Position intersect(Position a, Position b, int edge, BBox bbox) { + if ((edge & 8) != 0) { + // top + double lng = a.lng + (b.lng - a.lng) * (bbox.lat2 - a.lat) / (b.lat - a.lat); + return Position.named(lat: bbox.lat2, lng: lng); + } else if ((edge & 4) != 0) { + // bottom + double lng = a.lng + (b.lng - a.lng) * (bbox.lat1 - a.lat) / (b.lat - a.lat); + return Position.named(lat: bbox.lat1, lng: lng); + } else if ((edge & 2) != 0) { + // right + double lat = a.lat + (b.lat - a.lat) * (bbox.lng2 - a.lng) / (b.lng - a.lng); + return Position.named(lat: lat, lng: bbox.lng2); + } else if ((edge & 1) != 0) { + // left + double lat = a.lat + (b.lat - a.lat) * (bbox.lng1 - a.lng) / (b.lng - a.lng); + return Position.named(lat: lat, lng: bbox.lng1); + } + + throw Exception("No intersection found"); +} + +// Cohen-Sutherland line clipping for polylines +List> lineclip(List points, BBox bbox, [List>? result]) { + int len = points.length; + int codeA = bitCode(points[0], bbox); + List part = []; + result ??= []; + + for (int i = 1; i < len; i++) { + Position a = points[i - 1]; + Position b = points[i]; + int codeB = bitCode(b, bbox); + int lastCode = codeB; + + while (true) { + if ((codeA | codeB) == 0) { + part.add(a); + if (codeB != lastCode) { + part.add(b); + if (i < len - 1) { + result.add(List.from(part)); + part = []; + } + } else if (i == len - 1) { + part.add(b); + } + break; + } else if ((codeA & codeB) != 0) { + break; + } else if (codeA != 0) { + a = intersect(a, b, codeA, bbox); + codeA = bitCode(a, bbox); + } else { + b = intersect(a, b, codeB, bbox); + codeB = bitCode(b, bbox); + } + } + codeA = lastCode; + } + + if (part.isNotEmpty) result.add(List.from(part)); + return result; +} + +// Sutherland-Hodgman polygon clipping +List polygonclip(List points, BBox bbox) { + List result = []; + int edge; + Position prev; + bool prevInside; + int i; + Position p; + bool inside; + + // First check if all points are outside the bounding box + bool allOutside = points.every((p) { + return (bitCode(p, bbox) != 0); // If bitCode is non-zero, the point is outside + }); + + if (allOutside) { + // If all points are outside, return an empty list + return []; + } + + // Otherwise, proceed with the normal clipping + for (edge = 1; edge <= 8; edge *= 2) { + result = []; + prev = points[points.length - 1]; + prevInside = (bitCode(prev, bbox) & edge) == 0; + + for (i = 0; i < points.length; i++) { + p = points[i]; + inside = (bitCode(p, bbox) & edge) == 0; + + if (inside != prevInside) { + result.add(intersect(prev, p, edge, bbox)); + } + if (inside) result.add(p); + + prev = p; + prevInside = inside; + } + + points = List.from(result); + + if (points.isEmpty) break; + } + + return result; +} \ No newline at end of file diff --git a/lib/turf.dart b/lib/turf.dart index 1ee09fcc..ce9d795f 100644 --- a/lib/turf.dart +++ b/lib/turf.dart @@ -24,6 +24,7 @@ export 'line_segment.dart'; export 'line_slice.dart'; export 'line_slice_along.dart'; export 'line_to_polygon.dart'; +export 'lineclip.dart'; export 'meta.dart'; export 'midpoint.dart'; export 'nearest_point_on_line.dart'; diff --git a/test/components/lineclip_test.dart b/test/components/lineclip_test.dart new file mode 100644 index 00000000..c831b880 --- /dev/null +++ b/test/components/lineclip_test.dart @@ -0,0 +1,100 @@ +import 'package:turf/lineclip.dart'; +import 'package:test/test.dart'; + +void main() { + group('Clipping Tests', () { + test('Line Clipping: Simple case inside bbox', () { + // Define a bounding box + BBox bbox = BBox.named(lat1: 0.0, lng1: 0.0, lat2: 10.0, lng2: 10.0); + + // Define a simple polyline (inside bbox) + List points = [ + Position.named(lat: 1.0, lng: 1.0), + Position.named(lat: 2.0, lng: 2.0), + Position.named(lat: 3.0, lng: 3.0), + ]; + + // Call the line clipping function + List> result = lineclip(points, bbox); + + // Expect the result to be the same as the input (since it's inside the bbox) + expect(result, equals([points])); + }); + + test('Line Clipping: Simple case outside bbox (left)', () { + // Define a bounding box + BBox bbox = BBox.named(lat1: 0.0, lng1: 0.0, lat2: 10.0, lng2: 10.0); + + // Define a polyline that extends outside the bbox (left) + List points = [ + Position.named(lat: -1.0, lng: 5.0), + Position.named(lat: 1.0, lng: 5.0), + ]; + + // Call the line clipping function + List> result = lineclip(points, bbox); + + // We expect one segment with a clipped point at the left edge + expect(result.length, equals(1)); + expect(result[0].length, equals(2)); + expect(result[0][0].lat, equals(0.0)); // The intersection should clip at lat=0.0 + }); + + test('Polygon Clipping: Simple square inside bbox', () { + // Define a bounding box + BBox bbox = BBox.named(lat1: 0.0, lng1: 0.0, lat2: 10.0, lng2: 10.0); + + // Define a square polygon inside the bbox + List points = [ + Position.named(lat: 1.0, lng: 1.0), + Position.named(lat: 1.0, lng: 3.0), + Position.named(lat: 3.0, lng: 3.0), + Position.named(lat: 3.0, lng: 1.0), + ]; + + // Call the polygon clipping function + List result = polygonclip(points, bbox); + + // The result should be the same as the input, since it's inside the bbox + expect(result, equals(points)); + }); + + test('Polygon Clipping: Polygon partially outside bbox', () { + // Define a bounding box + BBox bbox = BBox.named(lat1: 0.0, lng1: 0.0, lat2: 5.0, lng2: 5.0); + + // Define a polygon that partially crosses outside the bbox + List points = [ + Position.named(lat: -1.0, lng: 1.0), // outside bbox + Position.named(lat: 1.0, lng: 1.0), // inside bbox + Position.named(lat: 1.0, lng: 3.0), // inside bbox + Position.named(lat: -1.0, lng: 3.0), // outside bbox + ]; + + // Call the polygon clipping function + List result = polygonclip(points, bbox); + + // We expect the polygon to be clipped to the bounding box + expect(result.length, greaterThan(0)); // Expect the clipped polygon to have some vertices + }); + + test('Polygon Clipping: Polygon fully outside bbox', () { + // Define a bounding box + BBox bbox = BBox.named(lat1: 0.0, lng1: 0.0, lat2: 5.0, lng2: 5.0); + + // Define a polygon that is completely outside the bbox + List points = [ + Position.named(lat: -1.0, lng: -1.0), + Position.named(lat: -1.0, lng: 6.0), + Position.named(lat: 6.0, lng: 6.0), + Position.named(lat: 6.0, lng: -1.0), + ]; + + // Call the polygon clipping function + List result = polygonclip(points, bbox); + + // The result should be an empty list, as the polygon is completely outside + expect(result, equals([])); + }); + }); +} \ No newline at end of file