diff --git a/api/apps/api/src/modules/clone/export/adapters/memory-export.repository.ts b/api/apps/api/src/modules/clone/export/adapters/memory-export.repository.ts index cf38d676a3..b640298469 100644 --- a/api/apps/api/src/modules/clone/export/adapters/memory-export.repository.ts +++ b/api/apps/api/src/modules/clone/export/adapters/memory-export.repository.ts @@ -28,7 +28,7 @@ export class MemoryExportRepo implements ExportRepository { async findLatestExportsFor( projectId: string, - limit: number = 5, + limit = 5, options?: { isStandalone?: boolean; isFinished?: boolean; diff --git a/api/apps/api/src/modules/clone/import/application/import-scenario.handler.spec.ts b/api/apps/api/src/modules/clone/import/application/import-scenario.handler.spec.ts index 832e6541e3..3ee191788b 100644 --- a/api/apps/api/src/modules/clone/import/application/import-scenario.handler.spec.ts +++ b/api/apps/api/src/modules/clone/import/application/import-scenario.handler.spec.ts @@ -187,7 +187,7 @@ class FakeScenarioRepository { class FakeExportRepository implements ExportRepository { public returnUnfinishedExport = false; - public importResourceId: string = ''; + public importResourceId = ''; async save(exportInstance: Export): Promise> { throw new Error('Method not implemented.'); diff --git a/api/apps/api/src/modules/geo-features/geo-feature-property-sets.service.ts b/api/apps/api/src/modules/geo-features/geo-feature-property-sets.service.ts index f60e8a5fad..76d02dd730 100644 --- a/api/apps/api/src/modules/geo-features/geo-feature-property-sets.service.ts +++ b/api/apps/api/src/modules/geo-features/geo-feature-property-sets.service.ts @@ -10,6 +10,7 @@ import { GeoFeature } from './geo-feature.api.entity'; import { GeoFeaturePropertySet } from './geo-feature.geo.entity'; import { DbConnections } from '@marxan-api/ormconfig.connections'; import { BBox } from 'geojson'; +import { antimeridianBbox } from '@marxan/utils/geo'; @Injectable() export class GeoFeaturePropertySetService { @@ -32,16 +33,25 @@ export class GeoFeaturePropertySetService { .where(`propertySets.featureId IN (:...ids)`, { ids: geoFeatureIds }); if (withinBBox) { + const { westBbox, eastBbox } = antimeridianBbox([ + withinBBox[1], + withinBBox[3], + withinBBox[0], + withinBBox[2], + ]); query.andWhere( - `st_intersects( - st_makeenvelope(:xmin, :ymin, :xmax, :ymax, 4326), - "propertySets".bbox - )`, + `(st_intersects( + st_intersection(st_makeenvelope(:...westBbox, 4326), + ST_MakeEnvelope(0, -90, 180, 90, 4326)), + "propertySets".bbox) + or + st_intersects( + st_intersection(st_makeenvelope(:...eastBbox, 4326), + ST_MakeEnvelope(-180, -90, 0, 90, 4326)), + "propertySets".bbox))`, { - xmin: withinBBox[1], - ymin: withinBBox[3], - xmax: withinBBox[0], - ymax: withinBBox[2], + westBbox: westBbox, + eastBbox: eastBbox, }, ); } diff --git a/api/apps/api/src/modules/geo-features/geo-features.service.ts b/api/apps/api/src/modules/geo-features/geo-features.service.ts index ff50fcac0f..5a433c6794 100644 --- a/api/apps/api/src/modules/geo-features/geo-features.service.ts +++ b/api/apps/api/src/modules/geo-features/geo-features.service.ts @@ -28,6 +28,7 @@ import { DbConnections } from '@marxan-api/ormconfig.connections'; import { v4 } from 'uuid'; import { UploadShapefileDTO } from '../projects/dto/upload-shapefile.dto'; import { GeoFeaturesRequestInfo } from './geo-features-request-info'; +import { antimeridianBbox, nominatim2bbox } from '@marxan/utils/geo'; const geoFeatureFilterKeyNames = [ 'featureClassName', @@ -167,20 +168,24 @@ export class GeoFeaturesService extends AppBaseService< * */ if (projectId && info?.params?.bbox) { + const { westBbox, eastBbox } = antimeridianBbox(nominatim2bbox(info.params.bbox)); const geoFeaturesWithinProjectBbox = await this.geoFeaturesGeometriesRepository .createQueryBuilder('geoFeatureGeometries') .select('"geoFeatureGeometries"."feature_id"', 'featureId') .distinctOn(['"geoFeatureGeometries"."feature_id"']) .where( - `st_intersects( - st_makeenvelope(:xmin, :ymin, :xmax, :ymax, 4326), + `(st_intersects( + st_intersection(st_makeenvelope(:...eastBbox, 4326), + ST_MakeEnvelope(0, -90, 180, 90, 4326)), "geoFeatureGeometries".the_geom - )`, + ) or st_intersects( + st_intersection(st_makeenvelope(:...westBbox, 4326), + ST_MakeEnvelope(-180, -90, 0, 90, 4326)), + "geoFeatureGeometries".the_geom + ))`, { - xmin: info.params.bbox[1], - ymin: info.params.bbox[3], - xmax: info.params.bbox[0], - ymax: info.params.bbox[2], + westBbox: westBbox, + eastBbox: eastBbox, }, ) .getRawMany() diff --git a/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-piece-as-failed.command.ts b/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-piece-as-failed.command.ts index e536ff8dab..a964f62d0d 100644 --- a/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-piece-as-failed.command.ts +++ b/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-piece-as-failed.command.ts @@ -7,7 +7,6 @@ export class MarkLegacyProjectImportPieceAsFailed extends Command { public readonly projectId: ResourceId, public readonly legacyProjectImportComponentId: LegacyProjectImportComponentId, public readonly errors: string[] = [], - public readonly warnings: string[] = [], ) { super(); } diff --git a/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-piece-as-failed.handler.ts b/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-piece-as-failed.handler.ts index 0183b5d00c..ef983feefa 100644 --- a/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-piece-as-failed.handler.ts +++ b/api/apps/api/src/modules/legacy-project-import/application/mark-legacy-project-import-piece-as-failed.handler.ts @@ -40,7 +40,6 @@ export class MarkLegacyProjectImportPieceAsFailedHandler errors, legacyProjectImportComponentId, projectId, - warnings, }: MarkLegacyProjectImportPieceAsFailed): Promise { const result = await this.legacyProjectImportRepository.transaction( async (repo) => { @@ -61,7 +60,6 @@ export class MarkLegacyProjectImportPieceAsFailedHandler const result = aggregate.markPieceAsFailed( legacyProjectImportComponentId, errors, - warnings, ); if (isLeft(result)) { diff --git a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import-component.ts b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import-component.ts index df2d8140ea..6fcbcf5a97 100644 --- a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import-component.ts +++ b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import-component.ts @@ -1,4 +1,3 @@ -import { ArchiveLocation } from '@marxan/cloning/domain'; import { LegacyProjectImportPiece, LegacyProjectImportPieceOrderResolver, @@ -49,10 +48,9 @@ export class LegacyProjectImportComponent { this.warnings.push(...warnings); } - markAsFailed(errors: string[] = [], warnings: string[] = []) { + markAsFailed(errors: string[] = []) { this.status = this.status.markAsFailed(); this.errors.push(...errors); - this.warnings.push(...warnings); } toSnapshot(): LegacyProjectImportComponentSnapshot { diff --git a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import.ts b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import.ts index 2af3cdb4b6..027ba8dadd 100644 --- a/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import.ts +++ b/api/apps/api/src/modules/legacy-project-import/domain/legacy-project-import/legacy-project-import.ts @@ -190,14 +190,13 @@ export class LegacyProjectImport extends AggregateRoot { markPieceAsFailed( pieceId: LegacyProjectImportComponentId, errors: string[] = [], - warnings: string[] = [], ): Either { const piece = this.pieces.find((pc) => pc.id.value === pieceId.value); if (!piece) return left(legacyProjectImportComponentNotFound); if (piece.hasFailed()) return left(legacyProjectImportComponentAlreadyFailed); - piece.markAsFailed(errors, warnings); + piece.markAsFailed(errors); const hasThisBatchFinished = this.hasBatchFinished(piece.order); const hasThisBatchFailed = this.hasBatchFailed(piece.order); diff --git a/api/apps/api/src/modules/legacy-project-import/infra/import-legacy-project-piece.events-handler.ts b/api/apps/api/src/modules/legacy-project-import/infra/import-legacy-project-piece.events-handler.ts index 27b47900f8..e7666ebfa9 100644 --- a/api/apps/api/src/modules/legacy-project-import/infra/import-legacy-project-piece.events-handler.ts +++ b/api/apps/api/src/modules/legacy-project-import/infra/import-legacy-project-piece.events-handler.ts @@ -99,13 +99,16 @@ export class ImportLegacyProjectPieceEventsHandler private async failed(event: EventData) { const { pieceId, projectId } = await event.data; + const result = await event.result; + const errors: string[] = []; + + if (typeof result === 'string') errors.push(result); + await this.commandBus.execute( new MarkLegacyProjectImportPieceAsFailed( new ResourceId(projectId), new LegacyProjectImportComponentId(pieceId), - // TODO Obtain actual errors and/or warnings - [], - [], + errors, ), ); } diff --git a/api/apps/api/src/modules/legacy-project-import/infra/legacy-project-import.infra.module.ts b/api/apps/api/src/modules/legacy-project-import/infra/legacy-project-import.infra.module.ts index 6f8b66724e..939cc8c239 100644 --- a/api/apps/api/src/modules/legacy-project-import/infra/legacy-project-import.infra.module.ts +++ b/api/apps/api/src/modules/legacy-project-import/infra/legacy-project-import.infra.module.ts @@ -13,6 +13,7 @@ import { import { ScheduleDbCleanupForFailedLegacyProjectImportHandler } from './schedule-db-cleanup-for-failed-legacy-project-import.handler'; import { LegacyProjectImportRepositoryModule } from './legacy-project-import.repository.module'; import { ScheduleLegacyProjectImportPieceHandler } from './schedule-legacy-project-import-piece.handler'; +import { ImportLegacyProjectPieceEventsHandler } from './import-legacy-project-piece.events-handler'; @Module({ imports: [ @@ -30,6 +31,7 @@ import { ScheduleLegacyProjectImportPieceHandler } from './schedule-legacy-proje ScheduleDbCleanupForFailedLegacyProjectImportHandler, LegacyProjectImportPieceRequestedSaga, ScheduleLegacyProjectImportPieceHandler, + ImportLegacyProjectPieceEventsHandler, { provide: Logger, useClass: Logger, diff --git a/api/apps/api/src/modules/queue-api-events/adapter.ts b/api/apps/api/src/modules/queue-api-events/adapter.ts index ad03385d82..eb5d7ebfdc 100644 --- a/api/apps/api/src/modules/queue-api-events/adapter.ts +++ b/api/apps/api/src/modules/queue-api-events/adapter.ts @@ -50,8 +50,8 @@ export class QueueEventsAdapter< queueEvents.on(`completed`, ({ jobId }, eventId) => this.handleFinished(jobId, eventId), ); - queueEvents.on(`failed`, ({ jobId }, eventId) => - this.handleFailed(jobId, eventId), + queueEvents.on(`failed`, ({ jobId, failedReason }, eventId) => + this.handleFailed(jobId, eventId, failedReason), ); } @@ -84,7 +84,11 @@ export class QueueEventsAdapter< }); } - private async handleFailed(jobId: string, eventId: string) { + private async handleFailed( + jobId: string, + eventId: string, + failedReason: string, + ) { const lazyDataGetter = this.createLazyDataGetter(); const eventDto = await this.eventFactory.createFailedEvent({ jobId, @@ -107,7 +111,7 @@ export class QueueEventsAdapter< return lazyDataGetter(jobId); }, get result() { - return Promise.resolve(undefined); + return Promise.resolve(failedReason); }, }); } diff --git a/api/apps/api/src/utils/json.type.ts b/api/apps/api/src/utils/json.type.ts index f053b32aac..b1a68ab31d 100644 --- a/api/apps/api/src/utils/json.type.ts +++ b/api/apps/api/src/utils/json.type.ts @@ -4,4 +4,4 @@ export interface JSONObject { [x: string]: JSONValue; } -export interface JSONArray extends Array {} +export type JSONArray = Array; diff --git a/api/apps/api/test/fixtures/test-init-apidb.sql b/api/apps/api/test/fixtures/test-init-apidb.sql index 2ad36ab144..f56e5f1aab 100644 --- a/api/apps/api/test/fixtures/test-init-apidb.sql +++ b/api/apps/api/test/fixtures/test-init-apidb.sql @@ -5,3 +5,8 @@ VALUES ('bb@example.com', 'b', 'b', 'User B B', true, false, crypt('bbuserpassword', gen_salt('bf'))), ('cc@example.com', 'c', 'c', 'User C C', true, false, crypt('ccuserpassword', gen_salt('bf'))), ('dd@example.com', 'd', 'd', 'User D D', true, false, crypt('dduserpassword', gen_salt('bf'))); + +INSERT INTO organizations (name, created_by) +VALUES + ('Example Org 1', (SELECT id FROM users WHERE email = 'aa@example.com')), + ('Example Org 2', (SELECT id FROM users WHERE email = 'aa@example.com')); diff --git a/api/apps/geoprocessing/src/migrations/geoprocessing/1653565019335-ModifyBboxCalculationTakingAntimeridian.ts b/api/apps/geoprocessing/src/migrations/geoprocessing/1653565019335-ModifyBboxCalculationTakingAntimeridian.ts new file mode 100644 index 0000000000..20cb17820d --- /dev/null +++ b/api/apps/geoprocessing/src/migrations/geoprocessing/1653565019335-ModifyBboxCalculationTakingAntimeridian.ts @@ -0,0 +1,72 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ModifyBboxCalculationTakingAntimeridian1653565019335 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE OR REPLACE FUNCTION mx_bbox2json(geom geometry) + returns jsonb + language plpgsql + as + $function$ + DECLARE + -- variable declaration + both_hemispheres RECORD; + BEGIN + -- logic https://github.com/mapbox/carmen/blob/03fac2d7397ecdfcb4f0828fcfd9d8a54c845f21/lib/util/bbox.js#L59 + -- json form of the bbox should be in Nominatim bbox [xmin, xmax, ymin, ymax] [W, E, S, N]. + execute 'with region as (select st_intersection($1, geom) as the_geom, + st_intersects($1, geom) intersects, pos + from (values (ST_MakeEnvelope(-180, -90, 0, 90, 4326), ''west''), + (ST_MakeEnvelope(0, -90, 180, 90, 4326), ''east'')) as t(geom, pos)), + data as (select ST_XMax(the_geom), ST_XMin(the_geom), + ST_YMax(the_geom),ST_YMin(the_geom), pos, intersects, + ST_XMax(the_geom) + ABS(lag(ST_XMin(the_geom), 1) OVER ()) > + (180 - ST_XMin(the_geom)) + (180 - ABS(lag(ST_XMax(the_geom), 1) OVER ())) as pm_am + from region) + select bool_and(intersects) and bool_and(pm_am) result, + jsonb_build_array(max(st_xmax), min(st_xmin), max(st_ymax), min(st_ymin)) if_false, + jsonb_build_array(min(st_xmax), max(st_xmin), max(st_ymax), min(st_ymin))if_true from data;' + into both_hemispheres + using geom; + if both_hemispheres.result then + return both_hemispheres.if_true; + else + return both_hemispheres.if_false; + end if; + end; + $function$; + + CREATE OR REPLACE FUNCTION public.tr_getbbox() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + BEGIN + NEW.bbox := mx_bbox2json(NEW.the_geom); + RETURN NEW; + END; + $function$; + + UPDATE admin_regions SET id = id; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE OR REPLACE FUNCTION public.tr_getbbox() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + BEGIN + NEW.bbox := jsonb_build_array(ST_XMax(NEW.the_geom), ST_XMin(NEW.the_geom), + ST_YMax(NEW.the_geom), ST_YMin(NEW.the_geom)); + RETURN NEW; + END; + $function$; + + Drop FUNCTION mx_bbox2json(geom geometry); + + UPDATE admin_regions SET id = id; + `); + } +} diff --git a/api/apps/geoprocessing/src/modules/admin-areas/admin-areas.service.ts b/api/apps/geoprocessing/src/modules/admin-areas/admin-areas.service.ts index 9f8c3eee91..b900e77a27 100644 --- a/api/apps/geoprocessing/src/modules/admin-areas/admin-areas.service.ts +++ b/api/apps/geoprocessing/src/modules/admin-areas/admin-areas.service.ts @@ -15,7 +15,7 @@ import { import { Transform } from 'class-transformer'; import { BBox } from 'geojson'; import { AdminArea } from '@marxan/admin-regions'; -import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils'; +import { nominatim2bbox } from '@marxan/utils/geo'; import { TileRequest } from '@marxan/tiles'; export class TileSpecification extends TileRequest { diff --git a/api/apps/geoprocessing/src/modules/features/features.service.ts b/api/apps/geoprocessing/src/modules/features/features.service.ts index cdea6f09dc..b6939d2d1a 100644 --- a/api/apps/geoprocessing/src/modules/features/features.service.ts +++ b/api/apps/geoprocessing/src/modules/features/features.service.ts @@ -7,7 +7,7 @@ import { IsArray, IsNumber, IsString, IsOptional } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { BBox } from 'geojson'; -import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils'; +import { antimeridianBbox, nominatim2bbox } from '@marxan/utils/geo'; import { TileRequest } from '@marxan/tiles'; @@ -49,15 +49,24 @@ export class FeatureService { let whereQuery = `feature_id = '${id}'`; if (bbox) { - whereQuery += `AND st_intersects(ST_MakeEnvelope(${nominatim2bbox( - bbox, - )}, 4326), the_geom)`; + const { westBbox, eastBbox } = antimeridianBbox(nominatim2bbox(bbox)); + whereQuery += `AND + (st_intersects( + st_intersection(st_makeenvelope(${eastBbox}, 4326), + ST_MakeEnvelope(0, -90, 180, 90, 4326)), + the_geom + ) or st_intersects( + st_intersection(st_makeenvelope(${westBbox}, 4326), + ST_MakeEnvelope(-180, -90, 0, 90, 4326)), + the_geom + ))`; } return whereQuery; } /** * @todo get attributes from Entity, based on user selection + * @todo simplification level based on zoom level */ public findTile( tileSpecification: TileSpecification, @@ -65,7 +74,7 @@ export class FeatureService { ): Promise { const { z, x, y, id } = tileSpecification; const attributes = 'feature_id, properties'; - const table = `(select (st_dump(the_geom)).geom as the_geom, properties, feature_id from "${this.featuresRepository.metadata.tableName}")`; + const table = `(select ST_RemoveRepeatedPoints((st_dump(the_geom)).geom, 0.1) as the_geom, properties, feature_id from "${this.featuresRepository.metadata.tableName}")`; const customQuery = this.buildFeaturesWhereQuery(id, bbox); return this.tileService.getTile({ z, @@ -76,4 +85,5 @@ export class FeatureService { attributes, }); } + } diff --git a/api/apps/geoprocessing/src/modules/planning-area/planning-area-tiles/planning-area-tiles.service.ts b/api/apps/geoprocessing/src/modules/planning-area/planning-area-tiles/planning-area-tiles.service.ts index b86868c169..07f5a8270c 100644 --- a/api/apps/geoprocessing/src/modules/planning-area/planning-area-tiles/planning-area-tiles.service.ts +++ b/api/apps/geoprocessing/src/modules/planning-area/planning-area-tiles/planning-area-tiles.service.ts @@ -27,7 +27,7 @@ export class PlanningAreaTilesService { /** * @todo this generation query is a bit... */ - let whereQuery = `project_id = '${planningAreaId}'`; + const whereQuery = `project_id = '${planningAreaId}'`; return whereQuery; } diff --git a/api/apps/geoprocessing/src/modules/planning-units/planning-units.job.ts b/api/apps/geoprocessing/src/modules/planning-units/planning-units.job.ts index ae89b36c28..161f2e56ae 100644 --- a/api/apps/geoprocessing/src/modules/planning-units/planning-units.job.ts +++ b/api/apps/geoprocessing/src/modules/planning-units/planning-units.job.ts @@ -115,11 +115,16 @@ export class PlanningUnitsJobProcessor { SELECT ST_Transform(the_geom, 3410) as geom FROM planning_areas WHERE id = '${data.planningAreaId}' - ), grid AS ( - SELECT (${gridFn}(${size}, geom)).* - FROM region + ), + bboxes as (select * from (values (st_transform(ST_MakeEnvelope(-180, -90, 0, 90, 4326), 3410), 'west'), + (st_transform(ST_MakeEnvelope(0, -90, 180, 90, 4326),3410), 'east')) as t(geom, pos)), + grid AS ( + SELECT ST_ClipByBox2D((${gridFn}(${size}, st_intersection(region.geom, bboxes.geom))).geom, + st_transform(ST_MakeEnvelope(-180, -90, 180, 90, 4326), 3410)) as geom + FROM region, bboxes ) - SELECT grid.geom +SELECT distinct +grid.geom FROM grid, region WHERE ST_Intersects(grid.geom, region.geom) `; @@ -145,11 +150,16 @@ export class PlanningUnitsJobProcessor { SELECT ST_Transform(the_geom, 3410) as geom FROM admin_regions WHERE ${whereConditions.join(' AND ')} - ), grid AS ( - SELECT (${gridFn}(${size}, geom)).* - FROM region + ), + bboxes as (select * from (values (st_transform(ST_MakeEnvelope(-180, -90, 0, 90, 4326), 3410), 'west'), + (st_transform(ST_MakeEnvelope(0, -90, 180, 90, 4326),3410), 'east')) as t(geom, pos)), + grid AS ( + SELECT ST_ClipByBox2D((${gridFn}(${size}, st_intersection(region.geom, bboxes.geom))).geom, + st_transform(ST_MakeEnvelope(-180, -90, 180, 90, 4326), 3410)) as geom + FROM region, bboxes ) - SELECT grid.geom + SELECT distinct + grid.geom FROM grid, region WHERE ST_Intersects(grid.geom, region.geom) `; @@ -196,7 +206,7 @@ export class PlanningUnitsJobProcessor { const geometries: { id: string }[] = await em.query( ` INSERT INTO planning_units_geom (the_geom, type, size) - SELECT st_transform(geom, 4326) AS the_geom, $1::planning_unit_grid_shape AS type, $2 AS size + SELECT ST_MakeValid(st_transform(geom, 4326)) AS the_geom, $1::planning_unit_grid_shape AS type, $2 AS size FROM (${subquery}) grid ON CONFLICT (the_geom_hash, type) DO UPDATE SET type = $1::planning_unit_grid_shape RETURNING id diff --git a/api/apps/geoprocessing/src/modules/planning-units/planning-units.service.ts b/api/apps/geoprocessing/src/modules/planning-units/planning-units.service.ts index 4a801950a6..e460240fe9 100644 --- a/api/apps/geoprocessing/src/modules/planning-units/planning-units.service.ts +++ b/api/apps/geoprocessing/src/modules/planning-units/planning-units.service.ts @@ -1,11 +1,11 @@ import { TileService } from '@marxan-geoprocessing/modules/tile/tile.service'; -import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils'; +import { nominatim2bbox, antimeridianBbox } from '@marxan/utils/geo'; import { PlanningUnitGridShape } from '@marxan/scenarios-planning-unit'; import { TileRequest } from '@marxan/tiles'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsArray, IsIn, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsArray, IsIn, IsNumber, IsOptional } from 'class-validator'; import { BBox } from 'geojson'; import { calculateGridSize, @@ -110,6 +110,7 @@ export class PlanningUnitsService { * Because we want to reduce the overhead for the db if an uncontroled area requests * a large area. * If so the shape we are getting is down the optimal to visualize it as points + * */ const query = ratioPixelExtent < 8 && !filters?.bbox @@ -122,14 +123,20 @@ export class PlanningUnitsService { } /** * @param filters including only bounding box of the area where the grids would be generated + * */ private buildPlanningUnitsWhereQuery(filters?: PlanningUnitsFilters): string { let whereQuery = ``; if (filters?.bbox) { - whereQuery = `st_intersects(ST_Transform(ST_MakeEnvelope(${nominatim2bbox( - filters.bbox, - )}, 4326), 3857) ,the_geom)`; + const { westBbox, eastBbox } = antimeridianBbox( + nominatim2bbox(filters.bbox), + ); + whereQuery = `st_intersects(ST_Transform(st_intersection(ST_MakeEnvelope(${eastBbox}, 4326), + ST_MakeEnvelope(0, -90, 180, 90, 4326)), 3857), the_geom) + OR + st_intersects(ST_Transform(st_intersection(ST_MakeEnvelope(${westBbox}, 4326), + ST_MakeEnvelope(-180, -90, 0, 90, 4326)), 3857), the_geom)`; } return whereQuery; } diff --git a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas-tiles.service.ts b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas-tiles.service.ts index da02e94f3c..0c0f9b6bc9 100644 --- a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas-tiles.service.ts +++ b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas-tiles.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Brackets, Repository } from 'typeorm'; -import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils'; +import { nominatim2bbox, antimeridianBbox } from '@marxan/utils/geo'; import { TileService } from '@marxan-geoprocessing/modules/tile/tile.service'; import { ProtectedArea } from '@marxan/protected-areas'; @@ -73,10 +73,14 @@ export class ProtectedAreasTilesService { } if (bbox) { + const { westBbox, eastBbox } = antimeridianBbox(nominatim2bbox(bbox)); subQuery.andWhere( - `st_intersects(ST_MakeEnvelope(:...bbox, 4326), the_geom)`, + `(st_intersects(ST_MakeEnvelope(:...westBbox, 4326), the_geom) + or + st_intersects(ST_MakeEnvelope(:...eastBbox, 4326), the_geom))`, { - bbox: nominatim2bbox(bbox), + westBbox: westBbox, + eastBbox: eastBbox, }, ); } diff --git a/api/apps/geoprocessing/src/modules/scenarios/comparison-difference-tile/comparison-difference-tile.service.ts b/api/apps/geoprocessing/src/modules/scenarios/comparison-difference-tile/comparison-difference-tile.service.ts index 4c4dc4b28a..169c7ae2d4 100644 --- a/api/apps/geoprocessing/src/modules/scenarios/comparison-difference-tile/comparison-difference-tile.service.ts +++ b/api/apps/geoprocessing/src/modules/scenarios/comparison-difference-tile/comparison-difference-tile.service.ts @@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils'; import { TileService } from '@marxan-geoprocessing/modules/tile/tile.service'; import { ScenariosPuPaDataGeo } from '@marxan/scenarios-planning-unit'; diff --git a/api/apps/geoprocessing/src/utils/bbox.utils.ts b/api/apps/geoprocessing/src/utils/bbox.utils.ts deleted file mode 100644 index d95e062f1a..0000000000 --- a/api/apps/geoprocessing/src/utils/bbox.utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BBox } from 'geojson'; -/** - * Utility functions related to lower-level interaction with bbox operations. - * - * @debt This should be moved to a self-standing - */ - -/** - * conversion operation between bbox [xmin, ymin, xmax, ymax] - * to Nominatim bbox [xmin, xmax, ymin, ymax]. - * - */ -export function bbox2Nominatim(bbox: BBox): BBox { - return [bbox[0], bbox[2], bbox[1], bbox[3]]; -} -export function nominatim2bbox(nominatim: BBox): BBox { - return [nominatim[0], nominatim[2], nominatim[1], nominatim[3]]; -} diff --git a/api/apps/geoprocessing/test/integration/cloning/piece-exporters/export-config.project-piece-exporter.e2e-spec.ts b/api/apps/geoprocessing/test/integration/cloning/piece-exporters/export-config.project-piece-exporter.e2e-spec.ts index 89cd488328..69523c6606 100644 --- a/api/apps/geoprocessing/test/integration/cloning/piece-exporters/export-config.project-piece-exporter.e2e-spec.ts +++ b/api/apps/geoprocessing/test/integration/cloning/piece-exporters/export-config.project-piece-exporter.e2e-spec.ts @@ -96,7 +96,7 @@ const getFixtures = async () => { const getExpectedContent = ( options: FixtureOptions, ): ProjectExportConfigContent => { - let scenarios: Record = {}; + const scenarios: Record = {}; if (options.projectWithScenario) scenarios[scenarioId] = [ClonePiece.ScenarioMetadata]; return { diff --git a/api/apps/geoprocessing/test/integration/cloning/piece-exporters/scenario-protected-areas.piece-exporter.e2e-spec.ts b/api/apps/geoprocessing/test/integration/cloning/piece-exporters/scenario-protected-areas.piece-exporter.e2e-spec.ts index 57ba0b3a26..f1dbd1e2b3 100644 --- a/api/apps/geoprocessing/test/integration/cloning/piece-exporters/scenario-protected-areas.piece-exporter.e2e-spec.ts +++ b/api/apps/geoprocessing/test/integration/cloning/piece-exporters/scenario-protected-areas.piece-exporter.e2e-spec.ts @@ -85,8 +85,8 @@ const getFixtures = async () => { const projectId = v4(); const scenarioId = v4(); let customProtectedAreaId: string = v4(); - let commonProtectedAreasWdpaids: number[] = []; - let commonProtectedAreasIds: string[] = []; + const commonProtectedAreasWdpaids: number[] = []; + const commonProtectedAreasIds: string[] = []; const organizationId = v4(); const sut = sandbox.get(ScenarioProtectedAreasPieceExporter); const apiEntityManager: EntityManager = sandbox.get( diff --git a/api/libs/utils/src/geo/bbox.ts b/api/libs/utils/src/geo/bbox.ts new file mode 100644 index 0000000000..3ac95c6fc8 --- /dev/null +++ b/api/libs/utils/src/geo/bbox.ts @@ -0,0 +1,38 @@ +import { BBox } from 'geojson'; +/** + * Utility functions related to lower-level interaction with bbox operations. + * + * @debt This should be moved to a self-standing + * @debt there is also a mismatch in terms of the bbox creation [xmax,xmin,ymax,ymin] vs [xmin,xmax,ymin,ymax] + */ + +/** + * conversion operation between bbox [xmin, ymin, xmax, ymax] + * to Nominatim bbox [xmin, xmax, ymin, ymax]. + * + */ +export function bbox2Nominatim(bbox: BBox): BBox { + return [bbox[0], bbox[2], bbox[1], bbox[3]]; +} +export function nominatim2bbox(nominatim: BBox): BBox { + return [nominatim[0], nominatim[2], nominatim[1], nominatim[3]]; +} + +/** + * Check of antimeridian and division of bbox + * to Nominatim bbox [xmin, xmax, ymin, ymax]. + * @param bbox + * @returns {BBox, BBox} west and east bbox split + * + */ +export function antimeridianBbox( + bbox: BBox, +): { westBbox: BBox; eastBbox: BBox } { + if (bbox[2] > bbox[0]) { + return { + westBbox: [bbox[0], bbox[1], bbox[2] - 360, bbox[3]], + eastBbox: [bbox[0] + 360, bbox[1], bbox[2], bbox[3]], + }; + } + return { westBbox: bbox, eastBbox: bbox }; +} diff --git a/api/libs/utils/src/geo/index.ts b/api/libs/utils/src/geo/index.ts index e98f71dfa1..a2e8c31b45 100644 --- a/api/libs/utils/src/geo/index.ts +++ b/api/libs/utils/src/geo/index.ts @@ -1,3 +1,4 @@ export { defaultSrid } from './spatial-data-format'; export { isFeatureCollection } from './is-feature-collection'; export { decodeMvt } from './decode-mvt'; +export { bbox2Nominatim, nominatim2bbox, antimeridianBbox } from './bbox';