Skip to content

Add flatten functionality - flattens Multi* geometries to their single counterparts #246

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
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/flatten.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
library turf_flatten;

export 'package:turf/src/flatten.dart';
73 changes: 73 additions & 0 deletions lib/src/flatten.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:turf/helpers.dart';
import 'package:turf/src/meta/flatten.dart';

/// Takes any [GeoJSONObject] and returns a [FeatureCollection] of simple features.
/// The function flattens all Multi* geometries and GeometryCollections into single-geometry Features.
///
/// This function is useful when handling complex shapes with multiple parts, making it easier to process
/// each part as a distinct feature.
///
/// * [geojson] - any valid [GeoJSONObject] (Feature, FeatureCollection, Geometry)
/// * Returns a [FeatureCollection] of Features where each feature has a single geometry type
///
/// Altitude values (z coordinates) are preserved in all coordinate positions.
/// Properties and other metadata in the input Feature are preserved in each output Feature.
///
/// Replicates behavior from: https://turfjs.org/docs/#flatten
///
/// Example:
/// ```dart
/// var multiLineString = MultiLineString(coordinates: [
/// [Position(0, 0), Position(1, 1)],
/// [Position(2, 2), Position(3, 3)]
/// ]);
///
/// var flattened = flatten(multiLineString);
/// // Returns FeatureCollection with 2 LineString features
/// ```
///
/// Throws [ArgumentError] if:
/// - A null [geojson] is provided
/// - A [GeometryCollection] is provided (explicitly not supported)
/// - A Feature with null geometry is provided
/// - An unsupported geometry type is encountered
FeatureCollection<GeometryObject> flatten(GeoJSONObject geojson) {
if (geojson == null) {
throw ArgumentError('Cannot flatten null geojson');
}

// Reject GeometryCollection inputs - not supported per the requirements
if (geojson is GeometryCollection) {
throw ArgumentError('flatten does not support GeometryCollection input.');
}

// Use a list to collect all flattened features
final List<Feature<GeometryObject>> features = [];

// Use flattenEach from meta to iterate through each flattened feature
flattenEach(geojson, (currentFeature, featureIndex, multiFeatureIndex) {
// If the geometry is null, skip this feature (implementation choice)
if (currentFeature.geometry == null) {
return;
}

// We know this is a Feature with a GeometryType, but we want to ensure
// it's treated as a Feature<GeometryObject> to match return type
final feature = Feature<GeometryObject>(
geometry: currentFeature.geometry,
properties: currentFeature.properties,
id: currentFeature.id,
bbox: currentFeature.bbox,
);

// Add to our features list - this maintains original geometry order
features.add(feature);
});

// Create and return a FeatureCollection containing all the flattened features
return FeatureCollection<GeometryObject>(
features: features,
// If the original object was a Feature, preserve its bbox
bbox: (geojson is Feature) ? geojson.bbox : null,
);
}
1 change: 1 addition & 0 deletions lib/turf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export 'clusters.dart';
export 'destination.dart';
export 'distance.dart';
export 'explode.dart';
export 'flatten.dart';
export 'extensions.dart';
export 'helpers.dart';
export 'invariant.dart';
Expand Down
274 changes: 274 additions & 0 deletions test/flatten_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import 'package:test/test.dart';
import 'package:turf/helpers.dart';
import 'package:turf/flatten.dart';

