diff --git a/.github/turf-logo.png b/.github/turf-logo.png new file mode 100644 index 00000000..f874967f Binary files /dev/null and b/.github/turf-logo.png differ diff --git a/.github/workflows/dart-pub-publish-on-pr.yml b/.github/workflows/dart-pub-publish-on-pr.yml index 8a112eed..dbce0b12 100644 --- a/.github/workflows/dart-pub-publish-on-pr.yml +++ b/.github/workflows/dart-pub-publish-on-pr.yml @@ -27,9 +27,5 @@ jobs: run: dart analyze if: always() - - name: Run build_runner - run: dart run build_runner build --delete-conflicting-outputs - if: always() - - name: Preview publish package (dry-run) run: dart pub publish --dry-run diff --git a/.github/workflows/dart-pub-publish.yml b/.github/workflows/dart-pub-publish.yml index 6bd5db7f..77a70b78 100644 --- a/.github/workflows/dart-pub-publish.yml +++ b/.github/workflows/dart-pub-publish.yml @@ -26,10 +26,6 @@ jobs: - name: Analyze project source run: dart analyze if: always() - - - name: Run build_runner - run: dart run build_runner build --delete-conflicting-outputs - if: always() - name: Setup credentials env: diff --git a/.github/workflows/dart-unit-tests-on-pr.yml b/.github/workflows/dart-unit-tests-on-pr.yml index 10e4a4f5..b6ca9dd0 100644 --- a/.github/workflows/dart-unit-tests-on-pr.yml +++ b/.github/workflows/dart-unit-tests-on-pr.yml @@ -27,10 +27,6 @@ jobs: run: dart analyze if: always() - - name: Run build_runner - run: dart run build_runner build --delete-conflicting-outputs - if: always() - - name: Run tests with coverage enabled run: dart test --coverage=./coverage if: always() diff --git a/.github/workflows/dart-unit-tests.yml b/.github/workflows/dart-unit-tests.yml index 47784bbd..1206f668 100644 --- a/.github/workflows/dart-unit-tests.yml +++ b/.github/workflows/dart-unit-tests.yml @@ -6,18 +6,13 @@ on: jobs: test: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - sdk: [stable] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v1 with: - sdk: ${{ matrix.sdk }} + sdk: stable - name: Print Dart SDK version run: dart --version @@ -32,10 +27,6 @@ jobs: run: dart analyze if: always() - - name: Run build_runner - run: dart run build_runner build --delete-conflicting-outputs - if: always() - - name: Run tests run: dart test if: always() diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f825f79..3b3b1340 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.0.10 + +- Implements `lineSlice` [#158](https://github.com/dartclub/turf_dart/pull/158) +- Introduce [geotypes package](https://pub.dev/packages/geotypes) for GeoJSON serialization +- Other small improvements + ## 0.0.9 - Implements `length`, `along` [#153](https://github.com/dartclub/turf_dart/pull/153) diff --git a/Progress.md b/Progress.md new file mode 100644 index 00000000..3570f3f7 --- /dev/null +++ b/Progress.md @@ -0,0 +1,180 @@ + +## Progress +This document tracks the progress being made to port over all of the Turf functionality to +Dart. This is an on going project and functions are being added once needed. If you'd like to contribute by adding a Turf function that's missing, please open a GitHub issue still with information relative to why you need this functionality. + +### 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) +- [x] [lineOverlap](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_overlap.dart) +- [x] [lineSegment](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_segment.dart) +- [x] [lineSlice](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_slice.dart) +- [ ] 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) +- [x] [booleanOverlap](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_overlap.dart) +- [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) +- [x] [booleanWithin](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_within.dart) + +### 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 diff --git a/README.md b/README.md index 180e7c94..156317bb 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,24 @@ -# turf.dart +

+
+ 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