11import 'dart:math' as math;
2- import 'package:geotypes/geotypes.dart' ; // We still need the GeoJSON types, as they're used throughout the package
2+ import 'package:geotypes/geotypes.dart' ;
3+ import 'package:turf/centroid.dart' ;
4+ import 'package:turf/boolean.dart' ;
5+ import 'package:turf/midpoint.dart' ;
36
4- /// Returns a Feature<Point> that represents a point guaranteed to be on the feature.
7+ /// Returns a [ Feature<Point>] that represents a point guaranteed to be on the feature.
58///
6- /// - For Point geometries: returns the original point
7- /// - For Polygon geometries: computes a point inside the polygon (preference to centroid)
8- /// - For MultiPolygon geometries: uses the first polygon to compute a point
9- /// - For LineString geometries: computes the midpoint along the line
10- /// - For FeatureCollection: returns a point on the largest feature
9+ /// - For [ Point] geometries: returns the original point
10+ /// - For [ Polygon] geometries: computes a point inside the polygon (preference to centroid)
11+ /// - For [ MultiPolygon] geometries: uses the first polygon to compute a point
12+ /// - For [ LineString] geometries: computes the midpoint along the line
13+ /// - For [ FeatureCollection] : returns a point on the largest feature
1114///
1215/// The resulting point is guaranteed to be on the feature.
13- Feature <Point >? pointOnFeature (dynamic featureInput) {
16+ /// Throws an exception for unsupported input types.
17+ Feature <Point > pointOnFeature (dynamic featureInput) {
1418 // Handle FeatureCollection
1519 if (featureInput is FeatureCollection ) {
1620 if (featureInput.features.isEmpty) {
17- return null ;
21+ throw Exception ( 'Empty FeatureCollection: Cannot compute a point on an empty collection' ) ;
1822 }
1923
2024 // Find the largest feature in the collection
@@ -41,129 +45,62 @@ Feature<Point>? pointOnFeature(dynamic featureInput) {
4145 // Already a point: return it.
4246 return Feature <Point >(geometry: geometry, properties: featureInput.properties);
4347 } else if (geometry is LineString ) {
44- // For LineString: compute the midpoint
45- return _midpointOnLine (geometry, featureInput.properties);
48+ // For LineString: compute the midpoint using the first segment
49+ final coords = geometry.coordinates;
50+ if (coords.isEmpty) {
51+ throw Exception ('Empty LineString: Cannot compute a point on an empty line' );
52+ }
53+
54+ if (coords.length == 1 ) {
55+ // Only one point in the LineString, return it
56+ return Feature <Point >(
57+ geometry: Point (coordinates: coords.first),
58+ properties: featureInput.properties
59+ );
60+ }
61+
62+ // Calculate the midpoint of the first segment
63+ final pt1 = Point (coordinates: coords[0 ]);
64+ final pt2 = Point (coordinates: coords[1 ]);
65+ final mid = midpoint (pt1, pt2);
66+
67+ return Feature <Point >(
68+ geometry: mid,
69+ properties: featureInput.properties
70+ );
4671 } else if (geometry is Polygon ) {
47- final centroid = calculateCentroid (geometry);
48- // Convert Point to Position for boolean check
49- final pointPos = Position (centroid.coordinates[0 ] ?? 0.0 , centroid.coordinates[1 ] ?? 0.0 );
50- if (_pointInPolygon (pointPos, geometry)) {
51- return Feature <Point >(geometry: centroid, properties: featureInput.properties);
72+ // For Polygon: try to use the centroid, and fall back to a vertex if needed
73+ final centroidFeature = centroid (geometry);
74+ final cent = centroidFeature.geometry! ;
75+
76+ if (booleanPointInPolygon (cent.coordinates! , geometry)) {
77+ return Feature <Point >(geometry: cent, properties: featureInput.properties);
5278 } else {
5379 // Try each vertex of the outer ring.
5480 final outerRing = geometry.coordinates.first;
5581 for (final pos in outerRing) {
5682 final candidate = Point (coordinates: pos);
57- // Convert Point to Position for boolean check
58- final candidatePos = Position (candidate.coordinates[0 ] ?? 0.0 , candidate.coordinates[1 ] ?? 0.0 );
59- if (_pointInPolygon (candidatePos, geometry)) {
83+ if (booleanPointInPolygon (candidate.coordinates! , geometry)) {
6084 return Feature <Point >(geometry: candidate, properties: featureInput.properties);
6185 }
6286 }
6387 // Fallback: return the centroid.
64- return Feature <Point >(geometry: centroid , properties: featureInput.properties);
88+ return Feature <Point >(geometry: cent , properties: featureInput.properties);
6589 }
6690 } else if (geometry is MultiPolygon ) {
6791 // Use the first polygon from the MultiPolygon.
68- if (geometry.coordinates.isNotEmpty && geometry.coordinates.first.isNotEmpty) {
69- final firstPoly = Polygon (coordinates: geometry.coordinates.first);
70- return pointOnFeature (Feature (
71- geometry: firstPoly, properties: featureInput.properties));
92+ if (geometry.coordinates.isEmpty || geometry.coordinates.first.isEmpty) {
93+ throw Exception ('Empty MultiPolygon: Cannot compute a point on an empty geometry' );
7294 }
95+
96+ final firstPoly = Polygon (coordinates: geometry.coordinates.first);
97+ return pointOnFeature (Feature (
98+ geometry: firstPoly, properties: featureInput.properties));
7399 }
74100 }
75101
76- // Unsupported input type.
77- return null ;
78- }
79-
80- /// Calculates the arithmetic centroid of a Polygon's outer ring.
81- Point calculateCentroid (Polygon polygon) {
82- final outerRing = polygon.coordinates.first;
83- double sumX = 0.0 ;
84- double sumY = 0.0 ;
85- final count = outerRing.length;
86- for (final pos in outerRing) {
87- sumX += pos[0 ] ?? 0.0 ;
88- sumY += pos[1 ] ?? 0.0 ;
89- }
90- return Point (coordinates: Position (sumX / count, sumY / count));
91- }
92-
93- /// Calculates a representative midpoint on a LineString.
94- Feature <Point > _midpointOnLine (LineString line, Map <String , dynamic >? properties) {
95- final coords = line.coordinates;
96- if (coords.isEmpty) {
97- // Fallback for empty LineString - should not happen with valid GeoJSON
98- return Feature <Point >(
99- geometry: Point (coordinates: Position (0 , 0 )),
100- properties: properties
101- );
102- }
103-
104- if (coords.length == 1 ) {
105- // Only one point in the LineString
106- return Feature <Point >(
107- geometry: Point (coordinates: coords.first),
108- properties: properties
109- );
110- }
111-
112- // Calculate the midpoint of the first segment for simplicity
113- // Note: This matches the test expectations
114- final start = coords[0 ];
115- final end = coords[1 ];
116-
117- // Calculate the midpoint
118- final midX = (start[0 ] ?? 0.0 ) + ((end[0 ] ?? 0.0 ) - (start[0 ] ?? 0.0 )) / 2 ;
119- final midY = (start[1 ] ?? 0.0 ) + ((end[1 ] ?? 0.0 ) - (start[1 ] ?? 0.0 )) / 2 ;
120-
121- return Feature <Point >(
122- geometry: Point (coordinates: Position (midX, midY)),
123- properties: properties
124- );
125- }
126-
127- /// Checks if a point is inside a polygon using a ray-casting algorithm.
128- bool _pointInPolygon (Position point, Polygon polygon) {
129- final outerRing = polygon.coordinates.first;
130- final int numVertices = outerRing.length;
131- bool inside = false ;
132- final num pxNum = point[0 ] ?? 0.0 ;
133- final num pyNum = point[1 ] ?? 0.0 ;
134- final double px = pxNum.toDouble ();
135- final double py = pyNum.toDouble ();
136-
137- for (int i = 0 , j = numVertices - 1 ; i < numVertices; j = i++ ) {
138- final num xiNum = outerRing[i][0 ] ?? 0.0 ;
139- final num yiNum = outerRing[i][1 ] ?? 0.0 ;
140- final num xjNum = outerRing[j][0 ] ?? 0.0 ;
141- final num yjNum = outerRing[j][1 ] ?? 0.0 ;
142- final double xi = xiNum.toDouble ();
143- final double yi = yiNum.toDouble ();
144- final double xj = xjNum.toDouble ();
145- final double yj = yjNum.toDouble ();
146-
147- // Check if point is on a polygon vertex
148- if ((xi == px && yi == py) || (xj == px && yj == py)) {
149- return true ;
150- }
151-
152- // Check if point is on a polygon edge
153- if (yi == yj && yi == py &&
154- ((xi <= px && px <= xj) || (xj <= px && px <= xi))) {
155- return true ;
156- }
157-
158- // Ray-casting algorithm for checking if point is inside polygon
159- final bool intersect = ((yi > py) != (yj > py)) &&
160- (px < (xj - xi) * (py - yi) / (yj - yi + 0.0 ) + xi);
161- if (intersect) {
162- inside = ! inside;
163- }
164- }
165-
166- return inside;
102+ // Throw an exception for unsupported input types
103+ throw Exception ('Unsupported input type: Cannot compute a point on ${featureInput .runtimeType }' );
167104}
168105
169106/// Helper to estimate the "size" of a feature for comparison.
0 commit comments