-
Notifications
You must be signed in to change notification settings - Fork 36
Point on feature final #216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Newmanjack
wants to merge
10
commits into
dartclub:main
Choose a base branch
from
deanpapas:point_on_feature_final
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
8dc4b65
Add pointOnFeature functionality to find points on GeoJSON features
Newmanjack 1cdef12
Update pointOnFeature tests and export functionality in turf.dart
Newmanjack c1b3fc7
Move point_on_feature test to components directory and improve test o…
Newmanjack f6c0e41
Add point_on_feature benchmark for performance testing
Newmanjack 41bfee5
Added toWGS84 and toMercator as member functions of the coordinate types
Newmanjack a343ee7
Improve documentation for coordinate projection functions to follow D…
Newmanjack 1dfdb72
Add point_on_feature examples with visualizations
Newmanjack cd53862
Refactor pointOnFeature to use modular methods and improve error hand…
Newmanjack a08fca7
Remove coordinate system conversion changes from helpers.dart
Newmanjack b0b0fb0
Update helpers_test.dart with version from main
Newmanjack File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import 'package:benchmark/benchmark.dart'; | ||
import 'package:turf/turf.dart'; | ||
|
||
// Create some test features for benchmarkings | ||
final point = Feature( | ||
geometry: Point(coordinates: Position.of([5.0, 10.0])), | ||
properties: {'name': 'Test Point'}, | ||
); | ||
|
||
final polygon = Feature<Polygon>( | ||
geometry: Polygon(coordinates: [ | ||
[ | ||
Position.of([-10.0, 0.0]), | ||
Position.of([10.0, 0.0]), | ||
Position.of([0.0, 20.0]), | ||
Position.of([-10.0, 0.0]) | ||
] | ||
]), | ||
properties: {'name': 'Triangle Polygon'}, | ||
); | ||
|
||
final lineString = Feature<LineString>( | ||
geometry: LineString(coordinates: [ | ||
Position.of([0.0, 0.0]), | ||
Position.of([10.0, 10.0]), | ||
Position.of([20.0, 20.0]) | ||
]), | ||
properties: {'name': 'Line String Example'}, | ||
); | ||
|
||
final featureCollection = FeatureCollection<GeometryObject>(features: [ | ||
Feature(geometry: Point(coordinates: Position.of([0.0, 0.0]))), | ||
Feature<Polygon>( | ||
geometry: Polygon(coordinates: [ | ||
[ | ||
Position.of([-10.0, -10.0]), | ||
Position.of([10.0, -10.0]), | ||
Position.of([10.0, 10.0]), | ||
Position.of([-10.0, 10.0]), | ||
Position.of([-10.0, -10.0]), | ||
] | ||
]), | ||
properties: {'name': 'Square Polygon'}, | ||
) | ||
]); | ||
|
||
void main() { | ||
group('pointOnFeature', () { | ||
benchmark('point feature', () { | ||
pointOnFeature(point); | ||
}); | ||
|
||
benchmark('polygon feature', () { | ||
pointOnFeature(polygon); | ||
}); | ||
|
||
benchmark('lineString feature', () { | ||
pointOnFeature(lineString); | ||
}); | ||
|
||
benchmark('feature collection', () { | ||
pointOnFeature(featureCollection); | ||
}); | ||
}); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
library turf_point_on_feature; | ||
|
||
export 'package:geotypes/geotypes.dart'; | ||
export 'src/point_on_feature.dart'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import 'package:geotypes/geotypes.dart'; | ||
import 'package:turf/area.dart' as turf_area; | ||
import 'package:turf/centroid.dart' as turf_centroid; | ||
import 'package:turf/helpers.dart'; | ||
import 'package:turf/length.dart' as turf_length; | ||
import 'package:turf/midpoint.dart' as turf_midpoint; | ||
import 'package:turf_pip/turf_pip.dart'; | ||
|
||
/// Returns a [Feature<Point>] that represents a point guaranteed to be on the feature. | ||
/// | ||
/// - For [Point] geometries: returns the original point | ||
/// - For [Polygon] geometries: computes a point inside the polygon (preference to centroid) | ||
/// - For [MultiPolygon] geometries: uses the first polygon to compute a point | ||
/// - For [LineString] geometries: computes the midpoint along the line | ||
/// - For [FeatureCollection]: returns a point on the largest feature | ||
/// | ||
/// The resulting point is guaranteed to be on the feature. | ||
/// | ||
/// Throws an [ArgumentError] if the input type is unsupported or if a valid point | ||
/// cannot be computed. | ||
Feature<Point> pointOnFeature(dynamic featureInput) { | ||
// Handle FeatureCollection | ||
if (featureInput is FeatureCollection) { | ||
if (featureInput.features.isEmpty) { | ||
throw ArgumentError('Cannot compute point on empty FeatureCollection'); | ||
} | ||
|
||
// Find the largest feature in the collection | ||
Feature largestFeature = featureInput.features.first; | ||
double maxSize = _calculateFeatureSize(largestFeature); | ||
|
||
for (final feature in featureInput.features.skip(1)) { | ||
final size = _calculateFeatureSize(feature); | ||
if (size > maxSize) { | ||
maxSize = size; | ||
largestFeature = feature; | ||
} | ||
} | ||
|
||
// Get a point on the largest feature | ||
return pointOnFeature(largestFeature); | ||
} | ||
|
||
// Handle individual feature | ||
if (featureInput is Feature) { | ||
final geometry = featureInput.geometry; | ||
|
||
if (geometry is Point) { | ||
// Already a point: return it. | ||
return Feature<Point>(geometry: geometry, properties: featureInput.properties); | ||
} else if (geometry is LineString) { | ||
// For LineString: compute the midpoint | ||
return _midpointOnLine(geometry, featureInput.properties); | ||
} else if (geometry is Polygon) { | ||
// Use the existing centroid function | ||
final Feature<Point> centroidFeature = turf_centroid.centroid( | ||
featureInput, | ||
properties: featureInput.properties, | ||
); | ||
// Use non-null assertion operator since we know the geometry exists | ||
final Point centroid = centroidFeature.geometry!; | ||
// Convert Point to Position for boolean check | ||
final pointPos = Position(centroid.coordinates[0] ?? 0.0, centroid.coordinates[1] ?? 0.0); | ||
|
||
// Use point-in-polygon from turf_pip package directly | ||
final pipResult = pointInPolygon(Point(coordinates: pointPos), geometry); | ||
if (pipResult == PointInPolygonResult.isInside || pipResult == PointInPolygonResult.isOnEdge) { | ||
return centroidFeature; | ||
} else { | ||
// Try each vertex of the outer ring. | ||
final outerRing = geometry.coordinates.first; | ||
for (final pos in outerRing) { | ||
final candidate = Point(coordinates: pos); | ||
final candidatePos = Position(candidate.coordinates[0] ?? 0.0, candidate.coordinates[1] ?? 0.0); | ||
final candidatePipResult = pointInPolygon(Point(coordinates: candidatePos), geometry); | ||
if (candidatePipResult == PointInPolygonResult.isInside || candidatePipResult == PointInPolygonResult.isOnEdge) { | ||
return Feature<Point>(geometry: candidate, properties: featureInput.properties); | ||
} | ||
} | ||
// Fallback: return the centroid. | ||
return centroidFeature; | ||
} | ||
} else if (geometry is MultiPolygon) { | ||
// Use the first polygon from the MultiPolygon. | ||
if (geometry.coordinates.isNotEmpty && geometry.coordinates.first.isNotEmpty) { | ||
final firstPoly = Polygon(coordinates: geometry.coordinates.first); | ||
return pointOnFeature(Feature( | ||
geometry: firstPoly, properties: featureInput.properties)); | ||
} | ||
throw ArgumentError('Cannot compute point on empty MultiPolygon'); | ||
} else { | ||
throw ArgumentError('Unsupported geometry type: ${geometry.runtimeType}'); | ||
} | ||
} | ||
|
||
// If we reach here, the input type is unsupported | ||
throw ArgumentError('Unsupported input type: ${featureInput.runtimeType}'); | ||
} | ||
|
||
/// Calculates a representative midpoint on a [LineString]. | ||
Feature<Point> _midpointOnLine(LineString line, Map<String, dynamic>? properties) { | ||
final coords = line.coordinates; | ||
if (coords.isEmpty) { | ||
// Fallback for empty LineString - should not happen with valid GeoJSON | ||
return Feature<Point>( | ||
geometry: Point(coordinates: Position(0, 0)), | ||
properties: properties | ||
); | ||
} | ||
|
||
if (coords.length == 1) { | ||
// Only one point in the LineString | ||
return Feature<Point>( | ||
geometry: Point(coordinates: coords.first), | ||
properties: properties | ||
); | ||
} | ||
|
||
// Calculate the midpoint of the first segment using the midpoint library function | ||
// This gives a geodesically correct midpoint considering the curvature of the earth | ||
final start = coords[0]; | ||
final end = coords[1]; | ||
|
||
final startPoint = Point(coordinates: start); | ||
final endPoint = Point(coordinates: end); | ||
|
||
final midpoint = turf_midpoint.midpoint(startPoint, endPoint); | ||
|
||
return Feature<Point>( | ||
geometry: midpoint, | ||
properties: properties | ||
); | ||
} | ||
|
||
/// Helper to estimate the "size" of a feature for comparison. | ||
double _calculateFeatureSize(Feature feature) { | ||
final geometry = feature.geometry; | ||
|
||
if (geometry is Point) { | ||
return 0; // Points have zero area | ||
} else if (geometry is LineString) { | ||
// Use the library's length function for accurate distance calculation | ||
final num calculatedLength = turf_length.length( | ||
Feature<LineString>(geometry: geometry), | ||
Unit.kilometers | ||
); | ||
return calculatedLength.toDouble(); | ||
} else if (geometry is Polygon || geometry is MultiPolygon) { | ||
// Use the library's area function for accurate area calculation | ||
final num? calculatedArea = turf_area.area(Feature(geometry: geometry)); | ||
return calculatedArea?.toDouble() ?? 0.0; | ||
} | ||
|
||
// Return 0 for unsupported geometry types | ||
return 0; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import 'dart:convert'; | ||
import 'package:test/test.dart'; | ||
import 'package:turf/turf.dart'; | ||
|
||
void main() { | ||
group('Point On Feature', () { | ||
test('Point geometry - returns unchanged', () { | ||
// Create a Point feature | ||
final point = Feature( | ||
geometry: Point(coordinates: Position(5.0, 10.0)), | ||
properties: {'name': 'Test Point'}); | ||
|
||
final result = pointOnFeature(point); | ||
|
||
expect(result.geometry!.coordinates!.toList(), equals([5.0, 10.0])); | ||
}); | ||
|
||
test('Polygon geometry - returns point inside polygon', () { | ||
// Create a triangle polygon | ||
final polygon = Feature<Polygon>( | ||
geometry: Polygon(coordinates: [ | ||
[ | ||
Position(-10.0, 0.0), | ||
Position(10.0, 0.0), | ||
Position(0.0, 20.0), | ||
Position(-10.0, 0.0) | ||
] | ||
]), | ||
); | ||
|
||
final result = pointOnFeature(polygon); | ||
|
||
expect(result.geometry, isA<Point>()); | ||
|
||
// Simple check that result is within bounding box of polygon | ||
final coords = result.geometry!.coordinates!; | ||
expect(coords[0], greaterThanOrEqualTo(-10.0)); | ||
expect(coords[0], lessThanOrEqualTo(10.0)); | ||
expect(coords[1], greaterThanOrEqualTo(0.0)); | ||
expect(coords[1], lessThanOrEqualTo(20.0)); | ||
}); | ||
|
||
test('MultiPolygon - uses first polygon', () { | ||
// Create a MultiPolygon with two polygons | ||
final multiPolygon = Feature<MultiPolygon>( | ||
geometry: MultiPolygon(coordinates: [ | ||
[ | ||
[ | ||
Position(-10.0, 0.0), | ||
Position(10.0, 0.0), | ||
Position(0.0, 20.0), | ||
Position(-10.0, 0.0) | ||
] | ||
], | ||
[ | ||
[ | ||
Position(30.0, 10.0), | ||
Position(40.0, 10.0), | ||
Position(35.0, 20.0), | ||
Position(30.0, 10.0) | ||
] | ||
] | ||
]), | ||
); | ||
|
||
final result = pointOnFeature(multiPolygon); | ||
|
||
// Check if point is within first polygon's bounds | ||
final coords = result.geometry!.coordinates!; | ||
expect(coords[0], greaterThanOrEqualTo(-10.0)); | ||
expect(coords[0], lessThanOrEqualTo(10.0)); | ||
expect(coords[1], greaterThanOrEqualTo(0.0)); | ||
expect(coords[1], lessThanOrEqualTo(20.0)); | ||
}); | ||
|
||
test('LineString - computes midpoint of first segment using geodesic calculation', () { | ||
// Create a LineString with multiple segments | ||
final lineString = Feature<LineString>( | ||
geometry: LineString(coordinates: [ | ||
Position(0.0, 0.0), | ||
Position(10.0, 10.0), | ||
Position(20.0, 20.0) | ||
]), | ||
); | ||
|
||
final result = pointOnFeature(lineString); | ||
|
||
// The geodesic midpoint is calculated differently than arithmetic midpoint | ||
// Check that it returns a point (exact coordinates will vary based on the geodesic calculation) | ||
expect(result.geometry, isA<Point>()); | ||
|
||
final coords = result.geometry!.coordinates!; | ||
// Verify coordinates are near the expected midpoint region | ||
expect(coords[0], closeTo(5.0, 1.0)); // Allow some deviation due to geodesic calculation | ||
expect(coords[1], closeTo(5.0, 1.0)); // Allow some deviation due to geodesic calculation | ||
}); | ||
|
||
test('FeatureCollection - returns point on largest feature', () { | ||
// Create a FeatureCollection with a point and polygon | ||
final fc = FeatureCollection<GeometryObject>(features: [ | ||
Feature(geometry: Point(coordinates: Position(0.0, 0.0))), | ||
Feature<Polygon>( | ||
geometry: Polygon(coordinates: [ | ||
[ | ||
Position(-10.0, -10.0), | ||
Position(10.0, -10.0), | ||
Position(10.0, 10.0), | ||
Position(-10.0, 10.0), | ||
Position(-10.0, -10.0), | ||
] | ||
]), | ||
) | ||
]); | ||
|
||
final result = pointOnFeature(fc); | ||
|
||
// Check if point is within polygon bounds | ||
final coords = result.geometry!.coordinates!; | ||
expect(coords[0], greaterThanOrEqualTo(-10.0)); | ||
expect(coords[0], lessThanOrEqualTo(10.0)); | ||
expect(coords[1], greaterThanOrEqualTo(-10.0)); | ||
expect(coords[1], lessThanOrEqualTo(10.0)); | ||
}); | ||
|
||
test('Empty FeatureCollection throws ArgumentError', () { | ||
final emptyFC = FeatureCollection<GeometryObject>(features: []); | ||
expect(() => pointOnFeature(emptyFC), | ||
throwsA(isA<ArgumentError>().having( | ||
(e) => e.message, | ||
'message', | ||
'Cannot compute point on empty FeatureCollection' | ||
)) | ||
); | ||
}); | ||
}); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.