From a9fc0100b163659c81d1b0a5535eedf777064a17 Mon Sep 17 00:00:00 2001 From: Seth Fitzsimmons Date: Thu, 14 Nov 2024 14:59:10 -0800 Subject: [PATCH] Flatten simple geometry collections (#42) * Add failing test for GeometryCollection simplifcation * Simplify GeometryCollections containing a single Polygon GeoJSONLint complains about these types of features: > GeometryCollection with a single geometry should be avoided in favor > of single part or a single object of multi-part type This simultaneously expands (`GeometryCollection | Polygon`) and narrows (`GeometryCollection`) the type of GeoJSON features created from isolines. * Update CHANGELOG * 1.2.0 --- CHANGELOG.md | 11 ++++- README.md | 12 +++-- package-lock.json | 4 +- package.json | 2 +- src/to-geojson/georoutes-converter.test.ts | 53 ++++++++++++++++++++++ src/to-geojson/georoutes-converter.ts | 24 ++++++++-- 6 files changed, 92 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f141a8..655ef2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ -# 1.1.0 +# Changes + +## 1.2.0 + +### ✨ Features and improvements + +- Generate individual Polygons rather than GeometryCollections when isoline features contain single + geometries + +## 1.1.0 ### ✨ Features and improvements diff --git a/README.md b/README.md index aededdf..32622bd 100644 --- a/README.md +++ b/README.md @@ -266,11 +266,13 @@ const featureCollections = calculateRoutesResponseToFeatureCollections(response) ### calculateIsolinesResponseToFeatureCollection -This converts a CalculateIsolineResponse from the standalone Routes SDK to a GeoJSON FeatureCollection which contains one Feature for each isoline -in the response. Isolines can contain both polygons for isoline regions and lines for connectors between regions -(such as ferry travel), so each Feature is a GeometryCollection that can contain a mix of Polygons and LineStrings. -The `flattenProperties` option will flatten the nested response data into a flat properties list. -This option is enabled by default, as it makes the data easier to use from within MapLibre expressions. +This converts a CalculateIsolineResponse from the standalone Routes SDK to a GeoJSON +FeatureCollection which contains one Feature for each isoline in the response. Isolines can contain +both polygons for isoline regions and lines for connectors between regions (such as ferry travel), +so each Feature contains either a GeometryCollection with a mix of Polygons and LineStrings or a +single Polygon. The `flattenProperties` option will flatten the nested response data into a flat +properties list. This option is enabled by default, as it makes the data easier to use from within +MapLibre expressions. Any feature that is missing its geometry in the response or has invalid geometry will throw an Error(). diff --git a/package-lock.json b/package-lock.json index 09f9f89..9a16d47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aws/amazon-location-utilities-datatypes", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aws/amazon-location-utilities-datatypes", - "version": "1.1.0", + "version": "1.2.0", "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-geo-places": "^3.683.0", diff --git a/package.json b/package.json index 2644401..d4b6a6c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@aws/amazon-location-utilities-datatypes", "description": "Amazon Location Utilities - Data Types for JavaScript", "license": "Apache-2.0", - "version": "1.1.0", + "version": "1.2.0", "keywords": [], "author": { "name": "Amazon Web Services", diff --git a/src/to-geojson/georoutes-converter.test.ts b/src/to-geojson/georoutes-converter.test.ts index 91d9f96..17703f5 100644 --- a/src/to-geojson/georoutes-converter.test.ts +++ b/src/to-geojson/georoutes-converter.test.ts @@ -1710,6 +1710,59 @@ describe("calculateIsolinesResponseToFeatureCollection", () => { expect(calculateIsolinesResponseToFeatureCollection(input, { flattenProperties: true })).toEqual(expectedResult); }); + + it("should return a Polygon (not a GeometryCollection) if no LineString is produced", () => { + const input: CalculateIsolinesResponse = { + IsolineGeometryFormat: "FlexiblePolyline", + PricingBucket: "bucket", + Isolines: [ + { + Connections: [], + Geometries: [ + { + PolylinePolygon: [ + encodeFromLngLatArray([ + [0, 0], + [10, 0], + [10, 10], + [0, 10], + [0, 0], + ]), + ], + }, + ], + TimeThreshold: 1000, + }, + ], + }; + + const expectedResult: FeatureCollection = { + type: "FeatureCollection", + features: [ + { + type: "Feature", + id: 0, + geometry: { + type: "Polygon", + coordinates: [ + [ + [0, 0], + [10, 0], + [10, 10], + [0, 10], + [0, 0], + ], + ], + }, + properties: { + TimeThreshold: 1000, + }, + }, + ], + }; + + expect(calculateIsolinesResponseToFeatureCollection(input, { flattenProperties: false })).toEqual(expectedResult); + }); }); describe("optimizeWaypointsResponseToFeatureCollection", () => { diff --git a/src/to-geojson/georoutes-converter.ts b/src/to-geojson/georoutes-converter.ts index 11e3b30..039e2e3 100644 --- a/src/to-geojson/georoutes-converter.ts +++ b/src/to-geojson/georoutes-converter.ts @@ -343,7 +343,8 @@ const defaultCalculateIsolinesResponseOptions = defaultBaseGeoRoutesOptions; /** * This converts a CalculateIsolineResponse to a GeoJSON FeatureCollection which contains one Feature for each isoline * in the response. Isolines can contain both polygons for isoline regions and lines for connectors between regions - * (such as ferry travel), so each Feature is a GeometryCollection that can contain a mix of Polygons and LineStrings. + * (such as ferry travel), so each Feature contains either a GeometryCollection with a mix of Polygons and LineStrings + * or a single Polygon. * * Any feature that is missing its geometry in the response or has invalid geometry will throw an Error. * @@ -405,11 +406,11 @@ const defaultCalculateIsolinesResponseOptions = defaultBaseGeoRoutesOptions; export function calculateIsolinesResponseToFeatureCollection( isolinesResponse: CalculateIsolinesResponse, options?: CalculateIsolinesResponseOptions, -): FeatureCollection { +): FeatureCollection | Polygon> { // Set any options that weren't passed in to the default values. options = { ...defaultCalculateIsolinesResponseOptions, ...options }; - const isolines: FeatureCollection = { + const isolines: FeatureCollection | Polygon> = { type: "FeatureCollection", features: [], }; @@ -420,7 +421,7 @@ export function calculateIsolinesResponseToFeatureCollection( // eslint-disable-next-line @typescript-eslint/no-unused-vars const { Geometries, Connections, ...properties } = isoline; - const feature: Feature = { + const feature: Feature> = { type: "Feature", id: isolines.features.length, properties: options.flattenProperties ? flattenProperties(properties, "") : properties, @@ -448,7 +449,20 @@ export function calculateIsolinesResponseToFeatureCollection( // As long as this feature has at least one polygon or line, add it to the result set. if (feature.geometry.geometries.length > 0) { - isolines.features.push(feature); + if (feature.geometry.geometries.length === 1 && feature.geometry.geometries[0].type === "Polygon") { + // GeometryCollections containing single geometries trigger GeoJSONLint warnings: + // GeometryCollection with a single geometry should be avoided in favor of single part + // or a single object of multi-part type + // in practice, the geometry type for single-geometry isolines is Polygon; LineStrings are + // supplemental and appear when multiple polygons are present, representing the connection + // between those areas + isolines.features.push({ + ...feature, + geometry: feature.geometry.geometries[0], + }); + } else { + isolines.features.push(feature); + } } }