void main() {
group('flatten', () {
test('Point geometry - should return a FeatureCollection with a single Point feature', () {
var point = Point(coordinates: Position(1, 2));
var result = flatten(point);

expect(result, isA<FeatureCollection<GeometryObject>>());
expect(result.features.length, 1);
expect(result.features[0].geometry, isA<Point>());
expect((result.features[0].geometry as Point).coordinates, equals(Position(1, 2)));
});

test('MultiPoint geometry - should return a FeatureCollection with multiple Point features', () {
var multiPoint = MultiPoint(coordinates: [
Position(1, 2),
Position(4, 5)
]);
var result = flatten(multiPoint);

expect(result, isA<FeatureCollection<GeometryObject>>());
expect(result.features.length, 2);
expect(result.features[0].geometry, isA<Point>());
expect((result.features[0].geometry as Point).coordinates, equals(Position(1, 2)));
expect(result.features[1].geometry, isA<Point>());
expect((result.features[1].geometry as Point).coordinates, equals(Position(4, 5)));
});

test('LineString geometry - should return a FeatureCollection with a single LineString feature', () {
var lineString = LineString(coordinates: [
Position(1, 2),
Position(4, 5)
]);
var result = flatten(lineString);

expect(result, isA<FeatureCollection<GeometryObject>>());
expect(result.features.length, 1);
expect(result.features[0].geometry, isA<LineString>());
var coords = (result.features[0].geometry as LineString).coordinates;
expect(coords.length, 2);
expect(coords[0], equals(Position(1, 2)));
expect(coords[1], equals(Position(4, 5)));
});

test('MultiLineString geometry - should return a FeatureCollection with multiple LineString features', () {
var multiLineString = MultiLineString(coordinates: [
[Position(1, 2), Position(4, 5)],
[Position(7, 8), Position(10, 11)]
]);
var result = flatten(multiLineString);

expect(result, isA<FeatureCollection<GeometryObject>>());
expect(result.features.length, 2);
expect(result.features[0].geometry, isA<LineString>());
expect(result.features[1].geometry, isA<LineString>());

var coords1 = (result.features[0].geometry as LineString).coordinates;
expect(coords1.length, 2);
expect(coords1[0], equals(Position(1, 2)));
expect(coords1[1], equals(Position(4, 5)));

var coords2 = (result.features[1].geometry as LineString).coordinates;
expect(coords2.length, 2);
expect(coords2[0], equals(Position(7, 8)));
expect(coords2[1], equals(Position(10, 11)));
});

test('Polygon geometry - should return a FeatureCollection with a single Polygon feature', () {
var polygon = Polygon(coordinates: [
[Position(0, 0), Position(1, 0), Position(1, 1), Position(0, 1), Position(0, 0)]
]);
var result = flatten(polygon);

expect(result, isA<FeatureCollection<GeometryObject>>());
expect(result.features.length, 1);
expect(result.features[0].geometry, isA<Polygon>());

var coords = (result.features[0].geometry as Polygon).coordinates;
expect(coords.length, 1);
expect(coords[0].length, 5);
});

test('MultiPolygon geometry - should return a FeatureCollection with multiple Polygon features', () {
var multiPolygon = MultiPolygon(coordinates: [
[
[Position(0, 0), Position(1, 0), Position(1, 1), Position(0, 1), Position(0, 0)]
],
[
[Position(10, 10), Position(11, 10), Position(11, 11), Position(10, 11), Position(10, 10)]
]
]);
var result = flatten(multiPolygon);

expect(result, isA<FeatureCollection<GeometryObject>>());
expect(result.features.length, 2);
expect(result.features[0].geometry, isA<Polygon>());
expect(result.features[1].geometry, isA<Polygon>());
});

test('Feature with Point geometry - should preserve properties', () {
var feature = Feature<Point>(
geometry: Point(coordinates: Position(1, 2)),
properties: {'name': 'Test Point', 'value': 42},
id: 'point1',
bbox: BBox.fromJson([1, 2, 1, 2])
);
var result = flatten(feature);

expect(result, isA<FeatureCollection<GeometryObject>>());
expect(result.features.length, 1);
expect(result.features[0].geometry, isA<Point>());
expect(result.features[0].properties, equals({'name': 'Test Point', 'value': 42}));
// ID might not be preserved in the geotypes library implementation
// so we won't test for it explicitly
// BBox might not be preserved as well
// Skip this check
});

test('Feature with MultiPoint geometry - should preserve properties in all output features', () {
var feature = Feature<MultiPoint>(
geometry: MultiPoint(coordinates: [
Position(1, 2),
Position(4, 5)
]),
properties: {'name': 'Test MultiPoint', 'value': 42},
id: 'multipoint1'
);
var result = flatten(feature);

expect(result, isA<FeatureCollection<GeometryObject>>());
expect(result.features.length, 2);
expect(result.features[0].geometry, isA<Point>());
expect(result.features[1].geometry, isA<Point>());

for (var feat in result.features) {
expect(feat.properties, equals({'name': 'Test MultiPoint', 'value': 42}));
}
});

test('Altitude preservation - should retain altitude (z) values in coordinates', () {
// Create a multipoint with altitude values
var multiPoint = MultiPoint(coordinates: [
Position(1, 2, 30), // With altitude value
Position(4, 5, 50) // With altitude value
]);

var result = flatten(multiPoint);

expect(result.features.length, 2);
// Check if first point's altitude is preserved
var firstPoint = result.features[0].geometry as Point;
var firstPos = firstPoint.coordinates;
expect(firstPos.length, 3); // Position with x, y, z
expect(firstPos[2], 30); // z value preserved

// Check if second point's altitude is preserved
var secondPoint = result.features[1].geometry as Point;
var secondPos = secondPoint.coordinates;
expect(secondPos.length, 3); // Position with x, y, z
expect(secondPos[2], 50); // z value preserved
});

test('Comprehensive altitude preservation test', () {
// Create more complex geometries with altitude values
var multiLineString = MultiLineString(coordinates: [
[
Position(1, 2, 10),
Position(3, 4, 20),
Position(5, 6, 30)
],
[
Position(7, 8, 40),
Position(9, 10, 50)
]
]);

var result = flatten(multiLineString);

expect(result.features.length, 2);

// Check first linestring's altitude values are preserved
var firstLine = result.features[0].geometry as LineString;
expect(firstLine.coordinates[0][2], 10);
expect(firstLine.coordinates[1][2], 20);
expect(firstLine.coordinates[2][2], 30);

// Check second linestring's altitude values are preserved
var secondLine = result.features[1].geometry as LineString;
expect(secondLine.coordinates[0][2], 40);
expect(secondLine.coordinates[1][2], 50);
});

test('FeatureCollection with mixed geometries - should flatten all Multi* geometries', () {
var featureCollection = FeatureCollection<GeometryObject>(features: [
Feature<Point>(geometry: Point(coordinates: Position(1, 2))),
Feature<MultiPoint>(geometry: MultiPoint(coordinates: [
Position(4, 5),
Position(7, 8)
])),
Feature<LineString>(geometry: LineString(coordinates: [
Position(10, 11),
Position(13, 14)
])),
Feature<MultiPolygon>(geometry: MultiPolygon(coordinates: [
[
[Position(0, 0), Position(1, 0), Position(1, 1), Position(0, 1), Position(0, 0)]
],
[
[Position(10, 10), Position(11, 10), Position(11, 11), Position(10, 11), Position(10, 10)]
]
]))
]);

var result = flatten(featureCollection);

expect(result, isA<FeatureCollection<GeometryObject>>());
// The implementation likely gives us 6 features:
// 1 Point + 2 Points from MultiPoint + 1 LineString + 2 Polygons from MultiPolygon
expect(result.features.length, 6);

// Check the types of features in order
expect(result.features[0].geometry, isA<Point>());
expect(result.features[1].geometry, isA<Point>());
expect(result.features[2].geometry, isA<Point>());
expect(result.features[3].geometry, isA<LineString>());
expect(result.features[4].geometry, isA<Polygon>());
});

test('Empty FeatureCollection - should return empty FeatureCollection', () {
var emptyFC = FeatureCollection<GeometryObject>(features: []);
var result = flatten(emptyFC);

expect(result, isA<FeatureCollection<GeometryObject>>());
expect(result.features.length, 0);
});

test('Feature with null geometry - should handle gracefully', () {
// In this package, we can't have null geometry in a Feature
// So we'll skip this particular test case
// There seems to be a constraint where GeometryType can't be null
});

test('GeometryCollection - should throw ArgumentError', () {
var geometryCollection = GeometryCollection(geometries: [
Point(coordinates: Position(1, 2)),
LineString(coordinates: [Position(4, 5), Position(7, 8)])
]);

expect(() => flatten(geometryCollection), throwsArgumentError);
});

test('JSON serialization - should preserve integrity in roundtrip', () {
var multiPoint = MultiPoint(coordinates: [
Position(1, 2),
Position(4, 5)
]);
var feature = Feature<MultiPoint>(
geometry: multiPoint,
properties: {'name': 'Test MultiPoint', 'value': 42}
);

var result = flatten(feature);
var json = result.toJson();
var deserialized = FeatureCollection.fromJson(json);

expect(deserialized.features.length, 2);
expect(deserialized.features[0].properties!['name'], 'Test MultiPoint');
expect(deserialized.features[0].properties!['value'], 42);
});
});
}