diff --git a/src/to-geojson/place-converter.test.ts b/src/to-geojson/place-converter.test.ts index 57713b8..b75613b 100644 --- a/src/to-geojson/place-converter.test.ts +++ b/src/to-geojson/place-converter.test.ts @@ -11,7 +11,7 @@ import { FeatureCollection } from "geojson"; import { emptyFeatureCollection } from "./utils"; describe("placeToFeatureCollection", () => { - it("should convert GetPlaceResponse to a FeatureCollection with a single feature", () => { + it("should convert GetPlaceResponse to a FeatureCollection with a single feature and nested properties when flattenProperties is false or undefined", () => { const input: GetPlaceResponse = { Place: { Label: "Test Place", @@ -56,7 +56,50 @@ describe("placeToFeatureCollection", () => { expect(placeToFeatureCollection(input)).toEqual(output); }); - it("should convert SearchPlaceIndexForTextResponse to a FeatureCollection with a multiple features", () => { + it("should convert GetPlaceResponse to a FeatureCollection with a single feature and flattened properties when flattenProperties is true", () => { + const input: GetPlaceResponse = { + Place: { + Label: "Test Place", + Geometry: { + Point: [1, 2], + }, + AddressNumber: "111", + Street: "Burrard St", + Neighborhood: "Downtown", + Municipality: "Vancouver", + SubRegion: "Metro Vancouver", + Region: "British Columbia", + Country: "CAN", + PostalCode: "V6C", + }, + }; + const output: FeatureCollection = { + type: "FeatureCollection", + features: [ + { + type: "Feature", + properties: { + "Place.AddressNumber": "111", + "Place.Country": "CAN", + "Place.Label": "Test Place", + "Place.Municipality": "Vancouver", + "Place.Neighborhood": "Downtown", + "Place.PostalCode": "V6C", + "Place.Region": "British Columbia", + "Place.Street": "Burrard St", + "Place.SubRegion": "Metro Vancouver", + }, + geometry: { + type: "Point", + coordinates: [1, 2], + }, + }, + ], + }; + expect(placeToFeatureCollection(input, { flattenProperties: true })).toEqual(output); + }); + + it("should convert SearchPlaceIndexForTextResponse to a FeatureCollection with a multiple features when flattenProperties is false", () => { const input: SearchPlaceIndexForTextResponse = { Summary: { Text: "grocery store", @@ -139,7 +182,84 @@ describe("placeToFeatureCollection", () => { expect(placeToFeatureCollection(input)).toEqual(output); }); - it("should convert SearchPlaceIndexForPositionResponse to a FeatureCollection with a multiple features", () => { + it("should convert SearchPlaceIndexForTextResponse to a FeatureCollection with multiple features when flattenProperties is true", () => { + const input: SearchPlaceIndexForTextResponse = { + Summary: { + Text: "grocery store", + DataSource: "Esri", + }, + Results: [ + { + Place: { + Geometry: { + Point: [1, 2], + }, + AddressNumber: "1050", + }, + PlaceId: "abc", + }, + { + Place: { + Geometry: { + Point: [3, 3], + }, + AddressNumber: "609", + }, + Distance: 1, + }, + { + Place: { + Geometry: { + Point: [5, 5], + }, + AddressNumber: "575", + }, + PlaceId: "def", + }, + ], + }; + const output: FeatureCollection = { + type: "FeatureCollection", + features: [ + { + type: "Feature", + id: "abc", + properties: { + "Place.AddressNumber": "1050", + }, + geometry: { + type: "Point", + coordinates: [1, 2], + }, + }, + { + type: "Feature", + properties: { + "Place.AddressNumber": "609", + Distance: 1, + }, + geometry: { + type: "Point", + coordinates: [3, 3], + }, + }, + { + type: "Feature", + id: "def", + properties: { + "Place.AddressNumber": "575", + }, + geometry: { + type: "Point", + coordinates: [5, 5], + }, + }, + ], + }; + expect(placeToFeatureCollection(input, { flattenProperties: true })).toEqual(output); + }); + + it("should convert SearchPlaceIndexForPositionResponse to a FeatureCollection with a multiple features when flattenProperties is false", () => { const input: SearchPlaceIndexForPositionResponse = { Summary: { Position: [5, 5], @@ -228,6 +348,89 @@ describe("placeToFeatureCollection", () => { expect(placeToFeatureCollection(input)).toEqual(output); }); + it("should convert SearchPlaceIndexForPositionResponse to a FeatureCollection with multiple features when flattenProperties is true", () => { + const input: SearchPlaceIndexForPositionResponse = { + Summary: { + Position: [5, 5], + DataSource: "Esri", + }, + Results: [ + { + Place: { + Geometry: { + Point: [5, 5], + }, + AddressNumber: "1050", + }, + PlaceId: "abc", + Distance: 0, + }, + { + Place: { + Geometry: { + Point: [4, 4], + }, + AddressNumber: "609", + }, + PlaceId: "def", + Distance: 1, + }, + { + Place: { + Geometry: { + Point: [3, 3], + }, + AddressNumber: "575", + }, + PlaceId: "ghi", + Distance: 2, + }, + ], + }; + const output: FeatureCollection = { + type: "FeatureCollection", + features: [ + { + type: "Feature", + id: "abc", + properties: { + "Place.AddressNumber": "1050", + Distance: 0, + }, + geometry: { + type: "Point", + coordinates: [5, 5], + }, + }, + { + type: "Feature", + id: "def", + properties: { + "Place.AddressNumber": "609", + Distance: 1, + }, + geometry: { + type: "Point", + coordinates: [4, 4], + }, + }, + { + type: "Feature", + id: "ghi", + properties: { + "Place.AddressNumber": "575", + Distance: 2, + }, + geometry: { + type: "Point", + coordinates: [3, 3], + }, + }, + ], + }; + expect(placeToFeatureCollection(input, { flattenProperties: true })).toEqual(output); + }); + it("should skip a feature in the converted FeatureCollection if it is missing a Point field", () => { const input: SearchPlaceIndexForTextResponse = { Summary: { diff --git a/src/to-geojson/place-converter.ts b/src/to-geojson/place-converter.ts index 284bcb9..202cb6f 100644 --- a/src/to-geojson/place-converter.ts +++ b/src/to-geojson/place-converter.ts @@ -108,7 +108,7 @@ import { emptyFeatureCollection, toFeatureCollection } from "./utils"; * } * ``` * - * Output: + * Output flattenProperties is false: * * ```json * { @@ -138,6 +138,34 @@ import { emptyFeatureCollection, toFeatureCollection } from "./utils"; * } * ``` * + * - Output flattenProperties is true: + * + * ```json + * { + * "type": "FeatureCollection", + * "features": [ + * { + * "type": "Feature", + * "properties": { + * "Place.Label": "Whole Foods Market, 1675 Robson St, Vancouver, BC, V6G 1C8, CAN", + * "Place.AddressNumber": "1675", + * "Place.Street": "Robson St", + * "Place.Municipality": "Vancouver", + * "Place.SubRegion": "Greater Vancouver", + * "Place.Region": "British Columbia", + * "Place.Country": "CAN", + * "Place.PostalCode": "V6G 1C8", + * "Place.Interpolated": false + * }, + * "geometry": { + * "type": "Point", + * "coordinates": [-123.13, 49.28] + * } + * } + * ] + * } + * ``` + * * @example Converting a SearchPlaceIndexForTextResponse result with the second result missing the `Point` field * * Result of SearchPlaceIndexForTextResponse: @@ -208,7 +236,7 @@ import { emptyFeatureCollection, toFeatureCollection } from "./utils"; * } * ``` * - * Output: + * Output flattenProperties is true: * * ```json * { @@ -218,23 +246,19 @@ import { emptyFeatureCollection, toFeatureCollection } from "./utils"; * "type": "Feature", * "id": "AQAAAHAArZ9I7WtFD", * "properties": { - * "Place": { - * "Label": "Whole Foods Market, 1675 Robson St, Vancouver, BC V6G 1C8, Canada", - * "AddressNumber": "1675", - * "Street": "Robson St", - * "Neighborhood": "West End", - * "Municipality": "Vancouver", - * "SubRegion": "Metro Vancouver", - * "Region": "British Columbia", - * "Country": "CAN", - * "PostalCode": "V6G 1C8", - * "Interpolated": false, - * "TimeZone": { - * "Name": "America/Vancouver", - * "Offset": -25200 - * } - * }, - * "Distance": 1385.945532454018 + * "Distance": 1385.945532454018 + * "Place.Label": "Whole Foods Market, 1675 Robson St, Vancouver, BC V6G 1C8, Canada", + * "Place.AddressNumber": "1675", + * "Place.Street": "Robson St", + * "Place.Neighborhood": "West End", + * "Place.Municipality": "Vancouver", + * "Place.SubRegion": "Metro Vancouver", + * "Place.Region": "British Columbia", + * "Place.Country": "CAN", + * "Place.PostalCode": "V6G 1C8", + * "Place.Interpolated": false, + * "Place.TimeZone.Name": "America/Vancouver", + * "Place.TimeZone.Offset": -25200 * }, * "geometry": { * "type": "Point", @@ -245,23 +269,19 @@ import { emptyFeatureCollection, toFeatureCollection } from "./utils"; * "type": "Feature", * "id": "AQAAAHAAo5aDp0fMX", * "properties": { - * "Place": { - * "Label": "Whole Foods, 925 Main St, West Vancouver, BC V7T, Canada", - * "AddressNumber": "925", - * "Street": "Main St", - * "Neighborhood": "Capilano Indian Reserve 5", - * "Municipality": "West Vancouver", - * "SubRegion": "Metro Vancouver", - * "Region": "British Columbia", - * "Country": "CAN", - * "PostalCode": "V7T", - * "Interpolated": false, - * "TimeZone": { - * "Name": "America/Vancouver", - * "Offset": -25200 - * } - * }, - * "Distance": 3876.5708436735226 + * "Distance": 3876.5708436735226 + * "Place.Label": "Whole Foods, 925 Main St, West Vancouver, BC V7T, Canada", + * "Place.AddressNumber": "925", + * "Place.Street": "Main St", + * "Place.Neighborhood": "Capilano Indian Reserve 5", + * "Place.Municipality": "West Vancouver", + * "Place.SubRegion": "Metro Vancouver", + * "Place.Region": "British Columbia", + * "Place.Country": "CAN", + * "Place.PostalCode": "V7T", + * "Place.Interpolated": false, + * "Place.TimeZone.Name": "America/Vancouver", + * "Place.TimeZone.Offset": -25200 * }, * "geometry": { * "type": "Point", @@ -273,16 +293,18 @@ import { emptyFeatureCollection, toFeatureCollection } from "./utils"; * ``` * * @param place Response of the GetPlace or SearchPlace* API. + * @param options Options for flattening the properties. * @returns A GeoJSON FeatureCollection */ export function placeToFeatureCollection( place: GetPlaceResponse | SearchPlaceIndexForPositionResponse | SearchPlaceIndexForTextResponse, + options?: { flattenProperties?: boolean }, ): FeatureCollection { if ("Results" in place) { - const features = place.Results.map((result) => result && convertPlaceToFeature(result)); + const features = place.Results.map((result) => result && convertPlaceToFeature(result, options)); return toFeatureCollection(features); } else if ("Place" in place) { - const features = [convertPlaceToFeature(place)]; + const features = [convertPlaceToFeature(place, options)]; return toFeatureCollection(features); } else { return emptyFeatureCollection(); @@ -293,26 +315,50 @@ export function placeToFeatureCollection( * Convert an Amazon Location Place object to a GeoJSON Feature. * * @param place The Place object from Amazon Location SDK. + * @param options Options for flattening the properties. * @returns A GeoJSON Feature of the Place object, or null if there isn't the Geometry.Point property present. */ function convertPlaceToFeature( place: GetPlaceResponse | SearchForPositionResult | SearchForTextResult, + options?: { flattenProperties?: boolean }, ): Feature | null { const coordinates = place.Place?.Geometry?.Point; if (coordinates) { + const placeClone = { ...place }; + delete placeClone.Place?.Geometry; + if ("PlaceId" in placeClone) { + delete placeClone.PlaceId; + } + const properties = options?.flattenProperties ? flattenProperties({ ...placeClone }) : { ...placeClone }; const feature: Feature = { type: "Feature", id: "PlaceId" in place ? place.PlaceId : undefined, - properties: { ...place }, + properties: properties, geometry: { type: "Point", coordinates: coordinates, }, }; - delete feature.properties.Place.Geometry; - if ("PlaceId" in feature.properties) { - delete feature.properties.PlaceId; - } return feature; } + return null; +} + +/** + * Optionally flatten the Amazon Location Place object. + * + * @param obj Amazon Location Place object. + * @returns Flattened objects. + */ +function flattenProperties(obj: Record, prefix = ""): Record { + return Object.keys(obj).reduce((acc: Record, key: string) => { + const newKey = prefix ? `${prefix}.${key}` : key; + const value = obj[key]; + if (value && typeof value === "object" && key !== "Geometry") { + Object.assign(acc, flattenProperties(value as Record, newKey)); + } else { + acc[newKey] = value; + } + return acc; + }, {}); }