From c9c9dba5443543fef9d2b5b02a0f5fc0747f5959 Mon Sep 17 00:00:00 2001 From: Alfred Rosenthal Date: Tue, 16 Jan 2024 14:04:01 -0800 Subject: [PATCH] cleaned up bcgw code and adding some more comments --- api/src/repositories/bcgw-repository.ts | 43 -- api/src/repositories/region-repository.ts | 8 +- .../repositories/search-index-respository.ts | 4 - api/src/services/bcgw-layer-service.test.ts | 77 --- api/src/services/bcgw-layer-service.ts | 120 ----- api/src/services/bcgw-service.ts | 505 ------------------ .../fn_calculate_area_intersect.sql | 7 +- 7 files changed, 12 insertions(+), 752 deletions(-) delete mode 100644 api/src/repositories/bcgw-repository.ts delete mode 100644 api/src/services/bcgw-layer-service.test.ts delete mode 100644 api/src/services/bcgw-layer-service.ts delete mode 100644 api/src/services/bcgw-service.ts diff --git a/api/src/repositories/bcgw-repository.ts b/api/src/repositories/bcgw-repository.ts deleted file mode 100644 index a9af5ab3e..000000000 --- a/api/src/repositories/bcgw-repository.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Geometry } from 'geojson'; -import { z } from 'zod'; -import { getKnex } from '../database/db'; -import { ApiExecuteSQLError } from '../errors/api-error'; -import { Srid } from '../services/geo-service'; -import { BaseRepository } from './base-repository'; - -export class BCGWRepository extends BaseRepository { - /** - * Convert the provided GeoJSON geometry into Well-Known Text (WKT) in the provided Spatial Reference ID (SRID). - * - * @see https://postgis.net/docs/ST_AsText.html - * - * @param {Geometry} geometry - * @param {Srid} srid - * @return {*} {Promise} - * @memberof BCGWRepository - */ - async getGeoJsonGeometryAsWkt(geometry: Geometry, srid: Srid): Promise { - const knex = getKnex(); - - const queryBuilder = knex - .queryBuilder() - .select( - knex.raw( - `public.ST_AsText(public.ST_TRANSFORM(public.ST_Force2D(public.ST_GeomFromGeoJSON('${JSON.stringify( - geometry - )}')), ${srid})) as geometry` - ) - ); - - const response = await this.connection.knex(queryBuilder, z.object({ geometry: z.string() })); - - if (response.rowCount !== 1) { - throw new ApiExecuteSQLError('Failed to convert GeoJSON geometry to WKT', [ - 'BCGWRepository->getGeoJsonGeometryAsWkt', - 'rowCount was null or undefined, expected rowCount = 1' - ]); - } - - return response.rows[0].geometry; - } -} diff --git a/api/src/repositories/region-repository.ts b/api/src/repositories/region-repository.ts index da98ba720..5bb043ee6 100644 --- a/api/src/repositories/region-repository.ts +++ b/api/src/repositories/region-repository.ts @@ -49,8 +49,14 @@ export class RegionRepository extends BaseRepository { * Intersections are calculated based on area coverage passed in through intersectionThreshold * Any regions intersecting with this calculated value are returned. * + * intersectThreshold is expecting a range of values from 0.0 - 1.0. + * A value of 0.0 means 0% of the geometries area need to intersect meaning all values from `region_lookup` will be returned. + * A value of 1.0 means 100% of the geometries area need to be an exact match before returning a value. + * A value of 0.3 means that 30% of the geometries area need to intersect before returning a value. + * + * * @param {number} submissionId - * @param {number} [intersectThreshold=1] intersectThreshold Expected 0-1. Determines the percentage threshold for intersections to be valid + * @param {number} [intersectThreshold=1] intersectThreshold Expected 0.0 - 1.0. Determines the percentage threshold for intersections to be valid * @returns {*} {Promise<{region_id: number}}[]>} An array of found region ids * @memberof RegionRepository */ diff --git a/api/src/repositories/search-index-respository.ts b/api/src/repositories/search-index-respository.ts index 8503d267f..e4eed3a6e 100644 --- a/api/src/repositories/search-index-respository.ts +++ b/api/src/repositories/search-index-respository.ts @@ -267,8 +267,4 @@ export class SearchIndexRepository extends BaseRepository { return response.rows; } - - async getSpatialRecordsForSubmissionId(submissionId: number): Promise { - return []; - } } diff --git a/api/src/services/bcgw-layer-service.test.ts b/api/src/services/bcgw-layer-service.test.ts deleted file mode 100644 index c3de774b2..000000000 --- a/api/src/services/bcgw-layer-service.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import chai, { expect } from 'chai'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import { BcgwLayerService } from './bcgw-layer-service'; -import { WebFeatureService } from './geo-service'; - -chai.use(sinonChai); - -describe('BcgwLayerService', () => { - afterEach(() => { - sinon.restore(); - }); - - it('constructs', async () => { - const service = new BcgwLayerService(); - - expect(service).not.to.be.undefined; - }); - - describe('getEnvRegionNames', async () => { - it('fetches and returns env region names', async () => { - const wfsGetPropertyResponseXml = ` - - - - Vancouver Island - - - Lower Mainland - - - `; - - const webFeatureServiceStub = sinon - .stub(WebFeatureService.prototype, 'getPropertyValue') - .resolves(wfsGetPropertyResponseXml); - - const bcgwLayerService = new BcgwLayerService(); - - const geometryWKTString = 'POLYGON(123,456,789)'; - - const response = await bcgwLayerService.getEnvRegionNames(geometryWKTString); - - expect(webFeatureServiceStub).to.have.been.calledOnce; - expect(response).to.eql(['Vancouver Island', 'Lower Mainland']); - }); - }); - - describe('getNrmRegionNames', async () => { - it('fetches and returns nrm region names', async () => { - const wfsGetPropertyResponseXml = ` - - - - West Coast Natural Resource Region - - - South Coast Natural Resource Region - - - `; - - const webFeatureServiceStub = sinon - .stub(WebFeatureService.prototype, 'getPropertyValue') - .resolves(wfsGetPropertyResponseXml); - - const bcgwLayerService = new BcgwLayerService(); - - const geometryWKTString = 'POLYGON(123,456,789)'; - - const response = await bcgwLayerService.getNrmRegionNames(geometryWKTString); - - expect(webFeatureServiceStub).to.have.been.calledOnce; - expect(response).to.eql(['West Coast Natural Resource Region', 'South Coast Natural Resource Region']); - }); - }); -}); diff --git a/api/src/services/bcgw-layer-service.ts b/api/src/services/bcgw-layer-service.ts deleted file mode 100644 index f35425554..000000000 --- a/api/src/services/bcgw-layer-service.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { XMLParser } from 'fast-xml-parser'; -import { z } from 'zod'; -import { Epsg3005, WebFeatureService, WebMapService } from './geo-service'; - -/** - * BCGW (BCGov) ENV regional boundaries layer. - * - * @see https://catalogue.data.gov.bc.ca/dataset/env-regional-boundaries - */ -export type BcgwEnvRegionsLayer = 'WHSE_ADMIN_BOUNDARIES.EADM_WLAP_REGION_BND_AREA_SVW'; -export const BcgwEnvRegionsLayer: BcgwEnvRegionsLayer = 'WHSE_ADMIN_BOUNDARIES.EADM_WLAP_REGION_BND_AREA_SVW'; -/** - * The name of the field in the layer that contains the spatial data. - * - * Note: this value is needed when creating a CQL filter. - */ -export const BcgwEnvRegionsLayerGeometryField = 'GEOMETRY'; - -/** - * BCGW (BCGov) NRM regional boundaries layer. - * - * @see https://catalogue.data.gov.bc.ca/dataset/natural-resource-nr-regions - */ -export type BcgwNrmRegionsLayer = 'WHSE_ADMIN_BOUNDARIES.ADM_NR_REGIONS_SPG'; -export const BcgwNrmRegionsLayer: BcgwNrmRegionsLayer = 'WHSE_ADMIN_BOUNDARIES.ADM_NR_REGIONS_SPG'; -/** - * The name of the field in the layer that contains the spatial data. - * - * Note: this value is needed when creating a CQL filter. - */ -export const BcgwNrmRegionsLayerGeometryField = 'SHAPE'; - -/** - * Service for fetching information from known BCGW layers. - * - * @export - * @class BcgwLayerService - */ -export class BcgwLayerService { - webFeatureService: WebFeatureService; - webMapService: WebMapService; - - xmlParser: XMLParser; - - _xmlParserOptions = { - ignoreAttributes: false, - attributeNamePrefix: '@_', - // Passes all values through as strings. This avoids problems where text fields have numbers only but need to be - // interpreted as text. - parseTagValue: false, - isArray: (tagName: string) => { - // Ensure specified nodes are always parsed as arrays - const tagsArray: Array = ['wfs:member']; - - return tagsArray.includes(tagName); - } - }; - - // Zod schema used to verify the ENV and NRM region GetPropertyValue responses, after converting from XML to JSON. - _getGetRegionPropertyValueZodSchema = z.object({ - 'wfs:ValueCollection': z.object({ - 'wfs:member': z.array( - z.object({ - 'pub:REGION_NAME': z.string() - }) - ) - }) - }); - - constructor() { - this.webFeatureService = new WebFeatureService(); - this.webMapService = new WebMapService(); - - this.xmlParser = new XMLParser(this._xmlParserOptions); - } - - /** - * Get region names, from the ENV regions layer, that intersect the provided geometry. - * - * @param {string} geometry a WKT geometry string. Must already be in the SRID that matches the ENV layer. - * @return {*} {Promise} - * @memberof DatasetRegionService - */ - async getEnvRegionNames(geometry: string): Promise { - const responseXml = await this.webFeatureService.getPropertyValue({ - typeNames: BcgwEnvRegionsLayer, - srsName: Epsg3005, - cql_filter: `INTERSECTS(${BcgwEnvRegionsLayerGeometryField}, ${geometry})`, - valueReference: 'REGION_NAME' - }); - - // Convert XML response to JSON and verify with Zod - const responseObj = this._getGetRegionPropertyValueZodSchema.parse(this.xmlParser.parse(responseXml as string)); - - // Return array of region name values - return responseObj['wfs:ValueCollection']['wfs:member'].map((item) => item['pub:REGION_NAME']); - } - - /** - * Get region names, from the NRM regions layer, that intersect the provided geometry. - * - * @param {string} geometry a WKT geometry string. Must already be in the SRID that matches the NRM layer. - * @return {*} {Promise} - * @memberof DatasetRegionService - */ - async getNrmRegionNames(geometry: string): Promise { - const responseXml = await this.webFeatureService.getPropertyValue({ - typeNames: BcgwNrmRegionsLayer, - srsName: Epsg3005, - cql_filter: `INTERSECTS(${BcgwNrmRegionsLayerGeometryField}, ${geometry})`, - valueReference: 'REGION_NAME' - }); - - // Convert XML response to JSON and verify with Zod - const responseObj = this._getGetRegionPropertyValueZodSchema.parse(this.xmlParser.parse(responseXml as string)); - - // Return array of region name values - return responseObj['wfs:ValueCollection']['wfs:member'].map((item) => item['pub:REGION_NAME']); - } -} diff --git a/api/src/services/bcgw-service.ts b/api/src/services/bcgw-service.ts deleted file mode 100644 index 9df97c41c..000000000 --- a/api/src/services/bcgw-service.ts +++ /dev/null @@ -1,505 +0,0 @@ -import { XMLParser } from 'fast-xml-parser'; -import { Feature } from 'geojson'; -import { z } from 'zod'; -import { IDBConnection } from '../database/db'; -import { BCGWRepository } from '../repositories/bcgw-repository'; -import { getLogger } from '../utils/logger'; -import { Epsg3005, Srid3005, WebFeatureService, WebMapService } from './geo-service'; - -/** - * BCGW (BCGov) ENV regional boundaries layer. - * - * @see https://catalogue.data.gov.bc.ca/dataset/env-regional-boundaries - */ -export type BcgwEnvRegionsLayer = 'WHSE_ADMIN_BOUNDARIES.EADM_WLAP_REGION_BND_AREA_SVW'; -export const BcgwEnvRegionsLayer: BcgwEnvRegionsLayer = 'WHSE_ADMIN_BOUNDARIES.EADM_WLAP_REGION_BND_AREA_SVW'; -/** - * The name of the field in the layer that contains the spatial data. - * - * Note: this value is needed when creating a CQL filter. - */ -export const BcgwEnvRegionsLayerGeometryField = 'GEOMETRY'; - -/** - * BCGW (BCGov) NRM regional boundaries layer. - * - * @see https://catalogue.data.gov.bc.ca/dataset/natural-resource-nr-regions - */ -export type BcgwNrmRegionsLayer = 'WHSE_ADMIN_BOUNDARIES.ADM_NR_REGIONS_SPG'; -export const BcgwNrmRegionsLayer: BcgwNrmRegionsLayer = 'WHSE_ADMIN_BOUNDARIES.ADM_NR_REGIONS_SPG'; -/** - * The name of the field in the layer that contains the spatial data. - * - * Note: this value is needed when creating a CQL filter. - */ -export const BcgwNrmRegionsLayerGeometryField = 'SHAPE'; - -/** - * BCGW (BCGov) Parks and Ecoreserves boundaries layer. - * - * @see https://catalogue.data.gov.bc.ca/dataset/bc-parks-ecological-reserves-and-protected-areas - */ -export type BcgwParksAndEcoreservesLayer = 'WHSE_TANTALIS.TA_PARK_ECORES_PA_SVW'; -export const BcgwParksAndEcoreservesLayer: BcgwParksAndEcoreservesLayer = 'WHSE_TANTALIS.TA_PARK_ECORES_PA_SVW'; -/** - * The name of the field in the layer that contains the spatial data. - * - * Note: this value is needed when creating a CQL filter. - */ -export const BcgwParksAndEcoreservesLayerGeometryField = 'SHAPE'; - -/** - * BCGW (BCGov) wildlife management units layer. - * - * @see https://catalogue.data.gov.bc.ca/dataset/wildlife-management-units - */ -export type BcgwWildlifeManagementUnitsLayer = 'WHSE_WILDLIFE_MANAGEMENT.WAA_WILDLIFE_MGMT_UNITS_SVW'; -export const BcgwWildlifeManagementUnitsLayer: BcgwWildlifeManagementUnitsLayer = - 'WHSE_WILDLIFE_MANAGEMENT.WAA_WILDLIFE_MGMT_UNITS_SVW'; -/** - * The name of the field in the layer that contains the spatial data. - * - * Note: this value is needed when creating a CQL filter. - */ -export const BcgwWildlifeManagementUnitsLayerGeometryField = 'GEOMETRY'; - -/** - * A mapping of ENV to NRM regions, which are not necessarily 1:1. - * - * Note: some ENV regions map to multiple NRM regions. - */ -const envToNrmRegionsMapping = { - 'Vancouver Island': 'West Coast Natural Resource Region', - 'Lower Mainland': 'South Coast Natural Resource Region', - Thompson: 'Thompson-Okanagan Natural Resource Region', - Okanagan: 'Thompson-Okanagan Natural Resource Region', - Kootenay: 'Kootenay-Boundary Natural Resource Region', - Cariboo: 'Cariboo Natural Resource Region', - Skeena: 'Skeena Natural Resource Region', - Omineca: 'Omineca Natural Resource Region', - Peace: 'Northeast Natural Resource Region' -}; - -export type RegionDetails = { regionName: string; sourceLayer: string }; - -const defaultLog = getLogger('services/bcgw-layer-service'); -/** - * Service for fetching information from known BCGW layers. - * - * @export - * @class BcgwLayerService - */ -export class BCGWService { - webFeatureService: WebFeatureService; - webMapService: WebMapService; - - xmlParser: XMLParser; - - _xmlParserOptions = { - ignoreAttributes: false, - attributeNamePrefix: '@_', - // Passes all values through as strings. This avoids problems where text fields have numbers only but need to be - // interpreted as text. - parseTagValue: false, - isArray: (tagName: string) => { - // Ensure specified nodes are always parsed as arrays - const tagsArray: Array = ['wfs:member']; - - return tagsArray.includes(tagName); - } - }; - - // Zod schema used to verify the ENV and NRM region GetPropertyValue responses, after converting from XML to JSON. - _getGetRegionPropertyValueZodSchema = (propertyName: string) => - z.object({ - 'wfs:ValueCollection': z.object({ - 'wfs:member': z - .array( - z.object({ - [`pub:${propertyName}`]: z.string() - }) - ) - .optional() - }) - }); - - constructor() { - this.webFeatureService = new WebFeatureService(); - this.webMapService = new WebMapService(); - - this.xmlParser = new XMLParser(this._xmlParserOptions); - } - - /** - * Get region names, from the ENV regions layer, that intersect the provided geometry. - * - * @param {string} geometry a WKT geometry string. Must already be in the SRID that matches the ENV layer. - * @return {*} {Promise} - * @memberof DatasetRegionService - */ - async getEnvRegionNames(geometry: string): Promise { - const responseXml = await this.webFeatureService.getPropertyValue({ - typeNames: BcgwEnvRegionsLayer, - srsName: Epsg3005, - cql_filter: `INTERSECTS(${BcgwEnvRegionsLayerGeometryField}, ${geometry})`, - valueReference: 'REGION_NAME' - }); - - try { - // Convert XML response to JSON and verify with Zod - const responseObj = this._getGetRegionPropertyValueZodSchema('REGION_NAME').parse( - this.xmlParser.parse(responseXml as string) - ); - - // Return array of region name values - return responseObj['wfs:ValueCollection']['wfs:member']?.map((item) => item['pub:REGION_NAME']) ?? []; - } catch (error) { - defaultLog.error({ label: 'getEnvRegionNames', message: 'error', error }); - return []; - } - } - - /** - * Get region names, from the NRM regions layer, that intersect the provided geometry. - * - * @param {string} geometry a WKT geometry string. Must already be in the SRID that matches the NRM layer. - * @return {*} {Promise} - * @memberof DatasetRegionService - */ - async getNrmRegionNames(geometry: string): Promise { - const responseXml = await this.webFeatureService.getPropertyValue({ - typeNames: BcgwNrmRegionsLayer, - srsName: Epsg3005, - cql_filter: `INTERSECTS(${BcgwNrmRegionsLayerGeometryField}, ${geometry})`, - valueReference: 'REGION_NAME' - }); - - // Convert XML response to JSON and verify with Zod - const responseObj = this._getGetRegionPropertyValueZodSchema('REGION_NAME').parse( - this.xmlParser.parse(responseXml as string) - ); - - // Return array of region name values - return responseObj['wfs:ValueCollection']['wfs:member']?.map((item) => item['pub:REGION_NAME']) ?? []; - } - - /** - * Get region names, from the Parks and Ecoreserves layer, that intersect the provided geometry. - * - * @param {string} geometry a WKT geometry string. Must already be in the SRID that matches the NRM layer. - * @return {*} {Promise} - * @memberof DatasetRegionService - */ - async getParkAndEcoreserveRegionNames(geometry: string): Promise { - const responseXml = await this.webFeatureService.getPropertyValue({ - typeNames: BcgwParksAndEcoreservesLayer, - srsName: Epsg3005, - cql_filter: `INTERSECTS(${BcgwParksAndEcoreservesLayerGeometryField}, ${geometry})`, - valueReference: 'PROTECTED_LANDS_NAME' - }); - - // Convert XML response to JSON and verify with Zod - const responseObj = this._getGetRegionPropertyValueZodSchema('PROTECTED_LANDS_NAME').parse( - this.xmlParser.parse(responseXml as string) - ); - - // Return array of region name values - return responseObj['wfs:ValueCollection']['wfs:member']?.map((item) => item['pub:PROTECTED_LANDS_NAME']) ?? []; - } - - /** - * Get region names, from the Wildlife Management Units layer, that intersect the provided geometry. - * - * @param {string} geometry a WKT geometry string. Must already be in the SRID that matches the NRM layer. - * @return {*} {Promise} - * @memberof DatasetRegionService - */ - async getWildlifeManagementUnitRegionNames(geometry: string): Promise { - const responseXml = await this.webFeatureService.getPropertyValue({ - typeNames: BcgwWildlifeManagementUnitsLayer, - srsName: Epsg3005, - cql_filter: `INTERSECTS(${BcgwWildlifeManagementUnitsLayerGeometryField}, ${geometry})`, - valueReference: 'WILDLIFE_MGMT_UNIT_ID' - }); - - // Convert XML response to JSON and verify with Zod - const responseObj = this._getGetRegionPropertyValueZodSchema('WILDLIFE_MGMT_UNIT_ID').parse( - this.xmlParser.parse(responseXml as string) - ); - - // Return array of region name values - return responseObj['wfs:ValueCollection']['wfs:member']?.map((item) => item['pub:WILDLIFE_MGMT_UNIT_ID']) ?? []; - } - - /** - * Given a GeoJSON feature, attempt to determine if the feature came from a known BCGW layer, and if so, return - * its matching region name(s) and source layer details. - * - * @param {Feature} feature - * @return {*} {(RegionDetails | null)} - * @memberof BcgwLayerService - */ - findRegionDetails(feature: Feature): RegionDetails | null { - if (!feature.id || !feature.properties) { - // feature is missing any identifying attributes - return null; - } - - // check if feature is from the BCGW ENV layer - if ((feature.id as string).includes(BcgwEnvRegionsLayer)) { - const regionName = feature.properties?.['REGION_NAME']; - - if (!regionName) { - // feature has no region name property - return null; - } - // feature is a valid BCGW ENV feature - return { regionName: regionName, sourceLayer: BcgwEnvRegionsLayer }; - } - - // check if feature is from the BCGW NRM layer - if ((feature.id as string).includes(BcgwNrmRegionsLayer)) { - const regionName = feature.properties?.['REGION_NAME']; - - if (!regionName) { - // feature has no region name property - return null; - } - // feature is a valid BCGW NRM feature - return { regionName: regionName, sourceLayer: BcgwNrmRegionsLayer }; - } - - // check if feature is from the BCGW Parks and Ecoreserves layer - if ((feature.id as string).includes(BcgwParksAndEcoreservesLayer)) { - const regionName = feature.properties?.['PROTECTED_LANDS_NAME']; - - if (!regionName) { - // feature has no region name property - return null; - } - - // feature is a valid BCGW Parks and Ecoreserves feature - return { regionName: regionName, sourceLayer: BcgwParksAndEcoreservesLayer }; - } - - // check if feature is from the BCGW Wildlife Management Units layer - if ((feature.id as string).includes(BcgwWildlifeManagementUnitsLayer)) { - const regionName = feature.properties?.['WILDLIFE_MGMT_UNIT_ID']; - - if (!regionName) { - // feature has no region name property - return null; - } - - // feature is a valid BCGW Wildlife Management Units feature - return { regionName: regionName, sourceLayer: BcgwWildlifeManagementUnitsLayer }; - } - - // feature is not a known BCGW feature from any known BCGW layer - return null; - } - - /** - * Given an array of RegionDetails items, for each item, if the item came from a known BCGW layer that has a known - * mapping to another BCGW layer, then include the item's matching region(s) from the other layer in the response. - * - * Note: currently this only supports ENV to NRM layer mappings (ie: for all ENV regions, return their matching NRM - * regions, and vice versa) - * - * Why? ENV and NRM regions can be explicitly mapped from one to the other. This allows us to return a list of - * matching NRM regions for a given ENV region, and nice versa, without having to query the BCGW and more importantly, - * without running into any inconsistent layer issues (ex: NRM Region 1 should map to ENV Region 2, but due to - * the polygons in the layers overlapping, NRM Region 1 will return matches for ENV Region 2 AND ENV Region 3, when - * it shouldn't). As a result we cannot reply fully on the layer query results alone. - * - * @param {RegionDetails[]} regionDetails - * @return {*} {RegionDetails[]} - * @memberof BcgwLayerService - */ - getMappedRegionDetails(regionDetails: RegionDetails[]): RegionDetails[] { - if (!regionDetails?.length) { - // feature is missing any identifying attributes - return []; - } - - const response = [...regionDetails]; - - for (const regionDetail of regionDetails) { - // Check if the regionDetail item came from the ENV layer - if (regionDetail.sourceLayer === BcgwEnvRegionsLayer) { - // Check if the region is a known ENV region that maps to a known NRM region - const matchingNrmRegion = envToNrmRegionsMapping[regionDetail.regionName]; - if (matchingNrmRegion) { - // Add NRM region to response - response.push({ regionName: matchingNrmRegion, sourceLayer: BcgwNrmRegionsLayer }); - } - } - - // Check if the regionDetail item came from the NRM layer - if (regionDetail.sourceLayer === BcgwNrmRegionsLayer) { - // Check if the region is a known NRM region that maps to one or more known ENV regions - Object.entries(envToNrmRegionsMapping).forEach(([envRegion, nrmRegion]) => { - if (regionDetail.regionName === nrmRegion) { - // Return matching ENV region - response.push({ regionName: envRegion, sourceLayer: BcgwEnvRegionsLayer }); - } - }); - } - } - - return response; - } - - /** - * For a given GeoJSON Feature, fetch all region details from all supported BCGW layers. - * - * @param {Feature} feature - * @param {IDBConnection} connection - * @return {*} - * @memberof BcgwLayerService - */ - async getRegionsForFeature(feature: Feature, connection: IDBConnection): Promise { - // Array of all matching region details for the feature - let response: RegionDetails[] = []; - - const postgisService = new BCGWRepository(connection); - // Convert the feature geometry to WKT format - const geometryWKTString = await postgisService.getGeoJsonGeometryAsWkt(feature.geometry, Srid3005); - - // Attempt to detect if the feature is a known BCGW feature - const regionDetails = this.findRegionDetails(feature); - - if (!regionDetails) { - // Feature is not a known BCGW feature - // Fetch region details for the feature from ALL available layers - response = response.concat( - await this.getAllRegionDetailsForWktString(geometryWKTString, [BcgwEnvRegionsLayer, BcgwNrmRegionsLayer]) - ); - } else { - // Feature is a known BCGW feature, fetch any additional mapped region details, and add to the overall response - const mappedRegionDetails = this.getMappedRegionDetails([regionDetails]); - response = response.concat(mappedRegionDetails); - // Fetch region details for the feature, excluding the layer whose details were already added above - // Why? We want to avoid querying a layer using a feature from that same layer because the actual results will not - // be consistent with the expected results. (Ex: LayerA + Feature1 should return Feature1, but will often return - // Feature1 + Feature2 + Feature3 due to the layer features containing overlapping coordinates, when ideally they - // should not). - const layersToProcess = [BcgwEnvRegionsLayer, BcgwNrmRegionsLayer].filter( - (layerToProcess) => - !mappedRegionDetails.map((mappedRegionDetail) => mappedRegionDetail.sourceLayer).includes(layerToProcess) - ); - - response = response.concat(await this.getAllRegionDetailsForWktString(geometryWKTString, layersToProcess)); - } - - return response; - } - - async getUniqueRegionsForFeatures(features: Feature[], connection: IDBConnection): Promise { - let regionDetails: RegionDetails[] = []; - for (const feature of features) { - const result = await this.getRegionsForFeature(feature, connection); - regionDetails = regionDetails.concat(result); - } - - // Convert array first into JSON, then into Set, then back to array in order to - // remove duplicate region information. - const detailsJSON = regionDetails.map((value) => JSON.stringify(value)); - const uniqueRegionDetails = Array.from(new Set(detailsJSON)).map( - (value: string) => JSON.parse(value) as RegionDetails - ); - return uniqueRegionDetails; - } - - /** - * Given a geometry WKT string and array of layers to process, return an array of all matching region details for the - * specified layers. - * - * @param {string} geometryWktString a geometry string in Well-Known Text format - * @param {string[]} layersToProcess an array of supported layers to query against - * @return {*} - * @memberof BcgwLayerService - */ - async getAllRegionDetailsForWktString(geometryWktString: string, layersToProcess: string[]) { - let response: RegionDetails[] = []; - - for (const layerToProcess of layersToProcess) { - switch (layerToProcess) { - case BcgwEnvRegionsLayer: - response = response.concat(await this.getEnvRegionDetails(geometryWktString)); - break; - case BcgwNrmRegionsLayer: - response = response.concat(await this.getNrmRegionDetails(geometryWktString)); - break; - case BcgwParksAndEcoreservesLayer: - response = response.concat(await this.getParkAndEcoreserveRegionDetails(geometryWktString)); - break; - case BcgwWildlifeManagementUnitsLayer: - response = response.concat(await this.getWildlifeManagementUnitRegionDetails(geometryWktString)); - break; - default: - break; - } - } - - return response; - } - - /** - * Given a geometry WKT string, return an array of matching region detail objects from the BCGW ENV layer. - * - * @param {string} geometryWktString - * @return {*} {Promise} - * @memberof BcgwLayerService - */ - async getEnvRegionDetails(geometryWktString: string): Promise { - const regionNames = await this.getEnvRegionNames(geometryWktString); - - return regionNames.map((name) => ({ regionName: name, sourceLayer: BcgwEnvRegionsLayer })); - } - - /** - * Given a geometry WKT string, return an array of matching region detail objects from the BCGW NRM layer. - * - * @param {string} geometryWktString - * @return {*} {Promise} - * @memberof BcgwLayerService - */ - async getNrmRegionDetails(geometryWktString: string): Promise { - const regionNames = await this.getNrmRegionNames(geometryWktString); - - return regionNames.map((name) => ({ regionName: name, sourceLayer: BcgwNrmRegionsLayer })); - } - - /** - * Given a geometry WKT string, return an array of matching region detail objects from the BCGW Parks and Ecoreserves - * layer. - * - * @param {string} geometryWktString - * @return {*} {Promise} - * @memberof BcgwLayerService - */ - async getParkAndEcoreserveRegionDetails(geometryWktString: string): Promise { - const regionNames = await this.getParkAndEcoreserveRegionNames(geometryWktString); - - return regionNames.map((name) => ({ regionName: name, sourceLayer: BcgwParksAndEcoreservesLayer })); - } - - /** - * Given a geometry WKT string, return an array of matching region detail objects from the BCGW Wildlife Management - * Units layer. - * - * @param {string} geometryWktString - * @return {*} {Promise} - * @memberof BcgwLayerService - */ - async getWildlifeManagementUnitRegionDetails(geometryWktString: string): Promise { - const regionNames = await this.getWildlifeManagementUnitRegionNames(geometryWktString); - - return regionNames.map((name) => ({ regionName: name, sourceLayer: BcgwWildlifeManagementUnitsLayer })); - } - - async calculateRegionsForASubmissionId(submissionId: number): Promise { - return []; - } -} diff --git a/database/src/migrations/release.0.8.0/fn_calculate_area_intersect.sql b/database/src/migrations/release.0.8.0/fn_calculate_area_intersect.sql index 5d7471f09..a86849cd4 100644 --- a/database/src/migrations/release.0.8.0/fn_calculate_area_intersect.sql +++ b/database/src/migrations/release.0.8.0/fn_calculate_area_intersect.sql @@ -10,8 +10,11 @@ -- ******************************************************************* CREATE OR REPLACE FUNCTION fn_calculate_area_intersect(geom1 geometry, geom2 geometry, tolerance float) RETURNS boolean AS $$ +DECLARE + intersection_area double precision; BEGIN - RETURN ST_Area(ST_Intersection(geom1, geom2)) / ST_Area(geom1) >= tolerance - OR ST_Area(ST_Intersection(geom1, geom2)) / ST_Area(geom2) >= tolerance; + select ST_Area(ST_Intersection(geom1, geom2)) into intersection_area; + RETURN intersection_area / ST_Area(geom1) >= tolerance + OR intersection_area / ST_Area(geom2) >= tolerance; END; $$ LANGUAGE plpgsql; \ No newline at end of file