diff --git a/README.md b/README.md index 30c0ed2..c8a94c3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ - Built using [NestJS](https://github.com/nestjs/nest) TypeScript Framework by [ThatAPICompany](https://thatapicompany.com) - specialists in all things APIs +## Demo + +[See the API in Action at https://www.OvertureMapsAPI.com/](https://www.overturemapsapi.com/) + + ## Endpoints - [OpenAPI Spec Doc](https://overture-maps-api.thatapicompany.com/api-docs.json) diff --git a/package.json b/package.json index 14a733b..b33dfba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "overture-maps-api", - "version": "0.1.5", + "version": "0.1.6", "description": "", "author": "", "private": true, diff --git a/src/bigquery/row-parsers/bq-building-row.parser.ts b/src/bigquery/row-parsers/bq-building-row.parser.ts index 7b2e014..31f5c8a 100644 --- a/src/bigquery/row-parsers/bq-building-row.parser.ts +++ b/src/bigquery/row-parsers/bq-building-row.parser.ts @@ -1,12 +1,12 @@ import { Building } from "../../buildings/interfaces/building.interface" -import { parsePolygonToGeoJSON } from "../../utils/geojson" +import { parsePolygonToGeoJSON, parseWKTToGeoJSON } from "../../utils/geojson" export const parseBuildingRow = (row: any): Building => { return { id: row.id, - geometry: parsePolygonToGeoJSON(row.geometry.value), + geometry: parseWKTToGeoJSON(row.geometry.value), bbox: { xmin: parseFloat(row.bbox.xmin), xmax: parseFloat(row.bbox.xmax), diff --git a/src/bigquery/row-parsers/bq-place-row.parser.ts b/src/bigquery/row-parsers/bq-place-row.parser.ts index b3219ff..f35ecae 100644 --- a/src/bigquery/row-parsers/bq-place-row.parser.ts +++ b/src/bigquery/row-parsers/bq-place-row.parser.ts @@ -1,5 +1,5 @@ import { Place, PlaceWithBuilding } from "../../places/interfaces/place.interface" -import { parsePointToGeoJSON, parsePolygonToGeoJSON } from "../../utils/geojson" +import { parsePointToGeoJSON, parsePolygonToGeoJSON, parseWKTToGeoJSON } from "../../utils/geojson" export const parsePlaceRow = (row: any): Place => { @@ -59,7 +59,7 @@ export const parsePlaceWithBuildingRow = (row: any): PlaceWithBuilding => { const building = { id: row.building_id, distance: parseFloat(row.distance_to_nearest_building), - geometry: parsePolygonToGeoJSON(row.building_geometry.value), + geometry: parseWKTToGeoJSON(row.building_geometry.value), } return { diff --git a/src/buildings/interfaces/building.interface.ts b/src/buildings/interfaces/building.interface.ts index af99b69..fd28d0b 100644 --- a/src/buildings/interfaces/building.interface.ts +++ b/src/buildings/interfaces/building.interface.ts @@ -1,12 +1,12 @@ import { Source } from "../../places/interfaces/place.interface"; import { Bbox } from "../../common/interfaces/geometry.interface"; -import { Geometry, Polygon } from "geojson"; +import { Geometry, MultiPolygon, Polygon ,Point} from "geojson"; export interface Building { id: string; - geometry: Polygon; + geometry: Point|Polygon|MultiPolygon; bbox?: Bbox; version: string; sources: Source[]; diff --git a/src/places/dto/responses/place-response.dto.ts b/src/places/dto/responses/place-response.dto.ts index 9dca7c6..672fcd3 100644 --- a/src/places/dto/responses/place-response.dto.ts +++ b/src/places/dto/responses/place-response.dto.ts @@ -3,7 +3,7 @@ import { Place, PlaceWithBuilding } from '../../interfaces/place.interface'; import { BrandDto } from '../models/brand.dto'; import { CategoryDto } from '../models/category.dto'; import { GeometryDto } from '../../../common/dto/responses/geometry.dto'; -import { Geometry, Point, Polygon } from 'geojson'; +import { Geometry, MultiPolygon, Point, Polygon } from 'geojson'; import { GetByLocationDto } from '../../../common/dto/requests/get-by-location.dto'; import { applyIncludesToDto } from '../../../common/dto/responses/includes.dto'; @@ -114,7 +114,7 @@ export class PlacePropertiesDto { ext_building?: { id:string; - geometry:Polygon; + geometry:Point|Polygon|MultiPolygon; distance:number } @@ -137,7 +137,7 @@ export class PlaceResponseDto { description: 'Geometric representation of the place.', type: () => GeometryDto, }) - geometry: Point|Polygon; + geometry: Point|Polygon|MultiPolygon; @ApiProperty({ description: 'Properties and additional details of the place.', diff --git a/src/places/interfaces/place.interface.ts b/src/places/interfaces/place.interface.ts index 430cdbe..20364f1 100644 --- a/src/places/interfaces/place.interface.ts +++ b/src/places/interfaces/place.interface.ts @@ -1,9 +1,9 @@ -import { Point, Polygon } from "geojson"; +import { MultiPolygon, Point, Polygon } from "geojson"; import { Bbox } from "../../common/interfaces/geometry.interface"; export interface Place { id: string; - geometry: Point|Polygon; + geometry: Point|Polygon|MultiPolygon; bbox?: Bbox; version: string; sources: Source[]; @@ -25,7 +25,7 @@ export interface PlaceWithBuilding extends Place { building: { id:string - geometry: Polygon; + geometry: Point|Polygon|MultiPolygon; distance: number; } } diff --git a/src/utils/geojson.spec.ts b/src/utils/geojson.spec.ts new file mode 100644 index 0000000..0b5165f --- /dev/null +++ b/src/utils/geojson.spec.ts @@ -0,0 +1,44 @@ +import { parseMultiPolygonToGeoJSON, parsePolygonToGeoJSON, parseWKTToGeoJSON } from "./geojson"; + + +const PolygonStr = "POLYGON((151.2762519003 -33.8915747100951, 151.276258302075 -33.8914594937271, 151.276189186945 -33.8914340614275, 151.276066105576 -33.8914671922852, 151.276054954362 -33.8914667647, 151.275981484004 -33.8914858830191, 151.275999470338 -33.8915335108494, 151.275962780589 -33.8915430560486, 151.275960784231 -33.8915789311803, 151.275923313259 -33.8915774943851, 151.275920283167 -33.8916319459681, 151.275975754972 -33.891634069767, 151.275983409999 -33.8916543402525, 151.275981862028 -33.8916821576985, 151.27603392474 -33.8916841540002, 151.276035202591 -33.8916611558307, 151.276077326459 -33.8916501944598, 151.276088482028 -33.891650622211, 151.276167145946 -33.8916301570429, 151.276169244963 -33.8915924371208, 151.276165426201 -33.8915823250443, 151.276215281919 -33.8915842367227, 151.2762519003 -33.8915747100951))" + +const multiPolygonStr = `MULTIPOLYGON(((-73.9944007 40.7135703, -73.9943494 40.7134777, -73.9942995 40.7133877, -73.9938986 40.7135098, -73.993976 40.7136465, -73.9943661 40.7136157, -73.9943895 40.713585, -73.9944007 40.7135703)), ((-73.9942489 40.7132946, -73.9941596 40.7131396, -73.9941396 40.713141, -73.9941334 40.7131415, -73.9937179 40.713175, -73.9938473 40.7134172, -73.9942297 40.7133005, -73.9942489 40.7132946)))` + +describe('GeoJSON tests', () => { + + + it('should parse a valid MULTIPOLYGON string to GeoJSON', () => { + + try { + const geoJSON = parseMultiPolygonToGeoJSON(multiPolygonStr); + expect(geoJSON).toBeDefined(); + } catch (error) { + console.error("Error parsing MULTIPOLYGON:", error.message); + //fail + expect(true).toBe(false); + } + + }); + + + it('should parse a Polygon to GeoJSON', () => { + + try { + const geoJSON = parsePolygonToGeoJSON(PolygonStr); + expect(geoJSON).toBeDefined(); + } catch (error) { + console.error("Error parsing MULTIPOLYGON:", error.message); + //fail + expect(true).toBe(false); + } + }) + + it('should check that parseWKTToGeoJSON results in the correct GeoJSON type', () => { + expect(parseWKTToGeoJSON(PolygonStr).type).toBe('Polygon'); + }); + + it('should check that parseWKTToGeoJSON results in the correct GeoJSON type', () => { + expect(parseWKTToGeoJSON(multiPolygonStr).type).toBe('MultiPolygon'); + }); +}) \ No newline at end of file diff --git a/src/utils/geojson.ts b/src/utils/geojson.ts index 9999f5a..fa97a58 100644 --- a/src/utils/geojson.ts +++ b/src/utils/geojson.ts @@ -4,9 +4,25 @@ Expect string to be in format "POLYGON((151.2762519003 -33.8915747100951, 151.27 */ import * as turf from '@turf/turf' -import { Feature, Point, Polygon } from 'geojson'; +import { Feature, MultiPolygon, Point, Polygon } from 'geojson'; //import geojson +export const parseWKTToGeoJSON = (wkt: string): Point|Polygon|MultiPolygon => { + //if MULTI use parseMultiPolygonToGeoJSON + if(wkt.includes("MULTIPOLYGON")){ + console.log('function to handle parsing of MULTI e.g. ', wkt); + return parseMultiPolygonToGeoJSON(wkt); + } + //if POLYGON the use parsePolygonToGeoJSON + if(wkt.includes("POLYGON")){ + return parsePolygonToGeoJSON(wkt); + } + //if POINT + if(wkt.includes("POINT")){ + return parsePointToGeoJSON(wkt); + } +} + export const parsePolygonToGeoJSON = (polygon: string): Polygon => { try{ if(polygon === undefined){ @@ -25,7 +41,7 @@ export const parsePolygonToGeoJSON = (polygon: string): Polygon => { num = parseFloat(c); } if(isNaN(num)){ - console.log('Coordinate is NaN', c); + console.log('Coordinate is NaN', c, polygon); } return num } @@ -48,6 +64,49 @@ export const parsePolygonToGeoJSON = (polygon: string): Polygon => { } } +/* +function to handle parsing of MULTI e.g. MULTI(-73.9944007 MULTIPOLYGON(((-73.9944007 40.7135703, -73.9943494 40.7134777, -73.9942995 40.7133877, -73.9938986 40.7135098, -73.993976 40.7136465, -73.9943661 40.7136157, -73.9943895 40.713585, -73.9944007 40.7135703)), ((-73.9942489 40.7132946, -73.9941596 40.7131396, -73.9941396 40.713141, -73.9941334 40.7131415, -73.9937179 40.713175, -73.9938473 40.7134172, -73.9942297 40.7133005, -73.9942489 40.7132946))) +Coordinate is NaN (-73.9942489 MULTIPOLYGON(((-73.9944007 40.7135703, -73.9943494 40.7134777, -73.9942995 40.7133877, -73.9938986 40.7135098, -73.993976 40.7136465, -73.9943661 40.7136157, -73.9943895 40.713585, -73.9944007 40.7135703)), ((-73.9942489 40.7132946, -73.9941596 40.7131396, -73.9941396 40.713141, -73.9941334 40.7131415, -73.9937179 40.713175, -73.9938473 40.7134172, -73.9942297 40.7133005, -73.9942489 40.7132946))) +*/ +export const parseMultiPolygonToGeoJSON = (multiPolygonStr):MultiPolygon => { + // Regular expression to match polygons inside MULTIPOLYGON((...)) + const multiPolygonRegex = /MULTIPOLYGON\s*\(\(\((.*?)\)\)\)/g; + const coordinateRegex = /(-?\d+\.\d+)\s+(-?\d+\.\d+)/g; + + // Function to parse a single polygon string into GeoJSON coordinates + const parsePolygon = (polygonStr) => { + const coordinates = []; + let match; + while ((match = coordinateRegex.exec(polygonStr)) !== null) { + const [_, lon, lat] = match; + coordinates.push([parseFloat(lon), parseFloat(lat)]); + } + return coordinates; + }; + + // Check for MULTIPOLYGON match and iterate through each polygon set + const polygons = []; + let match; + while ((match = multiPolygonRegex.exec(multiPolygonStr)) !== null) { + const polygonStr = match[1]; + const polygonCoordinates = polygonStr + .split(/\)\s*,\s*\(/) // Split into individual polygons + .map(parsePolygon); // Parse each polygon string + polygons.push(polygonCoordinates); + } + + if (polygons.length === 0) { + throw new Error("No valid MULTIPOLYGON data found in input."); + } + + // Construct GeoJSON object + const geoJSON = { + type: "MultiPolygon", + coordinates: polygons + }; + + return geoJSON as MultiPolygon; +} /* export string of POINT(151.2772322 -33.8913828) */