Skip to content

Support for other members in all geojson objects #222

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 2 commits 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
114 changes: 114 additions & 0 deletions docs/OTHER_MEMBERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# GeoJSON "Other Members" Support in TurfDart

## Overview

In accordance with [RFC 7946 (The GeoJSON Format)](https://datatracker.ietf.org/doc/html/rfc7946), TurfDart now supports "other members" in GeoJSON objects. The specification states:

> "A GeoJSON object MAY have 'other members'. Implementations MUST NOT interpret foreign members as having any meaning unless part of an extension or profile."

This document explains how to use the "other members" support in TurfDart.

## Features

- Store and retrieve custom fields in any GeoJSON object
- Preserve custom fields when serializing to JSON
- Preserve custom fields when cloning GeoJSON objects
- Extract custom fields from JSON during deserialization

## Usage

### Adding Custom Fields to GeoJSON Objects

```dart
import 'package:turf/helpers.dart';
import 'package:turf/meta.dart';

// Create a GeoJSON object
final point = Point(coordinates: Position(10, 20));

// Add custom fields
point.setOtherMembers({
'custom_field': 'custom_value',
'metadata': {'source': 'my_data_source', 'date': '2025-04-18'}
});
```

### Retrieving Custom Fields

```dart
// Get all custom fields
final otherMembers = point.otherMembers;
print(otherMembers); // {'custom_field': 'custom_value', 'metadata': {...}}

// Access specific custom fields
final customField = point.otherMembers['custom_field'];
final metadataSource = point.otherMembers['metadata']['source'];
```

### Serializing with Custom Fields

```dart
// Convert to JSON including custom fields
final json = point.toJsonWithOtherMembers();
// Result:
// {
// 'type': 'Point',
// 'coordinates': [10, 20],
// 'custom_field': 'custom_value',
// 'metadata': {'source': 'my_data_source', 'date': '2025-04-18'}
// }
```

### Cloning with Custom Fields

```dart
// Clone the object while preserving custom fields
final clonedPoint = point.cloneWithOtherMembers<Point>();
print(clonedPoint.otherMembers); // Same custom fields as original
```

### Deserializing from JSON with Custom Fields

```dart
// Parse Feature with custom fields from JSON
final featureJson = {
'type': 'Feature',
'geometry': {'type': 'Point', 'coordinates': [10, 20]},
'properties': {'name': 'Example Point'},
'custom_field': 'custom_value'
};

final feature = FeatureOtherMembersExtension.fromJsonWithOtherMembers(featureJson);
print(feature.otherMembers['custom_field']); // 'custom_value'

// Parse FeatureCollection with custom fields from JSON
final featureCollectionJson = {
'type': 'FeatureCollection',
'features': [...],
'custom_field': 'custom_value'
};

final collection = FeatureCollectionOtherMembersExtension.fromJsonWithOtherMembers(featureCollectionJson);
print(collection.otherMembers['custom_field']); // 'custom_value'

// Parse GeometryObject with custom fields from JSON
final geometryJson = {
'type': 'Point',
'coordinates': [10, 20],
'custom_field': 'custom_value'
};

final geometry = GeometryObjectOtherMembersExtension.deserializeWithOtherMembers(geometryJson);
print(geometry.otherMembers['custom_field']); // 'custom_value'
```

## Implementation Notes

- Custom fields are stored in memory using a static map with object identity hash codes as keys
- The extension approach was chosen to avoid modifying the core GeoJSON classes defined in the geotypes package
- This implementation fully complies with RFC 7946's recommendations for handling "other members"

## Limitations

- Custom fields are stored in memory and not persisted across application restarts
- Care should be taken to prevent memory leaks by not storing too many objects with large custom fields
216 changes: 216 additions & 0 deletions lib/src/meta/geom.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,222 @@
import 'package:turf/helpers.dart';
import 'package:turf/src/meta/short_circuit.dart';

/// Utility to extract "other members" from GeoJSON objects
/// according to RFC 7946 specification.
Map<String, dynamic> extractOtherMembers(Map<String, dynamic> json, List<String> standardKeys) {
final otherMembers = <String, dynamic>{};

json.forEach((key, value) {
if (!standardKeys.contains(key)) {
otherMembers[key] = value;
}
});

return otherMembers;
}

/// Storage for other members using Expando to prevent memory leaks
final _otherMembersExpando = Expando<Map<String, dynamic>>('otherMembers');

/// Extension to add "other members" support to GeoJSONObject
/// This follows RFC 7946 specification:
/// "A GeoJSON object MAY have 'other members'. Implementations
/// MUST NOT interpret foreign members as having any meaning unless
/// part of an extension or profile."
extension GeoJSONObjectOtherMembersExtension on GeoJSONObject {
/// Get other members for this GeoJSON object
Map<String, dynamic> get otherMembers {
return _otherMembersExpando[this] ?? {};
}

/// Set other members for this GeoJSON object
void setOtherMembers(Map<String, dynamic> members) {
_otherMembersExpando[this] = Map<String, dynamic>.from(members);
}

/// Merge additional other members with existing ones
void mergeOtherMembers(Map<String, dynamic> newMembers) {
final current = Map<String, dynamic>.from(otherMembers);
current.addAll(newMembers);
setOtherMembers(current);
}

/// Convert to JSON with other members included
/// This is the compliant serialization method that includes other members
/// as per RFC 7946 specification.
Map<String, dynamic> toJsonWithOtherMembers() {
final json = toJson();
final others = otherMembers;

if (others.isNotEmpty) {
json.addAll(others);
}

return json;
}

/// Clone with other members preserved
T clonePreservingOtherMembers<T extends GeoJSONObject>() {
final clone = this.clone() as T;
clone.setOtherMembers(otherMembers);
return clone;
}

/// CopyWith method that preserves other members
/// This is used to create a new GeoJSONObject with some properties modified
/// while preserving all other members
GeoJSONObject copyWithPreservingOtherMembers() {
return clonePreservingOtherMembers();
}
}

/// Extension to add "other members" support specifically to Feature
extension FeatureOtherMembersExtension on Feature {
/// Standard keys for Feature objects as per GeoJSON specification
static const standardKeys = ['type', 'geometry', 'properties', 'id', 'bbox'];

/// Create a Feature from JSON with support for other members
static Feature fromJsonWithOtherMembers(Map<String, dynamic> json) {
final feature = Feature.fromJson(json);

// Extract other members
final otherMembers = extractOtherMembers(json, standardKeys);
if (otherMembers.isNotEmpty) {
feature.setOtherMembers(otherMembers);
}

return feature;
}

/// Create a new Feature with modified properties while preserving other members
Feature<T> copyWithPreservingOtherMembers<T extends GeometryObject>({
T? geometry,
Map<String, dynamic>? properties,
BBox? bbox,
dynamic id,
}) {
final newFeature = Feature<T>(
geometry: geometry ?? this.geometry as T,
properties: properties ?? this.properties,
bbox: bbox ?? this.bbox,
id: id ?? this.id,
);

newFeature.setOtherMembers(otherMembers);
return newFeature;
}
}

/// Extension to add "other members" support specifically to FeatureCollection
extension FeatureCollectionOtherMembersExtension on FeatureCollection {
/// Standard keys for FeatureCollection objects as per GeoJSON specification
static const standardKeys = ['type', 'features', 'bbox'];

/// Create a FeatureCollection from JSON with support for other members
static FeatureCollection fromJsonWithOtherMembers(Map<String, dynamic> json) {
final featureCollection = FeatureCollection.fromJson(json);

// Extract other members
final otherMembers = extractOtherMembers(json, standardKeys);
if (otherMembers.isNotEmpty) {
featureCollection.setOtherMembers(otherMembers);
}

return featureCollection;
}

/// Create a new FeatureCollection with modified properties while preserving other members
FeatureCollection<T> copyWithPreservingOtherMembers<T extends GeometryObject>({
List<Feature<T>>? features,
BBox? bbox,
}) {
final newFeatureCollection = FeatureCollection<T>(
features: features ?? this.features.cast<Feature<T>>(),
bbox: bbox ?? this.bbox,
);

newFeatureCollection.setOtherMembers(otherMembers);
return newFeatureCollection;
}
}

/// Extension to add "other members" support specifically to GeometryObject
extension GeometryObjectOtherMembersExtension on GeometryObject {
/// Standard keys for GeometryObject as per GeoJSON specification
static const standardKeys = ['type', 'coordinates', 'geometries', 'bbox'];

/// Create a GeometryObject from JSON with support for other members
static GeometryObject fromJsonWithOtherMembers(Map<String, dynamic> json) {
final geometryObject = GeometryObject.deserialize(json);

// Extract other members
final otherMembers = extractOtherMembers(json, standardKeys);
if (otherMembers.isNotEmpty) {
geometryObject.setOtherMembers(otherMembers);
}

return geometryObject;
}

/// Create a new GeometryObject with modified properties while preserving other members
GeometryObject copyWithPreservingOtherMembers({
BBox? bbox,
}) {
GeometryObject newObject;

// Handle the different geometry types
if (this is Point) {
final point = this as Point;
newObject = Point(
coordinates: point.coordinates,
bbox: bbox ?? point.bbox,
);
} else if (this is MultiPoint) {
final multiPoint = this as MultiPoint;
newObject = MultiPoint(
coordinates: multiPoint.coordinates,
bbox: bbox ?? multiPoint.bbox,
);
} else if (this is LineString) {
final lineString = this as LineString;
newObject = LineString(
coordinates: lineString.coordinates,
bbox: bbox ?? lineString.bbox,
);
} else if (this is MultiLineString) {
final multiLineString = this as MultiLineString;
newObject = MultiLineString(
coordinates: multiLineString.coordinates,
bbox: bbox ?? multiLineString.bbox,
);
} else if (this is Polygon) {
final polygon = this as Polygon;
newObject = Polygon(
coordinates: polygon.coordinates,
bbox: bbox ?? polygon.bbox,
);
} else if (this is MultiPolygon) {
final multiPolygon = this as MultiPolygon;
newObject = MultiPolygon(
coordinates: multiPolygon.coordinates,
bbox: bbox ?? multiPolygon.bbox,
);
} else if (this is GeometryCollection) {
final collection = this as GeometryCollection;
newObject = GeometryCollection(
geometries: collection.geometries,
bbox: bbox ?? collection.bbox,
);
} else {
// Fallback - just clone with proper casting
newObject = this.clone() as GeometryObject;
}

newObject.setOtherMembers(otherMembers);
return newObject;
}
}

typedef GeomEachCallback = dynamic Function(
GeometryType? currentGeometry,
int? featureIndex,
Expand Down
Loading