From 7db80eaef53bd4c730884c243cad6a52ba89b9b4 Mon Sep 17 00:00:00 2001 From: kgajowy Date: Tue, 7 Sep 2021 13:59:19 +0200 Subject: [PATCH] feat(custom-pu-grid): custom pu grid geometry --- .../scenario-planning-units-linker-service.ts | 3 - api/apps/api/test/jest-e2e.json | 4 +- api/apps/geoprocessing/src/app.module.ts | 2 + ...080265630-PuGeometryUniqueWithinProject.ts | 26 ++++++ .../grid-geojson-validator.spec.ts | 81 +++++++++++++++++ .../grid-geojson-validator.ts | 45 ++++++++++ .../src/modules/planning-units-grid/index.ts | 1 + .../planning-units-grid.module.ts | 13 +++ .../planning-units-grid.processor.ts | 76 ++++++++++++++++ .../planning-unit-grid/nam-shapefile.zip | Bin 0 -> 1526 bytes .../planning-unit-grid/nam.geojson | 1 + .../planning-units-grid.e2e-spec.ts | 18 ++++ .../planning-units-grid.fixtures.ts | 85 ++++++++++++++++++ api/apps/geoprocessing/test/jest-e2e.json | 4 +- api/libs/planning-units-grid/src/index.ts | 4 + api/libs/planning-units-grid/src/job-input.ts | 20 +++++ .../planning-units-grid/src/job-output.ts | 17 ++++ .../planning-units-grid/src/queue-name.ts | 1 + .../planning-units-grid/tsconfig.lib.json | 9 ++ .../src/shapefiles/shapefiles.service.ts | 11 ++- api/nest-cli.json | 9 ++ api/package.json | 4 +- api/tsconfig.json | 6 ++ 23 files changed, 432 insertions(+), 8 deletions(-) create mode 100644 api/apps/geoprocessing/src/migrations/geoprocessing/1631080265630-PuGeometryUniqueWithinProject.ts create mode 100644 api/apps/geoprocessing/src/modules/planning-units-grid/grid-geojson-validator.spec.ts create mode 100644 api/apps/geoprocessing/src/modules/planning-units-grid/grid-geojson-validator.ts create mode 100644 api/apps/geoprocessing/src/modules/planning-units-grid/index.ts create mode 100644 api/apps/geoprocessing/src/modules/planning-units-grid/planning-units-grid.module.ts create mode 100644 api/apps/geoprocessing/src/modules/planning-units-grid/planning-units-grid.processor.ts create mode 100644 api/apps/geoprocessing/test/integration/planning-unit-grid/nam-shapefile.zip create mode 100644 api/apps/geoprocessing/test/integration/planning-unit-grid/nam.geojson create mode 100644 api/apps/geoprocessing/test/integration/planning-unit-grid/planning-units-grid.e2e-spec.ts create mode 100644 api/apps/geoprocessing/test/integration/planning-unit-grid/planning-units-grid.fixtures.ts create mode 100644 api/libs/planning-units-grid/src/index.ts create mode 100644 api/libs/planning-units-grid/src/job-input.ts create mode 100644 api/libs/planning-units-grid/src/job-output.ts create mode 100644 api/libs/planning-units-grid/src/queue-name.ts create mode 100644 api/libs/planning-units-grid/tsconfig.lib.json diff --git a/api/apps/api/src/modules/scenarios/planning-units/scenario-planning-units-linker-service.ts b/api/apps/api/src/modules/scenarios/planning-units/scenario-planning-units-linker-service.ts index 10c66b803b..10e16cbe3a 100644 --- a/api/apps/api/src/modules/scenarios/planning-units/scenario-planning-units-linker-service.ts +++ b/api/apps/api/src/modules/scenarios/planning-units/scenario-planning-units-linker-service.ts @@ -81,9 +81,6 @@ export class ScenarioPlanningUnitsLinkerService { project: Project, ): QueryPartsForLinker | undefined { /** - * @TODO selection by planning_units_geom.project_id needs project_id column - * to be added and set. - * * We still intersect in case planning unit grid is not fully included in * the planning area. */ diff --git a/api/apps/api/test/jest-e2e.json b/api/apps/api/test/jest-e2e.json index bc2a265262..c849643c6e 100644 --- a/api/apps/api/test/jest-e2e.json +++ b/api/apps/api/test/jest-e2e.json @@ -66,6 +66,8 @@ "@marxan/iucn/(.*)": "/../../../libs/iucn/src/$1", "@marxan/iucn": "/../../../libs/iucn/src", "@marxan/shapefile-converter/(.*)": "/../../../libs/shapefile-converter/src/$1", - "@marxan/shapefile-converter": "/../../../libs/shapefile-converter/src" + "@marxan/shapefile-converter": "/../../../libs/shapefile-converter/src", + "@marxan/planning-units-grid/(.*)": "/../../../libs/planning-units-grid/src/$1", + "@marxan/planning-units-grid": "/../../../libs/planning-units-grid/src" } } diff --git a/api/apps/geoprocessing/src/app.module.ts b/api/apps/geoprocessing/src/app.module.ts index a84361c8ba..ef0e4af495 100644 --- a/api/apps/geoprocessing/src/app.module.ts +++ b/api/apps/geoprocessing/src/app.module.ts @@ -16,6 +16,7 @@ import { PlanningAreaModule } from '@marxan-geoprocessing/modules/planning-area/ import { MarxanSandboxedRunnerModule } from '@marxan-geoprocessing/marxan-sandboxed-runner/marxan-sandboxed-runner.module'; import { ScenariosModule } from '@marxan-geoprocessing/modules/scenarios/scenarios.module'; import { ScenarioProtectedAreaCalculationModule } from '@marxan-geoprocessing/modules/scenario-protected-area-calculation/scenario-protected-area-calculation.module'; +import { PlanningUnitsGridModule } from '@marxan-geoprocessing/modules/planning-units-grid'; @Module({ imports: [ @@ -40,6 +41,7 @@ import { ScenarioProtectedAreaCalculationModule } from '@marxan-geoprocessing/mo PlanningAreaModule, MarxanSandboxedRunnerModule, ScenariosModule, + PlanningUnitsGridModule, ], controllers: [AppController], providers: [AppService], diff --git a/api/apps/geoprocessing/src/migrations/geoprocessing/1631080265630-PuGeometryUniqueWithinProject.ts b/api/apps/geoprocessing/src/migrations/geoprocessing/1631080265630-PuGeometryUniqueWithinProject.ts new file mode 100644 index 0000000000..e632a1091b --- /dev/null +++ b/api/apps/geoprocessing/src/migrations/geoprocessing/1631080265630-PuGeometryUniqueWithinProject.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class PuGeometryUniqueWithinProject1631080265630 + implements MigrationInterface { + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + alter table planning_units_geom + drop constraint planning_units_geom_the_geom_type_key; + + alter table planning_units_geom + add constraint planning_units_geom_the_geom_type_key + unique (the_geom, type, project_id); + `); + } + + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + alter table planning_units_geom + drop constraint planning_units_geom_the_geom_type_key; + + alter table planning_units_geom + add constraint planning_units_geom_the_geom_type_key + unique (the_geom, type); + `); + } +} diff --git a/api/apps/geoprocessing/src/modules/planning-units-grid/grid-geojson-validator.spec.ts b/api/apps/geoprocessing/src/modules/planning-units-grid/grid-geojson-validator.spec.ts new file mode 100644 index 0000000000..7bdc6d9829 --- /dev/null +++ b/api/apps/geoprocessing/src/modules/planning-units-grid/grid-geojson-validator.spec.ts @@ -0,0 +1,81 @@ +import { Test } from '@nestjs/testing'; +import { FeatureCollection, Geometry } from 'geojson'; +import { + GridGeoJsonValidator, + invalidFeatureGeometry, + notFeatureCollections, +} from './grid-geojson-validator'; + +let sut: GridGeoJsonValidator; + +beforeEach(async () => { + const sandbox = await Test.createTestingModule({ + providers: [GridGeoJsonValidator], + }).compile(); + sut = sandbox.get(GridGeoJsonValidator); +}); + +test(`providing non feature collection`, () => { + expect(sut.validate(nonFeatureCollection())).toEqual({ + _tag: 'Left', + left: notFeatureCollections, + }); +}); + +test(`providing non polygons within collection`, () => { + expect(sut.validate(featureCollectionWithLine())).toEqual({ + _tag: 'Left', + left: invalidFeatureGeometry, + }); +}); + +test(`providing feature collection with polygons only`, () => { + expect(sut.validate(featureCollectionWithPolygons())).toEqual({ + _tag: 'Right', + right: featureCollectionWithPolygons(), + }); +}); + +const nonFeatureCollection = (): Geometry => ({ + type: 'Point', + bbox: [0, 0, 0, 0, 0, 0], + coordinates: [], +}); + +const featureCollectionWithLine = (): FeatureCollection => ({ + type: 'FeatureCollection', + bbox: [0, 0, 0, 0, 0, 0], + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [], + }, + }, + { + type: 'Feature', + properties: {}, + geometry: { + type: 'MultiPolygon', + coordinates: [], + }, + }, + ], +}); + +const featureCollectionWithPolygons = (): FeatureCollection => ({ + type: 'FeatureCollection', + bbox: [0, 0, 0, 0, 0, 0], + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [], + }, + }, + ], +}); diff --git a/api/apps/geoprocessing/src/modules/planning-units-grid/grid-geojson-validator.ts b/api/apps/geoprocessing/src/modules/planning-units-grid/grid-geojson-validator.ts new file mode 100644 index 0000000000..4da772b6bf --- /dev/null +++ b/api/apps/geoprocessing/src/modules/planning-units-grid/grid-geojson-validator.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { + FeatureCollection, + GeoJSON, + GeoJsonProperties, + Geometry, + Polygon, +} from 'geojson'; +import { Either, left, right } from 'fp-ts/Either'; + +export const notFeatureCollections = Symbol(`not a feature collections`); +export const invalidFeatureGeometry = Symbol(`can only contain polygons`); + +export type ValidationError = + | typeof notFeatureCollections + | typeof invalidFeatureGeometry; + +@Injectable() +export class GridGeoJsonValidator { + validate( + geo: GeoJSON, + ): Either> { + if (!this.isFeatureCollection(geo)) { + return left(notFeatureCollections); + } + + if (!this.hasOnlyPolygons(geo)) { + return left(invalidFeatureGeometry); + } + + return right(geo); + } + + private isFeatureCollection(geo: GeoJSON): geo is FeatureCollection { + return geo.type === 'FeatureCollection'; + } + + private hasOnlyPolygons( + geoJson: FeatureCollection, + ): geoJson is FeatureCollection { + return geoJson.features.every( + (feature) => feature.geometry.type === 'Polygon', + ); + } +} diff --git a/api/apps/geoprocessing/src/modules/planning-units-grid/index.ts b/api/apps/geoprocessing/src/modules/planning-units-grid/index.ts new file mode 100644 index 0000000000..98a0782a22 --- /dev/null +++ b/api/apps/geoprocessing/src/modules/planning-units-grid/index.ts @@ -0,0 +1 @@ +export { PlanningUnitsGridModule } from './planning-units-grid.module'; diff --git a/api/apps/geoprocessing/src/modules/planning-units-grid/planning-units-grid.module.ts b/api/apps/geoprocessing/src/modules/planning-units-grid/planning-units-grid.module.ts new file mode 100644 index 0000000000..62d277ffcc --- /dev/null +++ b/api/apps/geoprocessing/src/modules/planning-units-grid/planning-units-grid.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { WorkerModule } from '@marxan-geoprocessing/modules/worker'; +import { ShapefilesModule } from '@marxan/shapefile-converter'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { PlanningUnitsGridProcessor } from './planning-units-grid.processor'; +import { GridGeoJsonValidator } from './grid-geojson-validator'; + +@Module({ + imports: [WorkerModule, ShapefilesModule, TypeOrmModule.forFeature([])], + providers: [PlanningUnitsGridProcessor, GridGeoJsonValidator], +}) +export class PlanningUnitsGridModule {} diff --git a/api/apps/geoprocessing/src/modules/planning-units-grid/planning-units-grid.processor.ts b/api/apps/geoprocessing/src/modules/planning-units-grid/planning-units-grid.processor.ts new file mode 100644 index 0000000000..0c27ab58fb --- /dev/null +++ b/api/apps/geoprocessing/src/modules/planning-units-grid/planning-units-grid.processor.ts @@ -0,0 +1,76 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Job, Worker } from 'bullmq'; +import { plainToClass } from 'class-transformer'; +import { validateSync } from 'class-validator'; +import { InjectEntityManager } from '@nestjs/typeorm'; +import { EntityManager } from 'typeorm'; + +import { JobInput, JobOutput, queueName } from '@marxan/planning-units-grid'; +import { ShapefileService } from '@marxan/shapefile-converter'; +import { + WorkerBuilder, + WorkerProcessor, +} from '@marxan-geoprocessing/modules/worker'; + +import { ShapeType } from '@marxan-jobs/planning-unit-geometry'; +import { GridGeoJsonValidator } from './grid-geojson-validator'; +import { isLeft } from 'fp-ts/Either'; + +@Injectable() +export class PlanningUnitsGridProcessor + implements WorkerProcessor { + private readonly worker: Worker; + private readonly logger = new Logger(this.constructor.name); + + constructor( + @InjectEntityManager() private readonly entityManager: EntityManager, + private readonly shapefileConverter: ShapefileService, + private readonly gridGeoJsonValidator: GridGeoJsonValidator, + workerBuilder: WorkerBuilder, + ) { + this.worker = workerBuilder.build(queueName, this); + } + + async process({ + data: { projectId, shapefile }, + }: Job): Promise { + const { data: geoJson } = await this.shapefileConverter.transformToGeoJson( + shapefile, + ); + + const result = this.gridGeoJsonValidator.validate(geoJson); + if (isLeft(result)) { + throw new Error(result.left.toString()); + } + const puGeometriesIds: { id: string }[] = await this.entityManager.query( + ` + INSERT INTO "planning_units_geom"("the_geom", "type", "project_id") + SELECT ST_SetSRID( + ST_GeomFromGeoJSON(features ->> 'geometry'), + 4326)::geometry, + $2, + $3 + FROM ( + SELECT json_array_elements($1::json -> 'features') AS features + ) AS f + ON CONFLICT ON CONSTRAINT planning_units_geom_the_geom_type_key DO UPDATE SET type = 'irregular' + RETURNING "id" + `, + [result.right, ShapeType.Irregular, projectId], + ); + const output = plainToClass(JobOutput, { + geometryIds: puGeometriesIds.map((row) => row.id), + projectId, + }); + + const errors = validateSync(output); + + if (errors.length > 0) { + const errorMessage = errors.map((e) => e.toString()).join('. '); + this.logger.warn(`Invalid job output: ${errorMessage}`); + throw new Error(errorMessage); + } + + return output; + } +} diff --git a/api/apps/geoprocessing/test/integration/planning-unit-grid/nam-shapefile.zip b/api/apps/geoprocessing/test/integration/planning-unit-grid/nam-shapefile.zip new file mode 100644 index 0000000000000000000000000000000000000000..7c6544bd86515918ecbd3733bb49276768eb0ed5 GIT binary patch literal 1526 zcmWIWW@h1HW&i>cZ}s2+n?^oXARC1FfjBp@K(`=2r!qZ1PcOM3Jv78k*CGI@LjZfhwnA(UFprmV0tH=K%$VmWRzELX&hRoD!`0-o#<{?(w%IMY%`uGV*t> z<|#6`?6H{YJ!?(!ytn_Kcuh3DIYIa4L|e;0vZyvaJ{Xu)3ACjji%kVZS-xis`41@w zus-;I$Y)lO!$UV^7RjvHmyR{YSV=6>C@BqOn)@x+)}Q-ztc{wyCAK2 zIP>l?twW8cYC2C(aTcwY-tyVYn)8MA20n+|j>0D^PbNR9{k7%WzkTog6{{!RHeLVc zrra8y*!6QZN(roSGKtZ-$dBquVS6|I%}fjoIlv%955eM$g4U2+58*_a{h9lw=&#hw z+BYqK#_Zd9{h60P#79a+dIm0G&2sVM+$5;kmf9xf(WY57O#o2q1L%Z5Hqsm)1? zjTSHRRIPh?S4_G1?%SQu^KP@+70B29sn7p^ck|*EYeKKo_uqf9$KI9ogT~fyzN4(n z>EZ93c-pfUeR}XIWy#}WfwKXp?FJJI$ljhCGP2S1;W)Ae;BeqF5Ixi%7ebQo?S6s}C>UR~a-zLOblEN~rE!uEh zrCR&4yXMk|7?XeVvz{z_E$^yzP-dR7apm0S_rsj30w228YU`G`&SkY-S2V|8^y#6b z+UBKkr=NNM{VEjPB(kGc`rxbMdAEWB_Me%mOI<@2Y+bs|NF3O9ZarW}lv%#K@cmx@ zkO}tYcUI>gx;sNr^rhC_=v(LK*Pc4@du>6+o_Ae;`I}~5Jg_@`n#ns`X4`K*XI(Dr z+pPP!Lh|&{sCb_(P1nE0Z&c+pIv9Os(yz52-nj1l{H?1%=|$nJdm*u(-nph($IbQ9 z_}90ycfO16MrbY`nMk$4v3-yEqR{HGTj4@GVoGkGE<*WY)Y5+xMN5h5vNY^PEU0{)4x#_xD?= z>^UCB@^RVUSn+Gg?@kCc@t%MG-}KUteQcj?jI67kf3KK%g^@4hy4v-a}s40cA6|+TTnaIH0om8H1hy& zMkae^+yxfURY0K7u%r=0qZMBe&A7`hkQRt$BOr~e8Ca5`Yep}Qz; + +beforeEach(async () => { + fixtures = await getFixtures(); +}); + +test(`uploading shapefile as planning units`, async () => { + const shapefile = await fixtures.GivenShapefileWasUploaded(); + const output = await fixtures.WhenConvertingShapefileToPlanningUnits( + shapefile, + ); + await fixtures.ThenGeoJsonMatchesInput(output); +}); + +afterEach(async () => fixtures?.cleanup()); diff --git a/api/apps/geoprocessing/test/integration/planning-unit-grid/planning-units-grid.fixtures.ts b/api/apps/geoprocessing/test/integration/planning-unit-grid/planning-units-grid.fixtures.ts new file mode 100644 index 0000000000..e5ea302be9 --- /dev/null +++ b/api/apps/geoprocessing/test/integration/planning-unit-grid/planning-units-grid.fixtures.ts @@ -0,0 +1,85 @@ +import { bootstrapApplication } from '../../utils'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { copyFileSync, readFileSync } from 'fs'; +import { plainToClass } from 'class-transformer'; +import { v4 } from 'uuid'; +import { Job } from 'bullmq'; +import { Repository } from 'typeorm'; + +import { AppConfig } from '@marxan-geoprocessing/utils/config.utils'; +import { PlanningUnitsGeom } from '@marxan-jobs/planning-unit-geometry'; + +import { JobInput, JobOutput } from '@marxan/planning-units-grid'; +import { PlanningUnitsGridProcessor } from '@marxan-geoprocessing/modules/planning-units-grid/planning-units-grid.processor'; + +export const getFixtures = async () => { + const app = await bootstrapApplication(); + const sut: PlanningUnitsGridProcessor = app.get(PlanningUnitsGridProcessor); + const puGeoRepo: Repository = app.get( + getRepositoryToken(PlanningUnitsGeom), + ); + const projectId = v4(); + return { + cleanup: async () => { + await app.close(); + }, + projectId, + GivenShapefileWasUploaded: (): JobInput['shapefile'] => { + const fileName = 'nam-shapefile'; + const baseDir = AppConfig.get( + 'storage.sharedFileStorage.localPath', + ) as string; + const shapePath = baseDir + `/${fileName}.zip`; + copyFileSync(__dirname + `/${fileName}.zip`, shapePath); + return { + filename: fileName, + path: shapePath, + destination: baseDir, + }; + }, + WhenConvertingShapefileToPlanningUnits: async ( + input: JobInput['shapefile'], + ): Promise => { + return await sut.process(({ + data: plainToClass(JobInput, { + projectId, + shapefile: input, + }), + } as unknown) as Job); + }, + ThenGeoJsonMatchesInput: async (output: JobOutput) => { + const underlyingGeoJson = JSON.parse( + readFileSync(__dirname + `/nam.geojson`, { + encoding: 'utf8', + }), + ); + + const geoJsonFromGeometries = ( + await puGeoRepo.query( + ` + select json_build_object( + 'type', 'FeatureCollection', + 'features', json_agg(ST_AsGeoJSON((t.*)::record, '', 15)::json) + ) + from ( + select the_geom + from planning_units_geom + where type = 'irregular' + and project_id = $1 + ) as t(geom) + `, + [projectId], + ) + )[0].json_build_object; + + expect(underlyingGeoJson.features).toEqual( + expect.arrayContaining(geoJsonFromGeometries.features), + ); + + expect(output.projectId).toEqual(projectId); + expect(output.geometryIds.length).toEqual( + underlyingGeoJson.features.length, + ); + }, + }; +}; diff --git a/api/apps/geoprocessing/test/jest-e2e.json b/api/apps/geoprocessing/test/jest-e2e.json index cecb9bbce9..95596f2b62 100644 --- a/api/apps/geoprocessing/test/jest-e2e.json +++ b/api/apps/geoprocessing/test/jest-e2e.json @@ -39,6 +39,8 @@ "@marxan/iucn/(.*)": "/../../libs/iucn/src/$1", "@marxan/iucn": "/../../libs/iucn/src", "@marxan/shapefile-converter/(.*)": "/../../libs/shapefile-converter/src/$1", - "@marxan/shapefile-converter": "/../../libs/shapefile-converter/src" + "@marxan/shapefile-converter": "/../../libs/shapefile-converter/src", + "@marxan/planning-units-grid/(.*)": "/../../libs/planning-units-grid/src/$1", + "@marxan/planning-units-grid": "/../../libs/planning-units-grid/src" } } diff --git a/api/libs/planning-units-grid/src/index.ts b/api/libs/planning-units-grid/src/index.ts new file mode 100644 index 0000000000..d149f735cf --- /dev/null +++ b/api/libs/planning-units-grid/src/index.ts @@ -0,0 +1,4 @@ +export { queueName } from './queue-name'; + +export { JobInput, Shapefile } from './job-input'; +export { JobOutput } from './job-output'; diff --git a/api/libs/planning-units-grid/src/job-input.ts b/api/libs/planning-units-grid/src/job-input.ts new file mode 100644 index 0000000000..82bb1a2ba1 --- /dev/null +++ b/api/libs/planning-units-grid/src/job-input.ts @@ -0,0 +1,20 @@ +import { IsString, IsUUID, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class Shapefile { + @IsString() + path!: string; + @IsString() + filename!: string; + @IsString() + destination!: string; +} + +export class JobInput { + @IsUUID() + projectId!: string; + + @ValidateNested() + @Type(() => Shapefile) + shapefile!: Shapefile; +} diff --git a/api/libs/planning-units-grid/src/job-output.ts b/api/libs/planning-units-grid/src/job-output.ts new file mode 100644 index 0000000000..359f4cc9b2 --- /dev/null +++ b/api/libs/planning-units-grid/src/job-output.ts @@ -0,0 +1,17 @@ +import { IsUUID } from 'class-validator'; + +export class JobOutput { + /** + * It may happen that there are hundreds of IDs + * so please note that it may be wise NOT to use them + * further down in queries - rather select them within SQL + * using project_id + */ + @IsUUID('all', { + each: true, + }) + geometryIds!: string[]; + + @IsUUID() + projectId!: string; +} diff --git a/api/libs/planning-units-grid/src/queue-name.ts b/api/libs/planning-units-grid/src/queue-name.ts new file mode 100644 index 0000000000..112d5fd25a --- /dev/null +++ b/api/libs/planning-units-grid/src/queue-name.ts @@ -0,0 +1 @@ +export const queueName = `planning-units-grid`; diff --git a/api/libs/planning-units-grid/tsconfig.lib.json b/api/libs/planning-units-grid/tsconfig.lib.json new file mode 100644 index 0000000000..65cd1dbd54 --- /dev/null +++ b/api/libs/planning-units-grid/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/planning-units-grid" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/api/libs/shapefile-converter/src/shapefiles/shapefiles.service.ts b/api/libs/shapefile-converter/src/shapefiles/shapefiles.service.ts index 595cbcf874..680c5846e9 100644 --- a/api/libs/shapefile-converter/src/shapefiles/shapefiles.service.ts +++ b/api/libs/shapefile-converter/src/shapefiles/shapefiles.service.ts @@ -11,9 +11,12 @@ const mapshaper = require('mapshaper'); export class ShapefileService { private readonly logger: Logger = new Logger(ShapefileService.name); private readonly minRequiredFiles = ['.prj', '.dbf', '.shx', '.shp']; + constructor(private readonly fileService: FileService) {} - private async shapeFileToGeoJson(fileInfo: Express.Multer.File) { + private async shapeFileToGeoJson( + fileInfo: Pick, + ) { if ( !(await this.areRequiredShapefileFilesInFolder( fileInfo.path.replace('.zip', ''), @@ -64,7 +67,11 @@ export class ShapefileService { /** * converts file to a geojson and *removes* it */ - async transformToGeoJson(shapeFile: Express.Multer.File) { + async transformToGeoJson( + shapeFile: Pick, + ): Promise<{ + data: GeoJSON; + }> { try { this.logger.log( await this.fileService.unzipFile( diff --git a/api/nest-cli.json b/api/nest-cli.json index 5e6b8d4806..b897653801 100644 --- a/api/nest-cli.json +++ b/api/nest-cli.json @@ -169,6 +169,15 @@ "compilerOptions": { "tsConfigPath": "libs/shapefile-converter/tsconfig.lib.json" } + }, + "planning-units-grid": { + "type": "library", + "root": "libs/planning-units-grid", + "entryFile": "index", + "sourceRoot": "libs/planning-units-grid/src", + "compilerOptions": { + "tsConfigPath": "libs/planning-units-grid/tsconfig.lib.json" + } } } } \ No newline at end of file diff --git a/api/package.json b/api/package.json index fd85ba0222..64ce11124f 100644 --- a/api/package.json +++ b/api/package.json @@ -197,7 +197,9 @@ "@marxan/iucn/(.*)": "/libs/iucn/src/$1", "@marxan/iucn": "/libs/iucn/src", "@marxan/shapefile-converter/(.*)": "/libs/shapefile-converter/src/$1", - "@marxan/shapefile-converter": "/libs/shapefile-converter/src" + "@marxan/shapefile-converter": "/libs/shapefile-converter/src", + "@marxan/planning-units-grid/(.*)": "/libs/planning-units-grid/src/$1", + "@marxan/planning-units-grid": "/libs/planning-units-grid/src" } } } \ No newline at end of file diff --git a/api/tsconfig.json b/api/tsconfig.json index 974011101f..4b6d136493 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -114,6 +114,12 @@ ], "@marxan/shapefile-converter/*": [ "libs/shapefile-converter/src/*" + ], + "@marxan/planning-units-grid": [ + "libs/planning-units-grid/src" + ], + "@marxan/planning-units-grid/*": [ + "libs/planning-units-grid/src/*" ] } },