diff --git a/benchmark/point_on_feature_benchmark.dart b/benchmark/point_on_feature_benchmark.dart new file mode 100644 index 00000000..4cb188d6 --- /dev/null +++ b/benchmark/point_on_feature_benchmark.dart @@ -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( + 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( + 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(features: [ + Feature(geometry: Point(coordinates: Position.of([0.0, 0.0]))), + Feature( + 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); + }); + }); +} diff --git a/lib/point_on_feature.dart b/lib/point_on_feature.dart new file mode 100644 index 00000000..94b7daf3 --- /dev/null +++ b/lib/point_on_feature.dart @@ -0,0 +1,4 @@ +library turf_point_on_feature; + +export 'package:geotypes/geotypes.dart'; +export 'src/point_on_feature.dart'; diff --git a/lib/src/point_on_feature.dart b/lib/src/point_on_feature.dart new file mode 100644 index 00000000..1497e0c0 --- /dev/null +++ b/lib/src/point_on_feature.dart @@ -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] 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 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(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 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(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 _midpointOnLine(LineString line, Map? properties) { + final coords = line.coordinates; + if (coords.isEmpty) { + // Fallback for empty LineString - should not happen with valid GeoJSON + return Feature( + geometry: Point(coordinates: Position(0, 0)), + properties: properties + ); + } + + if (coords.length == 1) { + // Only one point in the LineString + return Feature( + 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( + 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(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; +} diff --git a/lib/turf.dart b/lib/turf.dart index 482694bb..374467c8 100644 --- a/lib/turf.dart +++ b/lib/turf.dart @@ -29,6 +29,7 @@ export 'midpoint.dart'; export 'nearest_point_on_line.dart'; export 'nearest_point.dart'; export 'point_to_line_distance.dart'; +export 'point_on_feature.dart'; export 'polygon_smooth.dart'; export 'polygon_to_line.dart'; export 'polyline.dart'; diff --git a/test/components/point_on_feature_test.dart b/test/components/point_on_feature_test.dart new file mode 100644 index 00000000..0ee5ed64 --- /dev/null +++ b/test/components/point_on_feature_test.dart @@ -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( + 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()); + + // 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( + 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( + 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()); + + 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(features: [ + Feature(geometry: Point(coordinates: Position(0.0, 0.0))), + Feature( + 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(features: []); + expect(() => pointOnFeature(emptyFC), + throwsA(isA().having( + (e) => e.message, + 'message', + 'Cannot compute point on empty FeatureCollection' + )) + ); + }); + }); +} diff --git a/test/examples/point_on_feature/README.md b/test/examples/point_on_feature/README.md new file mode 100644 index 00000000..e13b15c6 --- /dev/null +++ b/test/examples/point_on_feature/README.md @@ -0,0 +1,63 @@ +# Point on Feature Examples + +This directory contains examples demonstrating the `pointOnFeature` function in the turf_dart library. + +## Function Overview + +The `pointOnFeature` function returns a point that is guaranteed to be on or inside a feature. This is useful for placing labels, icons, or other markers on geographic features. + +The function behavior varies by geometry type: +- **Point**: Returns the original point unchanged +- **LineString**: Returns the midpoint of the first segment +- **Polygon**: Returns a point inside the polygon (preferably the centroid) +- **MultiPolygon**: Uses the first polygon to compute a point +- **FeatureCollection**: Returns a point on the largest feature + +## Directory Structure + +- **`/in`**: Input GeoJSON files with different geometry types +- **`/out`**: Output files showing the points generated by `pointOnFeature` +- **`visualization.geojson`**: Combined visualization of all inputs and their resulting points + +## Example Files + +1. **Point Example**: Shows that `pointOnFeature` returns the original point +2. **LineString Example**: Shows how `pointOnFeature` finds the midpoint of the first line segment +3. **Polygon Examples**: Show how `pointOnFeature` returns a point inside the polygon +4. **MultiPolygon Example**: Shows how `pointOnFeature` uses the first polygon +5. **FeatureCollection Example**: Shows how `pointOnFeature` finds a point on the largest feature + +## Visualization + +The `visualization.geojson` file combines all examples into one visualization. When viewed in a GeoJSON viewer, it shows: +- Original geometries in blue +- Points generated by `pointOnFeature` in red with different markers: + - Stars for points + - Circles for linestrings + - Triangles for polygons + - Squares for multipolygons + - Circle-stroked for feature collections + +Each point includes a description explaining how it was generated. + +## Running the Examples + +To regenerate the examples and visualization: + +1. Run the output generator: + ``` + dart test/examples/point_on_feature/generate_outputs.dart + ``` + +2. Run the visualization generator: + ``` + dart test/examples/point_on_feature/generate_visualization.dart + ``` + +## Use Cases + +The `pointOnFeature` function is commonly used for: +- Placing labels on geographic features +- Positioning icons or markers on features +- Finding representative points for complex geometries +- Generating points for clustering or other spatial operations diff --git a/test/examples/point_on_feature/generate_outputs.dart b/test/examples/point_on_feature/generate_outputs.dart new file mode 100644 index 00000000..b3a3e24e --- /dev/null +++ b/test/examples/point_on_feature/generate_outputs.dart @@ -0,0 +1,157 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:turf/turf.dart'; + +void main() async { + // Process each input file + await processFile('polygon_feature.geojson'); + await processFile('polygon.geojson'); + await processFile('point.geojson'); + await processFile('linestring.geojson'); + await processFile('multipolygon.geojson'); + await processFile('featurecollection.geojson'); + + print('All files processed successfully!'); +} + +Future processFile(String filename) async { + try { + final inputPath = 'test/examples/point_on_feature/in/$filename'; + final outputPath = 'test/examples/point_on_feature/out/$filename'; + + print('Processing $inputPath'); + + // Read the input file + final file = File(inputPath); + final jsonString = await file.readAsString(); + final geojson = jsonDecode(jsonString); + + // Parse the GeoJSON and create appropriate object based on type + dynamic featureInput; + + if (geojson['type'] == 'Feature') { + featureInput = Feature.fromJson(geojson); + } else if (geojson['type'] == 'FeatureCollection') { + featureInput = FeatureCollection.fromJson(geojson); + } else { + // For raw geometry objects, create a Feature with the geometry + final geometry = parseGeometry(geojson); + if (geometry != null) { + featureInput = Feature(geometry: geometry); + } else { + print(' Unsupported geometry type: ${geojson['type']}'); + return; + } + } + + // Apply point_on_feature function + final pointResult = pointOnFeature(featureInput); + + // Generate output - wrap in a FeatureCollection for better compatibility + Map outputJson; + + if (pointResult != null) { + final features = []; + + // Create a new feature based on the input geometry + GeometryObject? inputGeometry; + if (featureInput is Feature) { + inputGeometry = featureInput.geometry; + } else if (featureInput is FeatureCollection && featureInput.features.isNotEmpty) { + inputGeometry = featureInput.features[0].geometry; + } else { + inputGeometry = parseGeometry(geojson); + } + + if (inputGeometry != null) { + // Create a new feature with the input geometry + final styledInputFeature = Feature( + geometry: inputGeometry, + properties: { + 'name': 'Input Geometry', + 'description': 'Original geometry from $filename' + } + ); + + // Add styling based on geometry type + if (inputGeometry is Polygon || inputGeometry is MultiPolygon) { + styledInputFeature.properties!['stroke'] = '#0000FF'; + styledInputFeature.properties!['stroke-width'] = 2; + styledInputFeature.properties!['fill'] = '#0000FF'; + styledInputFeature.properties!['fill-opacity'] = 0.2; + } else if (inputGeometry is LineString || inputGeometry is MultiLineString) { + styledInputFeature.properties!['stroke'] = '#0000FF'; + styledInputFeature.properties!['stroke-width'] = 2; + } else if (inputGeometry is Point) { + styledInputFeature.properties!['marker-color'] = '#0000FF'; + } + + features.add(styledInputFeature); + } + + // Create a new feature for the point result to avoid modifying unmodifiable maps + final styledPointResult = Feature( + geometry: pointResult.geometry, + properties: { + 'marker-color': '#FF0000', + 'marker-size': 'medium', + 'marker-symbol': 'star', + 'name': 'Point on Feature Result', + 'description': 'Point generated by pointOnFeature function' + } + ); + + features.add(styledPointResult); + + outputJson = FeatureCollection(features: features).toJson(); + print(' Found point at coordinates: ${pointResult.geometry?.coordinates}'); + } else { + // Create an empty FeatureCollection with error info in properties + outputJson = { + 'type': 'FeatureCollection', + 'features': [ + { + 'type': 'Feature', + 'properties': { + 'error': 'Could not generate point for this input', + 'name': 'Error', + 'description': 'pointOnFeature function could not generate a point' + }, + 'geometry': null + } + ] + }; + print(' Could not generate point for this input'); + } + + // Write to output file with pretty formatting + final outputFile = File(outputPath); + await outputFile.writeAsString(JsonEncoder.withIndent(' ').convert(outputJson)); + print(' Saved result to $outputPath'); + } catch (e) { + print('Error processing $filename: $e'); + } +} + +GeometryObject? parseGeometry(Map json) { + final type = json['type']; + + switch (type) { + case 'Point': + return Point.fromJson(json); + case 'LineString': + return LineString.fromJson(json); + case 'Polygon': + return Polygon.fromJson(json); + case 'MultiPoint': + return MultiPoint.fromJson(json); + case 'MultiLineString': + return MultiLineString.fromJson(json); + case 'MultiPolygon': + return MultiPolygon.fromJson(json); + case 'GeometryCollection': + return GeometryCollection.fromJson(json); + default: + return null; + } +} diff --git a/test/examples/point_on_feature/generate_visualization.dart b/test/examples/point_on_feature/generate_visualization.dart new file mode 100644 index 00000000..6043cf48 --- /dev/null +++ b/test/examples/point_on_feature/generate_visualization.dart @@ -0,0 +1,139 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:turf/turf.dart'; + +void main() async { + // Create a single visualization file showing all inputs and their corresponding points + await generateVisualization(); + + print('Visualization file generated successfully!'); +} + +Future generateVisualization() async { + try { + final files = [ + 'polygon_feature.geojson', + 'polygon.geojson', + 'linestring.geojson', + 'multipolygon.geojson', + 'point.geojson', + 'featurecollection.geojson' + ]; + + // List to store all features + final allFeatures = []; + + // Process each output file only (since they now contain both input and result) + for (final filename in files) { + final outputPath = 'test/examples/point_on_feature/out/$filename'; + + print('Processing $filename for visualization'); + + // Read the output file + final outputFile = File(outputPath); + + if (!outputFile.existsSync()) { + print(' Missing output file for $filename, skipping'); + continue; + } + + final outputJson = jsonDecode(await outputFile.readAsString()); + + // The output files are already FeatureCollections with styled features + if (outputJson['type'] == 'FeatureCollection' && outputJson['features'] is List) { + final outFeatures = outputJson['features'] as List; + + // Add custom markers based on the geometry type for the result point + for (final feature in outFeatures) { + if (feature['properties'] != null && + feature['properties']['name'] == 'Point on Feature Result') { + + // Update description based on geometry type + if (filename == 'point.geojson') { + feature['properties']['description'] = 'Point on a Point: Returns the original point unchanged'; + feature['properties']['marker-symbol'] = 'star'; + } else if (filename == 'linestring.geojson') { + feature['properties']['description'] = 'Point on a LineString: Returns the midpoint of the first segment'; + feature['properties']['marker-symbol'] = 'circle'; + } else if (filename.contains('polygon') && !filename.contains('multi')) { + feature['properties']['description'] = 'Point on a Polygon: Returns a point inside the polygon (prefers centroid)'; + feature['properties']['marker-symbol'] = 'triangle'; + } else if (filename == 'multipolygon.geojson') { + feature['properties']['description'] = 'Point on a MultiPolygon: Returns a point from the first polygon'; + feature['properties']['marker-symbol'] = 'square'; + } else if (filename == 'featurecollection.geojson') { + feature['properties']['description'] = 'Point on a FeatureCollection: Returns a point on the largest feature'; + feature['properties']['marker-symbol'] = 'circle-stroked'; + } + + feature['properties']['name'] = 'Result: $filename'; + } + + // Add the feature to our collection + try { + final parsedFeature = Feature.fromJson(feature); + allFeatures.add(parsedFeature); + } catch (e) { + print(' Error parsing feature: $e'); + } + } + } + } + + // Create the feature collection + final featureCollection = FeatureCollection(features: allFeatures); + + // Save the visualization file with pretty formatting + final visualizationFile = File('test/examples/point_on_feature/visualization.geojson'); + await visualizationFile.writeAsString(JsonEncoder.withIndent(' ').convert(featureCollection.toJson())); + + print('Saved visualization to ${visualizationFile.path}'); + } catch (e) { + print('Error generating visualization: $e'); + } +} + +// Helper function to set style properties for features +void setFeatureStyle(Feature feature, String color, int width, double opacity) { + feature.properties = feature.properties ?? {}; + + // Different styling based on geometry type + if (feature.geometry is Polygon || feature.geometry is MultiPolygon) { + feature.properties!['stroke'] = color; + feature.properties!['stroke-width'] = width; + feature.properties!['stroke-opacity'] = 1; + feature.properties!['fill'] = color; + feature.properties!['fill-opacity'] = opacity; + } else if (feature.geometry is LineString || feature.geometry is MultiLineString) { + feature.properties!['stroke'] = color; + feature.properties!['stroke-width'] = width; + feature.properties!['stroke-opacity'] = 1; + } else if (feature.geometry is Point || feature.geometry is MultiPoint) { + feature.properties!['marker-color'] = color; + feature.properties!['marker-size'] = 'small'; + } +} + +// Helper function to parse geometries from JSON +GeometryObject? parseGeometry(Map json) { + final type = json['type']; + + switch (type) { + case 'Point': + return Point.fromJson(json); + case 'LineString': + return LineString.fromJson(json); + case 'Polygon': + return Polygon.fromJson(json); + case 'MultiPoint': + return MultiPoint.fromJson(json); + case 'MultiLineString': + return MultiLineString.fromJson(json); + case 'MultiPolygon': + return MultiPolygon.fromJson(json); + case 'GeometryCollection': + return GeometryCollection.fromJson(json); + default: + return null; + } +} diff --git a/test/examples/point_on_feature/in/featurecollection.geojson b/test/examples/point_on_feature/in/featurecollection.geojson new file mode 100644 index 00000000..5fc6f174 --- /dev/null +++ b/test/examples/point_on_feature/in/featurecollection.geojson @@ -0,0 +1,37 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "Point Feature" + }, + "geometry": { + "type": "Point", + "coordinates": [5.0, 10.0] + } + }, + { + "type": "Feature", + "properties": { + "name": "Polygon Feature", + "stroke": "#F00", + "stroke-width": 2, + "fill": "#F00", + "fill-opacity": 0.3 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-10.0, -10.0], + [10.0, -10.0], + [10.0, 10.0], + [-10.0, 10.0], + [-10.0, -10.0] + ] + ] + } + } + ] +} diff --git a/test/examples/point_on_feature/in/linestring.geojson b/test/examples/point_on_feature/in/linestring.geojson new file mode 100644 index 00000000..e23b747f --- /dev/null +++ b/test/examples/point_on_feature/in/linestring.geojson @@ -0,0 +1,8 @@ +{ + "type": "LineString", + "coordinates": [ + [0.0, 0.0], + [10.0, 10.0], + [20.0, 20.0] + ] +} diff --git a/test/examples/point_on_feature/in/multipolygon.geojson b/test/examples/point_on_feature/in/multipolygon.geojson new file mode 100644 index 00000000..f2530d7a --- /dev/null +++ b/test/examples/point_on_feature/in/multipolygon.geojson @@ -0,0 +1,21 @@ +{ + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [-10.0, 0.0], + [10.0, 0.0], + [0.0, 20.0], + [-10.0, 0.0] + ] + ], + [ + [ + [30.0, 10.0], + [40.0, 10.0], + [35.0, 20.0], + [30.0, 10.0] + ] + ] + ] +} diff --git a/test/examples/point_on_feature/in/point.geojson b/test/examples/point_on_feature/in/point.geojson new file mode 100644 index 00000000..d8771614 --- /dev/null +++ b/test/examples/point_on_feature/in/point.geojson @@ -0,0 +1,4 @@ +{ + "type": "Point", + "coordinates": [5.0, 10.0] +} diff --git a/test/examples/point_on_feature/in/polygon.geojson b/test/examples/point_on_feature/in/polygon.geojson new file mode 100644 index 00000000..2af49097 --- /dev/null +++ b/test/examples/point_on_feature/in/polygon.geojson @@ -0,0 +1,11 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [-10.0, 0.0], + [10.0, 0.0], + [0.0, 20.0], + [-10.0, 0.0] + ] + ] +} diff --git a/test/examples/point_on_feature/in/polygon_feature.geojson b/test/examples/point_on_feature/in/polygon_feature.geojson new file mode 100644 index 00000000..835b76a7 --- /dev/null +++ b/test/examples/point_on_feature/in/polygon_feature.geojson @@ -0,0 +1,21 @@ +{ + "type": "Feature", + "properties": { + "stroke": "#F00", + "stroke-width": 2, + "fill": "#F00", + "fill-opacity": 0.3, + "name": "Polygon Feature" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-10.0, 0.0], + [10.0, 0.0], + [0.0, 20.0], + [-10.0, 0.0] + ] + ] + } +} diff --git a/test/examples/point_on_feature/out/featurecollection.geojson b/test/examples/point_on_feature/out/featurecollection.geojson new file mode 100644 index 00000000..5555c22c --- /dev/null +++ b/test/examples/point_on_feature/out/featurecollection.geojson @@ -0,0 +1,42 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + 5.0, + 10.0 + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from featurecollection.geojson", + "marker-color": "#0000FF" + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + -2.0, + -2.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "star", + "name": "Point on Feature Result", + "description": "Point generated by pointOnFeature function" + } + } + ], + "bbox": null +} \ No newline at end of file diff --git a/test/examples/point_on_feature/out/linestring.geojson b/test/examples/point_on_feature/out/linestring.geojson new file mode 100644 index 00000000..34358e56 --- /dev/null +++ b/test/examples/point_on_feature/out/linestring.geojson @@ -0,0 +1,53 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": null, + "geometry": { + "type": "LineString", + "bbox": null, + "coordinates": [ + [ + 0.0, + 0.0 + ], + [ + 10.0, + 10.0 + ], + [ + 20.0, + 20.0 + ] + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from linestring.geojson", + "stroke": "#0000FF", + "stroke-width": 2 + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + 5.0, + 5.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "star", + "name": "Point on Feature Result", + "description": "Point generated by pointOnFeature function" + } + } + ], + "bbox": null +} \ No newline at end of file diff --git a/test/examples/point_on_feature/out/multipolygon.geojson b/test/examples/point_on_feature/out/multipolygon.geojson new file mode 100644 index 00000000..65666995 --- /dev/null +++ b/test/examples/point_on_feature/out/multipolygon.geojson @@ -0,0 +1,83 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": null, + "geometry": { + "type": "MultiPolygon", + "bbox": null, + "coordinates": [ + [ + [ + [ + -10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 0.0, + 20.0 + ], + [ + -10.0, + 0.0 + ] + ] + ], + [ + [ + [ + 30.0, + 10.0 + ], + [ + 40.0, + 10.0 + ], + [ + 35.0, + 20.0 + ], + [ + 30.0, + 10.0 + ] + ] + ] + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from multipolygon.geojson", + "stroke": "#0000FF", + "stroke-width": 2, + "fill": "#0000FF", + "fill-opacity": 0.2 + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + -2.5, + 5.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "star", + "name": "Point on Feature Result", + "description": "Point generated by pointOnFeature function" + } + } + ], + "bbox": null +} \ No newline at end of file diff --git a/test/examples/point_on_feature/out/point.geojson b/test/examples/point_on_feature/out/point.geojson new file mode 100644 index 00000000..659fe2d6 --- /dev/null +++ b/test/examples/point_on_feature/out/point.geojson @@ -0,0 +1,42 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + 5.0, + 10.0 + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from point.geojson", + "marker-color": "#0000FF" + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + 5.0, + 10.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "star", + "name": "Point on Feature Result", + "description": "Point generated by pointOnFeature function" + } + } + ], + "bbox": null +} \ No newline at end of file diff --git a/test/examples/point_on_feature/out/polygon.geojson b/test/examples/point_on_feature/out/polygon.geojson new file mode 100644 index 00000000..080d1430 --- /dev/null +++ b/test/examples/point_on_feature/out/polygon.geojson @@ -0,0 +1,61 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Polygon", + "bbox": null, + "coordinates": [ + [ + [ + -10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 0.0, + 20.0 + ], + [ + -10.0, + 0.0 + ] + ] + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from polygon.geojson", + "stroke": "#0000FF", + "stroke-width": 2, + "fill": "#0000FF", + "fill-opacity": 0.2 + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + -2.5, + 5.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "star", + "name": "Point on Feature Result", + "description": "Point generated by pointOnFeature function" + } + } + ], + "bbox": null +} \ No newline at end of file diff --git a/test/examples/point_on_feature/out/polygon_feature.geojson b/test/examples/point_on_feature/out/polygon_feature.geojson new file mode 100644 index 00000000..618e4037 --- /dev/null +++ b/test/examples/point_on_feature/out/polygon_feature.geojson @@ -0,0 +1,61 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Polygon", + "bbox": null, + "coordinates": [ + [ + [ + -10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 0.0, + 20.0 + ], + [ + -10.0, + 0.0 + ] + ] + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from polygon_feature.geojson", + "stroke": "#0000FF", + "stroke-width": 2, + "fill": "#0000FF", + "fill-opacity": 0.2 + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + -2.5, + 5.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "star", + "name": "Point on Feature Result", + "description": "Point generated by pointOnFeature function" + } + } + ], + "bbox": null +} \ No newline at end of file diff --git a/test/examples/point_on_feature/visualization.geojson b/test/examples/point_on_feature/visualization.geojson new file mode 100644 index 00000000..6f78d6c2 --- /dev/null +++ b/test/examples/point_on_feature/visualization.geojson @@ -0,0 +1,312 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Polygon", + "bbox": null, + "coordinates": [ + [ + [ + -10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 0.0, + 20.0 + ], + [ + -10.0, + 0.0 + ] + ] + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from polygon_feature.geojson", + "stroke": "#0000FF", + "stroke-width": 2, + "fill": "#0000FF", + "fill-opacity": 0.2 + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + -2.5, + 5.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "triangle", + "name": "Result: polygon_feature.geojson", + "description": "Point on a Polygon: Returns a point inside the polygon (prefers centroid)" + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Polygon", + "bbox": null, + "coordinates": [ + [ + [ + -10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 0.0, + 20.0 + ], + [ + -10.0, + 0.0 + ] + ] + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from polygon.geojson", + "stroke": "#0000FF", + "stroke-width": 2, + "fill": "#0000FF", + "fill-opacity": 0.2 + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + -2.5, + 5.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "triangle", + "name": "Result: polygon.geojson", + "description": "Point on a Polygon: Returns a point inside the polygon (prefers centroid)" + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "LineString", + "bbox": null, + "coordinates": [ + [ + 0.0, + 0.0 + ], + [ + 10.0, + 10.0 + ], + [ + 20.0, + 20.0 + ] + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from linestring.geojson", + "stroke": "#0000FF", + "stroke-width": 2 + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + 5.0, + 5.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "circle", + "name": "Result: linestring.geojson", + "description": "Point on a LineString: Returns the midpoint of the first segment" + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "MultiPolygon", + "bbox": null, + "coordinates": [ + [ + [ + [ + -10.0, + 0.0 + ], + [ + 10.0, + 0.0 + ], + [ + 0.0, + 20.0 + ], + [ + -10.0, + 0.0 + ] + ] + ], + [ + [ + [ + 30.0, + 10.0 + ], + [ + 40.0, + 10.0 + ], + [ + 35.0, + 20.0 + ], + [ + 30.0, + 10.0 + ] + ] + ] + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from multipolygon.geojson", + "stroke": "#0000FF", + "stroke-width": 2, + "fill": "#0000FF", + "fill-opacity": 0.2 + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + -2.5, + 5.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "square", + "name": "Result: multipolygon.geojson", + "description": "Point on a MultiPolygon: Returns a point from the first polygon" + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + 5.0, + 10.0 + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from point.geojson", + "marker-color": "#0000FF" + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + 5.0, + 10.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "star", + "name": "Result: point.geojson", + "description": "Point on a Point: Returns the original point unchanged" + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + 5.0, + 10.0 + ] + }, + "properties": { + "name": "Input Geometry", + "description": "Original geometry from featurecollection.geojson", + "marker-color": "#0000FF" + } + }, + { + "type": "Feature", + "id": null, + "geometry": { + "type": "Point", + "bbox": null, + "coordinates": [ + -2.0, + -2.0 + ] + }, + "properties": { + "marker-color": "#FF0000", + "marker-size": "medium", + "marker-symbol": "circle-stroked", + "name": "Result: featurecollection.geojson", + "description": "Point on a FeatureCollection: Returns a point on the largest feature" + } + } + ], + "bbox": null +} \ No newline at end of file