Skip to content

Commit

Permalink
#19 | Fix geodart GeoJSON serialisation/deserialisation compatibility (
Browse files Browse the repository at this point in the history
…#20)

* GeoJSON compatibility fix

* Ready for 1.2.5 release

* Format

* Restructure JSON Deser

* Update changelog

* Fix test file name

---------

Co-authored-by: Dhi13man <dhiman.seal@groww.in>
  • Loading branch information
Dhi13man and DhimanSeal-Groww authored Mar 28, 2024
1 parent 49cc65a commit 8f68a19
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 56 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Releases

## [1.2.5] - 6th Feb, 2024

- Fixed broken compatibility with `geodart` GeoJSON serialisation/deserialisation as reported in [Issue #19](https://github.com/Dhi13man/open_route_service/issues/19).
- Using `geojson` package as a dev dependency, for unit testing compatibility with GeoJSON.

## [1.2.4] - 6th Feb, 2024

- **MINOR BREAKING:** Removed getter and setter for `profile` in `OpenRouteService` class. It is now final and can only be set via the constructor `defaultProfile` parameter. This system is more concurrency-safe. If it needs to be overridden at the API call level, that can anyway be done by passing in `profileOverride` to the respective API method.
Expand Down
2 changes: 1 addition & 1 deletion lib/src/models/coordinate_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class ORSCoordinate {
factory ORSCoordinate.fromList(List<dynamic> json) => ORSCoordinate(
longitude: (json[0]! as num).toDouble(),
latitude: (json[1]! as num).toDouble(),
altitude: json.length > 2 ? (json[2] as num?)?.toDouble() : 0.0,
altitude: json.length > 2 ? (json[2] as num?)?.toDouble() : null,
);

/// The latitude of the coordinate.
Expand Down
187 changes: 133 additions & 54 deletions lib/src/models/geojson_feature_models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,15 @@ class GeoJsonFeatureCollection {
/// Includes its [geometry] and [properties].
class GeoJsonFeature {
const GeoJsonFeature({
required this.type,
required this.properties,
required this.geometry,
this.bbox,
});

GeoJsonFeature.fromJson(Map<String, dynamic> json)
: properties = json['properties'],
: type = json['type'],
properties = json['properties'],
geometry = GeoJsonFeatureGeometry.fromJson(json['geometry']),
bbox = json['bbox'] == null
? null
Expand All @@ -97,6 +99,9 @@ class GeoJsonFeature {
),
];

/// The type of the feature.
final String type;

/// The properties of the feature as [Map] of [String] keys and [dynamic]
/// values to keep up with the API's unconstrained response.
///
Expand All @@ -122,7 +127,7 @@ class GeoJsonFeature {
/// Converts the [GeoJsonFeature] to a [Map] with keys 'type', 'properties'
/// and 'geometry'.
Map<String, dynamic> toJson() => <String, dynamic>{
'type': 'Feature',
'type': type,
'properties': properties,
'geometry': geometry.toJson(),
if (bbox != null)
Expand All @@ -142,49 +147,38 @@ class GeoJsonFeature {
///
/// Includes its [type] and [List] of [List] of [ORSCoordinate], [coordinates].
class GeoJsonFeatureGeometry {
const GeoJsonFeatureGeometry({required this.type, required this.coordinates});
const GeoJsonFeatureGeometry({
required this.type,
required this.coordinates,
required this.internalType,
});

/// Generate a [GeoJsonFeatureGeometry] from a received [Map] having keys
/// 'type' and 'coordinates'.
///
/// Apologize for the completely unreadable [GeoJsonFeatureGeometry.fromJson]
/// code, but the Feature Geometry data model is very inconsistent with what
/// it wants [coordinates] to be.
GeoJsonFeatureGeometry.fromJson(Map<String, dynamic> json)
: type = json['type'],
coordinates = (json['coordinates'] as List<dynamic>).first is List
?
// For Isochrone feature geometry.
(((json['coordinates'] as List<dynamic>).first as List<dynamic>)
.first is List<dynamic>)
? (json['coordinates'] as List<dynamic>)
.map<List<ORSCoordinate>>(
(dynamic coords) => (coords as List<dynamic>)
.map<ORSCoordinate>(
(dynamic c) => ORSCoordinate.fromList(
c as List<dynamic>,
),
)
.toList(),
)
.toList()
:
// For direction feature geometry
<List<ORSCoordinate>>[
(json['coordinates'] as List<dynamic>)
.map<ORSCoordinate>(
(dynamic c) =>
ORSCoordinate.fromList(c as List<dynamic>),
)
.toList()
]
:
// For POIs feature geometry
<List<ORSCoordinate>>[
<ORSCoordinate>[
ORSCoordinate.fromList(json['coordinates'] as List<dynamic>),
]
];
factory GeoJsonFeatureGeometry.fromJson(Map<String, dynamic> json) {
final dynamic type = json['type'];
final dynamic coordinates = json['coordinates'];
if (coordinates is List<dynamic>) {
final List<dynamic> dynamicList = coordinates;
if (dynamicList.first is List<dynamic>) {
final List<List<dynamic>> dynamicListList = dynamicList
.map<List<dynamic>>((dynamic c) => c as List<dynamic>)
.toList();
// For Isochrone feature geometry, it has a list of list of coordinates.
if (dynamicListList.first.first is List<dynamic>) {
return _generateIsochroneGeometry(type, dynamicListList);
}

// For direction feature geometry, it has a list of coordinates.
if (dynamicListList.first.first is num) {
return _generateDirectionGeometry(type, dynamicListList);
}
}
}

// For Point feature geometry, it has a single coordinate.
return _generatePointGeometry(type, coordinates);
}

final GsonFeatureGeometryCoordinatesType internalType;

/// Coordinates associated with the feature geometry.
///
Expand All @@ -203,17 +197,102 @@ class GeoJsonFeatureGeometry {
///
/// The [coordinates] are converted to a [List] of [List]s of
/// [List]s of 2 elements.
Map<String, dynamic> toJson() => <String, dynamic>{
'type': type,
'coordinates': coordinates
.map<List<List<double>>>(
(List<ORSCoordinate> coordinate) => coordinate
.map<List<double>>((ORSCoordinate c) => c.toList())
.toList(),
)
.toList(),
};
Map<String, dynamic> toJson() {
switch (internalType) {
case GsonFeatureGeometryCoordinatesType.listList:
return <String, dynamic>{
'type': type,
'coordinates': coordinates
.map<List<List<double>>>(
(List<ORSCoordinate> c) => c
.map<List<double>>(
(ORSCoordinate c) => c.toList(),
)
.toList(),
)
.toList(),
};
case GsonFeatureGeometryCoordinatesType.list:
return <String, dynamic>{
'type': type,
'coordinates': coordinates
.map<List<double>>(
(List<ORSCoordinate> c) => c.first.toList(),
)
.toList(),
};
case GsonFeatureGeometryCoordinatesType.single:
return <String, dynamic>{
'type': type,
'coordinates': coordinates.first.first.toList(),
};
}
}

@override
String toString() => toJson().toString();

/// For Isochrone feature geometry, it has a list of list of coordinates.
static GeoJsonFeatureGeometry _generateIsochroneGeometry(
String type,
List<List<dynamic>> dynamicListList,
) {
final List<List<ORSCoordinate>> coordinateListList = dynamicListList
.map<List<List<dynamic>>>(
(List<dynamic> c) =>
c.map<List<dynamic>>((dynamic c) => c as List<dynamic>).toList(),
)
.map<List<List<num>>>(
(List<List<dynamic>> c) => c
.map<List<num>>(
(List<dynamic> c) =>
c.map<num>((dynamic c) => c as num).toList(),
)
.toList(),
)
.map<List<ORSCoordinate>>(
(List<List<num>> c) => c
.map<ORSCoordinate>((List<num> c) => ORSCoordinate.fromList(c))
.toList(),
)
.toList();
return GeoJsonFeatureGeometry(
type: type,
coordinates: coordinateListList,
internalType: GsonFeatureGeometryCoordinatesType.listList,
);
}

/// For direction feature geometry, it has a list of coordinates.
static _generateDirectionGeometry(
String type,
List<List<dynamic>> dynamicListList,
) {
final List<ORSCoordinate> coordinateList = dynamicListList
.map<ORSCoordinate>(
(List<dynamic> c) => ORSCoordinate.fromList(
c.map<double>((dynamic c) => (c as num).toDouble()).toList(),
),
)
.toList();
return GeoJsonFeatureGeometry(
type: type,
coordinates: <List<ORSCoordinate>>[coordinateList],
internalType: GsonFeatureGeometryCoordinatesType.list,
);
}

/// For Point feature geometry, it has a single coordinate.
static _generatePointGeometry(String type, dynamic coordinates) {
final ORSCoordinate coordinate = ORSCoordinate.fromList(coordinates);
return GeoJsonFeatureGeometry(
type: type,
coordinates: <List<ORSCoordinate>>[
<ORSCoordinate>[coordinate],
],
internalType: GsonFeatureGeometryCoordinatesType.single,
);
}
}

enum GsonFeatureGeometryCoordinatesType { listList, list, single }
3 changes: 2 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: open_route_service
description: An encapsulation made around openrouteservice APIs, for Dart and Flutter projects, to easily generate Routes and their data.
version: 1.2.4
version: 1.2.5
repository: https://github.com/dhi13man/open_route_service/
homepage: https://github.com/dhi13man/open_route_service/
issue_tracker: https://github.com/Dhi13man/open_route_service/issues
Expand All @@ -12,5 +12,6 @@ dependencies:
http: ^1.0.0

dev_dependencies:
geodart: ^0.3.2
lints: ^3.0.0
test: ^1.20.1
106 changes: 106 additions & 0 deletions test/miscellaneous/geojson_tests.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'package:geodart/geometries.dart';
import 'package:open_route_service/open_route_service.dart';
import 'package:test/test.dart';

void geoJsonTests() {
test('Test GeoJSON Coordinate Point serialization', () {
// Arrange
final List<ORSCoordinate> coordinates = <ORSCoordinate>[
ORSCoordinate(latitude: 1.5, longitude: 0.0)
];
final GeoJsonFeature feature = GeoJsonFeature(
type: 'Feature',
geometry: GeoJsonFeatureGeometry(
coordinates: <List<ORSCoordinate>>[coordinates],
type: 'Point',
internalType: GsonFeatureGeometryCoordinatesType.list,
),
properties: <String, dynamic>{},
);

// Act
final Map<String, dynamic> result = feature.toJson();

// Assert
expect(
result,
<String, dynamic>{
'type': 'Feature',
'geometry': <String, dynamic>{
'type': 'Point',
'coordinates': <List<double>>[
<double>[0.0, 1.5]
]
},
'properties': <String, dynamic>{}
},
);
});

test('Test GeoJSON Coordinate Point deserialization', () {
// Arrange
final Map<String, dynamic> json = <String, dynamic>{
'type': 'Feature',
'geometry': <String, dynamic>{
'type': 'Point',
'coordinates': <List<double>>[
<double>[0.0, 1.5]
]
},
'properties': <String, dynamic>{}
};

// Act
final GeoJsonFeature result = GeoJsonFeature.fromJson(json);

// Assert
final List<ORSCoordinate> coordinates = <ORSCoordinate>[
ORSCoordinate(longitude: 0.0, latitude: 1.5)
];
final GeoJsonFeature expected = GeoJsonFeature(
type: 'Feature',
geometry: GeoJsonFeatureGeometry(
coordinates: <List<ORSCoordinate>>[coordinates],
type: 'Point',
internalType: GsonFeatureGeometryCoordinatesType.list,
),
properties: <String, dynamic>{},
);
expect(result.bbox, expected.bbox);
expect(result.properties, expected.properties);
expect(result.type, expected.type);
expect(result.geometry.internalType, expected.geometry.internalType);
expect(result.geometry.type, expected.geometry.type);
for (int i = 0; i < result.geometry.coordinates.length; i++) {
for (int j = 0; j < result.geometry.coordinates[i].length; j++) {
expect(
result.geometry.coordinates[i][j].latitude,
expected.geometry.coordinates[i][j].latitude,
);
expect(
result.geometry.coordinates[i][j].longitude,
expected.geometry.coordinates[i][j].longitude,
);
}
}
});

test('Test GeoJSON Coordinate Point serialization and deserialization', () {
// Arrange
final Point original = Point(Coordinate(51.5, 0.0));

// Act
final Map<String, dynamic> jsonPoint = original.toJson();
final GeoJsonFeature feature = GeoJsonFeature.fromJson(jsonPoint);
final Map<String, dynamic> featureJson = feature.toJson();
final Point result = Point.fromJson(featureJson);

// Assert
expect(result.bbox.center, original.bbox.center);
expect(result.bbox.maxLat, original.bbox.maxLat);
expect(result.bbox.maxLong, original.bbox.maxLong);
expect(result.bbox.minLat, original.bbox.minLat);
expect(result.bbox.minLong, original.bbox.minLong);
expect(result.properties, original.properties);
});
}
3 changes: 3 additions & 0 deletions test/open_route_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';

import 'package:test/test.dart';

import 'miscellaneous/geojson_tests.dart';
import 'services/directions_tests.dart';
import 'services/elevation_tests.dart';
import 'services/geocode_tests.dart';
Expand Down Expand Up @@ -110,4 +111,6 @@ Future<void> main() async {
reverseGeocodeQueryLocality: 'Paris',
),
);

group('GeoJSON Compatibility Tests', () => geoJsonTests());
}

0 comments on commit 8f68a19

Please sign in to comment.