diff --git a/Progress.md b/Progress.md index 3570f3f..e675056 100644 --- a/Progress.md +++ b/Progress.md @@ -49,7 +49,7 @@ Dart. This is an on going project and functions are being added once needed. If - [ ] intersect - [ ] lineOffset - [x] [polygonSmooth](https://github.com/dartclub/turf_dart/blob/main/lib/src/polygon_smooth.dart) -- [ ] simplify +- [x] [simplify](https://github.com/dartclub/turf_dart/blob/main/lib/src/simplify.dart) - [ ] tesselate - [x] [transformRotate](https://github.com/dartclub/turf_dart/blob/main/lib/src/transform_rotate.dart) - [ ] transformTranslate diff --git a/lib/simplify.dart b/lib/simplify.dart new file mode 100644 index 0000000..16e9b89 --- /dev/null +++ b/lib/simplify.dart @@ -0,0 +1,4 @@ +library turf_simplify; + +export 'package:geotypes/geotypes.dart'; +export 'src/simplify.dart'; diff --git a/lib/src/simplify.dart b/lib/src/simplify.dart new file mode 100644 index 0000000..a2db3ca --- /dev/null +++ b/lib/src/simplify.dart @@ -0,0 +1,125 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/turf.dart'; + +/* + (c) 2013, Vladimir Agafonkin + Simplify.js, a high-performance JS polyline simplification library + mourner.github.io/simplify-js +*/ + +// to suit your point format, run search/replace for '.x' and '.y'; +// for 3D version, see 3d branch (configurability would draw significant performance overhead) + +/// square distance between 2 points +num _getSqDist(Position p1, Position p2) { + var dx = p1.lng - p2.lng, dy = p1.lat - p2.lat; + + return dx * dx + dy * dy; +} + +/// square distance from a point to a segment +num _getSqSegDist(Position p, Position p1, Position p2) { + var x = p1.lng, y = p1.lat, dx = p2.lng - x, dy = p2.lat - y; + + if (dx != 0 || dy != 0) { + var t = ((p.lng - x) * dx + (p.lat - y) * dy) / (dx * dx + dy * dy); + + if (t > 1) { + x = p2.lng; + y = p2.lat; + } else if (t > 0) { + x += dx * t; + y += dy * t; + } + } + + dx = p.lng - x; + dy = p.lat - y; + + return dx * dx + dy * dy; +} +// rest of the code doesn't care about point format + +/// basic distance-based simplification +List _simplifyRadialDist(List points, double sqTolerance) { + var prevPoint = points[0], newPoints = [prevPoint]; + late Position point; + + for (var i = 1, len = points.length; i < len; i++) { + point = points[i]; + + if (_getSqDist(point, prevPoint) > sqTolerance) { + newPoints.add(point); + prevPoint = point; + } + } + + if (prevPoint != point) newPoints.add(point); + + return newPoints; +} + +List _simplifyDPStep(List points, int first, int last, + double sqTolerance, List simplified) { + num maxSqDist = sqTolerance; + late int index; + + for (var i = first + 1; i < last; i++) { + var sqDist = _getSqSegDist(points[i], points[first], points[last]); + + if (sqDist > maxSqDist) { + index = i; + maxSqDist = sqDist; + } + } + + if (maxSqDist > sqTolerance) { + if (index - first > 1) { + simplified = + _simplifyDPStep(points, first, index, sqTolerance, simplified); + } + simplified.add(points[index]); + if (last - index > 1) { + simplified = + _simplifyDPStep(points, index, last, sqTolerance, simplified); + } + } + + return simplified; +} + +/// simplification using Ramer-Douglas-Peucker algorithm +List _simplifyDouglasPeucker(List points, sqTolerance) { + final last = points.length - 1; + + var simplified = [points[0]]; + simplified = _simplifyDPStep(points, 0, last, sqTolerance, simplified); + simplified.add(points[last]); + + return simplified; +} + +/// Simplify a LineString feature using dart port of simplify.js high-performance JS polyline simplification library. +/// +/// both algorithms combined for awesome performance +Feature simplify( + Feature points, { + double tolerance = 1, + bool highestQuality = false, +}) { + var coords = getCoords(points); + if (coords.length <= 2) return points; + if (coords is! List) return points; + + final sqTolerance = tolerance * tolerance; + + coords = highestQuality ? coords : _simplifyRadialDist(coords, sqTolerance); + coords = _simplifyDouglasPeucker(coords, sqTolerance); + + return Feature( + id: points.id, + geometry: LineString(coordinates: coords), + properties: points.properties, + bbox: points.bbox, + ); +} diff --git a/test/components/simplify_test.dart b/test/components/simplify_test.dart new file mode 100644 index 0000000..d400904 --- /dev/null +++ b/test/components/simplify_test.dart @@ -0,0 +1,85 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:turf/along.dart'; +import 'package:turf/area.dart'; +import 'package:turf/simplify.dart'; + +main() { + group( + 'simplify in == out', + () { + var inDir = Directory('./test/examples/simplify/in'); + for (var file in inDir.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.geojson')) { + test( + file.path, + () { + var inSource = file.readAsStringSync(); + var inGeom = Feature.fromJson(jsonDecode(inSource)); + + var inSimplified = simplify( + inGeom, + tolerance: inGeom.properties?['tolerance'] ?? 0.01, + highestQuality: inGeom.properties?['highQuality'] ?? false, + ); + + // ignore: prefer_interpolation_to_compose_strings + var outPath = './' + + file.uri.pathSegments + .sublist(0, file.uri.pathSegments.length - 2) + .join('/') + + '/out/${file.uri.pathSegments.last}'; + + var outSource = File(outPath).readAsStringSync(); + var outGeom = Feature.fromJson(jsonDecode(outSource)); + + final precision = 0.0001; + expect(inSimplified.id, outGeom.id); + expect(inSimplified.properties, equals(outGeom.properties)); + expect(inSimplified.geometry, isNotNull); + expect( + _roundCoords(inSimplified.geometry!.coordinates, precision), + _roundCoords(outGeom.geometry!.coordinates, precision)); + }, + ); + } + } + }, + ); + test( + 'simplify retains id, properties and bbox', + () { + const properties = {"foo": "bar"}; + const id = 12345; + final bbox = BBox(0, 0, 2, 2); + final poly = Feature( + geometry: LineString(coordinates: [ + Position(0, 0), + Position(2, 2), + Position(2, 0), + Position(0, 0), + ]), + properties: properties, + bbox: bbox, + id: id, + ); + final simple = simplify(poly, tolerance: 0.1); + + expect(simple.id, equals(id)); + expect(simple.bbox, equals(bbox)); + expect(simple.properties, equals(properties)); + }, + ); +} + +List _roundCoords(List coords, num precision) { + return coords + .map((p) => Position(_round(p.lng, precision), _round(p.lat, precision))) + .toList(); +} + +num _round(num value, num precision) { + return (value / precision).roundToDouble() * precision; +} diff --git a/test/examples/simplify/in/linestring.geojson b/test/examples/simplify/in/linestring.geojson new file mode 100644 index 0000000..dcdc642 --- /dev/null +++ b/test/examples/simplify/in/linestring.geojson @@ -0,0 +1,89 @@ +{ + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-80.51399230957031, 28.069556808283608], + [-80.51193237304688, 28.057438520876673], + [-80.49819946289062, 28.05622661698537], + [-80.5023193359375, 28.04471284867091], + [-80.48583984375, 28.042288740362853], + [-80.50575256347656, 28.028349057505775], + [-80.50163269042969, 28.02168161433489], + [-80.49476623535156, 28.021075462659883], + [-80.48652648925781, 28.021075462659883], + [-80.47691345214844, 28.021075462659883], + [-80.46936035156249, 28.015619944017807], + [-80.47760009765624, 28.007133032319448], + [-80.49201965332031, 27.998039170620494], + [-80.46730041503906, 27.962262536875905], + [-80.46524047851562, 27.91980029694533], + [-80.40550231933594, 27.930114089618602], + [-80.39657592773438, 27.980455528671527], + [-80.41305541992188, 27.982274659104082], + [-80.42953491210938, 27.990763528690582], + [-80.4144287109375, 28.00955793247135], + [-80.3594970703125, 27.972572275562527], + [-80.36224365234375, 27.948919060105453], + [-80.38215637207031, 27.913732900444284], + [-80.41786193847656, 27.881570017022806], + [-80.40550231933594, 27.860932192608534], + [-80.39382934570312, 27.85425440786446], + [-80.37803649902344, 27.86336037597851], + [-80.38215637207031, 27.880963078302393], + [-80.36842346191405, 27.888246118437756], + [-80.35743713378906, 27.882176952341734], + [-80.35469055175781, 27.86882358965466], + [-80.3594970703125, 27.8421119273228], + [-80.37940979003906, 27.83300417483936], + [-80.39932250976561, 27.82511017099003], + [-80.40069580078125, 27.79352841586229], + [-80.36155700683594, 27.786846483587688], + [-80.35537719726562, 27.794743268514615], + [-80.36705017089844, 27.800209937418252], + [-80.36889553070068, 27.801918215058347], + [-80.3690242767334, 27.803930152059845], + [-80.36713600158691, 27.805942051806845], + [-80.36584854125977, 27.805524490772143], + [-80.36563396453857, 27.80465140342285], + [-80.36619186401367, 27.803095012921272], + [-80.36623477935791, 27.801842292177923], + [-80.36524772644043, 27.80127286888392], + [-80.36224365234375, 27.801158983867033], + [-80.36065578460693, 27.802639479776524], + [-80.36138534545898, 27.803740348273823], + [-80.36220073699951, 27.804803245204976], + [-80.36190032958984, 27.806625330038287], + [-80.3609561920166, 27.80742248254359], + [-80.35932540893555, 27.806853088493792], + [-80.35889625549315, 27.806321651354835], + [-80.35902500152588, 27.805448570411585], + [-80.35863876342773, 27.804461600896783], + [-80.35739421844482, 27.804461600896783], + [-80.35700798034668, 27.805334689771293], + [-80.35696506500244, 27.80673920932572], + [-80.35726547241211, 27.80772615814989], + [-80.35808086395264, 27.808295547623707], + [-80.3585958480835, 27.80928248230861], + [-80.35653591156006, 27.80943431761813], + [-80.35572052001953, 27.808637179875486], + [-80.3555917739868, 27.80772615814989], + [-80.3555917739868, 27.806055931810487], + [-80.35572052001953, 27.803778309057556], + [-80.35537719726562, 27.801804330717825], + [-80.3554630279541, 27.799564581098746], + [-80.35670757293701, 27.799564581098746], + [-80.35499095916748, 27.796831264786892], + [-80.34610748291016, 27.79478123244122], + [-80.34404754638672, 27.802070060660014], + [-80.34748077392578, 27.804955086774896], + [-80.3433609008789, 27.805790211616266], + [-80.34353256225586, 27.8101555324401], + [-80.33499240875244, 27.810079615315917], + [-80.33383369445801, 27.805676331334084], + [-80.33022880554199, 27.801652484744796], + [-80.32872676849365, 27.80848534345178] + ] + } +} diff --git a/test/examples/simplify/out/linestring.geojson b/test/examples/simplify/out/linestring.geojson new file mode 100644 index 0000000..403e2ed --- /dev/null +++ b/test/examples/simplify/out/linestring.geojson @@ -0,0 +1,33 @@ +{ + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-80.513992, 28.069557], + [-80.48584, 28.042289], + [-80.505753, 28.028349], + [-80.476913, 28.021075], + [-80.49202, 27.998039], + [-80.4673, 27.962263], + [-80.46524, 27.9198], + [-80.405502, 27.930114], + [-80.396576, 27.980456], + [-80.429535, 27.990764], + [-80.414429, 28.009558], + [-80.359497, 27.972572], + [-80.382156, 27.913733], + [-80.417862, 27.88157], + [-80.393829, 27.854254], + [-80.368423, 27.888246], + [-80.354691, 27.868824], + [-80.359497, 27.842112], + [-80.399323, 27.82511], + [-80.400696, 27.793528], + [-80.361557, 27.786846], + [-80.359325, 27.806853], + [-80.354991, 27.796831], + [-80.328727, 27.808485] + ] + } +}