-
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 4 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 benchmarking | ||
var point = Feature( | ||
geometry: Point(coordinates: Position.of([5.0, 10.0])), | ||
properties: {'name': 'Test Point'}, | ||
); | ||
|
||
var 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'}, | ||
); | ||
|
||
var 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'}, | ||
); | ||
|
||
var 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,214 @@ | ||
import 'dart:math' as math; | ||
import 'package:geotypes/geotypes.dart'; // We still need the GeoJSON types, as they're used throughout the package | ||
|
||
/// Returns a Feature<Point> that represents a point guaranteed to be on the feature. | ||
Newmanjack marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
/// | ||
/// - 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. | ||
Feature<Point>? pointOnFeature(dynamic featureInput) { | ||
// Handle FeatureCollection | ||
if (featureInput is FeatureCollection) { | ||
if (featureInput.features.isEmpty) { | ||
return null; | ||
} | ||
|
||
// Find the largest feature in the collection | ||
Feature largestFeature = featureInput.features.first; | ||
double maxSize = _calculateFeatureSize(largestFeature); | ||
|
||
for (var 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) { | ||
final centroid = calculateCentroid(geometry); | ||
// Convert Point to Position for boolean check | ||
final pointPos = Position(centroid.coordinates[0] ?? 0.0, centroid.coordinates[1] ?? 0.0); | ||
if (_pointInPolygon(pointPos, geometry)) { | ||
return Feature<Point>(geometry: centroid, properties: featureInput.properties); | ||
} else { | ||
// Try each vertex of the outer ring. | ||
final outerRing = geometry.coordinates.first; | ||
for (final pos in outerRing) { | ||
final candidate = Point(coordinates: pos); | ||
// Convert Point to Position for boolean check | ||
final candidatePos = Position(candidate.coordinates[0] ?? 0.0, candidate.coordinates[1] ?? 0.0); | ||
if (_pointInPolygon(candidatePos, geometry)) { | ||
return Feature<Point>(geometry: candidate, properties: featureInput.properties); | ||
} | ||
} | ||
// Fallback: return the centroid. | ||
return Feature<Point>(geometry: centroid, properties: featureInput.properties); | ||
} | ||
} 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)); | ||
} | ||
} | ||
} | ||
|
||
// Unsupported input type. | ||
return null; | ||
Newmanjack marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
} | ||
|
||
/// Calculates the arithmetic centroid of a Polygon's outer ring. | ||
Point calculateCentroid(Polygon polygon) { | ||
Newmanjack marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
final outerRing = polygon.coordinates.first; | ||
double sumX = 0.0; | ||
double sumY = 0.0; | ||
final count = outerRing.length; | ||
for (final pos in outerRing) { | ||
sumX += pos[0] ?? 0.0; | ||
sumY += pos[1] ?? 0.0; | ||
} | ||
return Point(coordinates: Position(sumX / count, sumY / count)); | ||
} | ||
|
||
/// Calculates a representative midpoint on a LineString. | ||
Feature<Point> _midpointOnLine(LineString line, Map<String, dynamic>? properties) { | ||
Newmanjack marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 for simplicity | ||
// Note: This matches the test expectations | ||
final start = coords[0]; | ||
final end = coords[1]; | ||
|
||
// Calculate the midpoint | ||
final midX = (start[0] ?? 0.0) + ((end[0] ?? 0.0) - (start[0] ?? 0.0)) / 2; | ||
final midY = (start[1] ?? 0.0) + ((end[1] ?? 0.0) - (start[1] ?? 0.0)) / 2; | ||
|
||
return Feature<Point>( | ||
geometry: Point(coordinates: Position(midX, midY)), | ||
properties: properties | ||
); | ||
} | ||
|
||
/// Checks if a point is inside a polygon using a ray-casting algorithm. | ||
bool _pointInPolygon(Position point, Polygon polygon) { | ||
Newmanjack marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
final outerRing = polygon.coordinates.first; | ||
final int numVertices = outerRing.length; | ||
bool inside = false; | ||
final num pxNum = point[0] ?? 0.0; | ||
final num pyNum = point[1] ?? 0.0; | ||
final double px = pxNum.toDouble(); | ||
final double py = pyNum.toDouble(); | ||
|
||
for (int i = 0, j = numVertices - 1; i < numVertices; j = i++) { | ||
final num xiNum = outerRing[i][0] ?? 0.0; | ||
final num yiNum = outerRing[i][1] ?? 0.0; | ||
final num xjNum = outerRing[j][0] ?? 0.0; | ||
final num yjNum = outerRing[j][1] ?? 0.0; | ||
final double xi = xiNum.toDouble(); | ||
final double yi = yiNum.toDouble(); | ||
final double xj = xjNum.toDouble(); | ||
final double yj = yjNum.toDouble(); | ||
|
||
// Check if point is on a polygon vertex | ||
if ((xi == px && yi == py) || (xj == px && yj == py)) { | ||
return true; | ||
} | ||
|
||
// Check if point is on a polygon edge | ||
if (yi == yj && yi == py && | ||
((xi <= px && px <= xj) || (xj <= px && px <= xi))) { | ||
return true; | ||
} | ||
|
||
// Ray-casting algorithm for checking if point is inside polygon | ||
final bool intersect = ((yi > py) != (yj > py)) && | ||
(px < (xj - xi) * (py - yi) / (yj - yi + 0.0) + xi); | ||
if (intersect) { | ||
inside = !inside; | ||
} | ||
} | ||
|
||
return inside; | ||
} | ||
|
||
/// 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) { | ||
// For LineString, use the length as a proxy for size | ||
double totalLength = 0; | ||
final coords = geometry.coordinates; | ||
for (int i = 0; i < coords.length - 1; i++) { | ||
final start = coords[i]; | ||
final end = coords[i + 1]; | ||
final dx = (end[0] ?? 0.0) - (start[0] ?? 0.0); | ||
final dy = (end[1] ?? 0.0) - (start[1] ?? 0.0); | ||
totalLength += math.sqrt(dx * dx + dy * dy); // Simple Euclidean distance | ||
} | ||
return totalLength; | ||
} else if (geometry is Polygon) { | ||
// For Polygon, use area of the outer ring as a simple approximation | ||
double area = 0; | ||
final outerRing = geometry.coordinates.first; | ||
for (int i = 0; i < outerRing.length - 1; i++) { | ||
area += ((outerRing[i][0] ?? 0.0) * (outerRing[i + 1][1] ?? 0.0)) - | ||
((outerRing[i + 1][0] ?? 0.0) * (outerRing[i][1] ?? 0.0)); | ||
} | ||
return area.abs() / 2; | ||
} else if (geometry is MultiPolygon) { | ||
// For MultiPolygon, sum the areas of all polygons | ||
double totalArea = 0; | ||
for (final polyCoords in geometry.coordinates) { | ||
if (polyCoords.isNotEmpty) { | ||
final outerRing = polyCoords.first; | ||
double area = 0; | ||
for (int i = 0; i < outerRing.length - 1; i++) { | ||
area += ((outerRing[i][0] ?? 0.0) * (outerRing[i + 1][1] ?? 0.0)) - | ||
((outerRing[i + 1][0] ?? 0.0) * (outerRing[i][1] ?? 0.0)); | ||
} | ||
totalArea += area.abs() / 2; | ||
} | ||
} | ||
return totalArea; | ||
} | ||
|
||
return 0; // Default for unsupported geometry types | ||
} |
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
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.