From 14321840c3b46310ad2e59b06036671bc4505b97 Mon Sep 17 00:00:00 2001 From: Lukas Himsel Date: Wed, 8 May 2024 20:08:56 +0200 Subject: [PATCH] Prepare for release 0.0.10 (#186) * initial null-safety implementation * attributes non-nullable + tests, fix meta tests * make altitude for Position and BBox nullable * update action deps, build runner * delete conflicting outputs, dart test on PR action * Strongly type and refactor geomEach meta function This commit also renames Geometry to GeometryObject * simplify geomEach * Add coverage reporting to pull requests tests * Allow PR coverage comment to fail This allows PRs from forks to still run the tests successfully * Refactor coverage reporting into its own job The coverage job is allowed to continue on error, letting the entire workflow 'pass' if we hit issues running or reporting coverage * Move coverage reporting into separate workflow * geomEach: nullable fields #36 * fix types in tests * Update to latest romeovs/lcov-reporter-action This should resolve an issue where the 'comment code coverage on PR' action fails after being merged into main. See https://github.com/romeovs/lcov-reporter-action/issues/16 * raise version for release 0.0.3 * [meta] add implementation and test for featureEach and propEach (#24) * Assemble different types, refactor GeoJSON serialization, general improvements (#43) * #19, #20, #21, refactor * convert GeoJSONObjectType to enum, rename * rm overridden bboxes * Implement vector operations #23 * statically type 'round' helper func, fix unit test * gen coverage * fix typo * geojson unit tests, type fixes * fix deserialization with GeoJSONObjectType, implement hashCode * initial unit tests for new constructors * integration tests * fix path * test bbox on other geojson types * update enum tests * required params for new constructors, unit tests * license notices * bump version for release * Add basic benchmarking framework (#51) * Add pub version badge * [meta] add coordeach implementation (#50) * [meta] add coordeach implementation * [benchmark] add coordeach benchmark using test fixtures * Add implementation for getCoord & getCoords // invariant package (#53) * initial getCoord & getCoords implementation * WIP test coords/coord * test gitpod flutter config * ports invariants package (#62) * dart pub get added * wip, type problem * fixed return error of the test * Gitpod config * new test cases, changed comments * standardized the comments * wrapped List in brackets * some minor typo edit * added highlight in documentation * formatted conflicting files * deleted the gitpod files Co-authored-by: Arman Torkzaban Co-authored-by: arman Co-authored-by: arman Co-authored-by: Arman Torkzaban * Add flattenEach function and tests (#48) * Add flattenEach function and tests * Refactor flattenEach slightly * Refactor meta -- breaking of iterations tests * Explicitly do not support nested GeometryCollections * Update documentation links * Use more specific type checks in flattenEach test * rm duplicate fcMixed * remove geometryCollection clause * warning for nested GeometryCollection Co-authored-by: Lukas Himsel * refactor coordEach with geomEach, change type in geomEach callback (#68) * [meta] add coordeach implementation * Rework coordEach implementation to match setup more from geomEach * cleanup * optimize for geometry collection * refactor coordEach with geomEach, change type in geomEach callback Co-authored-by: Tobrun Van Nuland * Port coordAll function and test, also: refactor coordEach callback (#64) * beginning * wip coordAll * coordAll test, WIP * changes CoordEachCallback's signature * typecast removed, test corrected * changes before merging * refactor for Position in CoordEachCallback Co-authored-by: Lukas Himsel * Update README.md * Feature reducers (#49) * Temporarily type geomEach callback to GeometryType * Add geomReduce function and tests * propReduce function * WIP - featureReduce() * WIP * added examples and documentations * flattenReduce WIP * WIP * coordReduce * documentation * add static types * merge readme * prepare new merge * generic types for all reducer functions * TurfJS compatible solution with type checks * add cloning * fixed types passed to callbacks, propReduce test * flattenReduce and featureReduce test * coordReduce * minor type conv. num to int Co-authored-by: Arman Torkzaban Co-authored-by: Lukas Himsel * Port cluster functions and tests (#69) * port functions * getCluster documentation * WIP documentation * Did the documentation * wrote tests * benchmarks - in process * moved benchmarks to separate file * exported clusters.dart Co-authored-by: armantorkzaban * Port meta segment functions & lineSegment (#70) * initial commit to segment file * beginning * lineSegment() * lineSegment * refactor * lineSegment tests * polygon test * wip segmentEach * rewrite segmentEach witohut coordEach * fix flattenEachCallback * combine units * tests for segmentEach * segmentEach id check tests * segmentReduce impltd. * updated README * Documentation * benchmark init Co-authored-by: Arman Torkzaban * split up meta into separate files (#72) * Meta functions extension methods (#73) * initial setup * fix export, add all *each functions * implement 'other' functions * implement *reduce functions * fix tests * rm geojson param for reducers * Update functions and packages in README.md (#67) * WIP - updating README.md * missing new functions & packages * helpers function aint needed anymore - constructor * Links added and unnecessary funtioncs deleted Co-authored-by: Lukas Himsel * Raise version to 0.0.5 * Documentation (#75) * in process - early analysis of the work * linked the meta functions in readme * CONTRIBUTING.md * links to the sources * edited typos * documentation work * closes #77, closes #16 * proofread * improves pub.dev score, closes #79, #78, #75 * Added deleted model, complemented contributing.md Co-authored-by: Lukas Himsel * raise to version 0.0.6 * Improve pub score & raise to version 0.0.6+2 (#82) * add examples, fix segment * callbacks * raise version to 0.0.6+2 * Improve pub score (rename example file, raise to 0.0.6+3) (#84) * add examples, fix segment * callbacks * raise version to 0.0.6+2 * Rename example file, raise to 0.0.6+3 * Implement `nearestPointOn(Multi)Line` and `internal` intersects helper (Attempt 2) (#87) * Implement `nearestPointOn(Multi)Line` (#86) Co-authored-by: Levente Morva * localIndex and index * add documentation * Fix `globalIndex` behaviour Co-authored-by: Levente Morva * fix test, according to new globalIndex behaviour * add intersection test Co-authored-by: Levente Morva * Port explode function and test (#93) * initial version without tests * closes #93 * updated readme for explode * fixed formatting Co-authored-by: armantorkzaban * Merge bbox-polygon and bbox, center, polyline decode (#99) * Added coordEach translation * Added bBox translation * Added center translation * Added decode points from polyline * Update read me * Remove and exclude .idea from git indexing * Convert double to Position * Use existing coord each implementation * Return bBox points in the correct order * Add bbox unit test * Change libraries namespaces * Code refactor * Add doc blocs and some more missing functions * Add encode polyline feature * Add tests for both encode and decode polylines * Add bbox tests * Add helper functions * Convert array to Bbox * Remove redundant helper functions * Add documentation * implementation * tests init, documentation, addition of types and debug * implemented, docs * test initiation * added it to /lib, updated readme, can close 97 * renamed the test file * comments resolved * typo * unused lib removed * merge bbox-polygon * missing center_test.dart * imported bbox_polygon to center_test * test error fixes Co-authored-by: Dennis Mwea Co-authored-by: Lukas Himsel * Added missing alt1, alt2 in BBox constructor, BBox put in README (#100) * Added coordEach translation * Added bBox translation * Added center translation * Added decode points from polyline * Update read me * Remove and exclude .idea from git indexing * Convert double to Position * Use existing coord each implementation * Return bBox points in the correct order * Add bbox unit test * Change libraries namespaces * Code refactor * Add doc blocs and some more missing functions * Add encode polyline feature * Add tests for both encode and decode polylines * Add bbox tests * Add helper functions * Convert array to Bbox * Remove redundant helper functions * Add documentation * implementation * tests init, documentation, addition of types and debug * implemented, docs * test initiation * added it to /lib, updated readme, can close 97 * renamed the test file * comments resolved * typo * unused lib removed * merge bbox-polygon * missing center_test.dart * imported bbox_polygon to center_test * test error fixes * added alt 1,2 to Bbox * added bbox to readme * Fix typos * bbox constructure changed to standard * bbox optional positional param. set accordingly * Documentation on limitation of optional prms. Bbox * Update geojson.dart * fix order of BBox default constructor in test * update doc comment * fix previously missed null exceptions Co-authored-by: Dennis Mwea Co-authored-by: Lukas Himsel * Implement rhumbBearing function and test (#109) * initial commit, stolen from @armantorkzaban * fix formatting * cleanup comments * Ports lineToPolygon, polygonToLine, and their tests (#104) * line_to_polygon ported * test init of lineToPolygon * debugging the test on line to polygon * line_to_polygon_test done * polygonToLine init * check for features' tyope in FeatureCollections * file name change * test and impl. done for polygonToLine * refactor autocomplete, lineStringToPolygon * comments resolved * type added * implementation and test and type setting Co-authored-by: Lukas Himsel * Adds proper documentation to feature-conversion Part I (#115) * line_to_polygon ported * test init of lineToPolygon * debugging the test on line to polygon * line_to_polygon_test done * polygonToLine init * check for features' tyope in FeatureCollections * file name change * test and impl. done for polygonToLine * refactor autocomplete, lineStringToPolygon * comments resolved * type added * implementation and test and type setting * corrected documentation Co-authored-by: Lukas Himsel * Update impl status of feature conversion package * Ports truncate package and its test (#111) * implemented * test init * moving on with the test * test finished. * comments resolved * add truncate reference * Ports clean_coords and its test (#112) * init * implemented * test init - imported * test, moving on * implemented points in comments * resolving comments * test done * readme updated, js removed * fix turf_equality dep, replace forEach with for * update types * trying to fix mutation * refactor, fix tests * another variance * simplified, cleaned comments * seems like it works! Co-authored-by: Lukas Himsel * Debugs GeometryCollection condition in truncate (#121) * implemented * test init * moving on with the test * test finished. * comments resolved * updated readme.md * changed from forEach to for and used the element * changed forEach to a for loop * variable name changed - resolving a comment * rid of redundant case for GeometryCollection, test Co-authored-by: Lukas Himsel * close #116 - add src path to README * update changelog, bump version for release * Added support for the area function (#123) * Added the area function * made the geometry to be of type GeometryType * fixed the test and added a benchmark test * fixed the area docs * added some more tests * Marked functions as private and fixed documentation * fixed documentation * added exception in case of unsupported geometry type, formatted the code and removed some redundant code Co-authored-by: yarden * Implement polygon-smooth and its tests (#127) * initial implementation, and refactor * finished test and benchmark impl * reference in README * Port boolean functions and tests (#91) * in process - early analysis of the work * linked the meta functions in readme * CONTRIBUTING.md * links to the sources * edited typos * documentation work * closes #77, closes #16 * proofread * improves pub.dev score, closes #79, #78, #75 * Added deleted model, complemented contributing.md * booleans init * initial import * boolean_touches * valid - wip * within - init * rewiring the translated code * contains etc. * touches * valid * dependencies imported * Update CONTRIBUTING.md * clockwise test * concave test * intersect test * pointOnLine test * contains * crosses_test - blocked by findIntersections * boolean_contain * boolean_within * rhumb_bearing * rhumb_bearing\'s test - incomplete * rhumb_bearing's test - done, added to lib/bearing * boolean_contains, boolean_parallel, IP * working on contain - removed usages of runtimeType * rewriting pointInLine * tests reverted to false/true folder style * restructuring, clean-up * clockwise and its test, documentation * concave finalized * PointInPolygon plus test, dart format * removed the commented JS lines * return type added * disjoint and test - not done, depends on sweepline * equal implement, test, has dependencies * moving on * clean coords init * cleanCoord implemented, going to new branch * moving on with equal * update turf_equality dep * moving forward * refactors boolean_equal * #88 crosses, disjoint, equal, intersects, pip @lukas-h * cleanup on #88 * progress on overlap * bool: overlap,parallel,valid,touches & lineOverlap * issue with test is worked on * still at porting the overlap test * done: parallel * working on valid * touches done * valid - got stuck * progress on within * moved on with valid, refactored * valid test * lineIntersection testet - contains sweepline * package publishabe, rm git deps, fix equality dep * update params in equality * mod. on valid to cover the std. - MutiPs' 'finite' * RFCs resolved - readied for merge * return type of getGeom * linter effect! * reolved requests * more requests resolved * more requests resolved * test name, touching lineOverlap * fix conversations in booleanContains * fixes conversations in crosses and disjoint * resolves allllllllll outdated conversations, refactor * use getGeom in booleanTouches * use getGeom more frequently * rm linter rule, which is already inherited from dartclub_lint * cleanup, ready for merge Co-authored-by: Lukas Himsel * Add missing parameter 'unit'. (#145) * Implement rhumb_distance with tests. * Implement rhumb_destination with tests. * Implement centroid with tests. * Implement transform_rotate with tests. * Add truncate tests for MultiLineString, MultiPoint, MultiPolygon * run dart format * introduce localCoordIndex in coordEach, cover more test cases in truncate_test * prepare for next release, support * work student position * Fix excludeWrapCoord in coordEach for MultiPolygon type. (#148) * Fix excludeWrapCoord in coordEach for MultiPolygon type. * Added multipolygon test case for centroid. * format markdown * Document properties set by NearestPointOnLine on returned Point (#155) * Implement length and along (#153) * Implement length with tests * Implement along with tests * Fix export added functions * Document along behaviour when distance is outside line length range * Format along test * Change length and along to accept Feature instead of LineString * Change length to never return null. To my understanding, the reason why segmentReduce in its signature may return null is due to initialValue may be null, but in length() we supply 0.0 as the default. * Change along to throw Exception when empty line is passed instead of returning null It appears to be more in line with how turf.js operates and also existing error handling in turf_dart. * Change along to count from back when distance is negative I raised an issue with turf.js about behavour being undefined when distance is zero and they where in favour of counting from back behavour over clamping to the end. I think it is best to use same behavour as upstream turf.js so changing to their solution. * Update along test for negative distance * Fix along test for negative distance * Add along test using default unit * Add test of length() using default unit * Fix docs for along() when distance is negative * Change along() implementation to handle travelled == distance eagerly * Fix length_test.dart formatting (#159) * Change return type of along to Feature (#161) This goes along with preferring to pass around full Features, and it allow to in the future set properties on the returned point without it being a breaking change. * Prepare release 0.0.9 (#162) * Update Dart SDK version in pubspec.yaml * Update README.md * Remove .DS_Store files * Update changelog * Update package version to 0.0.9 * Update dependencies * Entfernt .DS_Store * Remove job offer * Adds lineSlice method (#158) * Implement lineSlice with tests * add line_slice link --------- Co-authored-by: Lukas Himsel * Implement booleanWithin (#167) * document feature booleanWithin * implement boolean within * implement boolean within * add boolean within tests * export boolean features * remove duplicate code * refactor tests * rename import * remove warnings * add source link to readme * add library declaration for turf_boolean * Code review suggestions * . * . * fix typos * fix analyzer warning due to recent dart 3.3.0 release * Implement lineOverlap, booleanOverlap, refactoring and bug fixes (#174) * implement lineOverlap, booleanOverlap, refactoring and bugFixes * minor updates * clearing up nisses mischief * update dependency: turf_equality * Introduce geotypes (#176) * introduce geotypes * removed build runner from workflows * Update readme (#179) * moved progress and update readme * add progress link * add logo * add badges * update url * Fix export strategy (#177) * fix warnings * fix export strategy * remove unused imports * review changes * Add discord community (#181) * Update README.md * Update README.md * Fixed typos in example code for nearestPointOnLine (#184) * prepare for release 0.0.10 --------- Co-authored-by: Brad Parham Co-authored-by: Tobrun Co-authored-by: arman Co-authored-by: arman Co-authored-by: Arman Torkzaban Co-authored-by: Levente Morva Co-authored-by: Dennis Mwea Co-authored-by: yardenfi Co-authored-by: yarden Co-authored-by: Kangmin An Co-authored-by: ggastv Co-authored-by: L Linse Co-authored-by: Leif Linse <118986082+leiflinse-trivector@users.noreply.github.com> Co-authored-by: Jonas Siedentop Co-authored-by: Jonas Siedentop Co-authored-by: Bas van Dijk --- .github/turf-logo.png | Bin 0 -> 21982 bytes .github/workflows/dart-pub-publish-on-pr.yml | 4 - .github/workflows/dart-pub-publish.yml | 4 - .github/workflows/dart-unit-tests-on-pr.yml | 4 - .github/workflows/dart-unit-tests.yml | 13 +- CHANGELOG.md | 6 + Progress.md | 180 +++++ README.md | 201 +---- benchmark/explode_benchmark.dart | 1 - benchmark/line_segment_benchmark.dart | 1 - lib/along.dart | 1 + lib/area.dart | 1 + lib/bbox.dart | 1 + lib/bbox_polygon.dart | 1 + lib/bearing.dart | 1 + lib/boolean.dart | 17 + lib/center.dart | 1 + lib/centroid.dart | 1 + lib/clean_coords.dart | 3 +- lib/clusters.dart | 1 + lib/destination.dart | 1 + lib/distance.dart | 1 + lib/explode.dart | 3 +- lib/extensions.dart | 1 + lib/helpers.dart | 2 +- lib/invariant.dart | 4 + lib/length.dart | 1 + lib/line_intersect.dart | 4 + lib/line_overlap.dart | 4 + lib/line_segment.dart | 1 + lib/line_slice.dart | 4 + lib/line_to_polygon.dart | 3 +- lib/meta.dart | 1 + lib/midpoint.dart | 1 + lib/nearest_point.dart | 1 + lib/nearest_point_on_line.dart | 1 + lib/polygon_smooth.dart | 1 + lib/polygon_to_line.dart | 1 + lib/polyline.dart | 1 + lib/src/along.dart | 16 +- lib/src/bbox.dart | 1 - lib/src/bearing.dart | 3 +- lib/src/booleans/boolean_contains.dart | 134 +--- lib/src/booleans/boolean_crosses.dart | 50 +- lib/src/booleans/boolean_disjoint.dart | 8 +- lib/src/booleans/boolean_helper.dart | 223 ++++++ lib/src/booleans/boolean_intersects.dart | 1 - lib/src/booleans/boolean_overlap.dart | 178 +++++ lib/src/booleans/boolean_point_on_line.dart | 101 ++- lib/src/booleans/boolean_valid.dart | 1 - lib/src/booleans/boolean_within.dart | 86 ++ lib/src/centroid.dart | 4 +- lib/src/clean_coords.dart | 34 +- lib/src/destination.dart | 3 +- lib/src/distance.dart | 2 +- lib/src/explode.dart | 3 +- lib/src/geojson.dart | 737 ------------------ lib/src/geojson.g.dart | 144 ---- lib/src/intersection.dart | 3 +- lib/src/line_overlap.dart | 351 +++++++++ lib/src/line_segment.dart | 13 +- lib/src/line_slice.dart | 48 ++ lib/src/line_to_polygon.dart | 4 +- lib/src/midpoint.dart | 5 +- lib/src/nearest_point.dart | 2 +- lib/src/nearest_point_on_line.dart | 21 +- lib/src/rhumb_destination.dart | 8 +- lib/src/rhumb_distance.dart | 2 +- lib/src/transform_rotate.dart | 1 - lib/transform.dart | 1 + lib/truncate.dart | 3 +- lib/turf.dart | 47 +- pubspec.yaml | 10 +- test/booleans/overlap_test.dart | 205 +++++ test/booleans/parallel_test.dart | 5 +- test/booleans/touches_test.dart | 5 +- test/booleans/valid_test.dart | 9 +- test/booleans/within_test.dart | 98 +++ test/components/bbox_test.dart | 1 - test/components/bearing_test.dart | 1 - test/components/center_test.dart | 3 +- test/components/centroid_test.dart | 1 - test/components/explode_test.dart | 2 +- test/components/geojson_test.dart | 1 - test/components/line_overlap_test.dart | 204 +++++ test/components/line_slice_test.dart | 78 ++ test/components/midpoint_test.dart | 1 - test/components/polyline.dart | 1 - test/components/transform_rotate_test.dart | 1 - test/components/truncate_test.dart | 1 - test/context/helper.dart | 70 ++ test/context/load_test_cases.dart | 101 +++ test/context/matcher.dart | 86 ++ .../equal/test/true/lines-reverse.geojson | 3 + .../equal/test/true/reverse-lines.geojson | 3 + .../line_overlap/in/issue-#901.geojson | 4 +- .../line_overlap/in/partial-overlap.geojson | 46 ++ .../line_overlap/in/partial-overlap2.geojson | 91 +++ .../line_overlap/out/issue-#901.geojson | 115 +-- .../line_overlap/out/partial-overlap.geojson | 79 ++ .../line_overlap/out/partial-overlap2.geojson | 166 ++++ 101 files changed, 2641 insertions(+), 1466 deletions(-) create mode 100644 .github/turf-logo.png create mode 100644 Progress.md create mode 100644 lib/boolean.dart create mode 100644 lib/invariant.dart create mode 100644 lib/line_intersect.dart create mode 100644 lib/line_overlap.dart create mode 100644 lib/line_slice.dart create mode 100644 lib/src/booleans/boolean_helper.dart create mode 100644 lib/src/booleans/boolean_overlap.dart create mode 100644 lib/src/booleans/boolean_within.dart delete mode 100644 lib/src/geojson.dart delete mode 100644 lib/src/geojson.g.dart create mode 100644 lib/src/line_overlap.dart create mode 100644 lib/src/line_slice.dart create mode 100644 test/booleans/overlap_test.dart create mode 100644 test/booleans/within_test.dart create mode 100644 test/components/line_overlap_test.dart create mode 100644 test/components/line_slice_test.dart create mode 100644 test/context/helper.dart create mode 100644 test/context/load_test_cases.dart create mode 100644 test/context/matcher.dart create mode 100644 test/examples/line_overlap/in/partial-overlap.geojson create mode 100644 test/examples/line_overlap/in/partial-overlap2.geojson create mode 100644 test/examples/line_overlap/out/partial-overlap.geojson create mode 100644 test/examples/line_overlap/out/partial-overlap2.geojson diff --git a/.github/turf-logo.png b/.github/turf-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f874967f024d9097ae6aefd478d870b9cc4046ab GIT binary patch literal 21982 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%0AFOB*)?T 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 +
+ TurfDart Logo + + +

A TurfJs-like geospatial analysis library written in pure Dart. +

+
[![pub package](https://img.shields.io/pub/v/turf.svg)](https://pub.dev/packages/turf) +![dart unit tests](https://github.com/dartclub/turf_dart/actions/workflows/dart-unit-tests.yml/badge.svg) +![dart publish](https://github.com/dartclub/turf_dart/actions/workflows/dart-pub-publish.yml/badge.svg) +![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) -THIS PROJECT IS WORK IN PROCESS +

–> Join our Dart / Flutter GIS Community on Discord <–

-A [turf.js](https://github.com/Turfjs/turf)-like geospatial analysis library working with GeoJSON, written in pure Dart. +TurfDart is a Dart library for [spatial analysis](https://en.wikipedia.org/wiki/Spatial_analysis). It includes traditional spatial operations, helper functions for creating GeoJSON data, and data classification and statistics tools. You can use TurfDart in your Flutter applications on the web, mobile and desktop or in pure Dart applications running on the server. -This includes a fully [RFC 7946](https://tools.ietf.org/html/rfc7946)-compliant object-representation and serialization for GeoJSON. +As the foundation, we are using [Geotypes](https://github.com/dartclub/geotypes), a lightweight dart library that provides a strong GeoJSON object model and fully [RFC 7946](https://tools.ietf.org/html/rfc7946) compliant serializers. -Most of the implementation is a direct translation from [turf.js](https://github.com/Turfjs/turf). +Most of the functionality is a translation from [turf.js](https://github.com/Turfjs/turf), the progress can be found [here](Progress.md). ## Get started @@ -59,7 +69,7 @@ void main() { ![polymorphism](https://user-images.githubusercontent.com/10634693/159876354-f9da2f37-02b3-4546-b32a-c0f82c372272.png) -## Notable Design Decisions +### Notable Design Decisions - Nested `GeometryCollections` (as described in [RFC 7946 section 3.1.8](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.8)) @@ -73,181 +83,4 @@ Tests are run with `dart test` and benchmarks can be run with Any new benchmarks must be named `*_benchmark.dart` and reside in the `./benchmark` folder. - -## Components - -### Measurement - -- [x] [along](https://github.com/dartclub/turf_dart/blob/main/lib/src/along.dart) -- [x] [area](https://github.com/dartclub/turf_dart/blob/main/lib/src/area.dart) -- [x] [bbox](https://github.com/dartclub/turf_dart/blob/main/lib/src/bbox.dart) -- [x] [bboxPolygon](https://github.com/dartclub/turf_dart/blob/main/lib/src/bbox_polygon.dart) -- [x] [bearing](https://github.com/dartclub/turf_dart/blob/main/lib/src/bearing.dart) -- [x] [center](https://github.com/Dennis-Mwea/turf_dart/blob/main/lib/src/center.dart) -- [ ] centerOfMass -- [x] [centroid](https://github.com/dartclub/turf_dart/blob/main/lib/src/centroid.dart) -- [x] [destination](https://github.com/dartclub/turf_dart/blob/main/lib/src/destination.dart) -- [x] [distance](https://github.com/dartclub/turf_dart/blob/main/lib/src/distance.dart) -- [ ] envelope -- [x] [length](https://github.com/dartclub/turf_dart/blob/main/lib/src/length.dart) -- [x] [midpoint](https://github.com/dartclub/turf_dart/blob/main/lib/src/midpoint.dart) -- [ ] pointOnFeature -- [ ] polygonTangents -- [ ] pointToLineDistance -- [x] [rhumbBearing](https://github.com/dartclub/turf_dart/blob/main/lib/src/rhumb_bearing.dart) -- [x] [rhumbDestination](https://github.com/dartclub/turf_dart/blob/main/lib/src/rhumb_destination.dart) -- [x] [rhumbDistance](https://github.com/dartclub/turf_dart/blob/main/lib/src/rhumb_distance.dart) -- [ ] square -- [ ] greatCircle - -### Coordinate Mutation - -- [x] [cleanCoords](https://github.com/dartclub/turf_dart/blob/main/lib/src/clean_coords.dart) -- [ ] flip -- [ ] rewind -- [ ] round -- [x] [truncate](https://github.com/dartclub/turf_dart/blob/main/lib/src/truncate.dart) - -### Transformation - -- [ ] bboxClip -- [ ] bezierSpline -- [ ] buffer -- [ ] circle -- [x] clone - implemented as a member function of each [GeoJSONObject] -- [ ] concave -- [ ] convex -- [ ] difference -- [ ] dissolve -- [ ] intersect -- [ ] lineOffset -- [x] [polygonSmooth](https://github.com/dartclub/turf_dart/blob/main/lib/src/polygon_smooth.dart) -- [ ] simplify -- [ ] tesselate -- [x] [transformRotate](https://github.com/dartclub/turf_dart/blob/main/lib/src/transform_rotate.dart) -- [ ] transformTranslate -- [ ] transformScale -- [ ] union -- [ ] voronoi -- [x] [polyLineDecode](https://github.com/dartclub/turf_dart/blob/main/lib/src/polyline.dart) - -### Feature Conversion - -- [ ] combine -- [x] [explode](https://github.com/dartclub/turf_dart/blob/main/lib/src/explode.dart) -- [ ] flatten -- [x] [lineToPolygon](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_to_polygon.dart) -- [ ] polygonize -- [x] [polygonToLine](https://github.com/dartclub/turf_dart/blob/main/lib/src/polygon_to_line.dart) - -### MISC - -- [ ] ellipse -- [ ] kinks -- [ ] lineArc -- [ ] lineChunk -- [ ] [lineIntersect](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_intersect.dart) -- [ ] lineOverlap -- [x] [lineSegment](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_segment.dart) -- [ ] lineSlice -- [ ] lineSliceAlong -- [ ] lineSplit -- [ ] mask -- [x] [nearestPointOnLine](https://github.com/dartclub/turf_dart/blob/main/lib/src/nearest_point_on_line.dart) -- [ ] sector -- [ ] shortestPath -- [ ] unkinkPolygon - -### Random - -- [ ] randomPosition -- [ ] randomPoint -- [ ] randomLineString -- [ ] randomPolygon - -### Data - -- [ ] sample - -### Interpolation - -- [ ] interpolate -- [ ] isobands -- [ ] isolines -- [ ] planepoint -- [ ] tin - -### Joins - -- [ ] pointsWithinPolygon -- [ ] tag - -### Grids - -- [ ] hexGrid -- [ ] pointGrid -- [ ] squareGrid -- [ ] triangleGrid - -### Classification - -- [x] [nearestPoint](https://github.com/dartclub/turf_dart/blob/main/lib/src/nearest_point.dart) - -### Aggregation - -- [ ] collect -- [ ] clustersDbscan -- [ ] clustersKmeans - -### META - -- [x] [coordAll](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/coord.dart) -- [x] [coordEach](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/coord.dart) -- [x] [coordReduce](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/coord.dart) -- [x] [featureEach](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/feature.dart) -- [x] [featureReduce](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/feature.dart) -- [x] [flattenEach](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/flatten.dart) -- [x] [flattenReduce](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/flatten.dart) -- [x] [geomEach](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/geom.dart) -- [x] [geomReduce](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/geom.dart) -- [x] [propEach](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/prop.dart) -- [x] [propReduce](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/prop.dart) -- [x] [segmentEach](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_segment.dart) -- [x] [segmentReduce](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_segment.dart) -- [x] [getCluster](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/cluster.dart) -- [x] [clusterEach](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/cluster.dart) -- [x] [clusterReduce](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/cluster.dart) - -### Invariants - -- [x] [getCoord](https://github.com/dartclub/turf_dart/blob/main/lib/src/meta/coord.dart) -- [x] [getCoords](https://github.com/dartclub/turf_dart/blob/main/lib/src/invariant.dart) -- [x] [getGeom](https://github.com/dartclub/turf_dart/blob/main/lib/src/invariant.dart) - -### Booleans - -- [x] [booleanClockwise](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_clockwise.dart) -- [x] [booleanConcave](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_concave.dart) -- [x] [booleanContains](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_contains.dart) -- [x] [booleanCrosses](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_crosses.dart) -- [x] [booleanDisjoint](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_disjoint.dart) -- [x] [booleanEqual](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_equal.dart) -- [x] [booleanIntersects](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_intersects.dart) -- [ ] booleanOverlap -- [x] [booleanParallel](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_parallel.dart) -- [x] [booleanPointInPolygon](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_point_in_polygon.dart) -- [x] [booleanPointOnLine](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_point_on_line.dart) -- [ ] booleanWithin - -### Unit Conversion - -- [x] [bearingToAzimuth](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) -- [x] [convertArea](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) -- [x] [convertLength](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) -- [x] [degreesToRadians](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) -- [x] [lengthToRadians](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) -- [x] [lengthToDegrees](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) -- [x] [radiansToLength](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) -- [x] [radiansToDegrees](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) -- [ ] toMercator -- [ ] toWgs84 \ No newline at end of file + \ No newline at end of file diff --git a/benchmark/explode_benchmark.dart b/benchmark/explode_benchmark.dart index 52f18084..1e916ae3 100644 --- a/benchmark/explode_benchmark.dart +++ b/benchmark/explode_benchmark.dart @@ -1,5 +1,4 @@ import 'package:benchmark/benchmark.dart'; -import 'package:turf/src/explode.dart'; import 'package:turf/turf.dart'; var poly = Polygon(coordinates: [ diff --git a/benchmark/line_segment_benchmark.dart b/benchmark/line_segment_benchmark.dart index b15ed89d..0bdd9823 100644 --- a/benchmark/line_segment_benchmark.dart +++ b/benchmark/line_segment_benchmark.dart @@ -1,6 +1,5 @@ import 'package:benchmark/benchmark.dart'; import 'package:turf/line_segment.dart'; -import 'package:turf/helpers.dart'; void main() { LineString lineString = LineString( diff --git a/lib/along.dart b/lib/along.dart index d8d18fa5..ab16ced3 100644 --- a/lib/along.dart +++ b/lib/along.dart @@ -1,3 +1,4 @@ library turf_along; +export 'package:geotypes/geotypes.dart'; export "src/along.dart"; diff --git a/lib/area.dart b/lib/area.dart index ebd1ae10..2bf37557 100644 --- a/lib/area.dart +++ b/lib/area.dart @@ -1,3 +1,4 @@ library turf_area; +export 'package:geotypes/geotypes.dart'; export "src/area.dart"; diff --git a/lib/bbox.dart b/lib/bbox.dart index 22703d24..0e4de607 100644 --- a/lib/bbox.dart +++ b/lib/bbox.dart @@ -1,3 +1,4 @@ library turf_bbox; +export 'package:geotypes/geotypes.dart'; export "src/bbox.dart"; diff --git a/lib/bbox_polygon.dart b/lib/bbox_polygon.dart index 3c5c8462..de41f44d 100644 --- a/lib/bbox_polygon.dart +++ b/lib/bbox_polygon.dart @@ -1,3 +1,4 @@ library turf_bbox_polygon.dart; +export 'package:geotypes/geotypes.dart'; export 'src/bbox_polygon.dart'; diff --git a/lib/bearing.dart b/lib/bearing.dart index 64c1d498..fffd2e8b 100644 --- a/lib/bearing.dart +++ b/lib/bearing.dart @@ -1,4 +1,5 @@ library turf_bearing; +export 'package:geotypes/geotypes.dart'; export 'src/bearing.dart'; export 'src/rhumb_bearing.dart'; diff --git a/lib/boolean.dart b/lib/boolean.dart new file mode 100644 index 00000000..7642b662 --- /dev/null +++ b/lib/boolean.dart @@ -0,0 +1,17 @@ +library turf_boolean; + +export 'package:geotypes/geotypes.dart'; +export 'src/booleans/boolean_clockwise.dart'; +export 'src/booleans/boolean_concave.dart'; +export 'src/booleans/boolean_contains.dart'; +export 'src/booleans/boolean_crosses.dart'; +export 'src/booleans/boolean_disjoint.dart'; +export 'src/booleans/boolean_equal.dart'; +export 'src/booleans/boolean_intersects.dart'; +export 'src/booleans/boolean_overlap.dart'; +export 'src/booleans/boolean_parallel.dart'; +export 'src/booleans/boolean_point_in_polygon.dart'; +export 'src/booleans/boolean_point_on_line.dart'; +export 'src/booleans/boolean_touches.dart'; +export 'src/booleans/boolean_valid.dart'; +export 'src/booleans/boolean_within.dart'; diff --git a/lib/center.dart b/lib/center.dart index 898d82d7..afc3874c 100644 --- a/lib/center.dart +++ b/lib/center.dart @@ -1,3 +1,4 @@ library turf_center; +export 'package:geotypes/geotypes.dart'; export 'src/center.dart'; diff --git a/lib/centroid.dart b/lib/centroid.dart index 9cf9ee5b..f1c3a648 100644 --- a/lib/centroid.dart +++ b/lib/centroid.dart @@ -1,3 +1,4 @@ library turf_centroid; +export 'package:geotypes/geotypes.dart'; export 'src/centroid.dart'; diff --git a/lib/clean_coords.dart b/lib/clean_coords.dart index 7c61dea5..701cd629 100644 --- a/lib/clean_coords.dart +++ b/lib/clean_coords.dart @@ -1,3 +1,4 @@ -library clean_coords.dart; +library turf_clean_coords; +export 'package:geotypes/geotypes.dart'; export 'src/clean_coords.dart'; diff --git a/lib/clusters.dart b/lib/clusters.dart index 5e7bdea9..35e6c5f8 100644 --- a/lib/clusters.dart +++ b/lib/clusters.dart @@ -1,3 +1,4 @@ library turf_clusters; +export 'package:geotypes/geotypes.dart'; export 'src/meta/cluster.dart'; diff --git a/lib/destination.dart b/lib/destination.dart index 72965b4e..8d420ecf 100644 --- a/lib/destination.dart +++ b/lib/destination.dart @@ -1,4 +1,5 @@ library turf_destination; +export 'package:geotypes/geotypes.dart'; export 'src/destination.dart'; export 'src/rhumb_destination.dart'; diff --git a/lib/distance.dart b/lib/distance.dart index 0debc57d..e29087ed 100644 --- a/lib/distance.dart +++ b/lib/distance.dart @@ -1,4 +1,5 @@ library turf_distance; +export 'package:geotypes/geotypes.dart'; export 'src/distance.dart'; export 'src/rhumb_distance.dart'; diff --git a/lib/explode.dart b/lib/explode.dart index babaf584..02e49076 100644 --- a/lib/explode.dart +++ b/lib/explode.dart @@ -1,3 +1,4 @@ -library explode; +library turf_explode; +export 'package:geotypes/geotypes.dart'; export 'src/explode.dart'; diff --git a/lib/extensions.dart b/lib/extensions.dart index a0d28853..290e4242 100644 --- a/lib/extensions.dart +++ b/lib/extensions.dart @@ -1,3 +1,4 @@ library turf_extensions; +export 'package:geotypes/geotypes.dart'; export 'src/meta/extensions.dart'; diff --git a/lib/helpers.dart b/lib/helpers.dart index 9398f68a..a12bd6f7 100644 --- a/lib/helpers.dart +++ b/lib/helpers.dart @@ -1,4 +1,4 @@ library turf_helpers; +export 'package:geotypes/geotypes.dart'; export 'src/helpers.dart'; -export 'src/geojson.dart'; diff --git a/lib/invariant.dart b/lib/invariant.dart new file mode 100644 index 00000000..5718d5d4 --- /dev/null +++ b/lib/invariant.dart @@ -0,0 +1,4 @@ +library turf_invariant; + +export 'package:geotypes/geotypes.dart'; +export 'src/invariant.dart'; diff --git a/lib/length.dart b/lib/length.dart index fa72f3ee..0bb35996 100644 --- a/lib/length.dart +++ b/lib/length.dart @@ -1,3 +1,4 @@ library turf_length; +export 'package:geotypes/geotypes.dart'; export "src/length.dart"; diff --git a/lib/line_intersect.dart b/lib/line_intersect.dart new file mode 100644 index 00000000..b77315be --- /dev/null +++ b/lib/line_intersect.dart @@ -0,0 +1,4 @@ +library turf_line_intersect; + +export 'package:geotypes/geotypes.dart'; +export "src/line_intersect.dart"; diff --git a/lib/line_overlap.dart b/lib/line_overlap.dart new file mode 100644 index 00000000..6aabe6f0 --- /dev/null +++ b/lib/line_overlap.dart @@ -0,0 +1,4 @@ +library turf_line_overlap; + +export 'package:geotypes/geotypes.dart'; +export "src/line_overlap.dart"; diff --git a/lib/line_segment.dart b/lib/line_segment.dart index 8afc6343..c839627d 100644 --- a/lib/line_segment.dart +++ b/lib/line_segment.dart @@ -1,3 +1,4 @@ library turf_line_segment; +export 'package:geotypes/geotypes.dart'; export "src/line_segment.dart"; diff --git a/lib/line_slice.dart b/lib/line_slice.dart new file mode 100644 index 00000000..694f675c --- /dev/null +++ b/lib/line_slice.dart @@ -0,0 +1,4 @@ +library turf_line_slice; + +export 'package:geotypes/geotypes.dart'; +export "src/line_slice.dart"; diff --git a/lib/line_to_polygon.dart b/lib/line_to_polygon.dart index 1dfc8be7..2209a6dc 100644 --- a/lib/line_to_polygon.dart +++ b/lib/line_to_polygon.dart @@ -1,3 +1,4 @@ -library turf_line_to_polygon.dart; +library turf_line_to_polygon; +export 'package:geotypes/geotypes.dart'; export 'src/line_to_polygon.dart'; diff --git a/lib/meta.dart b/lib/meta.dart index 4aaa43b7..399bbb0a 100644 --- a/lib/meta.dart +++ b/lib/meta.dart @@ -1,5 +1,6 @@ library turf_meta; +export 'package:geotypes/geotypes.dart'; export 'src/meta/cluster.dart'; export 'src/meta/coord.dart'; export 'src/meta/feature.dart'; diff --git a/lib/midpoint.dart b/lib/midpoint.dart index acd65240..528a03ac 100644 --- a/lib/midpoint.dart +++ b/lib/midpoint.dart @@ -1,3 +1,4 @@ library turf_midpoint; +export 'package:geotypes/geotypes.dart'; export 'src/midpoint.dart'; diff --git a/lib/nearest_point.dart b/lib/nearest_point.dart index ed2a2e93..1467c717 100644 --- a/lib/nearest_point.dart +++ b/lib/nearest_point.dart @@ -1,3 +1,4 @@ library turf_nearest_point; +export 'package:geotypes/geotypes.dart'; export 'src/nearest_point.dart'; diff --git a/lib/nearest_point_on_line.dart b/lib/nearest_point_on_line.dart index 71d7ebc5..ad1e6c3f 100644 --- a/lib/nearest_point_on_line.dart +++ b/lib/nearest_point_on_line.dart @@ -1,3 +1,4 @@ library turf_nearest_point_on_line; +export 'package:geotypes/geotypes.dart'; export 'src/nearest_point_on_line.dart'; diff --git a/lib/polygon_smooth.dart b/lib/polygon_smooth.dart index 7d38df1f..a9331d6c 100644 --- a/lib/polygon_smooth.dart +++ b/lib/polygon_smooth.dart @@ -1,3 +1,4 @@ library turf_polygon_smooth; +export 'package:geotypes/geotypes.dart'; export 'src/polygon_smooth.dart'; diff --git a/lib/polygon_to_line.dart b/lib/polygon_to_line.dart index 0b38e83f..a79f7f37 100644 --- a/lib/polygon_to_line.dart +++ b/lib/polygon_to_line.dart @@ -1,3 +1,4 @@ library turf_polygon_to_line; +export 'package:geotypes/geotypes.dart'; export 'src/polygon_to_line.dart'; diff --git a/lib/polyline.dart b/lib/polyline.dart index 8ef7e96f..760395f3 100644 --- a/lib/polyline.dart +++ b/lib/polyline.dart @@ -1,3 +1,4 @@ library turf_polyline; +export 'package:geotypes/geotypes.dart'; export 'src/polyline.dart'; diff --git a/lib/src/along.dart b/lib/src/along.dart index f4b50267..59124311 100644 --- a/lib/src/along.dart +++ b/lib/src/along.dart @@ -4,8 +4,8 @@ import 'package:turf/bearing.dart'; import 'package:turf/destination.dart'; import 'package:turf/helpers.dart'; import 'package:turf/length.dart'; -import 'package:turf/src/distance.dart' as measure_distance; -import 'package:turf/src/invariant.dart'; +import 'distance.dart' as measure_distance; +import 'invariant.dart'; /// Takes a [line] and returns a [Point] at a specified [distance] along the line. /// @@ -24,16 +24,16 @@ Feature along(Feature line, num distance, if (distance < 0) { distance = max(0, length(line, unit) + distance); } - num travelled = 0; + num traveled = 0; for (int i = 0; i < coords.length; i++) { - if (distance >= travelled && i == coords.length - 1) { + if (distance >= traveled && i == coords.length - 1) { break; } - if (travelled == distance) { + if (traveled == distance) { return Feature(geometry: Point(coordinates: coords[i])); } - if (travelled > distance) { - final overshot = distance - travelled; + if (traveled > distance) { + final overshot = distance - traveled; final direction = bearing(Point(coordinates: coords[i]), Point(coordinates: coords[i - 1])) - 180; @@ -45,7 +45,7 @@ Feature along(Feature line, num distance, ); return Feature(geometry: interpolated); } else { - travelled += measure_distance.distance(Point(coordinates: coords[i]), + traveled += measure_distance.distance(Point(coordinates: coords[i]), Point(coordinates: coords[i + 1]), unit); } } diff --git a/lib/src/bbox.dart b/lib/src/bbox.dart index a7d37ab5..7ac1bb43 100644 --- a/lib/src/bbox.dart +++ b/lib/src/bbox.dart @@ -1,4 +1,3 @@ -import 'package:turf/helpers.dart'; import 'package:turf/meta.dart'; /// Calculates the bounding box for any [geoJson] object, including [FeatureCollection]. diff --git a/lib/src/bearing.dart b/lib/src/bearing.dart index f979375b..4fe75046 100644 --- a/lib/src/bearing.dart +++ b/lib/src/bearing.dart @@ -1,7 +1,6 @@ import 'dart:math'; - -import 'geojson.dart'; import 'helpers.dart'; +import 'package:geotypes/geotypes.dart'; // http://en.wikipedia.org/wiki/Haversine_formula // http://www.movable-type.co.uk/scripts/latlong.html diff --git a/lib/src/booleans/boolean_contains.dart b/lib/src/booleans/boolean_contains.dart index eeeb7260..3e1b6b79 100644 --- a/lib/src/booleans/boolean_contains.dart +++ b/lib/src/booleans/boolean_contains.dart @@ -1,8 +1,5 @@ -import 'package:turf/src/invariant.dart'; import 'package:turf/turf.dart'; - -import 'boolean_point_in_polygon.dart'; -import 'boolean_point_on_line.dart'; +import 'boolean_helper.dart'; /// [booleanContains] returns [true] if the second geometry is completely contained /// by the first geometry. @@ -11,162 +8,75 @@ import 'boolean_point_on_line.dart'; /// [booleanContains] returns the exact opposite result of the [booleanWithin]. /// example: /// ```dart -/// var line = LineString(coordinates: [ +/// final line = LineString(coordinates: [ /// Position.of([1, 1]), /// Position.of([1, 2]), /// Position.of([1, 3]), /// Position.of([1, 4]) /// ]); -/// var point = Point(coordinates: Position.of([1, 2])); +/// final point = Point(coordinates: Position.of([1, 2])); /// booleanContains(line, point); /// //=true /// ``` bool booleanContains(GeoJSONObject feature1, GeoJSONObject feature2) { - var geom1 = getGeom(feature1); - var geom2 = getGeom(feature2); + final geom1 = getGeom(feature1); + final geom2 = getGeom(feature2); - var coords1 = (geom1 as GeometryType).coordinates; - var coords2 = (geom2 as GeometryType).coordinates; - final exception = Exception("{feature2 $geom2 geometry not supported}"); + final coords1 = (geom1 as GeometryType).coordinates; + final coords2 = (geom2 as GeometryType).coordinates; if (geom1 is Point) { if (geom2 is Point) { return coords1 == coords2; } else { - throw exception; + throw GeometryCombinationNotSupported(geom1, geom2); } } else if (geom1 is MultiPoint) { if (geom2 is Point) { - return _isPointInMultiPoint(geom1, geom2); + return isPointInMultiPoint(geom2, geom1); } else if (geom2 is MultiPoint) { - return _isMultiPointInMultiPoint(geom1, geom2); + return isMultiPointInMultiPoint(geom2, geom1); } else { - throw exception; + throw GeometryCombinationNotSupported(geom1, geom2); } } else if (geom1 is LineString) { if (geom2 is Point) { return booleanPointOnLine(geom2, geom1, ignoreEndVertices: true); } else if (geom2 is LineString) { - return _isLineOnLine(geom1, geom2); + return isLineOnLine(geom2, geom1); } else if (geom2 is MultiPoint) { - return _isMultiPointOnLine(geom1, geom2); + return isMultiPointOnLine(geom2, geom1); } else { - throw exception; + throw GeometryCombinationNotSupported(geom1, geom2); } } else if (geom1 is Polygon) { if (geom2 is Point) { return booleanPointInPolygon((geom2).coordinates, geom1, ignoreBoundary: true); } else if (geom2 is LineString) { - return _isLineInPoly(geom1, geom2); + return isLineInPolygon(geom2, geom1); } else if (geom2 is Polygon) { return _isPolyInPoly(geom1, geom2); } else if (geom2 is MultiPoint) { - return _isMultiPointInPoly(geom1, geom2); + return isMultiPointInPolygon(geom2, geom1); } else { - throw exception; + throw GeometryCombinationNotSupported(geom1, geom2); } } else { - throw exception; - } -} - -bool _isPointInMultiPoint(MultiPoint multiPoint, Point pt) { - for (int i = 0; i < multiPoint.coordinates.length; i++) { - if ((multiPoint.coordinates[i] == pt.coordinates)) { - return true; - } - } - return false; -} - -bool _isMultiPointInMultiPoint(MultiPoint multiPoint1, MultiPoint multiPoint2) { - for (Position coord2 in multiPoint2.coordinates) { - bool match = false; - for (Position coord1 in multiPoint1.coordinates) { - if (coord2 == coord1) { - match = true; - } - } - if (!match) return false; - } - return true; -} - -bool _isMultiPointOnLine(LineString lineString, MultiPoint multiPoint) { - var haveFoundInteriorPoint = false; - for (var coord in multiPoint.coordinates) { - if (booleanPointOnLine(Point(coordinates: coord), lineString, - ignoreEndVertices: true)) { - haveFoundInteriorPoint = true; - } - if (!booleanPointOnLine(Point(coordinates: coord), lineString)) { - return false; - } - } - return haveFoundInteriorPoint; -} - -bool _isMultiPointInPoly(Polygon polygon, MultiPoint multiPoint) { - for (var coord in multiPoint.coordinates) { - if (!booleanPointInPolygon(coord, polygon, ignoreBoundary: true)) { - return false; - } - } - return true; -} - -bool _isLineOnLine(LineString lineString1, LineString lineString2) { - var haveFoundInteriorPoint = false; - for (Position coord in lineString2.coordinates) { - if (booleanPointOnLine( - Point(coordinates: coord), - lineString1, - ignoreEndVertices: true, - )) { - haveFoundInteriorPoint = true; - } - if (!booleanPointOnLine( - Point(coordinates: coord), - lineString1, - ignoreEndVertices: false, - )) { - return false; - } - } - return haveFoundInteriorPoint; -} - -bool _isLineInPoly(Polygon polygon, LineString linestring) { - var polyBbox = bbox(polygon); - var lineBbox = bbox(linestring); - if (!_doBBoxesOverlap(polyBbox, lineBbox)) { - return false; - } - for (var i = 0; i < linestring.coordinates.length - 1; i++) { - var midPoint = - midpointRaw(linestring.coordinates[i], linestring.coordinates[i + 1]); - if (booleanPointInPolygon( - midPoint, - polygon, - ignoreBoundary: true, - )) { - return true; - } + throw GeometryCombinationNotSupported(geom1, geom2); } - return false; } /// Is Polygon2 in Polygon1 /// Only takes into account outer rings bool _isPolyInPoly(GeoJSONObject geom1, GeoJSONObject geom2) { - var poly1Bbox = bbox(geom1); - var poly2Bbox = bbox(geom2); + final poly1Bbox = bbox(geom1); + final poly2Bbox = bbox(geom2); if (!_doBBoxesOverlap(poly1Bbox, poly2Bbox)) { return false; } - for (var ring in (geom2 as GeometryType).coordinates) { - for (var coord in ring) { + for (final ring in (geom2 as GeometryType).coordinates) { + for (final coord in ring) { if (!booleanPointInPolygon(coord, geom1)) { return false; } diff --git a/lib/src/booleans/boolean_crosses.dart b/lib/src/booleans/boolean_crosses.dart index 54ccdc54..2ee2064e 100644 --- a/lib/src/booleans/boolean_crosses.dart +++ b/lib/src/booleans/boolean_crosses.dart @@ -4,11 +4,12 @@ import '../../helpers.dart'; import '../line_intersect.dart'; import '../polygon_to_line.dart'; import 'boolean_point_in_polygon.dart'; +import 'boolean_point_on_line.dart'; /// [booleanCrosses] returns [true] if the intersection results in a geometry whose /// dimension is one less than the maximum dimension of the two source geometries /// and the intersection set is interior to both source geometries. -/// [booleanCsses] returns [true] for only [MultiPoint]/[Polygon], [MultiPoint]/[LineString], +/// [booleanCrosses] returns [true] for only [MultiPoint]/[Polygon], [MultiPoint]/[LineString], /// [LineString]/[LineString], [LineString]/[Polygon], and [LineString]/[MultiPolygon] comparisons. /// Other comparisons are not supported as they are outside the OpenGIS Simple /// [Feature]s spec and may give unexpected results. @@ -31,7 +32,7 @@ bool booleanCrosses(GeoJSONObject feature1, GeoJSONObject feature2) { var geom1 = getGeom(feature1); var geom2 = getGeom(feature2); - var exception = Exception("$geom2 is not supperted"); + var exception = Exception("$geom2 is not supported"); if (geom1 is MultiPoint) { if (geom2 is LineString) { return _doMultiPointAndLineStringCross(geom1, geom2); @@ -78,7 +79,7 @@ bool _doMultiPointAndLineStringCross( if (i2 == 0 || i2 == lineString.coordinates.length - 2) { incEndVertices = false; } - if (isPointOnLineSegment( + if (isPointOnLineSegmentCrossesVariant( lineString.coordinates[i2], lineString.coordinates[i2 + 1], multiPoint.coordinates[i], @@ -102,7 +103,7 @@ bool _doLineStringsCross(LineString lineString1, LineString lineString2) { if (i2 == 0 || i2 == lineString2.coordinates.length - 2) { incEndVertices = false; } - if (isPointOnLineSegment( + if (isPointOnLineSegmentCrossesVariant( lineString1.coordinates[i], lineString1.coordinates[i + 1], lineString2.coordinates[i2], @@ -137,44 +138,3 @@ bool _doesMultiPointCrossPoly(MultiPoint multiPoint, Polygon polygon) { return foundExtPoint && foundIntPoint; } - -/// Only takes into account outer rings -/// See http://stackoverflow.com/a/4833823/1979085 -/// lineSegmentStart [Position] of start of line -/// lineSegmentEnd [Position] of end of line -/// pt [Position] of point to check -/// [incEnd] controls whether the [Point] is allowed to fall on the line ends -bool isPointOnLineSegment( - Position lineSegmentStart, - Position lineSegmentEnd, - Position pt, - bool incEnd, -) { - var dxc = pt[0]! - lineSegmentStart[0]!; - var dyc = pt[1]! - lineSegmentStart[1]!; - var dxl = lineSegmentEnd[0]! - lineSegmentStart[0]!; - var dyl = lineSegmentEnd[1]! - lineSegmentStart[1]!; - var cross = dxc * dyl - dyc * dxl; - if (cross != 0) { - return false; - } - if (incEnd) { - if ((dxl).abs() >= (dyl).abs()) { - return dxl > 0 - ? lineSegmentStart[0]! <= pt[0]! && pt[0]! <= lineSegmentEnd[0]! - : lineSegmentEnd[0]! <= pt[0]! && pt[0]! <= lineSegmentStart[0]!; - } - return dyl > 0 - ? lineSegmentStart[1]! <= pt[1]! && pt[1]! <= lineSegmentEnd[1]! - : lineSegmentEnd[1]! <= pt[1]! && pt[1]! <= lineSegmentStart[1]!; - } else { - if ((dxl).abs() >= (dyl).abs()) { - return dxl > 0 - ? lineSegmentStart[0]! < pt[0]! && pt[0]! < lineSegmentEnd[0]! - : lineSegmentEnd[0]! < pt[0]! && pt[0]! < lineSegmentStart[0]!; - } - return dyl > 0 - ? lineSegmentStart[1]! < pt[1]! && pt[1]! < lineSegmentEnd[1]! - : lineSegmentEnd[1]! < pt[1]! && pt[1]! < lineSegmentStart[1]!; - } -} diff --git a/lib/src/booleans/boolean_disjoint.dart b/lib/src/booleans/boolean_disjoint.dart index ff104ca4..0920f448 100644 --- a/lib/src/booleans/boolean_disjoint.dart +++ b/lib/src/booleans/boolean_disjoint.dart @@ -1,10 +1,8 @@ -import 'package:turf/src/booleans/boolean_crosses.dart'; - -import '../../helpers.dart'; +import 'boolean_point_on_line.dart'; +import 'boolean_point_in_polygon.dart'; import '../../meta.dart'; import '../line_intersect.dart'; import '../polygon_to_line.dart'; -import 'boolean_point_in_polygon.dart'; /// Returns [true] if the intersection of the two geometries is an empty set. /// example: @@ -73,7 +71,7 @@ bool _disjoint(GeometryType geom1, GeometryType geom2) { // http://stackoverflow.com/a/11908158/1979085 bool _isPointOnLine(LineString lineString, Point pt) { for (var i = 0; i < lineString.coordinates.length - 1; i++) { - if (isPointOnLineSegment(lineString.coordinates[i], + if (isPointOnLineSegmentCrossesVariant(lineString.coordinates[i], lineString.coordinates[i + 1], pt.coordinates, true)) { return true; } diff --git a/lib/src/booleans/boolean_helper.dart b/lib/src/booleans/boolean_helper.dart new file mode 100644 index 00000000..88eaa5b2 --- /dev/null +++ b/lib/src/booleans/boolean_helper.dart @@ -0,0 +1,223 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/src/bbox.dart'; + +import 'boolean_point_on_line.dart'; +import 'boolean_point_in_polygon.dart'; + +class GeometryNotSupported implements Exception { + final GeometryObject geometry; + GeometryNotSupported(this.geometry); + + @override + String toString() => "geometry not supported ($geometry)."; +} + +class GeometryCombinationNotSupported implements Exception { + final GeometryObject geometry1; + final GeometryObject geometry2; + + GeometryCombinationNotSupported(this.geometry1, this.geometry2); + + @override + String toString() => "geometry not supported ($geometry1, $geometry2)."; +} + +bool isPointInMultiPoint(Point point, MultiPoint multipoint) { + return multipoint.coordinates + .any((position) => position == point.coordinates); +} + +bool isPointOnLine(Point point, LineString line) { + return booleanPointOnLine(point, line, ignoreEndVertices: true); +} + +bool isPointInPolygon(Point point, Polygon polygon) { + return booleanPointInPolygon( + point.coordinates, + polygon, + ignoreBoundary: true, + ); +} + +bool isPointInMultiPolygon(Point point, MultiPolygon polygon) { + return booleanPointInPolygon( + point.coordinates, + polygon, + ignoreBoundary: true, + ); +} + +bool isMultiPointInMultiPoint(MultiPoint points1, MultiPoint points2) { + return points1.coordinates.every( + (point1) => points2.coordinates.any( + (point2) => point1 == point2, + ), + ); +} + +bool isMultiPointOnLine(MultiPoint points, LineString line) { + final allPointsOnLine = points.coordinates.every( + (point) => booleanPointOnLine( + Point(coordinates: point), + line, + ), + ); + if (allPointsOnLine) { + final anyInteriorPoint = points.coordinates.any( + (point) => booleanPointOnLine( + Point(coordinates: point), + line, + ignoreEndVertices: true, + ), + ); + + if (anyInteriorPoint) { + return true; + } + } + return false; +} + +bool isMultiPointInPolygon(MultiPoint points, Polygon polygon) => + _isMultiPointInGeoJsonPolygon(points, polygon); + +bool isMultiPointInMultiPolygon(MultiPoint points, MultiPolygon polygon) => + _isMultiPointInGeoJsonPolygon(points, polygon); + +bool _isMultiPointInGeoJsonPolygon(MultiPoint points, GeoJSONObject polygon) { + final allPointsInsideThePolygon = points.coordinates.every( + (point) => booleanPointInPolygon( + point, + polygon, + ), + ); + + if (allPointsInsideThePolygon) { + final onePointNotOnTheBorder = points.coordinates.any( + (point) => booleanPointInPolygon( + point, + polygon, + ignoreBoundary: true, + ), + ); + + if (onePointNotOnTheBorder) { + return true; + } + } + return false; +} + +bool isLineOnLine(LineString line1, LineString line2) { + return line1.coordinates.every((point) { + return booleanPointOnLine( + Point(coordinates: point), + line2, + ); + }); +} + +bool isLineInPolygon(LineString line, Polygon polygon) => + _isLineInGeoJsonPolygon(line, polygon); + +bool isLineInMultiPolygon(LineString line, MultiPolygon polygon) => + _isLineInGeoJsonPolygon(line, polygon); + +bool _isLineInGeoJsonPolygon(LineString line, GeoJSONObject polygon) { + final boundingBoxOfPolygon = bbox(polygon); + final boundingBoxOfLine = bbox(line); + + if (!_doBBoxesOverlap(boundingBoxOfPolygon, boundingBoxOfLine)) { + return false; + } + + final allPointsInsideThePolygon = line.coordinates.every( + (position) => booleanPointInPolygon( + position, + polygon, + ), + ); + + if (allPointsInsideThePolygon) { + if (_anyLinePointNotOnBoundary(line, polygon)) { + return true; + } + + if (_isLineCrossingThePolygon(line, polygon)) { + return true; + } + } + + return false; +} + +bool _anyLinePointNotOnBoundary(LineString line, GeoJSONObject polygon) { + return line.coordinates.any( + (position) => booleanPointInPolygon( + position, + polygon, + ignoreBoundary: true, + ), + ); +} + +bool _isLineCrossingThePolygon(LineString line, GeoJSONObject polygon) { + List midpoints = List.generate( + line.coordinates.length - 1, + (index) => _getMidpoint( + line.coordinates[index], + line.coordinates[index + 1], + ), + ); + + return midpoints.any( + (position) => booleanPointInPolygon( + position, + polygon, + ignoreBoundary: true, + ), + ); +} + +bool _doBBoxesOverlap(BBox bbox1, BBox bbox2) { + if (bbox1[0]! > bbox2[0]!) return false; + if (bbox1[2]! < bbox2[2]!) return false; + if (bbox1[1]! > bbox2[1]!) return false; + if (bbox1[3]! < bbox2[3]!) return false; + return true; +} + +Position _getMidpoint(Position position1, Position position2) { + return Position( + (position1.lng + position2.lng) / 2, + (position1.lat + position2.lat) / 2, + ); +} + +bool isPolygonInPolygon(Polygon polygon1, Polygon polygon2) => + _isPolygonInGeoJsonPolygon(polygon1, polygon2); + +bool isPolygonInMultiPolygon(Polygon polygon1, MultiPolygon polygon2) => + _isPolygonInGeoJsonPolygon(polygon1, polygon2); + +bool _isPolygonInGeoJsonPolygon( + Polygon polygon1, + GeoJSONObject polygon2, +) { + final boundingBoxOfPolygon1 = bbox(polygon1); + final boundingBoxOfPolygon2 = bbox(polygon2); + if (!_doBBoxesOverlap(boundingBoxOfPolygon2, boundingBoxOfPolygon1)) { + return false; + } + + final positions = polygon1.coordinates[0]; + final anyPointNotInPolygon = positions.any( + (point) => !booleanPointInPolygon(point, polygon2), + ); + + if (anyPointNotInPolygon) { + return false; + } + + return true; +} diff --git a/lib/src/booleans/boolean_intersects.dart b/lib/src/booleans/boolean_intersects.dart index 2c978701..695452a7 100644 --- a/lib/src/booleans/boolean_intersects.dart +++ b/lib/src/booleans/boolean_intersects.dart @@ -1,4 +1,3 @@ -import '../../helpers.dart'; import '../../meta.dart'; import 'boolean_disjoint.dart'; diff --git a/lib/src/booleans/boolean_overlap.dart b/lib/src/booleans/boolean_overlap.dart new file mode 100644 index 00000000..5a12df57 --- /dev/null +++ b/lib/src/booleans/boolean_overlap.dart @@ -0,0 +1,178 @@ +import 'package:turf/line_overlap.dart'; +import 'package:turf/line_segment.dart'; +import 'package:turf/src/invariant.dart'; +import 'package:turf/src/line_intersect.dart'; +import 'package:turf_equality/turf_equality.dart'; +import 'boolean_helper.dart'; + +/// Takes two geometries [firstFeature] and [secondFeature] and checks if they +/// share an common area but are not completely contained by each other. +/// +/// Supported Geometries are `Feature`, `Feature`, +/// `Feature`, `Feature`, `Feature`. +/// Features must be of the same type. LineString/MultiLineString and +/// Polygon/MultiPolygon combinations are supported. If the Geometries are not +/// supported an [GeometryNotSupported] or [GeometryCombinationNotSupported] +/// error is thrown. +/// +/// Returns false if [firstFeature] and [secondFeature] are equal. +/// - MultiPoint: returns Returns true if the two MultiPoints share any point. +/// - LineString: returns true if the two Lines share any line segment. +/// - Polygon: returns true if the two Polygons intersect. +/// +/// Example: +/// ```dart +/// final first = Polygon(coordinates: [ +/// [ +/// Position(0, 0), +/// Position(0, 5), +/// Position(5, 5), +/// Position(5, 0), +/// Position(0, 0) +/// ] +/// ]); +/// final second = Polygon(coordinates: [ +/// [ +/// Position(1, 1), +/// Position(1, 6), +/// Position(6, 6), +/// Position(6, 1), +/// Position(1, 1) +/// ] +/// ]); +/// final third = Polygon(coordinates: [ +/// [ +/// Position(10, 10), +/// Position(10, 15), +/// Position(15, 15), +/// Position(15, 10), +/// Position(10, 10) +/// ] +/// ]); +/// +/// final isOverlapping = booleanOverlap(first, second); +/// final isNotOverlapping = booleanOverlap(second, third); +/// ``` +bool booleanOverlap( + Feature firstFeature, + Feature secondFeature, +) { + final first = getGeom(firstFeature); + final second = getGeom(secondFeature); + + _checkIfGeometryCombinationIsSupported(first, second); + + final eq = Equality( + reversedGeometries: true, + shiftedPolygons: true, + ); + if (eq.compare(first, second)) { + return false; + } + + switch (first.runtimeType) { + case MultiPoint: + switch (second.runtimeType) { + case MultiPoint: + return _isMultiPointOverlapping( + first as MultiPoint, + second as MultiPoint, + ); + default: + throw GeometryCombinationNotSupported(first, second); + } + case MultiLineString: + case LineString: + switch (second.runtimeType) { + case LineString: + case MultiLineString: + return _isLineOverlapping(first, second); + default: + throw GeometryCombinationNotSupported(first, second); + } + case MultiPolygon: + case Polygon: + switch (second.runtimeType) { + case Polygon: + case MultiPolygon: + return _isPolygonOverlapping(first, second); + default: + throw GeometryCombinationNotSupported(first, second); + } + default: + throw GeometryCombinationNotSupported(first, second); + } +} + +bool _isGeometrySupported(GeometryObject geometry) => + geometry is MultiPoint || + geometry is LineString || + geometry is MultiLineString || + geometry is Polygon || + geometry is MultiPolygon; + +void _checkIfGeometryCombinationIsSupported( + GeometryObject first, + GeometryObject second, +) { + if (!_isGeometrySupported(first) || !_isGeometrySupported(second)) { + throw GeometryCombinationNotSupported(first, second); + } +} + +void _checkIfGeometryIsSupported(GeometryObject geometry) { + if (!_isGeometrySupported(geometry)) { + throw GeometryNotSupported(geometry); + } +} + +List> _segmentsOfGeometry(GeometryObject geometry) { + _checkIfGeometryIsSupported(geometry); + List> segments = []; + segmentEach( + geometry, + (Feature segment, _, __, ___, ____) { + segments.add(segment); + }, + ); + return segments; +} + +bool _isLineOverlapping(GeometryObject firstLine, GeometryObject secondLine) { + for (final firstSegment in _segmentsOfGeometry(firstLine)) { + for (final secondSegment in _segmentsOfGeometry(secondLine)) { + if (lineOverlap(firstSegment, secondSegment).features.isNotEmpty) { + return true; + } + } + } + return false; +} + +bool _isPolygonOverlapping( + GeometryObject firstPolygon, + GeometryObject secondPolygon, +) { + for (final firstSegment in _segmentsOfGeometry(firstPolygon)) { + for (final secondSegment in _segmentsOfGeometry(secondPolygon)) { + if (lineIntersect(firstSegment, secondSegment).features.isNotEmpty) { + return true; + } + } + } + return false; +} + +bool _isMultiPointOverlapping( + MultiPoint first, + MultiPoint second, +) { + for (final firstPoint in first.coordinates) { + for (final secondPoint in second.coordinates) { + if (firstPoint == secondPoint) { + return true; + } + } + } + return false; +} diff --git a/lib/src/booleans/boolean_point_on_line.dart b/lib/src/booleans/boolean_point_on_line.dart index ffc0dd5b..e1e8187b 100644 --- a/lib/src/booleans/boolean_point_on_line.dart +++ b/lib/src/booleans/boolean_point_on_line.dart @@ -41,22 +41,30 @@ bool booleanPointOnLine(Point pt, LineString line, return false; } +// ToDo: These variants of isPointOnLineSegment have the +// potential to be brought together. + // See http://stackoverflow.com/a/4833823/1979085 // See https://stackoverflow.com/a/328122/1048847 -/// [pt] is the coord pair of the [Point] to check. +/// [point] is the coord pair of the [Point] to check. /// [excludeBoundary] controls whether the point is allowed to fall on the line ends. /// [epsilon] is the Fractional number to compare with the cross product result. /// Useful for dealing with floating points such as lng/lat points. -bool _isPointOnLineSegment(Position lineSegmentStart, Position lineSegmentEnd, - Position pt, BoundaryType excludeBoundary, num? epsilon) { - var x = pt[0]!; - var y = pt[1]!; - var x1 = lineSegmentStart[0]; - var y1 = lineSegmentStart[1]; - var x2 = lineSegmentEnd[0]; - var y2 = lineSegmentEnd[1]; - var dxc = pt[0]! - x1!; - var dyc = pt[1]! - y1!; +bool _isPointOnLineSegment( + Position start, + Position end, + Position point, + BoundaryType excludeBoundary, + num? epsilon, +) { + var x = point[0]!; + var y = point[1]!; + var x1 = start[0]; + var y1 = start[1]; + var x2 = end[0]; + var y2 = end[1]; + var dxc = point[0]! - x1!; + var dyc = point[1]! - y1!; var dxl = x2! - x1; var dyl = y2! - y1; var cross = dxc * dyl - dyc * dxl; @@ -90,3 +98,74 @@ bool _isPointOnLineSegment(Position lineSegmentStart, Position lineSegmentEnd, } return false; } + +/// Returns if [point] is on the segment between [start] and [end]. +/// Borrowed from `booleanPointOnLine` to speed up the evaluation (instead of +/// using the module as dependency). +/// [start] is the coord pair of start of line, [end] is the coord pair of end +/// of line, and [point] is the coord pair of point to check. +bool isPointOnLineSegmentCleanCoordsVariant( + Position start, + Position end, + Position point, +) { + var x = point.lat; + var y = point.lng; + var startX = start.lat, startY = start.lng; + var endX = end.lat, endY = end.lng; + + var dxc = x - startX; + var dyc = y - startY; + var dxl = endX - startX; + var dyl = endY - startY; + var cross = dxc * dyl - dyc * dxl; + + if (cross != 0) { + return false; + } else if ((dxl).abs() >= (dyl).abs()) { + return dxl > 0 ? startX <= x && x <= endX : endX <= x && x <= startX; + } else { + return dyl > 0 ? startY <= y && y <= endY : endY <= y && y <= startY; + } +} + +/// Only takes into account outer rings +/// See http://stackoverflow.com/a/4833823/1979085 +/// lineSegmentStart [Position] of start of line +/// lineSegmentEnd [Position] of end of line +/// pt [Position] of point to check +/// [incEnd] controls whether the [Point] is allowed to fall on the line ends +bool isPointOnLineSegmentCrossesVariant( + Position start, + Position end, + Position pt, + bool incEnd, +) { + var dxc = pt[0]! - start[0]!; + var dyc = pt[1]! - start[1]!; + var dxl = end[0]! - start[0]!; + var dyl = end[1]! - start[1]!; + var cross = dxc * dyl - dyc * dxl; + if (cross != 0) { + return false; + } + if (incEnd) { + if ((dxl).abs() >= (dyl).abs()) { + return dxl > 0 + ? start[0]! <= pt[0]! && pt[0]! <= end[0]! + : end[0]! <= pt[0]! && pt[0]! <= start[0]!; + } + return dyl > 0 + ? start[1]! <= pt[1]! && pt[1]! <= end[1]! + : end[1]! <= pt[1]! && pt[1]! <= start[1]!; + } else { + if ((dxl).abs() >= (dyl).abs()) { + return dxl > 0 + ? start[0]! < pt[0]! && pt[0]! < end[0]! + : end[0]! < pt[0]! && pt[0]! < start[0]!; + } + return dyl > 0 + ? start[1]! < pt[1]! && pt[1]! < end[1]! + : end[1]! < pt[1]! && pt[1]! < start[1]!; + } +} diff --git a/lib/src/booleans/boolean_valid.dart b/lib/src/booleans/boolean_valid.dart index 5a06e741..cd37dac8 100644 --- a/lib/src/booleans/boolean_valid.dart +++ b/lib/src/booleans/boolean_valid.dart @@ -4,7 +4,6 @@ import 'package:turf/src/booleans/boolean_point_on_line.dart'; import 'package:turf/src/invariant.dart'; import 'package:turf/src/meta/extensions.dart'; -import '../../helpers.dart'; import '../line_intersect.dart'; import 'boolean_crosses.dart'; diff --git a/lib/src/booleans/boolean_within.dart b/lib/src/booleans/boolean_within.dart new file mode 100644 index 00000000..d4697abb --- /dev/null +++ b/lib/src/booleans/boolean_within.dart @@ -0,0 +1,86 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/src/invariant.dart'; + +import 'boolean_helper.dart'; + +/// Returns [true] if the first [GeoJSONObject] is completely within the second [GeoJSONObject]. +/// The interiors of both geometries must intersect and, the interior and boundary +/// of the primary (geometry a) must not intersect the exterior of the secondary +/// (geometry b). [booleanWithin] returns the exact opposite result of [booleanContains]. +/// +/// +/// example: +/// ```dart +/// final point = Point(coordinates: [1, 2]); +/// final line = LineString( +/// coordinates: [ +/// Position.of([1, 1]), +/// Position.of([1, 2]), +/// Position.of([1, 3]), +/// Position.of([1, 4]) +/// ], +/// ); +/// final isWithin = booleanWithin(point, line); // true +/// ``` +bool booleanWithin( + GeoJSONObject feature1, + GeoJSONObject feature2, +) { + final geom1 = getGeom(feature1); + final geom2 = getGeom(feature2); + + switch (geom1.runtimeType) { + case Point: + final point = geom1 as Point; + switch (geom2.runtimeType) { + case MultiPoint: + return isPointInMultiPoint(point, geom2 as MultiPoint); + case LineString: + return isPointOnLine(point, geom2 as LineString); + case Polygon: + return isPointInPolygon(point, geom2 as Polygon); + case MultiPolygon: + return isPointInMultiPolygon(point, geom2 as MultiPolygon); + default: + throw GeometryCombinationNotSupported(geom1, geom2); + } + case MultiPoint: + final multipoint = geom1 as MultiPoint; + switch (geom2.runtimeType) { + case MultiPoint: + return isMultiPointInMultiPoint(multipoint, geom2 as MultiPoint); + case LineString: + return isMultiPointOnLine(multipoint, geom2 as LineString); + case Polygon: + return isMultiPointInPolygon(multipoint, geom2 as Polygon); + case MultiPolygon: + return isMultiPointInMultiPolygon(multipoint, geom2 as MultiPolygon); + default: + throw GeometryCombinationNotSupported(geom1, geom2); + } + case LineString: + final line = geom1 as LineString; + switch (geom2.runtimeType) { + case LineString: + return isLineOnLine(line, geom2 as LineString); + case Polygon: + return isLineInPolygon(line, geom2 as Polygon); + case MultiPolygon: + return isLineInMultiPolygon(line, geom2 as MultiPolygon); + default: + throw GeometryCombinationNotSupported(geom1, geom2); + } + case Polygon: + final polygon = geom1 as Polygon; + switch (geom2.runtimeType) { + case Polygon: + return isPolygonInPolygon(polygon, geom2 as Polygon); + case MultiPolygon: + return isPolygonInMultiPolygon(polygon, geom2 as MultiPolygon); + default: + throw GeometryCombinationNotSupported(geom1, geom2); + } + default: + throw GeometryCombinationNotSupported(geom1, geom2); + } +} diff --git a/lib/src/centroid.dart b/lib/src/centroid.dart index 33874ad1..73f76660 100644 --- a/lib/src/centroid.dart +++ b/lib/src/centroid.dart @@ -1,7 +1,7 @@ -import 'package:turf/helpers.dart'; import 'package:turf/meta.dart'; -/// Takes a [Feature] or a [FeatureCollection] and computes the centroid as the mean of all vertices within the object. +/// Takes a [Feature] or a [FeatureCollection] and computes the centroid as +/// the mean of all vertices within the object. /// /// example: /// ```dart diff --git a/lib/src/clean_coords.dart b/lib/src/clean_coords.dart index 41989a66..78596bd9 100644 --- a/lib/src/clean_coords.dart +++ b/lib/src/clean_coords.dart @@ -1,4 +1,5 @@ import '../helpers.dart'; +import 'booleans/boolean_point_on_line.dart'; import 'invariant.dart'; /// Removes redundant coordinates from any [GeometryType]. @@ -95,8 +96,10 @@ List _cleanLine(List coords, GeoJSONObject geojson) { newPoints.add(coords[i]); newPointsLength = newPoints.length; if (newPointsLength > 2) { - if (isPointOnLineSegment(newPoints[newPointsLength - 3], - newPoints[newPointsLength - 1], newPoints[newPointsLength - 2])) { + if (isPointOnLineSegmentCleanCoordsVariant( + newPoints[newPointsLength - 3], + newPoints[newPointsLength - 1], + newPoints[newPointsLength - 2])) { newPoints.removeAt(newPoints.length - 2); } } @@ -112,34 +115,9 @@ List _cleanLine(List coords, GeoJSONObject geojson) { throw Exception("invalid polygon"); } - if (isPointOnLineSegment(newPoints[newPointsLength - 3], + if (isPointOnLineSegmentCleanCoordsVariant(newPoints[newPointsLength - 3], newPoints[newPointsLength - 1], newPoints[newPointsLength - 2])) { newPoints.removeAt(newPoints.length - 2); } return newPoints; } - -/// Returns if [point] is on the segment between [start] and [end]. -/// Borrowed from `booleanPointOnLine` to speed up the evaluation (instead of -/// using the module as dependency). -/// [start] is the coord pair of start of line, [end] is the coord pair of end -/// of line, and [point] is the coord pair of point to check. -bool isPointOnLineSegment(Position start, Position end, Position point) { - var x = point.lat, y = point.lng; - var startX = start.lat, startY = start.lng; - var endX = end.lat, endY = end.lng; - - var dxc = x - startX; - var dyc = y - startY; - var dxl = endX - startX; - var dyl = endY - startY; - var cross = dxc * dyl - dyc * dxl; - - if (cross != 0) { - return false; - } else if ((dxl).abs() >= (dyl).abs()) { - return dxl > 0 ? startX <= x && x <= endX : endX <= x && x <= startX; - } else { - return dyl > 0 ? startY <= y && y <= endY : endY <= y && y <= startY; - } -} diff --git a/lib/src/destination.dart b/lib/src/destination.dart index 48461b98..3dfb8bdc 100644 --- a/lib/src/destination.dart +++ b/lib/src/destination.dart @@ -1,6 +1,5 @@ import 'dart:math'; - -import 'geojson.dart'; +import 'package:geotypes/geotypes.dart'; import 'helpers.dart'; Position destinationRaw(Position origin, num distance, num bearing, diff --git a/lib/src/distance.dart b/lib/src/distance.dart index f8bd5267..39e042c2 100644 --- a/lib/src/distance.dart +++ b/lib/src/distance.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'geojson.dart'; +import 'package:geotypes/geotypes.dart'; import 'helpers.dart'; //http://en.wikipedia.org/wiki/Haversine_formula diff --git a/lib/src/explode.dart b/lib/src/explode.dart index b2e0811f..b3814868 100644 --- a/lib/src/explode.dart +++ b/lib/src/explode.dart @@ -1,8 +1,7 @@ -import 'package:turf/helpers.dart'; import 'package:turf/meta.dart'; /// Takes a feature or set of features and returns all positions as [Point]s. -/// Takes [GeoJSONObhect] input. +/// Takes [GeoJSONObject] input. /// Returns [FeatureCollection] representing the exploded input features /// Throws [Exception] if it encounters an unknown geometry type /// ```dart diff --git a/lib/src/geojson.dart b/lib/src/geojson.dart deleted file mode 100644 index 619fe127..00000000 --- a/lib/src/geojson.dart +++ /dev/null @@ -1,737 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'geojson.g.dart'; - -@JsonEnum(alwaysCreate: true) -enum GeoJSONObjectType { - @JsonValue('Point') - point, - @JsonValue('MultiPoint') - multiPoint, - @JsonValue('LineString') - lineString, - @JsonValue('MultiLineString') - multiLineString, - @JsonValue('Polygon') - polygon, - @JsonValue('MultiPolygon') - multiPolygon, - @JsonValue('GeometryCollection') - geometryCollection, - @JsonValue('Feature') - feature, - @JsonValue('FeatureCollection') - featureCollection, -} - -abstract class GeoJSONObject { - final GeoJSONObjectType type; - BBox? bbox; - - GeoJSONObject.withType(this.type, {this.bbox}); - - Map serialize(Map map) => { - 'type': _$GeoJSONObjectTypeEnumMap[type], - ...map, - }; - - static GeoJSONObject fromJson(Map json) { - GeoJSONObjectType decoded = json['type'] is GeoJSONObjectType - ? json['type'] - : $enumDecode(_$GeoJSONObjectTypeEnumMap, json['type']); - switch (decoded) { - case GeoJSONObjectType.point: - return Point.fromJson(json); - case GeoJSONObjectType.multiPoint: - return MultiPoint.fromJson(json); - case GeoJSONObjectType.lineString: - return LineString.fromJson(json); - case GeoJSONObjectType.multiLineString: - return MultiLineString.fromJson(json); - case GeoJSONObjectType.polygon: - return Polygon.fromJson(json); - case GeoJSONObjectType.multiPolygon: - return MultiPolygon.fromJson(json); - case GeoJSONObjectType.geometryCollection: - return GeometryCollection.fromJson(json); - case GeoJSONObjectType.feature: - return Feature.fromJson(json); - case GeoJSONObjectType.featureCollection: - return FeatureCollection.fromJson(json); - } - } - - Map toJson(); - - GeoJSONObject clone(); -} - -/// Coordinate types, following https://tools.ietf.org/html/rfc7946#section-4 -abstract class CoordinateType implements Iterable { - final List _items; - - CoordinateType(List list) : _items = List.of(list, growable: false); - - @override - num get first => _items.first; - - @override - num get last => _items.last; - - @override - int get length => _items.length; - - num? operator [](int index) => _items[index]; - - void operator []=(int index, num value) => _items[index] = value; - - @override - bool any(bool Function(num element) test) => _items.any(test); - - @override - List cast() => _items.cast(); - - @override - bool contains(Object? element) => _items.contains(element); - - @override - num elementAt(int index) => _items.elementAt(index); - - @override - bool every(bool Function(num element) test) => _items.every(test); - - @override - Iterable expand(Iterable Function(num element) f) => - _items.expand(f); - - @override - num firstWhere(bool Function(num element) test, {num Function()? orElse}) => - _items.firstWhere(test); - - @override - T fold(T initialValue, T Function(T previousValue, num element) combine) => - _items.fold(initialValue, combine); - - @override - Iterable followedBy(Iterable other) => _items.followedBy(other); - - @override - void forEach(void Function(num element) f) => _items.forEach(f); - - @override - bool get isEmpty => _items.isEmpty; - - @override - bool get isNotEmpty => _items.isNotEmpty; - - @override - Iterator get iterator => _items.iterator; - - @override - String join([String separator = '']) => _items.join(separator); - - @override - num lastWhere(bool Function(num element) test, {num Function()? orElse}) => - _items.lastWhere(test, orElse: orElse); - - @override - Iterable map(T Function(num e) f) => _items.map(f); - - @override - num reduce(num Function(num value, num element) combine) => - _items.reduce(combine); - - @override - num get single => _items.single; - - @override - num singleWhere(bool Function(num element) test, {num Function()? orElse}) => - _items.singleWhere(test, orElse: orElse); - - @override - Iterable skip(int count) => _items.skip(count); - - @override - Iterable skipWhile(bool Function(num value) test) => - _items.skipWhile(test); - - @override - Iterable take(int count) => _items.take(count); - - @override - Iterable takeWhile(bool Function(num value) test) => - _items.takeWhile(test); - - @override - List toList({bool growable = true}) => _items; - - @override - Set toSet() => _items.toSet(); - - @override - Iterable where(bool Function(num element) test) => _items.where(test); - - @override - Iterable whereType() => _items.whereType(); - - List toJson() => _items; - - CoordinateType clone(); - - CoordinateType toSigned(); - - bool get isSigned; - - num _untilSigned(num val, limit) { - if (val > limit) { - return _untilSigned(val - 360, limit); - } else { - return val; - } - } -} - -// Position, as specified here https://tools.ietf.org/html/rfc7946#section-3.1.1 -/// Please make sure, you arrange your parameters like this: -/// 1. Longitude, 2. Latitude, 3. Altitude (optional) -class Position extends CoordinateType { - Position(num lng, num lat, [num? alt]) - : super([ - lng, - lat, - if (alt != null) alt, - ]); - - Position.named({required num lat, required num lng, num? alt}) - : super([ - lng, - lat, - if (alt != null) alt, - ]); - - /// Position.of([, , ]) - Position.of(List list) - : assert(list.length >= 2 && list.length <= 3), - super(list); - - factory Position.fromJson(List list) => Position.of(list); - - Position operator +(Position p) => Position.of([ - lng + p.lng, - lat + p.lat, - if (alt != null && p.alt != null) alt! + p.alt! - ]); - - Position operator -(Position p) => Position.of([ - lng - p.lng, - lat - p.lat, - if (alt != null && p.alt != null) alt! - p.alt!, - ]); - - num dotProduct(Position p) => - (lng * p.lng) + - (lat * p.lat) + - (alt != null && p.alt != null ? (alt! * p.alt!) : 0); - - Position crossProduct(Position p) { - if (alt != null && p.alt != null) { - return Position( - lat * p.alt! - alt! * p.lat, - alt! * p.lng - lng * p.alt!, - lng * p.lat - lat * p.lng, - ); - } - throw Exception('Cross product only implemented for 3 dimensions'); - } - - Position operator *(Position p) => crossProduct(p); - - num get lng => _items[0]; - - num get lat => _items[1]; - - num? get alt => length == 3 ? _items[2] : null; - - @override - bool get isSigned => lng <= 180 && lat <= 90; - - @override - Position toSigned() => Position.named( - lng: _untilSigned(lng, 180), - lat: _untilSigned(lat, 90), - alt: alt, - ); - - @override - Position clone() => Position.of(_items); - - @override - int get hashCode => Object.hashAll(_items); - - @override - bool operator ==(dynamic other) => other is Position - ? lng == other.lng && lat == other.lat && alt == other.alt - : false; -} - -// Bounding box, as specified here https://tools.ietf.org/html/rfc7946#section-5 -/// Please make sure, you arrange your parameters like this: -/// Longitude 1, Latitude 1, Altitude 1 (optional), Longitude 2, Latitude 2, Altitude 2 (optional) -/// You can either specify 4 or 6 parameters -/// If you are using the default constructor with two dimensional positions (lng + lat only), please use the constructor like this: -/// `BBox(lng1, lat1, lng2, lat2);` -class BBox extends CoordinateType { - BBox( - /// longitude 1 - num lng1, - - /// latitude 1 - num lat1, - - /// longitude 2 for 2 dim. positions; altitude 1 for 3 dim. positions - num alt1, - - /// latitude 2 for 2 dim. positions; longitude 2 for 3 dim. positions - num lng2, [ - /// latitude 2 for 3 dim. positions - num? lat2, - - /// altitude 2 for 3 dim. positions - num? alt2, - ]) : super([ - lng1, - lat1, - alt1, - lng2, - if (lat2 != null) lat2, - if (alt2 != null) alt2, - ]); - - BBox.named({ - required num lng1, - required num lat1, - num? alt1, - required num lng2, - required num lat2, - num? alt2, - }) : super([ - lng1, - lat1, - if (alt1 != null) alt1, - lng2, - lat2, - if (alt2 != null) alt2, - ]); - - /// Position.of([, , ]) - BBox.of(List list) - : assert(list.length == 4 || list.length == 6), - super(list); - - factory BBox.fromJson(List list) => BBox.of(list); - - bool get _is3D => length == 6; - - num get lng1 => _items[0]; - - num get lat1 => _items[1]; - - num? get alt1 => _is3D ? _items[2] : null; - - num get lng2 => _items[_is3D ? 3 : 2]; - - num get lat2 => _items[_is3D ? 4 : 3]; - - num? get alt2 => _is3D ? _items[5] : null; - - BBox copyWith({ - num? lng1, - num? lat1, - num? alt1, - num? lat2, - num? lng2, - num? alt2, - }) => - BBox.named( - lng1: lng1 ?? this.lng1, - lat1: lat1 ?? this.lat1, - alt1: alt1 ?? this.alt1, - lng2: lng2 ?? this.lng2, - lat2: lat2 ?? this.lat2, - alt2: alt2 ?? this.alt2, - ); - - @override - BBox clone() => BBox.of(_items); - - @override - bool get isSigned => lng1 <= 180 && lng2 <= 180 && lat1 <= 90 && lat2 <= 90; - - @override - BBox toSigned() => BBox.named( - alt1: alt1, - alt2: alt2, - lat1: _untilSigned(lat1, 90), - lat2: _untilSigned(lat2, 90), - lng1: _untilSigned(lng1, 180), - lng2: _untilSigned(lng2, 180), - ); - - @override - int get hashCode => Object.hashAll(_items); - - @override - bool operator ==(Object other) => other is BBox - ? lng1 == other.lng1 && - lat1 == other.lat1 && - alt1 == other.alt1 && - lng2 == other.lng2 && - lat2 == other.lat2 && - alt2 == other.alt2 - : false; -} - -abstract class GeometryObject extends GeoJSONObject { - GeometryObject.withType(GeoJSONObjectType type, {BBox? bbox}) - : super.withType(type, bbox: bbox); - - static GeometryObject deserialize(Map json) { - return json['type'] == 'GeometryCollection' || - json['type'] == GeoJSONObjectType.geometryCollection - ? GeometryCollection.fromJson(json) - : GeometryType.deserialize(json); - } -} - -abstract class GeometryType extends GeometryObject { - T coordinates; - - GeometryType.withType(this.coordinates, GeoJSONObjectType type, {BBox? bbox}) - : super.withType(type, bbox: bbox); - - static GeometryType deserialize(Map json) { - GeoJSONObjectType decoded = json['type'] is GeoJSONObjectType - ? json['type'] - : $enumDecode(_$GeoJSONObjectTypeEnumMap, json['type']); - switch (decoded) { - case GeoJSONObjectType.point: - return Point.fromJson(json); - case GeoJSONObjectType.multiPoint: - return MultiPoint.fromJson(json); - case GeoJSONObjectType.lineString: - return LineString.fromJson(json); - case GeoJSONObjectType.multiLineString: - return MultiLineString.fromJson(json); - case GeoJSONObjectType.polygon: - return Polygon.fromJson(json); - case GeoJSONObjectType.multiPolygon: - return MultiPolygon.fromJson(json); - case GeoJSONObjectType.geometryCollection: - throw Exception( - 'This implementation does not support nested GeometryCollections'); - default: - throw Exception('${json['type']} is not a valid GeoJSON type'); - } - } - - @override - GeometryType clone(); -} - -/// Point, as specified here https://tools.ietf.org/html/rfc7946#section-3.1.2 -@JsonSerializable(explicitToJson: true) -class Point extends GeometryType { - Point({BBox? bbox, required Position coordinates}) - : super.withType(coordinates, GeoJSONObjectType.point, bbox: bbox); - - factory Point.fromJson(Map json) => _$PointFromJson(json); - - @override - Map toJson() => super.serialize(_$PointToJson(this)); - - @override - Point clone() => Point(coordinates: coordinates.clone(), bbox: bbox?.clone()); - - @override - int get hashCode => Object.hashAll([ - type, - ...coordinates, - if (bbox != null) ...bbox!, - ]); - - @override - bool operator ==(Object other) => - other is Point ? coordinates == other.coordinates : false; -} - -/// MultiPoint, as specified here https://tools.ietf.org/html/rfc7946#section-3.1.3 -@JsonSerializable(explicitToJson: true) -class MultiPoint extends GeometryType> { - MultiPoint({BBox? bbox, List coordinates = const []}) - : super.withType(coordinates, GeoJSONObjectType.multiPoint, bbox: bbox); - - factory MultiPoint.fromJson(Map json) => - _$MultiPointFromJson(json); - - MultiPoint.fromPoints({BBox? bbox, required List points}) - : assert(points.length >= 2), - super.withType(points.map((e) => e.coordinates).toList(), - GeoJSONObjectType.multiPoint, - bbox: bbox); - - @override - Map toJson() => super.serialize(_$MultiPointToJson(this)); - - @override - MultiPoint clone() => MultiPoint( - coordinates: coordinates.map((e) => e.clone()).toList(), - bbox: bbox?.clone(), - ); -} - -/// LineString, as specified here https://tools.ietf.org/html/rfc7946#section-3.1.4 -@JsonSerializable(explicitToJson: true) -class LineString extends GeometryType> { - LineString({BBox? bbox, List coordinates = const []}) - : super.withType(coordinates, GeoJSONObjectType.lineString, bbox: bbox); - - factory LineString.fromJson(Map json) => - _$LineStringFromJson(json); - - LineString.fromPoints({BBox? bbox, required List points}) - : assert(points.length >= 2), - super.withType(points.map((e) => e.coordinates).toList(), - GeoJSONObjectType.lineString, - bbox: bbox); - - @override - Map toJson() => super.serialize(_$LineStringToJson(this)); - - @override - LineString clone() => LineString( - coordinates: coordinates.map((e) => e.clone()).toList(), - bbox: bbox?.clone()); -} - -/// MultiLineString, as specified here https://tools.ietf.org/html/rfc7946#section-3.1.5 -@JsonSerializable(explicitToJson: true) -class MultiLineString extends GeometryType>> { - MultiLineString({BBox? bbox, List> coordinates = const []}) - : super.withType(coordinates, GeoJSONObjectType.multiLineString, - bbox: bbox); - - factory MultiLineString.fromJson(Map json) => - _$MultiLineStringFromJson(json); - - MultiLineString.fromLineStrings( - {BBox? bbox, required List lineStrings}) - : assert(lineStrings.length >= 2), - super.withType(lineStrings.map((e) => e.coordinates).toList(), - GeoJSONObjectType.multiLineString, - bbox: bbox); - - @override - Map toJson() => - super.serialize(_$MultiLineStringToJson(this)); - - @override - MultiLineString clone() => MultiLineString( - coordinates: - coordinates.map((e) => e.map((e) => e.clone()).toList()).toList(), - bbox: bbox?.clone(), - ); -} - -/// Polygon, as specified here https://tools.ietf.org/html/rfc7946#section-3.1.6 -@JsonSerializable(explicitToJson: true) -class Polygon extends GeometryType>> { - Polygon({BBox? bbox, List> coordinates = const []}) - : super.withType(coordinates, GeoJSONObjectType.polygon, bbox: bbox); - - factory Polygon.fromJson(Map json) => - _$PolygonFromJson(json); - - Polygon.fromPoints({BBox? bbox, required List> points}) - : assert(points.expand((list) => list).length >= 3), - super.withType( - points.map((e) => e.map((e) => e.coordinates).toList()).toList(), - GeoJSONObjectType.polygon, - bbox: bbox); - - @override - Map toJson() => super.serialize(_$PolygonToJson(this)); - - @override - Polygon clone() => Polygon( - coordinates: - coordinates.map((e) => e.map((e) => e.clone()).toList()).toList(), - bbox: bbox?.clone(), - ); -} - -/// MultiPolygon, as specified here https://tools.ietf.org/html/rfc7946#section-3.1.7 -@JsonSerializable(explicitToJson: true) -class MultiPolygon extends GeometryType>>> { - MultiPolygon({BBox? bbox, List>> coordinates = const []}) - : super.withType(coordinates, GeoJSONObjectType.multiPolygon, bbox: bbox); - - factory MultiPolygon.fromJson(Map json) => - _$MultiPolygonFromJson(json); - - MultiPolygon.fromPolygons({BBox? bbox, required List polygons}) - : assert(polygons.length >= 2), - super.withType(polygons.map((e) => e.coordinates).toList(), - GeoJSONObjectType.multiPolygon, - bbox: bbox); - - @override - Map toJson() => super.serialize(_$MultiPolygonToJson(this)); - - @override - MultiPolygon clone() => MultiPolygon( - coordinates: coordinates - .map((e) => e.map((e) => e.map((e) => e.clone()).toList()).toList()) - .toList(), - bbox: bbox?.clone(), - ); -} - -/// GeometryCollection, as specified here https://tools.ietf.org/html/rfc7946#section-3.1.8 -@JsonSerializable(explicitToJson: true, createFactory: false) -class GeometryCollection extends GeometryObject { - List geometries; - - GeometryCollection({BBox? bbox, this.geometries = const []}) - : super.withType(GeoJSONObjectType.geometryCollection, bbox: bbox); - - factory GeometryCollection.fromJson(Map json) => - GeometryCollection( - bbox: json['bbox'] == null - ? null - : BBox.fromJson( - (json['bbox'] as List).map((e) => e as num).toList(), - ), - geometries: (json['geometries'] as List?) - ?.map((e) => GeometryType.deserialize(e)) - .toList() ?? - const [], - ); - - @override - Map toJson() => - super.serialize(_$GeometryCollectionToJson(this)); - - @override - GeometryCollection clone() => GeometryCollection( - geometries: geometries.map((e) => e.clone()).toList(), - bbox: bbox?.clone(), - ); -} - -/// Feature, as specified here https://tools.ietf.org/html/rfc7946#section-3.2 -class Feature extends GeoJSONObject { - dynamic id; - Map? properties; - T? geometry; - Map fields; - - Feature({ - BBox? bbox, - this.id, - this.properties = const {}, - this.geometry, - this.fields = const {}, - }) : super.withType(GeoJSONObjectType.feature, bbox: bbox); - - factory Feature.fromJson(Map json) => Feature( - id: json['id'], - geometry: json['geometry'] == null - ? null - : GeometryObject.deserialize(json['geometry']) as T, - properties: json['properties'], - bbox: json['bbox'] == null - ? null - : BBox.fromJson( - (json['bbox'] as List).map((e) => e as num).toList()), - fields: Map.fromEntries( - json.entries.where( - (el) => - el.key != 'geometry' && - el.key != 'properties' && - el.key != 'id', - ), - ), - ); - - dynamic operator [](String key) { - switch (key) { - case 'id': - return id; - case 'properties': - return properties; - case 'geometry': - return geometry; - case 'type': - return type; - case 'bbox': - return bbox; - default: - return fields[key]; - } - } - - @override - int get hashCode => Object.hash(type, id); - - @override - bool operator ==(dynamic other) => other is Feature ? id == other.id : false; - - @override - Map toJson() => super.serialize({ - 'id': id, - 'geometry': geometry!.toJson(), - 'properties': properties, - ...fields, - }); - - @override - Feature clone() => Feature( - geometry: geometry?.clone() as T?, - bbox: bbox?.clone(), - fields: Map.of(fields), - properties: Map.of(properties ?? {}), - id: id, - ); -} - -/// FeatureCollection, as specified here https://tools.ietf.org/html/rfc7946#section-3.3 -class FeatureCollection extends GeoJSONObject { - List> features; - - FeatureCollection({BBox? bbox, this.features = const []}) - : super.withType(GeoJSONObjectType.featureCollection, bbox: bbox); - - factory FeatureCollection.fromJson(Map json) => - FeatureCollection( - bbox: json['bbox'] == null - ? null - : BBox.fromJson( - (json['bbox'] as List).map((e) => e as num).toList()), - features: (json['features'] as List?) - ?.map((e) => Feature.fromJson(e as Map)) - .toList() ?? - const [], - ); - - @override - Map toJson() => super.serialize({ - 'features': features.map((e) => e.toJson()).toList(), - 'bbox': bbox?.toJson(), - }); - - @override - FeatureCollection clone() => FeatureCollection( - features: features.map((e) => e.clone()).toList(), - bbox: bbox?.clone(), - ); -} diff --git a/lib/src/geojson.g.dart b/lib/src/geojson.g.dart deleted file mode 100644 index d8ae8cdf..00000000 --- a/lib/src/geojson.g.dart +++ /dev/null @@ -1,144 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'geojson.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Point _$PointFromJson(Map json) => Point( - bbox: json['bbox'] == null - ? null - : BBox.fromJson( - (json['bbox'] as List).map((e) => e as num).toList()), - coordinates: Position.fromJson( - (json['coordinates'] as List).map((e) => e as num).toList()), - ); - -Map _$PointToJson(Point instance) => { - 'bbox': instance.bbox?.toJson(), - 'coordinates': instance.coordinates.toJson(), - }; - -MultiPoint _$MultiPointFromJson(Map json) => MultiPoint( - bbox: json['bbox'] == null - ? null - : BBox.fromJson( - (json['bbox'] as List).map((e) => e as num).toList()), - coordinates: (json['coordinates'] as List?) - ?.map((e) => Position.fromJson( - (e as List).map((e) => e as num).toList())) - .toList() ?? - const [], - ); - -Map _$MultiPointToJson(MultiPoint instance) => - { - 'bbox': instance.bbox?.toJson(), - 'coordinates': instance.coordinates.map((e) => e.toJson()).toList(), - }; - -LineString _$LineStringFromJson(Map json) => LineString( - bbox: json['bbox'] == null - ? null - : BBox.fromJson( - (json['bbox'] as List).map((e) => e as num).toList()), - coordinates: (json['coordinates'] as List?) - ?.map((e) => Position.fromJson( - (e as List).map((e) => e as num).toList())) - .toList() ?? - const [], - ); - -Map _$LineStringToJson(LineString instance) => - { - 'bbox': instance.bbox?.toJson(), - 'coordinates': instance.coordinates.map((e) => e.toJson()).toList(), - }; - -MultiLineString _$MultiLineStringFromJson(Map json) => - MultiLineString( - bbox: json['bbox'] == null - ? null - : BBox.fromJson( - (json['bbox'] as List).map((e) => e as num).toList()), - coordinates: (json['coordinates'] as List?) - ?.map((e) => (e as List) - .map((e) => Position.fromJson( - (e as List).map((e) => e as num).toList())) - .toList()) - .toList() ?? - const [], - ); - -Map _$MultiLineStringToJson(MultiLineString instance) => - { - 'bbox': instance.bbox?.toJson(), - 'coordinates': instance.coordinates - .map((e) => e.map((e) => e.toJson()).toList()) - .toList(), - }; - -Polygon _$PolygonFromJson(Map json) => Polygon( - bbox: json['bbox'] == null - ? null - : BBox.fromJson( - (json['bbox'] as List).map((e) => e as num).toList()), - coordinates: (json['coordinates'] as List?) - ?.map((e) => (e as List) - .map((e) => Position.fromJson( - (e as List).map((e) => e as num).toList())) - .toList()) - .toList() ?? - const [], - ); - -Map _$PolygonToJson(Polygon instance) => { - 'bbox': instance.bbox?.toJson(), - 'coordinates': instance.coordinates - .map((e) => e.map((e) => e.toJson()).toList()) - .toList(), - }; - -MultiPolygon _$MultiPolygonFromJson(Map json) => MultiPolygon( - bbox: json['bbox'] == null - ? null - : BBox.fromJson( - (json['bbox'] as List).map((e) => e as num).toList()), - coordinates: (json['coordinates'] as List?) - ?.map((e) => (e as List) - .map((e) => (e as List) - .map((e) => Position.fromJson( - (e as List).map((e) => e as num).toList())) - .toList()) - .toList()) - .toList() ?? - const [], - ); - -Map _$MultiPolygonToJson(MultiPolygon instance) => - { - 'bbox': instance.bbox?.toJson(), - 'coordinates': instance.coordinates - .map((e) => e.map((e) => e.map((e) => e.toJson()).toList()).toList()) - .toList(), - }; - -Map _$GeometryCollectionToJson(GeometryCollection instance) => - { - 'type': _$GeoJSONObjectTypeEnumMap[instance.type], - 'bbox': instance.bbox?.toJson(), - 'geometries': instance.geometries.map((e) => e.toJson()).toList(), - }; - -const _$GeoJSONObjectTypeEnumMap = { - GeoJSONObjectType.point: 'Point', - GeoJSONObjectType.multiPoint: 'MultiPoint', - GeoJSONObjectType.lineString: 'LineString', - GeoJSONObjectType.multiLineString: 'MultiLineString', - GeoJSONObjectType.polygon: 'Polygon', - GeoJSONObjectType.multiPolygon: 'MultiPolygon', - GeoJSONObjectType.geometryCollection: 'GeometryCollection', - GeoJSONObjectType.feature: 'Feature', - GeoJSONObjectType.featureCollection: 'FeatureCollection', -}; diff --git a/lib/src/intersection.dart b/lib/src/intersection.dart index 72ecf35a..f8af3e76 100644 --- a/lib/src/intersection.dart +++ b/lib/src/intersection.dart @@ -1,4 +1,5 @@ -import 'geojson.dart'; +// internal functions, not meant to be exposed +import 'package:geotypes/geotypes.dart'; Point? intersects(LineString line1, LineString line2) { if (line1.coordinates.length != 2) { diff --git a/lib/src/line_overlap.dart b/lib/src/line_overlap.dart new file mode 100644 index 00000000..5c7ecca7 --- /dev/null +++ b/lib/src/line_overlap.dart @@ -0,0 +1,351 @@ +import 'package:rbush/rbush.dart'; +import 'package:turf/boolean.dart'; +import 'package:turf/line_segment.dart'; +import 'package:turf/meta.dart'; +import 'package:turf/src/booleans/boolean_helper.dart'; +import '../helpers.dart'; +import 'destination.dart'; +import 'invariant.dart'; +import 'nearest_point_on_line.dart'; + +/// Takes any [LineString], [MultiLineString], [Polygon] or [MultiPolygon] and +/// returns the overlapping lines between both features. +/// [feature1] first feature +/// [feature2] second feature +/// [tolerance] tolerance distance to match overlapping line segments, default is 0 +/// [unit] the unit in which the tolerance is expressed, default is kilometers +/// returns [FeatureCollection] lines(s) that are overlapping between both features +/// +/// Example +/// ```dart +/// final line1 = lineString([[115, -35], [125, -30], [135, -30], [145, -35]]); +/// final line2 = lineString([[115, -25], [125, -30], [135, -30], [145, -25]]); +/// final overlapping = lineOverlap(line1, line2); +/// ``` +FeatureCollection lineOverlap( + Feature feature1, + Feature feature2, { + num tolerance = 0, + Unit unit = Unit.kilometers, +}) { + if (!_isGeometrySupported(getGeom(feature1)) || + !_isGeometrySupported(getGeom(feature1))) { + throw GeometryCombinationNotSupported( + feature1.geometry!, feature2.geometry!); + } + + final result = >[]; + final tree = _FeatureRBush.create(lineSegment(feature1), tolerance, unit); + + // Iterate over segments of feature1 + segmentEach(feature2, (Feature segmentF2, _, __, ___, ____) { + // detect segments of feature1, that falls within the same + // bonds of the current feature2 segment + featureEach(tree.searchArea(segmentF2), (Feature current, _) { + final segmentF1 = current as Feature; + + // Are the current segments equal? + if (booleanEqual(segmentF2.geometry!, segmentF1.geometry!)) { + // add the complete segment to the result + _addSegmentToResult(result, segmentF2); + // continue with the next feature2 segment + return false; + } + + // Is the segment of feature2 a subset of the feature1 segment? + if (_isSegmentOnLine(segmentF2, segmentF1, tolerance, unit)) { + // add the complete segment to the result + _addSegmentToResult(result, segmentF2); + // continue with the next feature2 segment + return false; + } + + // Is the segment of feature1 a subset of the feature2 segment? + if (_isSegmentOnLine(segmentF1, segmentF2, tolerance, unit)) { + // add only the overlapping part + _addSegmentToResult(result, segmentF1); + // and continue with the next feature1 segment + return true; + } + + // If the segments of feature1 and feature2 didn't share any point and + // the lines are overlapping partially, then we need to create a new + // line segment with the overlapping part and add it to the result. + final overlappingPart = + _getOverlappingPart(segmentF2, segmentF1, tolerance, unit) ?? + _getOverlappingPart(segmentF1, segmentF2, tolerance, unit); + if (overlappingPart != null) { + // add only the overlapping part + _addSegmentToResult(result, overlappingPart); + // and continue with the next feature1 segment + return true; + } + }); + }); + + return FeatureCollection(features: result); +} + +// If both lines didn't share any point, but +// - the start point of the second line is on the first line and +// - the end point of the first line is on the second line and +// - startPoint and endPoint are different, +// we can assume, that both lines are overlapping partially. +// first: .-----------. +// second: .-----------. +// startPoint: . +// endPoint: . +// This solves the issue #901 and #2580 of TurfJs. +Feature? _getOverlappingPart( + Feature first, + Feature second, + num tolerance, + Unit unit, +) { + final firstCoords = _getCoorsSorted(first); + final secondCoords = _getCoorsSorted(second); + final startPoint = Point(coordinates: secondCoords.first); + final endPoint = Point(coordinates: firstCoords.last); + + assert(firstCoords.length == 2, 'only 2 vertex lines are supported'); + assert(secondCoords.length == 2, 'only 2 vertex lines are supported'); + + if (startPoint != endPoint && + _isPointOnLine(startPoint, first, tolerance, unit) && + _isPointOnLine(endPoint, second, tolerance, unit)) { + return Feature( + geometry: LineString(coordinates: [ + startPoint.coordinates, + endPoint.coordinates, + ]), + ); + } + return null; +} + +List _getCoorsSorted(Feature feature) { + final positions = getCoords(feature) as List; + positions.sort((a, b) => a.lng < b.lng + ? -1 + : a.lng > b.lng + ? 1 + : 0); + return positions; +} + +bool _isPointOnLine( + Point point, + Feature line, + num tolerance, + Unit unit, +) { + final lineString = line.geometry as LineString; + + if (tolerance == 0) { + return booleanPointOnLine(point, lineString); + } + final nearestPoint = nearestPointOnLine(lineString, point, unit); + return nearestPoint.properties!['dist'] <= tolerance; +} + +bool _isSegmentOnLine( + Feature segment, + Feature line, + num tolerance, + Unit unit, +) { + final segmentCoords = getCoords(segment) as List; + for (var i = 0; i < segmentCoords.length; i++) { + final point = Point(coordinates: segmentCoords[i]); + if (!_isPointOnLine(point, line, tolerance, unit)) { + return false; + } + } + return true; +} + +void _addSegmentToResult( + List> result, + Feature segment, +) { + // Only add the geometry to the result and remove the feature meta data + final lineSegment = Feature(geometry: segment.geometry); + + // find the feature that can be concatenated with the current segment + for (var i = result.length - 1; i >= 0; i--) { + final combined = _concat(result[i], lineSegment); + if (combined != null) { + result[i] = combined; + return; + } + } + // if no feature was found, add the segment as a new feature + result.add(lineSegment); +} + +Feature? _concat( + Feature line, + Feature segment, +) { + final lineCoords = getCoords(line) as List; + final segmentCoords = getCoords(segment) as List; + assert(lineCoords.length >= 2, 'line must have at least two coordinates.'); + assert(segmentCoords.length == 2, 'segment must have two coordinates.'); + + final lineStart = lineCoords.first; + final lineEnd = lineCoords.last; + final segmentStart = segmentCoords.first; + final segmentEnd = segmentCoords.last; + + List linePositions = + (line.geometry as LineString).clone().coordinates; + + if (segmentStart == lineStart) { + linePositions.insert(0, segmentEnd); + } else if (segmentEnd == lineStart) { + linePositions.insert(0, segmentStart); + } else if (segmentStart == lineEnd) { + linePositions.add(segmentEnd); + } else if (segmentEnd == lineEnd) { + linePositions.add(segmentStart); + } else { + // Segment couldn't be concatenated, because the segment didn't + // share any point with the line. + return null; + } + + return Feature(geometry: LineString(coordinates: linePositions)); +} + +// The RBush Package generally supports own types for the spatial index. +// Something like RBushBase> should be possible, but +// I had problems to get it to work. This is a workaround until I have the +// time to figure out how to use the RBush Package with the Feature +class _FeatureRBush { + _FeatureRBush._( + List>> segments, + this.tolerance, + this.unit, + ) { + _tree = RBushBase>>( + maxEntries: 4, + toBBox: (segment) => _boundingBoxOf(segment), + getMinX: (segment) => _boundingBoxOf(segment).minX, + getMinY: (segment) => _boundingBoxOf(segment).minY, + ); + _tree.load(segments); + } + + late RBushBase>> _tree; + final num tolerance; + final Unit unit; + + static _FeatureRBush create( + FeatureCollection segments, + num tolerance, + Unit unit, + ) { + final converted = segments.features + .map((e) => (e.geometry as LineString) + .coordinates + .map((e) => [e.lng.toDouble(), e.lat.toDouble()]) + .toList()) + .toList(); + + return _FeatureRBush._(converted, tolerance, unit); + } + + FeatureCollection searchArea(Feature segment) { + final coordinates = segment.geometry!.coordinates + .map((e) => [e.lng.toDouble(), e.lat.toDouble()]) + .toList(); + return _buildFeatureCollection( + _tree.search( + _boundingBoxOf(coordinates), + ), + ); + } + + FeatureCollection _buildFeatureCollection( + List>> result, + ) { + return FeatureCollection( + features: result + .map( + (e) => Feature( + geometry: LineString( + coordinates: e.map((e) => Position.of(e)).toList(), + ), + ), + ) + .toList(), + ); + } + + RBushBox _withTolerance(RBushBox box) { + final min = destination( + destination( + Point( + coordinates: Position.named( + lat: box.minX, + lng: box.minY, + ), + ), + tolerance, + 180, + unit, + ), + tolerance, + 270, + unit, + ); + + final max = destination( + destination( + Point( + coordinates: Position.named( + lat: box.maxX, + lng: box.maxY, + ), + ), + tolerance, + 0, + unit), + tolerance, + 90, + unit, + ); + + return RBushBox( + minX: min.coordinates.lat.toDouble(), + minY: min.coordinates.lng.toDouble(), + maxX: max.coordinates.lat.toDouble(), + maxY: max.coordinates.lng.toDouble(), + ); + } + + RBushBox _boundingBoxOf(List> coordinates) { + final box = RBushBox(); + + for (List coordinate in coordinates) { + box.extend(RBushBox( + minX: coordinate[1], // lat1 + minY: coordinate[0], // lng1 + maxX: coordinate[1], // lat2 + maxY: coordinate[0], // lng2 + )); + } + + return tolerance == 0 ? box : _withTolerance(box); + } +} + +bool _isGeometrySupported(GeometryObject geometry) { + if (geometry is LineString || + geometry is MultiLineString || + geometry is Polygon || + geometry is MultiPolygon) { + return true; + } + return false; +} diff --git a/lib/src/line_segment.dart b/lib/src/line_segment.dart index ba7dd164..444a4e80 100644 --- a/lib/src/line_segment.dart +++ b/lib/src/line_segment.dart @@ -1,7 +1,6 @@ -import 'package:turf/src/meta/coord.dart'; -import 'package:turf/src/meta/flatten.dart'; - -import 'geojson.dart'; +import 'meta/coord.dart'; +import 'meta/flatten.dart'; +import 'package:geotypes/geotypes.dart'; /// Creates a [FeatureCollection] of 2-vertex [LineString] segments from a /// [LineString] or [MultiLineString] or [Polygon] and [MultiPolygon] @@ -92,7 +91,7 @@ void segmentEach( } if (geometry != null && combineNestedGeometries) { - segmentIndex = _segmentEachforEachUnit( + segmentIndex = _segmentEachForEachUnit( geometry, callback, currentFeature.properties, @@ -112,7 +111,7 @@ void segmentEach( for (int i = 0; i < coords.length; i++) { var line = LineString(coordinates: coords[i]); - segmentIndex = _segmentEachforEachUnit( + segmentIndex = _segmentEachForEachUnit( line, callback, currentFeature.properties, @@ -126,7 +125,7 @@ void segmentEach( ); } -int _segmentEachforEachUnit( +int _segmentEachForEachUnit( GeometryType geometry, SegmentEachCallback callback, Map? currentProperties, diff --git a/lib/src/line_slice.dart b/lib/src/line_slice.dart new file mode 100644 index 00000000..d5fdf943 --- /dev/null +++ b/lib/src/line_slice.dart @@ -0,0 +1,48 @@ +import 'package:geotypes/geotypes.dart'; + +import 'nearest_point_on_line.dart'; +import 'invariant.dart'; + +/// Takes a [line], at a start point [startPt], and a stop point [stopPt] +/// and returns a subsection of the line in-between those points. +/// The start & stop points don't need to fall exactly on the line. +/// +/// If [startPt] and [stopPt] resolve to the same point on [line], null is returned +/// as the resolved line would only contain one point which isn't supported by LineString. +/// +/// This can be useful for extracting only the part of a route between waypoints. +Feature lineSlice( + Feature startPt, Feature stopPt, Feature line) { + final coords = line.geometry; + final startPtGeometry = startPt.geometry; + final stopPtGeometry = stopPt.geometry; + if (coords == null) { + throw Exception('line has no geometry'); + } + if (startPtGeometry == null) { + throw Exception('startPt has no geometry'); + } + if (stopPtGeometry == null) { + throw Exception('stopPt has no geometry'); + } + + final startVertex = nearestPointOnLine(coords, startPtGeometry); + final stopVertex = nearestPointOnLine(coords, stopPtGeometry); + late final List> ends; + if (startVertex.properties!['index'] <= stopVertex.properties!['index']) { + ends = [startVertex, stopVertex]; + } else { + ends = [stopVertex, startVertex]; + } + final List clipCoords = [getCoord(ends[0])]; + for (var i = ends[0].properties!['index'] + 1; + i < ends[1].properties!['index']; + i++) { + clipCoords.add(coords.coordinates[i]); + } + clipCoords.add(getCoord(ends[1])); + return Feature( + geometry: LineString(coordinates: clipCoords), + properties: line.properties, + ); +} diff --git a/lib/src/line_to_polygon.dart b/lib/src/line_to_polygon.dart index bf537dad..d76072e1 100644 --- a/lib/src/line_to_polygon.dart +++ b/lib/src/line_to_polygon.dart @@ -1,5 +1,4 @@ import 'package:turf/bbox.dart'; -import 'package:turf/helpers.dart'; import 'package:turf/meta.dart'; import 'package:turf/src/invariant.dart'; @@ -60,10 +59,9 @@ Feature lineToPolygon( ...list, ...currentGeometry.coordinates .map((e) => e.map((p) => p.clone()).toList()) - .toList() ]; } else { - throw Exception("$currentGeometry type is not supperted"); + throw Exception("$currentGeometry type is not supported"); } }, ); diff --git a/lib/src/midpoint.dart b/lib/src/midpoint.dart index 71764572..e571348d 100644 --- a/lib/src/midpoint.dart +++ b/lib/src/midpoint.dart @@ -1,7 +1,7 @@ import 'bearing.dart'; import 'destination.dart'; import 'distance.dart'; -import 'geojson.dart'; +import 'package:geotypes/geotypes.dart'; Position midpointRaw(Position point1, Position point2) { var dist = distanceRaw(point1, point2); @@ -12,7 +12,8 @@ Position midpointRaw(Position point1, Position point2) { } /// Takes two [Point]s and returns a point midway between them. -/// The midpoint is calculated geodesically, meaning the curvature of the earth is taken into account. +/// The midpoint is calculated geodesically, meaning the curvature of the +/// earth is taken into account. /// For example: /// /// ``` diff --git a/lib/src/nearest_point.dart b/lib/src/nearest_point.dart index afd1b94b..60253d1e 100644 --- a/lib/src/nearest_point.dart +++ b/lib/src/nearest_point.dart @@ -1,5 +1,5 @@ import 'distance.dart'; -import 'geojson.dart'; +import 'package:geotypes/geotypes.dart'; /// Takes a reference [Point] and a FeatureCollection of Features /// with Point geometries and returns the diff --git a/lib/src/nearest_point_on_line.dart b/lib/src/nearest_point_on_line.dart index 4656bef1..2961d3b7 100644 --- a/lib/src/nearest_point_on_line.dart +++ b/lib/src/nearest_point_on_line.dart @@ -1,9 +1,9 @@ import 'dart:math'; +import 'package:geotypes/geotypes.dart'; import 'bearing.dart'; import 'destination.dart'; import 'distance.dart'; -import 'geojson.dart'; import 'helpers.dart'; import 'intersection.dart'; @@ -37,18 +37,13 @@ class _NearestMulti extends _Nearest { final int localIndex; _NearestMulti({ - required Point point, - required num distance, - required int index, + required super.point, + required super.distance, + required super.index, required this.localIndex, - required num location, + required super.location, required this.line, - }) : super( - point: point, - distance: distance, - index: index, - location: location, - ); + }); @override Feature toFeature() { @@ -198,9 +193,9 @@ _NearestMulti? _nearestPointOnMultiLine( /// Position.of([-77.020339, 38.884084]), /// Position.of([-77.025661, 38.885821]), /// Position.of([-77.021884, 38.889563]), -/// Position.of([-77.019824, 38.892368)] +/// Position.of([-77.019824, 38.892368]) /// ]); -/// var pt = Point(coordinates: Position(lat: -77.037076, lng: 38.884017)); +/// var pt = Point(coordinates: Position(-77.037076, 38.884017)); /// /// var snapped = nearestPointOnLine(line, pt, Unit.miles); /// ``` diff --git a/lib/src/rhumb_destination.dart b/lib/src/rhumb_destination.dart index b9016324..c94c1a20 100644 --- a/lib/src/rhumb_destination.dart +++ b/lib/src/rhumb_destination.dart @@ -4,8 +4,8 @@ import 'package:turf/helpers.dart'; import 'package:turf/src/invariant.dart'; /// -/// Returns the destination [Point] having travelled the given distance along a Rhumb line from the -/// origin Point with the (varant) given bearing. +/// Returns the destination [Point] having traveled the given distance along a Rhumb line from the +/// origin Point with the (variant) given bearing. /// /// example: /// ```dart @@ -62,7 +62,7 @@ Position calculateRhumbDestination(Position origin, num distance, num bearing, final dPhi = delta * math.cos(theta); var phi2 = phi1 + dPhi; - // check for some daft bugger going past the pole, normalise latitude if so + // check for some daft bugger going past the pole, normalize latitude if so if (phi2.abs() > math.pi / 2) { phi2 = phi2 > 0 ? math.pi - phi2 : -math.pi - phi2; } @@ -75,7 +75,7 @@ Position calculateRhumbDestination(Position origin, num distance, num bearing, final dLambda = (delta * math.sin(theta)) / q; final lambda2 = lambda1 + dLambda; - // normalise to −180..+180° + // normalize to −180..+180° final lng = (((lambda2 * 180) / math.pi + 540) % 360) - 180; final lat = (phi2 * 180) / math.pi; diff --git a/lib/src/rhumb_distance.dart b/lib/src/rhumb_distance.dart index 2524c4d1..08a7c6f6 100644 --- a/lib/src/rhumb_distance.dart +++ b/lib/src/rhumb_distance.dart @@ -33,7 +33,7 @@ num rhumbDistance(Point from, Point to, [Unit unit = Unit.kilometers]) { } /// -/// Returns the distance travelling from ‘this’ point to destination point along a rhumb line. +/// Returns the distance traveling from ‘this’ point to destination point along a rhumb line. /// Adapted from Geodesy ‘distanceTo‘: https://github.com/chrisveness/geodesy/blob/master/latlon-spherical.js /// /// example: diff --git a/lib/src/transform_rotate.dart b/lib/src/transform_rotate.dart index 02346061..aa3ca91b 100644 --- a/lib/src/transform_rotate.dart +++ b/lib/src/transform_rotate.dart @@ -1,6 +1,5 @@ import 'package:turf/bearing.dart'; import 'package:turf/distance.dart'; -import 'package:turf/helpers.dart'; import 'package:turf/src/centroid.dart'; import 'package:turf/src/invariant.dart'; import 'package:turf/src/meta/coord.dart'; diff --git a/lib/transform.dart b/lib/transform.dart index fdd9f4d7..bc389780 100644 --- a/lib/transform.dart +++ b/lib/transform.dart @@ -1,3 +1,4 @@ library turf_transform; +export 'package:geotypes/geotypes.dart'; export 'src/transform_rotate.dart'; diff --git a/lib/truncate.dart b/lib/truncate.dart index d50cd333..905e69be 100644 --- a/lib/truncate.dart +++ b/lib/truncate.dart @@ -1,3 +1,4 @@ -library truncate.dart; +library turf_truncate; +export 'package:geotypes/geotypes.dart'; export 'src/truncate.dart'; diff --git a/lib/turf.dart b/lib/turf.dart index 25d03aea..b36424c0 100644 --- a/lib/turf.dart +++ b/lib/turf.dart @@ -1,17 +1,34 @@ library turf; -export 'src/along.dart'; -export 'src/area.dart'; -export 'src/bbox.dart'; -export 'src/bearing.dart'; -export 'src/center.dart'; -export 'src/centroid.dart'; -export 'src/destination.dart'; -export 'src/distance.dart'; -export 'src/geojson.dart'; -export 'src/helpers.dart'; -export 'src/length.dart'; -export 'src/midpoint.dart'; -export 'src/nearest_point.dart'; -export 'src/polyline.dart'; -export 'src/nearest_point_on_line.dart'; +export 'package:geotypes/geotypes.dart'; +export 'along.dart'; +export 'area.dart'; +export 'bbox_polygon.dart'; +export 'bbox.dart'; +export 'bearing.dart'; +export 'boolean.dart'; +export 'center.dart'; +export 'centroid.dart'; +export 'clean_coords.dart'; +export 'clusters.dart'; +export 'destination.dart'; +export 'distance.dart'; +export 'explode.dart'; +export 'extensions.dart'; +export 'helpers.dart'; +export 'invariant.dart'; +export 'length.dart'; +export 'line_intersect.dart'; +export 'line_overlap.dart'; +export 'line_segment.dart'; +export 'line_slice.dart'; +export 'line_to_polygon.dart'; +export 'meta.dart'; +export 'midpoint.dart'; +export 'nearest_point_on_line.dart'; +export 'nearest_point.dart'; +export 'polygon_smooth.dart'; +export 'polygon_to_line.dart'; +export 'polyline.dart'; +export 'transform.dart'; +export 'truncate.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 26a6587b..e22960f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,23 +1,21 @@ name: turf description: A turf.js-like geospatial analysis library working with GeoJSON, written in pure Dart. -version: 0.0.9 +version: 0.0.10 environment: sdk: ">=2.17.0 <4.0.0" homepage: https://github.com/dartclub/turf_dart repository: https://github.com/dartclub/turf_dart dependencies: - json_annotation: ^4.8.1 - - turf_equality: ^0.0.3 + geotypes: ^0.0.2 + turf_equality: ^0.1.0 turf_pip: ^0.0.2 - rbush: ^1.1.0 + rbush: ^1.1.1 sweepline_intersections: ^0.0.4 dev_dependencies: lints: ^3.0.0 test: ^1.24.3 - json_serializable: ^6.7.0 build_runner: ^2.4.5 analyzer: ^6.4.0 benchmark: ^0.3.0 diff --git a/test/booleans/overlap_test.dart b/test/booleans/overlap_test.dart new file mode 100644 index 00000000..a2bbb49a --- /dev/null +++ b/test/booleans/overlap_test.dart @@ -0,0 +1,205 @@ +import 'package:geotypes/geotypes.dart'; +import 'package:test/test.dart'; +import 'package:turf/src/booleans/boolean_helper.dart'; +import 'package:turf/src/booleans/boolean_overlap.dart'; + +import '../context/helper.dart'; +import '../context/load_test_cases.dart'; + +void main() { + group('booleanOverlap', () { + final pt = point([9, 50]); + final multiPoint1 = multiPoint([ + [9, 50], + [10, 50], + ]); + final multiPoint2 = multiPoint([ + [9, 50], + [10, 100], + ]); + final line1 = lineString([ + [7, 50], + [8, 50], + [9, 50], + ]); + final line2 = lineString([ + [8, 50], + [9, 50], + [10, 50], + ]); + final poly1 = polygon([ + [ + [8.5, 50], + [9.5, 50], + [9.5, 49], + [8.5, 49], + [8.5, 50], + ], + ]); + final poly2 = polygon([ + [ + [8, 50], + [9, 50], + [9, 49], + [8, 49], + [8, 50], + ], + ]); + final poly3 = polygon([ + [ + [10, 50], + [10.5, 50], + [10.5, 49], + [10, 49], + [10, 50], + ], + ]); + final multiline1 = multiLineString([ + [ + [7, 50], + [8, 50], + [9, 50], + ], + ]); + final multipoly1 = multiPolygon([ + [ + [ + [8.5, 50], + [9.5, 50], + [9.5, 49], + [8.5, 49], + [8.5, 50], + ], + ], + ]); + + test('supported geometries', () { + // points + expect( + () => booleanOverlap(pt, pt), + throwsA(isA()), + ); + expect( + () => booleanOverlap(pt, multiPoint1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(pt, line1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(pt, multiline1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(pt, poly1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(pt, multipoly1), + throwsA(isA()), + ); + + // multiPoints + expect( + () => booleanOverlap(multiPoint1, multiPoint1), + returnsNormally, + ); + expect( + () => booleanOverlap(multiPoint1, line1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(multiPoint1, multiline1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(multiPoint1, poly1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(multiPoint1, multipoly1), + throwsA(isA()), + ); + + // lines + expect( + () => booleanOverlap(line1, line1), + returnsNormally, + ); + expect( + () => booleanOverlap(line1, multiline1), + returnsNormally, + ); + expect( + () => booleanOverlap(line1, poly1), + throwsA(isA()), + ); + + expect( + () => booleanOverlap(line1, multipoly1), + throwsA(isA()), + ); + + // multiline + expect( + () => booleanOverlap(multiline1, multiline1), + returnsNormally, + ); + expect( + () => booleanOverlap(multiline1, poly1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(multiline1, multipoly1), + throwsA(isA()), + ); + + // polygons + expect( + () => booleanOverlap(poly1, poly1), + returnsNormally, + ); + expect( + () => booleanOverlap(poly1, multipoly1), + returnsNormally, + ); + + // multiPolygons + expect( + () => booleanOverlap(multipoly1, multipoly1), + returnsNormally, + ); + }); + + test('equal geometries return false', () { + expect(booleanOverlap(multiPoint1, multiPoint1), false); + expect(booleanOverlap(line1, line1), false); + expect(booleanOverlap(multiline1, multiline1), false); + + expect(booleanOverlap(poly1, poly1), false); + expect(booleanOverlap(multipoly1, multipoly1), false); + }); + + test('overlapping geometries', () { + expect(booleanOverlap(multiPoint1, multiPoint2), true); + expect(booleanOverlap(line1, line2), true); + expect(booleanOverlap(poly1, poly2), true); + expect(booleanOverlap(poly1, poly3), false); + }); + }); + + group('booleanOverlap - examples', () { + loadBooleanTestCases('test/examples/booleans/overlap', ( + path, + geoJsonGiven, + expected, + ) { + final first = (geoJsonGiven as FeatureCollection).features[0]; + final second = geoJsonGiven.features[1]; + test(path, () { + expect(booleanOverlap(first, second), expected, reason: path); + }); + }); + }); +} diff --git a/test/booleans/parallel_test.dart b/test/booleans/parallel_test.dart index 02c569c1..4010fa86 100644 --- a/test/booleans/parallel_test.dart +++ b/test/booleans/parallel_test.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:test/test.dart'; -import 'package:turf/src/booleans/boolean_parallel.dart'; import 'package:turf/turf.dart'; void main() { @@ -10,7 +9,7 @@ void main() { 'boolean-overlap', () { test( - "turf-boolean-overlap-trues", + "turf-boolean-overlap-true", () { // True Fixtures Directory dir = Directory('./test/examples/booleans/parallel/true'); @@ -29,7 +28,7 @@ void main() { ); test( - "turf-boolean-overlap-falses", + "turf-boolean-overlap-false", () { // True Fixtures Directory dir = Directory('./test/examples/booleans/parallel/false'); diff --git a/test/booleans/touches_test.dart b/test/booleans/touches_test.dart index f3983435..7db9db1e 100644 --- a/test/booleans/touches_test.dart +++ b/test/booleans/touches_test.dart @@ -2,8 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:test/test.dart'; -import 'package:turf/src/booleans/boolean_touches.dart'; -import 'package:turf/turf.dart'; +import 'package:turf/boolean.dart'; void main() { group( @@ -28,7 +27,7 @@ void main() { ); test( - "turf-boolean-overlap-falses", + "turf-boolean-overlap-false", () { // True Fixtures Directory dir = Directory('./test/examples/booleans/touches/false'); diff --git a/test/booleans/valid_test.dart b/test/booleans/valid_test.dart index 6720eafb..9f82c805 100644 --- a/test/booleans/valid_test.dart +++ b/test/booleans/valid_test.dart @@ -2,14 +2,13 @@ import 'dart:convert'; import 'dart:io'; import 'package:test/test.dart'; -import 'package:turf/src/booleans/boolean_valid.dart'; -import 'package:turf/turf.dart'; +import 'package:turf/boolean.dart'; void main() { group( 'boolean-valid', () { - /// Assertion error is caught in the fromJSON factory contructor of [GeometryType]s + /// Assertion error is caught in the fromJSON factory constructor of [GeometryType]s Directory dir = Directory('./test/examples/booleans/valid/assertion'); for (var file in dir.listSync(recursive: true)) { test( @@ -30,8 +29,8 @@ void main() { }, ); } - Directory dirFale = Directory('./test/examples/booleans/valid/false'); - for (var file in dirFale.listSync(recursive: true)) { + Directory dirFile = Directory('./test/examples/booleans/valid/false'); + for (var file in dirFile.listSync(recursive: true)) { test( file.path, () { diff --git a/test/booleans/within_test.dart b/test/booleans/within_test.dart new file mode 100644 index 00000000..5bd95821 --- /dev/null +++ b/test/booleans/within_test.dart @@ -0,0 +1,98 @@ +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/booleans/boolean_helper.dart'; +import 'package:turf/src/booleans/boolean_within.dart'; +import '../context/helper.dart'; +import '../context/load_test_cases.dart'; + +void main() { + group('within - true', () { + loadGeoJsonFiles('./test/examples/booleans/within/true', (path, geoJson) { + final feature1 = (geoJson as FeatureCollection).features[0]; + final feature2 = geoJson.features[1]; + test(path, () => expect(booleanWithin(feature1, feature2), true)); + }); + }); + + group('within - false', () { + loadGeoJsonFiles('./test/examples/booleans/within/false', (path, geoJson) { + final feature1 = (geoJson as FeatureCollection).features[0]; + final feature2 = geoJson.features[1]; + test(path, () => expect(booleanWithin(feature1, feature2), false)); + }); + }); + + group('within', () { + loadGeoJson( + './test/examples/booleans/within/true/MultiPolygon/MultiPolygon/skip-multipolygon-within-multipolygon.geojson', + (path, geoJson) { + final feature1 = (geoJson as FeatureCollection).features[0]; + final feature2 = geoJson.features[1]; + + test( + 'FeatureNotSupported', + () => expect( + () => booleanWithin(feature1, feature2), + throwsA(isA()), + ), + ); + }); + + test('within - point in multipolygon with hole', () { + loadGeoJson( + './test/examples/booleans/point_in_polygon/in/multipoly-with-hole.geojson', + (path, geoJson) { + final multiPolygon = (geoJson as Feature); + final pointInHole = point([-86.69208526611328, 36.20373274711739]); + final pointInPolygon = point([-86.72229766845702, 36.20258997094334]); + final pointInSecondPolygon = + point([-86.75079345703125, 36.18527313913089]); + + expect(booleanWithin(pointInHole, multiPolygon), false, + reason: "point in hole"); + expect(booleanWithin(pointInPolygon, multiPolygon), true, + reason: "point in polygon"); + expect(booleanWithin(pointInSecondPolygon, multiPolygon), true, + reason: "point outside polygon"); + }); + }); + + test("within - point in polygon", () { + final simplePolygon = polygon([ + [ + [0, 0], + [0, 100], + [100, 100], + [100, 0], + [0, 0], + ], + ]); + final pointIn = point([50, 50]); + final pointOut = point([140, 150]); + + expect(booleanWithin(pointIn, simplePolygon), true, + reason: "point inside polygon"); + expect(booleanWithin(pointOut, simplePolygon), false, + reason: "point outside polygon"); + + final concavePolygon = polygon([ + [ + [0, 0], + [50, 50], + [0, 100], + [100, 100], + [100, 0], + [0, 0], + ], + ]); + + final pointInConcave = point([75, 75]); + final pointOutConcave = point([25, 50]); + + expect(booleanWithin(pointInConcave, concavePolygon), true, + reason: "point inside concave polygon"); + expect(booleanWithin(pointOutConcave, concavePolygon), false, + reason: "point outside concave polygon"); + }); + }); +} diff --git a/test/components/bbox_test.dart b/test/components/bbox_test.dart index e9efefcf..8354d7eb 100644 --- a/test/components/bbox_test.dart +++ b/test/components/bbox_test.dart @@ -1,6 +1,5 @@ import 'package:test/test.dart'; import 'package:turf/bbox.dart'; -import 'package:turf/helpers.dart'; void main() { final pt = Feature( diff --git a/test/components/bearing_test.dart b/test/components/bearing_test.dart index 42949459..3abe74e7 100644 --- a/test/components/bearing_test.dart +++ b/test/components/bearing_test.dart @@ -1,6 +1,5 @@ import 'package:test/test.dart'; import 'package:turf/bearing.dart'; -import 'package:turf/helpers.dart'; void main() { test( diff --git a/test/components/center_test.dart b/test/components/center_test.dart index c642d22e..1f8cd79b 100644 --- a/test/components/center_test.dart +++ b/test/components/center_test.dart @@ -2,9 +2,10 @@ import 'dart:convert'; import 'dart:io'; import 'package:test/test.dart'; +import 'package:turf/bbox.dart'; +import 'package:turf/center.dart'; import 'package:turf/meta.dart'; import 'package:turf/src/bbox_polygon.dart'; -import 'package:turf/turf.dart'; void main() { group( diff --git a/test/components/centroid_test.dart b/test/components/centroid_test.dart index 30ceb0b2..64490194 100644 --- a/test/components/centroid_test.dart +++ b/test/components/centroid_test.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:test/test.dart'; -import 'package:turf/src/meta/feature.dart'; import 'package:turf/turf.dart'; import 'package:turf_equality/turf_equality.dart'; diff --git a/test/components/explode_test.dart b/test/components/explode_test.dart index d5c55984..443d84e2 100644 --- a/test/components/explode_test.dart +++ b/test/components/explode_test.dart @@ -1,9 +1,9 @@ import 'dart:convert'; import 'dart:io'; +import 'package:geotypes/geotypes.dart'; import 'package:test/test.dart'; import 'package:turf/src/explode.dart'; -import 'package:turf/turf.dart'; void main() { group( diff --git a/test/components/geojson_test.dart b/test/components/geojson_test.dart index 9133302e..d2f9188d 100644 --- a/test/components/geojson_test.dart +++ b/test/components/geojson_test.dart @@ -6,7 +6,6 @@ import 'dart:math'; import 'package:test/test.dart'; import 'package:turf/distance.dart'; -import 'package:turf/helpers.dart'; void main() { group('Coordinate Types:', () { diff --git a/test/components/line_overlap_test.dart b/test/components/line_overlap_test.dart new file mode 100644 index 00000000..41911abf --- /dev/null +++ b/test/components/line_overlap_test.dart @@ -0,0 +1,204 @@ +import 'package:turf/line_overlap.dart'; +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; + +import '../context/helper.dart'; +import '../context/load_test_cases.dart'; +import '../context/matcher.dart' as geo; + +void main() { + group('lineOverlap', () { + final first = lineString([ + [100, -30], + [150, -30], + ]); + test('inner part', () { + final second = lineString([ + [110, -30], + [120, -30], + ]); + final expected = featureCollection([second]); + + expect(lineOverlap(first, second), geo.equals(expected)); + expect(lineOverlap(second, first), geo.equals(expected)); + }); + test('start part', () { + final second = lineString([ + [100, -30], + [110, -30], + ]); + final expected = featureCollection([second]); + + expect(lineOverlap(first, second), geo.equals(expected)); + expect(lineOverlap(second, first), geo.equals(expected)); + }); + test('two inner segments', () { + final second = lineString([ + [110, -30], + [120, -30], + [130, -30], + ]); + final expected = featureCollection([second]); + + expect(lineOverlap(first, second), geo.equals(expected)); + expect(lineOverlap(second, first), geo.equals(expected)); + }); + test('multiple segments on the same line', () { + final first = lineString([ + [0, 1], + [1, 1], + [1, 0], + [2, 0], + [2, 1], + [3, 1], + [3, 0], + [4, 0], + [4, 1], + [4, 0], + ]); + final second = lineString([ + [0, 0], + [6, 0], + ]); + + final expected = [ + lineString([ + [1, 0], + [2, 0] + ]), + lineString([ + [3, 0], + [4, 0] + ]), + ]; + + expect(lineOverlap(first, second), geo.contains(expected)); + expect(lineOverlap(second, first), geo.contains(expected)); + }); + test('partial overlap', () { + // bug: https://github.com/Turfjs/turf/issues/2580 + final second = lineString([ + [90, -30], + [110, -30], + ]); + + final expected = featureCollection([ + lineString([ + [100, -30], + [110, -30], + ]) + ]); + + expect(lineOverlap(first, second), geo.equals(expected)); + expect(lineOverlap(second, first), geo.equals(expected)); + }); + test('two separate inner segments', () { + final second = lineString([ + [140, -30], + [150, -30], + [150, -20], + [100, -20], + [100, -30], + [110, -30], + ]); + + final expected = featureCollection( + [ + lineString([ + [140, -30], + [150, -30] + ]), + lineString([ + [100, -30], + [110, -30] + ]), + ], + ); + + expect(lineOverlap(first, second), geo.equals(expected)); + expect(lineOverlap(second, first), geo.equals(expected)); + }); + test('validate tolerance', () { + // bug: https://github.com/Turfjs/turf/issues/2582 + // distance between the lines are 11.x km + final first = lineString([ + [10.0, 0.1], + [11.0, 0.1] + ]); + final second = lineString([ + [10.0, 0.0], + [11.0, 0.0] + ]); + + final expected = featureCollection([second]); + + expect( + lineOverlap( + first, + second, + tolerance: 12.0, + ), + geo.equals(expected), + ); + + expect( + lineOverlap( + first, + second, + tolerance: 12.0, + unit: Unit.kilometers, + ), + geo.equals(expected), + ); + + expect( + lineOverlap( + first, + second, + tolerance: 11.0, + unit: Unit.kilometers, + ), + geo.length(0), + ); + + expect( + lineOverlap( + first, + second, + tolerance: 12.0, + unit: Unit.meters, + ), + geo.length(0), + ); + }); + }); + + group('lineOverlap - examples', () { + loadTestCases("test/examples/line_overlap", ( + path, + geoJsonGiven, + geoJsonExpected, + ) { + final first = (geoJsonGiven as FeatureCollection).features[0]; + final second = geoJsonGiven.features[1]; + final expectedCollection = geoJsonExpected as FeatureCollection; + + // The last 2 features are equal to the given input. If there are only 2 + // features in the collection it means, that we expect an empty result. + // Otherwise the remaining features are expected. + final expected = expectedCollection.features.length == 2 + ? featureCollection() + : featureCollection( + expectedCollection.features + .sublist(0, expectedCollection.features.length - 2) + .map((e) => Feature(geometry: e.geometry as LineString)) + .toList(), + ); + test(path, () { + final tolerance = first.properties?['tolerance'] ?? 0.0; + final result = lineOverlap(first, second, tolerance: tolerance); + expect(result, geo.equals(expected)); + }); + }); + }); +} diff --git a/test/components/line_slice_test.dart b/test/components/line_slice_test.dart new file mode 100644 index 00000000..721e64ec --- /dev/null +++ b/test/components/line_slice_test.dart @@ -0,0 +1,78 @@ +import 'package:test/test.dart'; +import 'package:turf/along.dart'; +import 'package:turf/distance.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/length.dart'; +import 'package:turf/src/line_slice.dart'; + +void main() { + test('lineSlice - exact points', () { + final slice = lineSlice(startFeature, viaFeature, lineFeature); + expect(slice, isNotNull); + expect(slice.properties, isNotNull); + expect(slice.properties!.keys, contains(propName)); + expect(slice.properties![propName], equals(propValue)); + + final expectedLineFeature = Feature( + geometry: LineString(coordinates: [start, via]), + ); + expect(slice.geometry, isNotNull); + expect(slice.geometry!.coordinates, hasLength(2)); + expect(length(slice).round(), equals(length(expectedLineFeature).round())); + }); + test('lineSlice - interpolation', () { + const skipDist = 10; + + final sliceFrom = along(lineFeature, skipDist, Unit.meters); + expect(sliceFrom, isNotNull); + + final slice = lineSlice(sliceFrom, viaFeature, lineFeature); + expect(slice, isNotNull); + expect(slice.properties, isNotNull); + expect(slice.properties!.keys, contains(propName)); + expect(slice.properties![propName], equals(propValue)); + + final expectedLine = Feature( + geometry: LineString(coordinates: [start, via]), + ); + expect(slice.geometry, isNotNull); + expect(slice.geometry!.coordinates, hasLength(2)); + expect( + length(slice, Unit.meters).round(), + equals(length(expectedLine, Unit.meters).round() - skipDist), + ); + + // Sanity check of test data. No interpolation occurs if start and via are skipDist apart. + expect(distance(Point(coordinates: start), Point(coordinates: via)).round(), + isNot(equals(skipDist))); + }); +} + +final start = Position.named( + lat: 55.7090430186194, + lng: 13.184645393920405, +); +final via = Position.named( + lat: 55.70901279569489, + lng: 13.185546616182755, +); +final end = Position.named( + lat: 55.70764669578079, + lng: 13.187563637197076, +); +const propName = 'prop1'; +const propValue = 1; +final lineFeature = Feature( + geometry: LineString( + coordinates: [ + start, + via, + end, + ], + ), + properties: { + propName: propValue, + }, +); +final startFeature = Feature(geometry: Point(coordinates: start)); +final viaFeature = Feature(geometry: Point(coordinates: via)); diff --git a/test/components/midpoint_test.dart b/test/components/midpoint_test.dart index 51dc60de..44e26db5 100644 --- a/test/components/midpoint_test.dart +++ b/test/components/midpoint_test.dart @@ -2,7 +2,6 @@ import 'package:test/test.dart'; import 'package:turf/distance.dart'; -import 'package:turf/helpers.dart'; import 'package:turf/midpoint.dart'; void checkLatLngInRange(Point result) { diff --git a/test/components/polyline.dart b/test/components/polyline.dart index 1083e688..3cf29062 100644 --- a/test/components/polyline.dart +++ b/test/components/polyline.dart @@ -1,5 +1,4 @@ import 'package:test/test.dart'; -import 'package:turf/helpers.dart'; import 'package:turf/polyline.dart'; void main() { diff --git a/test/components/transform_rotate_test.dart b/test/components/transform_rotate_test.dart index 03202942..12d7e058 100644 --- a/test/components/transform_rotate_test.dart +++ b/test/components/transform_rotate_test.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:test/test.dart'; -import 'package:turf/helpers.dart'; import 'package:turf/src/centroid.dart'; import 'package:turf/src/invariant.dart'; import 'package:turf/src/transform_rotate.dart'; diff --git a/test/components/truncate_test.dart b/test/components/truncate_test.dart index aff618d0..b39bf589 100644 --- a/test/components/truncate_test.dart +++ b/test/components/truncate_test.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:test/test.dart'; -import 'package:turf/helpers.dart'; import 'package:turf/truncate.dart'; import 'package:turf_equality/turf_equality.dart'; diff --git a/test/context/helper.dart b/test/context/helper.dart new file mode 100644 index 00000000..c4782b76 --- /dev/null +++ b/test/context/helper.dart @@ -0,0 +1,70 @@ +// A List of builders that are similar to the way TurfJs creates GeoJSON +// objects. The idea is to make it easier to port JavaScript tests to Dart. +import 'package:turf/turf.dart'; + +Feature point(List coordinates) { + return Feature( + geometry: Point( + coordinates: Position.of(coordinates), + ), + ); +} + +Feature multiPoint(List> coordinates) { + return Feature( + geometry: MultiPoint( + coordinates: + coordinates.map((e) => Position.of(e)).toList(growable: false), + ), + ); +} + +Position position(List coordinates) { + return Position.of(coordinates); +} + +List positions(List> coordinates) { + return coordinates.map((e) => position(e)).toList(growable: false); +} + +Feature lineString(List> coordinates, {dynamic id}) { + return Feature( + id: id, + geometry: LineString(coordinates: positions(coordinates)), + ); +} + +Feature multiLineString( + List>?> coordinates) { + return Feature( + geometry: MultiLineString( + coordinates: coordinates + .map((element) => positions(element!)) + .toList(growable: false)), + ); +} + +Feature polygon(List>> coordinates) { + return Feature( + geometry: Polygon( + coordinates: coordinates + .map((element) => positions(element)) + .toList(growable: false)), + ); +} + +Feature multiPolygon( + List>>?> coordinates) { + return Feature( + geometry: MultiPolygon( + coordinates: coordinates + .map((element) => + element!.map((e) => positions(e)).toList(growable: false)) + .toList(growable: false)), + ); +} + +FeatureCollection featureCollection( + [List> features = const []]) { + return FeatureCollection(features: features); +} diff --git a/test/context/load_test_cases.dart b/test/context/load_test_cases.dart new file mode 100644 index 00000000..7620926b --- /dev/null +++ b/test/context/load_test_cases.dart @@ -0,0 +1,101 @@ +// ignore_for_file: use_rethrow_when_possible + +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; + +void loadGeoJson( + String path, void Function(String path, GeoJSONObject geoJson) test) { + final file = File(path); + final content = file.readAsStringSync(); + final geoJson = GeoJSONObject.fromJson(jsonDecode(content)); + test(file.path, geoJson); +} + +void loadGeoJsonFiles( + String path, + void Function(String path, GeoJSONObject geoJson) test, +) { + var testDirectory = Directory(path); + + for (var file in testDirectory.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.geojson')) { + if (file.path.contains('skip')) continue; + + loadGeoJson(file.path, test); + } + } +} + +void loadBooleanTestCases( + String basePath, + void Function( + String path, + GeoJSONObject geoJson, + bool expected, + ) callback, +) { + try { + loadGeoJsonFiles("$basePath/true", (path, geoJson) { + callback(path, geoJson, true); + }); + + loadGeoJsonFiles("$basePath/false", (path, geoJson) { + callback(path, geoJson, false); + }); + } catch (e) { + test('loadBooleanTestCases', () { + expect(() { + throw e; + }, returnsNormally); + }); + } +} + +void loadTestCases( + String basePath, + void Function( + String path, + GeoJSONObject geoJsonGiven, + GeoJSONObject geoJsonExpected, + ) test, +) { + var inDirectory = Directory("$basePath/in"); + var outDirectory = Directory("$basePath/out"); + + if (!inDirectory.existsSync()) { + throw Exception("directory ${inDirectory.path} not found"); + } + if (!outDirectory.existsSync()) { + throw Exception("directory ${outDirectory.path} not found"); + } + + final inFiles = inDirectory + .listSync(recursive: true) + .whereType() + .where( + (file) => + file.path.endsWith('.geojson') && + file.path.contains('skip') == false, + ) + .toList(); + + for (var file in inFiles) { + final outFile = File(file.path.replaceFirst('/in/', '/out/')); + if (outFile.existsSync() == false) { + throw Exception("file ${outFile.path} not found"); + } + + final geoJsonGiven = GeoJSONObject.fromJson( + jsonDecode(file.readAsStringSync()), + ); + + final geoJsonExpected = GeoJSONObject.fromJson( + jsonDecode(outFile.readAsStringSync()), + ); + + test(file.path, geoJsonGiven, geoJsonExpected); + } +} diff --git a/test/context/matcher.dart b/test/context/matcher.dart new file mode 100644 index 00000000..aa350057 --- /dev/null +++ b/test/context/matcher.dart @@ -0,0 +1,86 @@ +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf_equality/turf_equality.dart'; + +Matcher equals(T? expected) => _Equals(expected); + +class _Equals extends Matcher { + _Equals(this.expected); + final T? expected; + + @override + Description describe(Description description) { + return description.add('is equal'); + } + + @override + bool matches(actual, Map matchState) { + if (actual is! GeoJSONObject) return false; + + Equality eq = Equality(); + return eq.compare(actual, expected); + } +} + +Matcher contains(List expected) => _Contains(expected); + +class _Contains extends Matcher { + _Contains(this.expected); + final List expected; + + @override + Description describe(Description description) { + return description.add('contains'); + } + + @override + bool matches(actual, Map matchState) { + if (actual is! FeatureCollection) throw UnimplementedError(); + + Equality eq = Equality(); + + for (var feature in expected) { + if (!actual.features.any((f) => eq.compare(f, feature))) { + return false; + } + } + return true; + } +} + +Matcher length(int length) => _Length(length); + +class _Length extends Matcher { + _Length(this.length); + final int length; + + @override + Description describe(Description description) { + return description.add('length is $length'); + } + + @override + bool matches(actual, Map matchState) { + if (actual is FeatureCollection) { + return actual.features.length == length; + } + + if (actual is GeometryCollection) { + return actual.geometries.length == length; + } + + if (actual is MultiPoint) { + return actual.coordinates.length == length; + } + + if (actual is MultiPolygon) { + return actual.coordinates.length == length; + } + + if (actual is MultiLineString) { + return actual.coordinates.length == length; + } + + return false; + } +} diff --git a/test/examples/booleans/equal/test/true/lines-reverse.geojson b/test/examples/booleans/equal/test/true/lines-reverse.geojson index 9aa67775..ee136435 100644 --- a/test/examples/booleans/equal/test/true/lines-reverse.geojson +++ b/test/examples/booleans/equal/test/true/lines-reverse.geojson @@ -1,5 +1,8 @@ { "type": "FeatureCollection", + "properties": { + "direction": true + }, "features": [ { "type": "Feature", diff --git a/test/examples/booleans/equal/test/true/reverse-lines.geojson b/test/examples/booleans/equal/test/true/reverse-lines.geojson index 4c6e1810..681f481c 100644 --- a/test/examples/booleans/equal/test/true/reverse-lines.geojson +++ b/test/examples/booleans/equal/test/true/reverse-lines.geojson @@ -1,5 +1,8 @@ { "type": "FeatureCollection", + "properties": { + "direction": true + }, "features": [ { "type": "Feature", diff --git a/test/examples/line_overlap/in/issue-#901.geojson b/test/examples/line_overlap/in/issue-#901.geojson index b897d69b..bbab9617 100644 --- a/test/examples/line_overlap/in/issue-#901.geojson +++ b/test/examples/line_overlap/in/issue-#901.geojson @@ -1,12 +1,10 @@ { "type": "FeatureCollection", - "properties": { - "tolerance": 0.05 - }, "features": [ { "type": "Feature", "properties": { + "tolerance": 0.005, "stroke": "#F00", "fill": "#F00", "stroke-width": 10, diff --git a/test/examples/line_overlap/in/partial-overlap.geojson b/test/examples/line_overlap/in/partial-overlap.geojson new file mode 100644 index 00000000..de005cb3 --- /dev/null +++ b/test/examples/line_overlap/in/partial-overlap.geojson @@ -0,0 +1,46 @@ + +{ + "type": "FeatureCollection", + "features": [ { + "type": "Feature", + "properties": { + "stroke": "#F00", + "stroke-width": 10, + "stroke-opacity": 1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [130, -35], + [125, -35], + [125, -30], + [135, -30], + [135, -35] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#00F", + "stroke-width": 3, + "stroke-opacity": 1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [127, -35], + [125, -35], + [123, -35], + [123, -30], + [125, -30], + [130, -30], + [135, -30], + [135, -33], + [135, -34], + [140, -34] + ] + } + } + ] +} diff --git a/test/examples/line_overlap/in/partial-overlap2.geojson b/test/examples/line_overlap/in/partial-overlap2.geojson new file mode 100644 index 00000000..125866fb --- /dev/null +++ b/test/examples/line_overlap/in/partial-overlap2.geojson @@ -0,0 +1,91 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "stroke": "#F00", + "fill": "#F00", + "stroke-width": 10, + "stroke-opacity": 1, + "fill-opacity": 0.1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 110, + -25 + ], + [ + 120, + -25 + ], + [ + 120, + -20 + ], + [ + 125, + -20 + ], + [ + 125, + -25 + ], + [ + 130, + -25 + ], + [ + 135, + -25 + ], + [ + 135, + -20 + ], + [ + 140, + -20 + ], + [ + 140, + -25 + ], + [ + 145, + -25 + ], + [ + 155, + -25 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#00F", + "fill": "#00F", + "stroke-width": 3, + "stroke-opacity": 1, + "fill-opacity": 0.1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 115, + -25 + ], + [ + 150, + -25 + ] + ] + } + } + ] + } \ No newline at end of file diff --git a/test/examples/line_overlap/out/issue-#901.geojson b/test/examples/line_overlap/out/issue-#901.geojson index 09d13e5e..b6828142 100644 --- a/test/examples/line_overlap/out/issue-#901.geojson +++ b/test/examples/line_overlap/out/issue-#901.geojson @@ -11,67 +11,28 @@ "geometry": { "type": "LineString", "coordinates": [ - [-113.33698987543352, 53.53214475018778], - [-113.33690471442213, 53.53212132654082], - [-113.33698987543352, 53.53214475018778], - [-113.33704111881613, 53.53215959791441] - ] - }, - "bbox": [ - -113.33704111881613, - 53.53214475018778, - -113.33698987543352, - 53.53215959791441 - ], - "id": 13 - }, - { - "type": "Feature", - "properties": { - "stroke": "#0F0", - "fill": "#0F0", - "stroke-width": 25 - }, - "geometry": { - "type": "LineString", - "coordinates": [ - [-113.33698987543352, 53.53214475018778], - [-113.33690471442213, 53.53212132654082], - [-113.33698987543352, 53.53214475018778], - [-113.33704111881613, 53.53215959791441] - ] - }, - "bbox": [ - -113.33704111881613, - 53.53214475018778, - -113.33698987543352, - 53.53215959791441 - ], - "id": 13 - }, - { - "type": "Feature", - "properties": { - "stroke": "#0F0", - "fill": "#0F0", - "stroke-width": 25 - }, - "geometry": { - "type": "LineString", - "coordinates": [ - [-113.33832502043951, 53.52244398828247], - [-113.3384152645109, 53.52244409344282], - [-113.33847575084239, 53.52244416392682], - [-113.3384152645109, 53.52244409344282] + [ + -113.33690471442213, + 53.53212132654082 + ], + [ + -113.33698987543352, + 53.53214475018778 + ], + [ + -113.33704111881613, + 53.53215959791441 + ], + [ + -113.33698987543352, + 53.53214475018778 + ], + [ + -113.33704111881613, + 53.53215959791441 + ] ] - }, - "bbox": [ - -113.3384152645109, - 53.52244398828247, - -113.33832502043951, - 53.52244409344282 - ], - "id": 20 + } }, { "type": "Feature", @@ -83,20 +44,30 @@ "geometry": { "type": "LineString", "coordinates": [ - [-113.33832502043951, 53.52244398828247], - [-113.3384152645109, 53.52244409344282], - [-113.33847575084239, 53.52244416392682], - [-113.3384152645109, 53.52244409344282] + [ + -113.33832502043951, + 53.52244398828247 + ], + [ + -113.3384152645109, + 53.52244409344282 + ], + [ + -113.33847575084239, + 53.52244416392682 + ], + [ + -113.3384152645109, + 53.52244409344282 + ], + [ + -113.33847575084239, + 53.52244416392682 + ] ] - }, - "bbox": [ - -113.3384152645109, - 53.52244398828247, - -113.33832502043951, - 53.52244409344282 - ], - "id": 20 + } }, + { "type": "Feature", "properties": { diff --git a/test/examples/line_overlap/out/partial-overlap.geojson b/test/examples/line_overlap/out/partial-overlap.geojson new file mode 100644 index 00000000..dc73c96f --- /dev/null +++ b/test/examples/line_overlap/out/partial-overlap.geojson @@ -0,0 +1,79 @@ + +{ + "type": "FeatureCollection", + "features": [ { + "type": "Feature", + "properties": { + "stroke": "#0F0", + "fill": "#0F0", + "stroke-width": 25 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [127,-35], + [125,-35] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#0F0", + "fill": "#0F0", + "stroke-width": 25 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [125,-30], + [130,-30], + [135,-30], + [135,-33], + [135,-34] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#F00", + "stroke-width": 10, + "stroke-opacity": 1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [130, -35], + [125, -35], + [125, -30], + [135, -30], + [135, -35] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#00F", + "stroke-width": 3, + "stroke-opacity": 1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [127, -35], + [125, -35], + [123, -35], + [123, -30], + [125, -30], + [130, -30], + [135, -30], + [135, -33], + [135, -34], + [140, -34] + ] + } + } + ] +} diff --git a/test/examples/line_overlap/out/partial-overlap2.geojson b/test/examples/line_overlap/out/partial-overlap2.geojson new file mode 100644 index 00000000..9a9456dd --- /dev/null +++ b/test/examples/line_overlap/out/partial-overlap2.geojson @@ -0,0 +1,166 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "stroke": "#0F0", + "fill": "#0F0", + "stroke-width": 25 + }, + "geometry": { + "type": "LineString", + "bbox": null, + "coordinates": [ + [ + 125, + -25 + ], + [ + 130, + -25 + ], + [ + 135, + -25 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#0F0", + "fill": "#0F0", + "stroke-width": 25 + }, + "geometry": { + "type": "LineString", + "bbox": null, + "coordinates": [ + [ + 140, + -25 + ], + [ + 145, + -25 + ], + [ + 150, + -25 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#0F0", + "fill": "#0F0", + "stroke-width": 25 + }, + "geometry": { + "type": "LineString", + "bbox": null, + "coordinates": [ + [ + 115, + -25 + ], + [ + 120, + -25 + ] + ] + } + }, + + { + "type": "Feature", + "properties": { + "stroke": "#F00", + "fill": "#F00", + "stroke-width": 10, + "stroke-opacity": 1, + "fill-opacity": 0.1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 110, + -25 + ], + [ + 120, + -25 + ], + [ + 120, + -20 + ], + [ + 125, + -20 + ], + [ + 125, + -25 + ], + [ + 130, + -25 + ], + [ + 135, + -25 + ], + [ + 135, + -20 + ], + [ + 140, + -20 + ], + [ + 140, + -25 + ], + [ + 145, + -25 + ], + [ + 155, + -25 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#00F", + "fill": "#00F", + "stroke-width": 3, + "stroke-opacity": 1, + "fill-opacity": 0.1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 115, + -25 + ], + [ + 150, + -25 + ] + ] + } + } + ] + } \ No newline at end of file