From 213bea9de7bf2b0121a7f6081cd72a80239d5076 Mon Sep 17 00:00:00 2001 From: Alicia Date: Thu, 27 May 2021 16:34:20 +0200 Subject: [PATCH 01/21] moved proxy endpoints to their respective modules --- api/apps/api/src/app.module.ts | 2 + .../admin-areas/admin-areas.controller.ts | 64 ++++- .../modules/admin-areas/admin-areas.module.ts | 4 +- .../geo-features/geo-features.controller.ts | 61 ++++- .../geo-features/geo-features.module.ts | 3 +- .../planning-units.controller.ts | 74 +++++ .../planning-units/planning-units.module.ts | 12 +- .../protected-areas.controller.ts | 51 +++- .../protected-areas/protected-areas.module.ts | 3 +- .../api/src/modules/proxy/proxy.module.ts | 2 - .../modules/features/features.controller.ts | 2 +- api/apps/geoprocessing/test/index.html | 257 +++++++++--------- 12 files changed, 389 insertions(+), 146 deletions(-) create mode 100644 api/apps/api/src/modules/planning-units/planning-units.controller.ts diff --git a/api/apps/api/src/app.module.ts b/api/apps/api/src/app.module.ts index 17ed41d664..fe828aabc3 100644 --- a/api/apps/api/src/app.module.ts +++ b/api/apps/api/src/app.module.ts @@ -28,6 +28,7 @@ import { ProxyModule } from '@marxan-api/modules/proxy/proxy.module'; import { ScenariosPlanningUnitModule } from './modules/scenarios-planning-unit/scenarios-planning-unit.module'; import { PlanningUnitsProtectionLevelModule } from './modules/planning-units-protection-level'; import { AnalysisModule } from './modules/analysis/analysis.module'; +import { PlanningUnitsModule } from 'modules/planning-units/planning-units.module'; @Module({ imports: [ @@ -55,6 +56,7 @@ import { AnalysisModule } from './modules/analysis/analysis.module'; ScenariosPlanningUnitModule, PlanningUnitsProtectionLevelModule, AnalysisModule, + PlanningUnitsModule, ], controllers: [AppController, PingController], providers: [ diff --git a/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts b/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts index 8caca14e81..140d57e8e4 100644 --- a/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts +++ b/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, Query, Req, Res, UseGuards } from '@nestjs/common'; import { adminAreaResource, AdminAreaResult } from './admin-area.geo.entity'; import { AdminAreaLevel, AdminAreasService } from './admin-areas.service'; import { @@ -18,13 +18,16 @@ import { FetchSpecification, ProcessFetchSpecification, } from 'nestjs-base-service'; +import { ProxyService } from 'modules/proxy/proxy.service'; +import { Request, Response } from 'express'; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiTags(adminAreaResource.className) @Controller(`${apiGlobalPrefixes.v1}`) export class AdminAreasController { - constructor(public readonly service: AdminAreasService) {} + constructor(public readonly service: AdminAreasService, + private readonly proxyService: ProxyService) {} @ApiOperation({ description: 'Find administrative areas within a given country.', @@ -62,6 +65,63 @@ export class AdminAreasController { return this.service.serialize(results.data, results.metadata); } + @ApiOperation({ + description: + 'Find administrative areas within a given country in mvt format.', + }) + /** + *@todo Change ApiOkResponse mvt type + */ + @ApiOkResponse({ + type: 'mvt', + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @ApiParam({ + name: 'z', + description: 'The zoom level ranging from 0 - 20', + type: Number, + required: true, + }) + @ApiParam({ + name: 'x', + description: 'The tile x offset on Mercator Projection', + type: Number, + required: true, + }) + @ApiParam({ + name: 'y', + description: 'The tile y offset on Mercator Projection', + type: Number, + required: true, + }) + @ApiParam({ + name: 'level', + description: + 'Specific level to filter the administrative areas (0, 1 or 2)', + type: Number, + required: true, + example: '1', + }) + @ApiQuery({ + name: 'guid', + description: 'Parent country of administrative areas in ISO code', + type: String, + required: false, + example: 'BRA.1_1', + }) + @ApiQuery({ + name: 'bbox', + description: 'Bounding box of the project', + type: [Number], + required: false, + example: [-1, 40, 1, 42], + }) + @Get('/administrative-areas/:level/preview/tiles/:z/:x/:y.mvt') + async proxyAdminAreaTile(@Req() request: Request, @Res() response: Response) { + return this.proxyService.proxyTileRequest(request, response); + } + @ApiOperation({ description: 'Find administrative areas that are children of a given one.', }) diff --git a/api/apps/api/src/modules/admin-areas/admin-areas.module.ts b/api/apps/api/src/modules/admin-areas/admin-areas.module.ts index 39b7e11eed..8c02ebae19 100644 --- a/api/apps/api/src/modules/admin-areas/admin-areas.module.ts +++ b/api/apps/api/src/modules/admin-areas/admin-areas.module.ts @@ -5,12 +5,14 @@ import { AdminArea } from '@marxan/admin-regions'; import { AdminAreasController } from './admin-areas.controller'; import { AdminAreasService } from './admin-areas.service'; import { apiConnections } from '../../ormconfig'; +import { ProxyService } from 'modules/proxy/proxy.service'; @Module({ imports: [ TypeOrmModule.forFeature([AdminArea], apiConnections.geoprocessingDB.name), + ], - providers: [AdminAreasService], + providers: [AdminAreasService, ProxyService], controllers: [AdminAreasController], exports: [AdminAreasService], }) diff --git a/api/apps/api/src/modules/geo-features/geo-features.controller.ts b/api/apps/api/src/modules/geo-features/geo-features.controller.ts index 72c8fd3317..6b9968f962 100644 --- a/api/apps/api/src/modules/geo-features/geo-features.controller.ts +++ b/api/apps/api/src/modules/geo-features/geo-features.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, Req, Res, UseGuards } from '@nestjs/common'; import { geoFeatureResource, GeoFeatureResult } from './geo-feature.geo.entity'; import { GeoFeaturesService } from './geo-features.service'; import { @@ -6,6 +6,8 @@ import { ApiForbiddenResponse, ApiOkResponse, ApiOperation, + ApiParam, + ApiQuery, ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; @@ -19,15 +21,19 @@ import { FetchSpecification, ProcessFetchSpecification, } from 'nestjs-base-service'; +import { Request, Response } from 'express'; +import { ProxyService } from 'modules/proxy/proxy.service'; +import { BBox} from 'geojson'; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiTags(geoFeatureResource.className) @Controller( - `${apiGlobalPrefixes.v1}/${geoFeatureResource.moduleControllerPrefix}`, + `${apiGlobalPrefixes.v1}/${geoFeatureResource.moduleControllerPrefix}`, ) export class GeoFeaturesController { - constructor(public readonly service: GeoFeaturesService) {} + constructor(public readonly service: GeoFeaturesService, + private readonly proxyService: ProxyService) {} @ApiOperation({ description: 'Find all geo features', @@ -46,6 +52,53 @@ export class GeoFeaturesController { return this.service.serialize(results.data, results.metadata); } + @ApiOperation({ + description: 'Get tile for a feature by id.', + }) + /** + *@todo Change ApiOkResponse mvt type + */ + @ApiOkResponse({ + type: 'mvt', + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @ApiParam({ + name: 'z', + description: 'The zoom level ranging from 0 - 20', + type: Number, + required: true, + }) + @ApiParam({ + name: 'x', + description: 'The tile x offset on Mercator Projection', + type: Number, + required: true, + }) + @ApiParam({ + name: 'y', + description: 'The tile y offset on Mercator Projection', + type: Number, + required: true, + }) + @ApiParam({ + name: 'id', + description: 'Specific id of the feature', + type: String, + required: true, + }) + @ApiQuery({ + name: 'bbox', + description: 'Bounding box of the project', + type: [Number], + required: false, + example: [-1, 40, 1, 42], + }) + @Get(':id/preview/tiles/:z/:x/:y.mvt') + async proxyFeaturesTile(@Req() request: Request, @Res() response: Response) { + return this.proxyService.proxyTileRequest(request, response); + } + @ApiOperation({ description: 'Find geo feature by id' }) @ApiOkResponse({ type: GeoFeatureResult }) @JSONAPISingleEntityQueryParams() @@ -53,4 +106,6 @@ export class GeoFeaturesController { async findOne(@Param('id') id: string): Promise { return await this.service.serialize(await this.service.fakeFindOne(id)); } + + } diff --git a/api/apps/api/src/modules/geo-features/geo-features.module.ts b/api/apps/api/src/modules/geo-features/geo-features.module.ts index 9b9d4e93ca..af20b90dd0 100644 --- a/api/apps/api/src/modules/geo-features/geo-features.module.ts +++ b/api/apps/api/src/modules/geo-features/geo-features.module.ts @@ -10,6 +10,7 @@ import { import { GeoFeaturesController } from './geo-features.controller'; import { GeoFeaturesService } from './geo-features.service'; import { apiConnections } from '../../ormconfig'; +import { ProxyService } from 'modules/proxy/proxy.service'; @Module({ imports: [ @@ -19,7 +20,7 @@ import { apiConnections } from '../../ormconfig'; ), TypeOrmModule.forFeature([GeoFeature, Project]), ], - providers: [GeoFeaturesService], + providers: [GeoFeaturesService, ProxyService], controllers: [GeoFeaturesController], exports: [GeoFeaturesService], }) diff --git a/api/apps/api/src/modules/planning-units/planning-units.controller.ts b/api/apps/api/src/modules/planning-units/planning-units.controller.ts new file mode 100644 index 0000000000..a5bff83085 --- /dev/null +++ b/api/apps/api/src/modules/planning-units/planning-units.controller.ts @@ -0,0 +1,74 @@ +import { Controller, UseGuards,Req, Res, Get} from "@nestjs/common"; +import { ApiBearerAuth, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger"; +import { apiGlobalPrefixes } from "api.config"; +import { JwtAuthGuard } from "guards/jwt-auth.guard"; +import { ProxyService } from "modules/proxy/proxy.service"; +import { Request, Response } from 'express'; + +@UseGuards(JwtAuthGuard) +@ApiBearerAuth() +@ApiTags('Plannig units') +@Controller( + `${apiGlobalPrefixes.v1}/planning-units`, +) +export class PlanningUnitsController { + constructor(private readonly proxyService: ProxyService) {} + + @ApiOperation({ + description: 'Get planning unit geometries.', + }) + /** + *@todo Change ApiOkResponse mvt type + */ + @ApiOkResponse({ + type: 'mvt', + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @ApiParam({ + name: 'z', + description: 'The zoom level ranging from 0 - 20', + type: Number, + required: true, + }) + @ApiParam({ + name: 'x', + description: 'The tile x offset on Mercator Projection', + type: Number, + required: true, + }) + @ApiParam({ + name: 'y', + description: 'The tile y offset on Mercator Projection', + type: Number, + required: true, + }) + @ApiParam({ + name: 'planningUnitGridShape', + description: 'Planning unit grid shape', + type: String, + required: true, + }) + @ApiParam({ + name: 'planningUnitAreakm2', + description: 'Planning unit area in km2', + type: Number, + required: true, + }) + @ApiQuery({ + name: 'bbox', + description: 'Bounding box of the project', + type: [Number], + required: false, + example: [-1, 40, 1, 42], + }) + @Get( + '/preview/regular/:planningUnitGridShape/:planningUnitAreakm2/tiles/:z/:x/:y.mvt', + ) + async proxyProtectedAreasTile( + @Req() request: Request, + @Res() response: Response, + ) { + return this.proxyService.proxyTileRequest(request, response); + } +} diff --git a/api/apps/api/src/modules/planning-units/planning-units.module.ts b/api/apps/api/src/modules/planning-units/planning-units.module.ts index 9f80e2d963..d66739a194 100644 --- a/api/apps/api/src/modules/planning-units/planning-units.module.ts +++ b/api/apps/api/src/modules/planning-units/planning-units.module.ts @@ -1,15 +1,11 @@ import { Module } from '@nestjs/common'; -import { QueueModule } from '@marxan-api/modules/queue/queue.module'; - +import { ProxyService } from 'modules/proxy/proxy.service'; +import { PlanningUnitsController } from './planning-units.controller'; import { PlanningUnitsService } from './planning-units.service'; @Module({ - imports: [ - QueueModule.register({ - name: 'planning-units', - }), - ], - providers: [PlanningUnitsService], + providers: [PlanningUnitsService, ProxyService], exports: [PlanningUnitsService], + controllers: [PlanningUnitsController], }) export class PlanningUnitsModule {} diff --git a/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts b/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts index 1e8205d61b..570d981073 100644 --- a/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts +++ b/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts @@ -4,6 +4,7 @@ import { Param, ParseUUIDPipe, UseGuards, + Req, Res, } from '@nestjs/common'; import { ProtectedAreaResult } from './protected-area.geo.entity'; import { @@ -11,6 +12,7 @@ import { ApiForbiddenResponse, ApiOkResponse, ApiOperation, + ApiParam, ApiQuery, ApiTags, ApiUnauthorizedResponse, @@ -30,13 +32,16 @@ import { ProtectedAreasService, } from './protected-areas.service'; import { IUCNProtectedAreaCategoryResult } from './dto/iucn-protected-area-category.dto'; +import { Request, Response } from 'express'; +import { ProxyService } from 'modules/proxy/proxy.service'; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiTags(protectedAreaResource.className) @Controller(`${apiGlobalPrefixes.v1}/protected-areas`) export class ProtectedAreasController { - constructor(public readonly service: ProtectedAreasService) {} + constructor(public readonly service: ProtectedAreasService, + private readonly proxyService: ProxyService) {} @ApiOperation({ description: 'Find all protected areas', @@ -63,6 +68,50 @@ export class ProtectedAreasController { return this.service.serialize(results.data, results.metadata); } + @ApiOperation({ + description: 'Get tile for protected areas.', + }) + /** + *@todo Change ApiOkResponse mvt type + */ + @ApiOkResponse({ + type: 'mvt', + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @ApiParam({ + name: 'z', + description: 'The zoom level ranging from 0 - 20', + type: Number, + required: true, + }) + @ApiParam({ + name: 'x', + description: 'The tile x offset on Mercator Projection', + type: Number, + required: true, + }) + @ApiParam({ + name: 'y', + description: 'The tile y offset on Mercator Projection', + type: Number, + required: true, + }) + @ApiQuery({ + name: 'id', + description: 'Id of WDPA area', + type: String, + required: false, + example: 'e5c3b978-908c-49d3-b1e3-89727e9f999c', + }) + @Get('/preview/tiles/:z/:x/:y.mvt') + async proxyProtectedAreaTile( + @Req() request: Request, + @Res() response: Response, + ) { + return this.proxyService.proxyTileRequest(request, response); + } + @ApiOperation({ description: 'Find unique IUCN categories among protected areas in a single given administrative area.', diff --git a/api/apps/api/src/modules/protected-areas/protected-areas.module.ts b/api/apps/api/src/modules/protected-areas/protected-areas.module.ts index 91eadc6eb8..39b6e73e97 100644 --- a/api/apps/api/src/modules/protected-areas/protected-areas.module.ts +++ b/api/apps/api/src/modules/protected-areas/protected-areas.module.ts @@ -5,6 +5,7 @@ import { ProtectedAreasController } from './protected-areas.controller'; import { ProtectedArea } from './protected-area.geo.entity'; import { ProtectedAreasService } from './protected-areas.service'; import { apiConnections } from '../../ormconfig'; +import { ProxyService } from 'modules/proxy/proxy.service'; @Module({ imports: [ @@ -13,7 +14,7 @@ import { apiConnections } from '../../ormconfig'; apiConnections.geoprocessingDB.name, ), ], - providers: [ProtectedAreasService], + providers: [ProtectedAreasService, ProxyService], controllers: [ProtectedAreasController], exports: [ProtectedAreasService], }) diff --git a/api/apps/api/src/modules/proxy/proxy.module.ts b/api/apps/api/src/modules/proxy/proxy.module.ts index 3fcb8d3356..4b79972f2b 100644 --- a/api/apps/api/src/modules/proxy/proxy.module.ts +++ b/api/apps/api/src/modules/proxy/proxy.module.ts @@ -1,12 +1,10 @@ import { Logger, Module } from '@nestjs/common'; -import { ProxyController } from './proxy.controller'; import { ProxyService } from './proxy.service'; export const logger = new Logger('ProxyService'); @Module({ imports: [], - controllers: [ProxyController], providers: [ProxyService], exports: [], }) diff --git a/api/apps/geoprocessing/src/modules/features/features.controller.ts b/api/apps/geoprocessing/src/modules/features/features.controller.ts index fd886b7179..8b6f00f115 100644 --- a/api/apps/geoprocessing/src/modules/features/features.controller.ts +++ b/api/apps/geoprocessing/src/modules/features/features.controller.ts @@ -21,7 +21,7 @@ import { BBox } from 'geojson'; import { Response } from 'express'; -@Controller(`${apiGlobalPrefixes.v1}/features`) +@Controller(`${apiGlobalPrefixes.v1}/geo-features`) export class FeaturesController { private readonly logger: Logger = new Logger(FeaturesController.name); constructor(public service: FeatureService) {} diff --git a/api/apps/geoprocessing/test/index.html b/api/apps/geoprocessing/test/index.html index 7a4d5ad93e..d31a7baec6 100644 --- a/api/apps/geoprocessing/test/index.html +++ b/api/apps/geoprocessing/test/index.html @@ -1,151 +1,156 @@ - - + + Mapbox GL JS Examples - - - + + + - - + + +
+ - - - + }); + + - From 3de40d735ff21a8be1554625dd0c62285fab9285 Mon Sep 17 00:00:00 2001 From: Alicia Date: Thu, 27 May 2021 17:36:29 +0200 Subject: [PATCH 02/21] wip - review issue with pu query --- .../planning-units/planning-units.service.ts | 17 ++++++------- api/apps/geoprocessing/test/index.html | 24 +++++++++---------- 2 files changed, 21 insertions(+), 20 deletions(-) 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 5b8ab80c55..e51e3b360a 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 @@ -6,7 +6,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { IsOptional, IsString, IsArray, IsNumber } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { BBox } from 'geojson'; import { Transform } from 'class-transformer'; @@ -40,7 +40,7 @@ export class PlanningUnitsService { private readonly logger: Logger = new Logger(PlanningUnitsService.name); constructor( @InjectRepository(PlanningUnitsGeom) - private readonly protectedAreasRepository: Repository, + private readonly planningUnitsRepository: Repository, @Inject(TileService) private readonly tileService: TileService, ) {} @@ -61,18 +61,18 @@ export class PlanningUnitsService { planningUnitAreakm2: number, filters?: PlanningUnitsFilters, ): string { - let PlanningUnitGridShape = ''; let whereQuery = ''; + let gridShape = 'ST_SquareGrid' if (planningUnitGridShape == 'hexagon') { - PlanningUnitGridShape = 'ST_HexagonGrid'; + gridShape = 'ST_HexagonGrid'; } if (planningUnitGridShape == 'square') { - PlanningUnitGridShape = 'ST_SquareGrid'; + gridShape = 'ST_SquareGrid'; } if (filters?.bbox) { - whereQuery = `(${PlanningUnitGridShape}(${planningUnitAreakm2} * 1000000, ST_Transform(ST_MakeEnvelope(${filters?.bbox}), 3857)))`; + whereQuery = `(${gridShape}(${planningUnitAreakm2} * 1000000, ST_Transform(ST_MakeEnvelope(${filters?.bbox}), 3857)))`; } else { - whereQuery = `(${PlanningUnitGridShape}(${planningUnitAreakm2} * 1000000, ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 4326), 3857)))`; + whereQuery = `(${gridShape}(${planningUnitAreakm2} * 1000000, ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 4326), 3857)))`; } return whereQuery; } @@ -92,7 +92,7 @@ export class PlanningUnitsService { planningUnitAreakm2, } = tileSpecification; const attributes = 'id'; - const table = this.protectedAreasRepository.metadata.tableName; + const table = this.planningUnitsRepository.metadata.tableName; const customQuery = undefined; const querytest = this.buildPlanningUnitsWhereQuery( z, @@ -102,6 +102,7 @@ export class PlanningUnitsService { planningUnitAreakm2, filters, ); + this.logger.debug(querytest) return this.tileService.getTile({ z, x, diff --git a/api/apps/geoprocessing/test/index.html b/api/apps/geoprocessing/test/index.html index d31a7baec6..8f9f415956 100644 --- a/api/apps/geoprocessing/test/index.html +++ b/api/apps/geoprocessing/test/index.html @@ -74,7 +74,7 @@ minzoom: 1, maxzoom: 12, tiles: [ - 'http://localhost:3040/api/v1/planning-units/preview/tiles/{z}/{x}/{y}.mvt', + 'http://localhost:3040/api/v1/planning-units/preview/regular/square/1000/tiles/{z}/{x}/{y}.mvt', ], }, BASEMAP: { @@ -137,17 +137,17 @@ 'fill-opacity': 0.7, }, }, - // { - // 'id': 'postgis-tiles-layer-PU', - // 'type': 'fill', - // 'source': 'postgis-tilesPU', - // 'source-layer': 'layer0', - // 'paint': { - // 'fill-outline-color': 'yellow', - // 'fill-color': 'red', - // 'fill-opacity': 0.7 - // } - // }, + { + id: 'postgis-tiles-layer-PU', + type: 'fill', + source: 'postgis-tilesPU', + 'source-layer': 'layer0', + paint: { + 'fill-outline-color': 'yellow', + 'fill-color': 'red', + 'fill-opacity': 0.7, + }, + }, ], }, }); From 06e21ffac3191a43430685175939d38afa75d277 Mon Sep 17 00:00:00 2001 From: Alicia Date: Wed, 2 Jun 2021 13:25:04 +0200 Subject: [PATCH 03/21] added an improvement to PUs preview tiles --- .../planning-units.controller.ts | 2 +- .../planning-units/planning-units.service.ts | 90 ++++++++++---- .../src/modules/tile/tile.service.ts | 37 +++--- api/apps/geoprocessing/test/index.html | 23 +++- .../Lab/featuresForScenario_logic.ipynb | 24 ++-- data/notebooks/Lab/pu_creation_logic.ipynb | 116 ++++++++++++++---- 6 files changed, 219 insertions(+), 73 deletions(-) diff --git a/api/apps/geoprocessing/src/modules/planning-units/planning-units.controller.ts b/api/apps/geoprocessing/src/modules/planning-units/planning-units.controller.ts index b0d5f804d6..5ef1e4c8d1 100644 --- a/api/apps/geoprocessing/src/modules/planning-units/planning-units.controller.ts +++ b/api/apps/geoprocessing/src/modules/planning-units/planning-units.controller.ts @@ -105,7 +105,7 @@ export class PlanningUnitsController { @Query() PlanningUnitsFilters: PlanningUnitsFilters, @Res() response: Response, ): Promise { - const tile: Buffer = await this.service.findTile( + const tile: Buffer = await this.service.findPreviewTile( tileSpecification, PlanningUnitsFilters, ); 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 e51e3b360a..d9dce27843 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 @@ -46,14 +46,37 @@ export class PlanningUnitsService { ) {} /** - * @param bbox boundingbox of the area where the grids would be generated * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape * can be square or hexagon. If any grid shape is provided, square would be the default. * @param planningUnitAreakm2 area in km2 of the individual grid that would be generated. * If any value is not provided, 4000 would be the default. */ + regularFuncionGridSelector( + planningUnitGridShape: PlanningUnitGridShape + ): string { + const functEquivalence: { [key in keyof typeof PlanningUnitGridShape]: string} = { + hexagon: 'ST_HexagonGrid', + square: 'ST_SquareGrid', + } - buildPlanningUnitsWhereQuery( + return functEquivalence[planningUnitGridShape] + } + + calculateGridSize( + planningUnitGridShape: PlanningUnitGridShape, + planningUnitAreakm2: number + ): number { + + return Math.sqrt(planningUnitAreakm2) * 1000 + } + /** + * @param bbox bounding box of the area where the grids would be generated + * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape + * can be square or hexagon. If any grid shape is provided, square would be the default. + * @param planningUnitAreakm2 area in km2 of the individual grid that would be generated. + * If any value is not provided, 4000 would be the default. + */ + buildPlanningUnitsCustomQuery( x: number, y: number, z: number, @@ -61,18 +84,42 @@ export class PlanningUnitsService { planningUnitAreakm2: number, filters?: PlanningUnitsFilters, ): string { - let whereQuery = ''; - let gridShape = 'ST_SquareGrid' - if (planningUnitGridShape == 'hexagon') { - gridShape = 'ST_HexagonGrid'; - } - if (planningUnitGridShape == 'square') { - gridShape = 'ST_SquareGrid'; + this.logger.debug('tile') + const gridShape = this.regularFuncionGridSelector(planningUnitGridShape) + const gridSize = this.calculateGridSize(planningUnitGridShape, + planningUnitAreakm2) + + let Query = `( SELECT row_number() over() as id, (${gridShape}(${gridSize}, \ + ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom as the_geom)`; + // 156412 references to m per pixel at z level 0 at the equator in EPSG:3857 + // (so we are checking that the pixel ration is < 1) + // If so the shape we are getting is down the optimal to visualize it + if ((gridSize / (156412/(2**z))) < 1){ + Query = `( SELECT row_number() over() as id, st_centroid((${gridShape}(${gridSize}, \ + ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom ) as the_geom)`; } + + return Query; +} + /** + * @param bbox bounding box of the area where the grids would be generated + * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape + * can be square or hexagon. If any grid shape is provided, square would be the default. + * @param planningUnitAreakm2 area in km2 of the individual grid that would be generated. + * If any value is not provided, 4000 would be the default. + */ + buildPlanningUnitsWhereQuery( + x: number, + y: number, + z: number, + planningUnitGridShape: PlanningUnitGridShape, + planningUnitAreakm2: number, + filters?: PlanningUnitsFilters, + ): string { + let whereQuery = `st_intersects(the_geom, ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 4326), 3857)))`; + if (filters?.bbox) { - whereQuery = `(${gridShape}(${planningUnitAreakm2} * 1000000, ST_Transform(ST_MakeEnvelope(${filters?.bbox}), 3857)))`; - } else { - whereQuery = `(${gridShape}(${planningUnitAreakm2} * 1000000, ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 4326), 3857)))`; + whereQuery += ` && ST_Transform(ST_MakeEnvelope(${filters.bbox}), 3857))`; } return whereQuery; } @@ -80,7 +127,7 @@ export class PlanningUnitsService { /** * @todo get attributes from Entity, based on user selection */ - public findTile( + public findPreviewTile( tileSpecification: tileSpecification, filters?: PlanningUnitsFilters, ): Promise { @@ -91,25 +138,26 @@ export class PlanningUnitsService { planningUnitGridShape, planningUnitAreakm2, } = tileSpecification; + + const inputProjection = 3857 + const attributes = 'id'; - const table = this.planningUnitsRepository.metadata.tableName; - const customQuery = undefined; - const querytest = this.buildPlanningUnitsWhereQuery( - z, + const table = this.buildPlanningUnitsCustomQuery( x, y, + z, planningUnitGridShape, planningUnitAreakm2, - filters, - ); - this.logger.debug(querytest) + filters + ); + return this.tileService.getTile({ z, x, y, table, - customQuery, attributes, + inputProjection }); } } diff --git a/api/apps/geoprocessing/src/modules/tile/tile.service.ts b/api/apps/geoprocessing/src/modules/tile/tile.service.ts index 544d2e6cca..20a282ee27 100644 --- a/api/apps/geoprocessing/src/modules/tile/tile.service.ts +++ b/api/apps/geoprocessing/src/modules/tile/tile.service.ts @@ -58,7 +58,12 @@ export interface TileInput extends TileRequest { /** * @description Custom query for the different entities */ - customQuery: T | undefined; + customQuery?: T | undefined; + + /** + * @description Input projection, by default 4326 + */ + inputProjection?: number; /** * @description attributes to be retrieved from the different entities */ @@ -80,6 +85,11 @@ export class TileService { */ private readonly logger: Logger = new Logger(TileService.name); + simplification(z:number, geometry: string): string { + return (z > 7) ? `${geometry}` : `ST_RemoveRepeatedPoints(${geometry}, ${ + 0.1 / (z * 2) + })` + } /** * All database interaction is encapsulated in this function. The design-goal is to keep the time where a database- * connection is open to a minimum. This reduces the risk for the database-instance to run out of connections. @@ -95,7 +105,8 @@ export class TileService { geometry = 'the_geom', extent = 4096, buffer = 256, - customQuery, + customQuery = undefined, + inputProjection = 4326, attributes, }: TileInput): Promise[]> { const connection = getConnection(); @@ -103,30 +114,22 @@ export class TileService { .createQueryBuilder() .select(`ST_AsMVT(tile, 'layer0', ${extent}, 'mvt_geom')`, 'mvt') .from((subQuery) => { - if (z > 7) { + subQuery.select( - `${attributes}, ST_AsMVTGeom(ST_Transform(${geometry}, 3857), + `${attributes}, ST_AsMVTGeom(ST_Transform(${this.simplification(z, geometry)}, 3857), ST_TileEnvelope(${z}, ${x}, ${y}), ${extent}, ${buffer}, true) AS mvt_geom`, ); - } else { - subQuery.select( - `${attributes}, ST_AsMVTGeom(ST_Transform(ST_RemoveRepeatedPoints(${geometry}, ${ - 0.1 / (z * 2) - }), 3857), ST_TileEnvelope(${z}, ${x}, ${y}), ${extent}, ${buffer}, true) AS mvt_geom`, - ); - } - subQuery - .from(table, '') - .where( - `ST_Intersects(ST_Transform(ST_TileEnvelope(:z, :x, :y), 4326), ${geometry} )`, + + subQuery.from(table, '').where( + `ST_Intersects(ST_Transform(ST_TileEnvelope(:z, :x, :y), ${inputProjection}), ${geometry} )`, { z, x, y }, - ); + ); if (customQuery) { subQuery.andWhere(customQuery); } return subQuery; }, 'tile'); - const result = await query.getRawMany(); + const result = await query.printSql().getRawMany(); if (result) { return result; diff --git a/api/apps/geoprocessing/test/index.html b/api/apps/geoprocessing/test/index.html index 8f9f415956..dad27f695f 100644 --- a/api/apps/geoprocessing/test/index.html +++ b/api/apps/geoprocessing/test/index.html @@ -34,7 +34,10 @@ var map = new mapboxgl.Map({ container: 'map', zoom: 5, - center: [10, 49], + center: [ + 22.686767578125, + -19.072501451715087 + ], light: { intensity: 0.2 }, style: { version: 8, @@ -74,7 +77,7 @@ minzoom: 1, maxzoom: 12, tiles: [ - 'http://localhost:3040/api/v1/planning-units/preview/regular/square/1000/tiles/{z}/{x}/{y}.mvt', + 'http://localhost:3040/api/v1/planning-units/preview/regular/hexagon/100/tiles/{z}/{x}/{y}.mvt', ], }, BASEMAP: { @@ -142,10 +145,24 @@ type: 'fill', source: 'postgis-tilesPU', 'source-layer': 'layer0', + minzoom:7, paint: { 'fill-outline-color': 'yellow', 'fill-color': 'red', - 'fill-opacity': 0.7, + 'fill-opacity': 0.1, + }, + }, + { + id: 'postgis-tiles-layer-PU-points', + type: 'circle', + source: 'postgis-tilesPU', + maxzoom: 7, + 'source-layer': 'layer0', + paint: { + 'circle-color': '#11b4da', + 'circle-radius': 1, + 'circle-stroke-width': 1, + 'circle-stroke-color': '#11b4da' }, }, ], diff --git a/data/notebooks/Lab/featuresForScenario_logic.ipynb b/data/notebooks/Lab/featuresForScenario_logic.ipynb index 5b127b8b16..4e0363fd07 100644 --- a/data/notebooks/Lab/featuresForScenario_logic.ipynb +++ b/data/notebooks/Lab/featuresForScenario_logic.ipynb @@ -3,7 +3,7 @@ { "cell_type": "code", "execution_count": 1, - "id": "bac2f0fa", + "id": "27a3e5bd", "metadata": {}, "outputs": [], "source": [ @@ -24,7 +24,7 @@ { "cell_type": "code", "execution_count": 2, - "id": "21f8da16", + "id": "1294a94a", "metadata": {}, "outputs": [ { @@ -62,7 +62,7 @@ { "cell_type": "code", "execution_count": 3, - "id": "9f6e5638", + "id": "b14c7b18", "metadata": {}, "outputs": [ { @@ -93,7 +93,7 @@ }, { "cell_type": "markdown", - "id": "655d9b8b", + "id": "b29427b4", "metadata": {}, "source": [ "We have here again 2 steps logic.\n", @@ -111,7 +111,7 @@ }, { "cell_type": "markdown", - "id": "47d9697c", + "id": "f448ecac", "metadata": {}, "source": [ "### Recipe definition for feature creation (TBD - with Andrea, Alex and Barre)\n", @@ -184,7 +184,7 @@ }, { "cell_type": "markdown", - "id": "7b9035cf", + "id": "1fb70a6b", "metadata": {}, "source": [ "### Feature creation from recipe\n", @@ -247,7 +247,7 @@ }, { "cell_type": "markdown", - "id": "d6bdc32b", + "id": "f8e1392e", "metadata": {}, "source": [ "### Attach features to our scenario\n", @@ -330,7 +330,7 @@ }, { "cell_type": "markdown", - "id": "7e8ff1dd", + "id": "1e04fc5d", "metadata": {}, "source": [ "Once all of this is done we will be able to generate the next requires files for marxan: \n", @@ -338,6 +338,14 @@ "* conservationFeature\n", "* planningUnits" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd800996", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/data/notebooks/Lab/pu_creation_logic.ipynb b/data/notebooks/Lab/pu_creation_logic.ipynb index d8c7024fa7..1fde95ad1d 100644 --- a/data/notebooks/Lab/pu_creation_logic.ipynb +++ b/data/notebooks/Lab/pu_creation_logic.ipynb @@ -2,8 +2,8 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, - "id": "geological-search", + "execution_count": 18, + "id": "68eeffb5", "metadata": {}, "outputs": [], "source": [ @@ -24,7 +24,7 @@ { "cell_type": "code", "execution_count": 2, - "id": "national-tracker", + "id": "56a91691", "metadata": {}, "outputs": [ { @@ -49,7 +49,7 @@ { "cell_type": "code", "execution_count": 3, - "id": "reported-maria", + "id": "c3529ac6", "metadata": {}, "outputs": [ { @@ -80,7 +80,7 @@ }, { "cell_type": "markdown", - "id": "revised-evolution", + "id": "e155b53b", "metadata": {}, "source": [ "[how many squares can we create in a certain extent?](https://www.redblobgames.com/grids) \n", @@ -117,10 +117,55 @@ "\n" ] }, + { + "cell_type": "code", + "execution_count": 33, + "id": "74ffc4eb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2.5878547713829665" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zlevel = 7\n", + "grid_size = 10\n", + "m_per_pixel = 156412\n", + "(math.sqrt(grid_size) * 1000) / (m_per_pixel/(2**zlevel))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5f550623", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2.0" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "78206/39103" + ] + }, { "cell_type": "code", "execution_count": 4, - "id": "unknown-yesterday", + "id": "16630c48", "metadata": {}, "outputs": [], "source": [ @@ -212,7 +257,7 @@ { "cell_type": "code", "execution_count": 5, - "id": "heard-characteristic", + "id": "81707818", "metadata": {}, "outputs": [], "source": [ @@ -235,7 +280,7 @@ { "cell_type": "code", "execution_count": 6, - "id": "sweet-passing", + "id": "09706782", "metadata": {}, "outputs": [ { @@ -299,7 +344,7 @@ { "cell_type": "code", "execution_count": 7, - "id": "juvenile-soldier", + "id": "d1ea6057", "metadata": {}, "outputs": [ { @@ -362,7 +407,7 @@ { "cell_type": "code", "execution_count": 8, - "id": "unnecessary-brand", + "id": "99d0fbb8", "metadata": {}, "outputs": [ { @@ -390,7 +435,7 @@ { "cell_type": "code", "execution_count": 9, - "id": "rubber-participation", + "id": "2190b4dc", "metadata": {}, "outputs": [ { @@ -441,7 +486,7 @@ { "cell_type": "code", "execution_count": 10, - "id": "guilty-consensus", + "id": "0bbeaba5", "metadata": {}, "outputs": [ { @@ -490,7 +535,7 @@ { "cell_type": "code", "execution_count": 11, - "id": "faced-ready", + "id": "1db86039", "metadata": {}, "outputs": [ { @@ -529,7 +574,7 @@ { "cell_type": "code", "execution_count": 161, - "id": "obvious-lawyer", + "id": "037b9d6d", "metadata": {}, "outputs": [ { @@ -558,6 +603,31 @@ "ax.spines['bottom'].set_position('zero')\n", "ax.spines['right'].set_color('none')\n", "ax.spines['top'].set_color('none')\n", + "def calculateSquareOverExtent(geometry):\n", + " \"\"\"Calculates the number of squares in grid covered by an extent and a grid_size \n", + " Parameters:\n", + " geometry (List(float)): [x_min, x_max, y_min, y_max] in epsg 4326\n", + " grid_size_km (float): grid cell size in km\n", + "\n", + " Returns:\n", + " int: Returning an aproach number of grids cells\n", + " \n", + " \"\"\"\n", + " wgs84 = pyproj.CRS('EPSG:4326')\n", + " webMercator = pyproj.CRS('EPSG:3857')\n", + " project = pyproj.Transformer.from_crs(wgs84, webMercator, always_xy=True).transform\n", + " utm_extent = transform(project, geometry).bounds\n", + " width = utm_extent[2] - utm_extent[0]\n", + " height = utm_extent[3] - utm_extent[1]\n", + " areaExtent = width*height\n", + " min_grid = calcMinGrid(areaExtent)\n", + " max_grid = calcMaxGrid(areaExtent)\n", + " grid_size_m = round(min_grid/1000) * 1000\n", + " areaGrid = grid_size_m ** 2\n", + " print(f'geometry extent: {round(areaExtent/1000,2)} km2')\n", + " print(f'min grid size: {round(min_grid/1000)}')\n", + " print(f'max grid size: {round(max_grid/1000)}')\n", + " return round(areaExtent/areaGrid)\n", "ax.xaxis.set_ticks_position('bottom')\n", "ax.yaxis.set_ticks_position('left')\n", "plt.xlabel(\"Extent km2\")\n", @@ -572,7 +642,7 @@ { "cell_type": "code", "execution_count": 160, - "id": "brazilian-michigan", + "id": "ba20de14", "metadata": {}, "outputs": [ { @@ -616,7 +686,7 @@ { "cell_type": "code", "execution_count": 154, - "id": "first-soundtrack", + "id": "ace63ddd", "metadata": {}, "outputs": [ { @@ -654,7 +724,7 @@ { "cell_type": "code", "execution_count": 174, - "id": "tracked-argentina", + "id": "4b04a990", "metadata": {}, "outputs": [ { @@ -697,7 +767,7 @@ { "cell_type": "code", "execution_count": 175, - "id": "humanitarian-collective", + "id": "f90599a9", "metadata": {}, "outputs": [ { @@ -717,7 +787,7 @@ }, { "cell_type": "markdown", - "id": "swiss-congo", + "id": "2c67d14a", "metadata": {}, "source": [ "We can say that for square grids, we can use our function to aprox the ratio between extent and min grid size because smaller grids definitions are more accurate and bigger grid size in relation with the extent. Also our function is eager that the real calculation done by posgis so we can set a smaller limit in our function. Also as a minimum guide to choose grid size we will take into account a relation between km/pixel and the extent it represents" @@ -725,7 +795,7 @@ }, { "cell_type": "markdown", - "id": "played-provider", + "id": "e3ed7dee", "metadata": {}, "source": [ "# Queries to generate grids on the fly:" @@ -733,7 +803,7 @@ }, { "cell_type": "markdown", - "id": "needed-termination", + "id": "254a5bd4", "metadata": {}, "source": [ "`ST_SquareGrid` or `ST_HexagonGrid`\n", @@ -746,7 +816,7 @@ }, { "cell_type": "markdown", - "id": "accessory-monaco", + "id": "fcd315ad", "metadata": {}, "source": [ "Q" @@ -754,7 +824,7 @@ }, { "cell_type": "markdown", - "id": "neutral-adult", + "id": "bfe7422a", "metadata": {}, "source": [ "```sql\n", From 31c5bf9f989b62bfaa83c4c93e550a491526302c Mon Sep 17 00:00:00 2001 From: Alicia Date: Wed, 2 Jun 2021 14:14:44 +0200 Subject: [PATCH 04/21] updated way to simplify pus --- .../planning-units/planning-units.service.ts | 7 +- api/apps/geoprocessing/test/index.html | 116 +++++++++--------- 2 files changed, 62 insertions(+), 61 deletions(-) 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 d9dce27843..6c06468858 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 @@ -84,17 +84,16 @@ export class PlanningUnitsService { planningUnitAreakm2: number, filters?: PlanningUnitsFilters, ): string { - this.logger.debug('tile') const gridShape = this.regularFuncionGridSelector(planningUnitGridShape) const gridSize = this.calculateGridSize(planningUnitGridShape, planningUnitAreakm2) - + const ratioPixelExtent = (gridSize / (156412/(2**z))) let Query = `( SELECT row_number() over() as id, (${gridShape}(${gridSize}, \ ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom as the_geom)`; // 156412 references to m per pixel at z level 0 at the equator in EPSG:3857 - // (so we are checking that the pixel ration is < 1) + // (so we are checking that the pixel ration is < 8 px) // If so the shape we are getting is down the optimal to visualize it - if ((gridSize / (156412/(2**z))) < 1){ + if ( ratioPixelExtent < 8){ Query = `( SELECT row_number() over() as id, st_centroid((${gridShape}(${gridSize}, \ ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom ) as the_geom)`; } diff --git a/api/apps/geoprocessing/test/index.html b/api/apps/geoprocessing/test/index.html index dad27f695f..2ff79c3bc8 100644 --- a/api/apps/geoprocessing/test/index.html +++ b/api/apps/geoprocessing/test/index.html @@ -42,44 +42,46 @@ style: { version: 8, sources: { - 'postgis-tiles0': { - type: 'vector', - maxzoom: 12, - tiles: [ - 'http://localhost:3040/api/v1/administrative-areas/0/preview/tiles/{z}/{x}/{y}.mvt', - ], - }, - 'postgis-tiles1': { - type: 'vector', - maxzoom: 12, - tiles: [ - 'http://localhost:3040/api/v1/administrative-areas/1/preview/tiles/{z}/{x}/{y}.mvt', - ], - }, - 'postgis-tiles2': { - type: 'vector', - maxzoom: 12, - tiles: [ - 'http://localhost:3040/api/v1/administrative-areas/2/preview/tiles/{z}/{x}/{y}.mvt', - ], - }, - 'postgis-tileswdpa': { - type: 'vector', - minzoom: 1, - maxzoom: 12, - tiles: [ - 'http://localhost:3040/api/v1/protected-areas/preview/tiles/{z}/{x}/{y}.mvt', - ], - }, + // 'postgis-tiles0': { + // type: 'vector', + // maxzoom: 12, + // tiles: [ + // 'http://localhost:3040/api/v1/administrative-areas/0/preview/tiles/{z}/{x}/{y}.mvt', + // ], + // }, + // 'postgis-tiles1': { + // type: 'vector', + // maxzoom: 12, + // tiles: [ + // 'http://localhost:3040/api/v1/administrative-areas/1/preview/tiles/{z}/{x}/{y}.mvt', + // ], + // }, + // 'postgis-tiles2': { + // type: 'vector', + // maxzoom: 12, + // tiles: [ + // 'http://localhost:3040/api/v1/administrative-areas/2/preview/tiles/{z}/{x}/{y}.mvt', + // ], + // }, + // 'postgis-tileswdpa': { + // type: 'vector', + // minzoom: 1, + // maxzoom: 12, + // tiles: [ + // 'http://localhost:3040/api/v1/protected-areas/preview/tiles/{z}/{x}/{y}.mvt', + // ], + // }, 'postgis-tilesPU': { type: 'vector', minzoom: 1, - maxzoom: 12, + maxzoom: 20, tiles: [ - 'http://localhost:3040/api/v1/planning-units/preview/regular/hexagon/100/tiles/{z}/{x}/{y}.mvt', + 'http://localhost:3040/api/v1/planning-units/preview/regular/hexagon/10/tiles/{z}/{x}/{y}.mvt', ], }, + + BASEMAP: { type: 'raster', tiles: [ @@ -96,17 +98,17 @@ maxzoom: 22, }, - { - id: 'postgis-tiles-layer', - type: 'fill', - source: 'postgis-tiles2', - 'source-layer': 'layer0', - paint: { - 'fill-outline-color': 'red', - 'fill-color': 'green', - 'fill-opacity': 0.2, - }, - }, + // { + // id: 'postgis-tiles-layer', + // type: 'fill', + // source: 'postgis-tiles2', + // 'source-layer': 'layer0', + // paint: { + // 'fill-outline-color': 'red', + // 'fill-color': 'green', + // 'fill-opacity': 0.2, + // }, + // }, // { // 'id': 'postgis-tiles-layer1', // 'type': 'fill', @@ -129,23 +131,23 @@ // 'fill-opacity': 0.2 // } // }, - { - id: 'postgis-tiles-layer-wdpa', - type: 'fill', - source: 'postgis-tileswdpa', - 'source-layer': 'layer0', - paint: { - 'fill-outline-color': 'yellow', - 'fill-color': 'red', - 'fill-opacity': 0.7, - }, - }, + // { + // id: 'postgis-tiles-layer-wdpa', + // type: 'fill', + // source: 'postgis-tileswdpa', + // 'source-layer': 'layer0', + // paint: { + // 'fill-outline-color': 'yellow', + // 'fill-color': 'red', + // 'fill-opacity': 0.7, + // }, + // }, { id: 'postgis-tiles-layer-PU', type: 'fill', source: 'postgis-tilesPU', 'source-layer': 'layer0', - minzoom:7, + paint: { 'fill-outline-color': 'yellow', 'fill-color': 'red', @@ -156,13 +158,13 @@ id: 'postgis-tiles-layer-PU-points', type: 'circle', source: 'postgis-tilesPU', - maxzoom: 7, + 'source-layer': 'layer0', paint: { - 'circle-color': '#11b4da', + 'circle-color': 'yellow', 'circle-radius': 1, 'circle-stroke-width': 1, - 'circle-stroke-color': '#11b4da' + 'circle-stroke-color': 'yellow' }, }, ], From 5abbc80479f7c3e72463b5b2ba870fb64aa7ac76 Mon Sep 17 00:00:00 2001 From: Alicia Date: Fri, 4 Jun 2021 09:12:34 +0200 Subject: [PATCH 05/21] add decoder for vector layers so we can validate them --- .../api/test/proxy.vector-tiles.e2e-spec.ts | 250 ++++++++++++++---- api/package.json | 6 + api/yarn.lock | 27 +- 3 files changed, 232 insertions(+), 51 deletions(-) diff --git a/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts b/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts index b863a8cade..08e60066c6 100644 --- a/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts +++ b/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts @@ -1,77 +1,227 @@ -import { INestApplication } from '@nestjs/common'; +import { HttpStatus, INestApplication, ValidationPipe, Logger } from '@nestjs/common'; import * as request from 'supertest'; -import { bootstrapApplication } from './utils/api-application'; -import { GivenUserIsLoggedIn } from './steps/given-user-is-logged-in'; +import * as JSONAPISerializer from 'jsonapi-serializer'; +import PBF from 'pbf'; import { tearDown } from './utils/tear-down'; +import { Scenario } from 'modules/scenarios/scenario.api.entity'; +import { Organization } from 'modules/organizations/organization.api.entity'; +import { Project } from 'modules/projects/project.api.entity'; +import { AppModule } from 'app.module'; +import { Test, TestingModule } from '@nestjs/testing'; +import { E2E_CONFIG } from './e2e.config'; +import { OrganizationsTestUtils } from './utils/organizations.test.utils'; +import { ProjectsTestUtils } from './utils/projects.test.utils'; +import { ScenariosTestUtils } from './utils/scenarios.test.utils'; +import { IUCNCategory } from 'modules/protected-areas/protected-area.geo.entity'; + +const logger = new Logger('test-vtiles') afterAll(async () => { await tearDown(); }); -// import { ProxyService } from '../src/modules/proxy/proxy.service' -describe.skip('ProxyVectorTilesModule (e2e)', () => { +describe('ProxyVectorTilesModule (e2e)', () => { let app: INestApplication; - let jwtToken: string; + const Deserializer = new JSONAPISerializer.Deserializer({ + keyForAttribute: 'camelCase', + }); + let anOrganization: Organization; + let aProjectWithCountryAsPlanningArea: Project; + let aScenario: Scenario; + /** + * Seed test data includes protected areas in (among a handful of other + * countries) Namibia, so we create a project in this country, and likewise + * for tests related to protected areas in a L1 or L2 admin area below. + * + */ + const country = 'NAM'; + const l1AdminArea = 'NAM.13_1'; + const l2AdminArea = 'NAM.13.5_1'; + const geoFeaturesFilters = { + cheeta: { featureClassName: 'iucn_acinonyxjubatus', alias: 'cheetah' }, + partialMatches: { us: 'us' }, + }; beforeAll(async () => { - app = await bootstrapApplication(); - jwtToken = await GivenUserIsLoggedIn(app); + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = moduleFixture.createNestApplication(); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + }), + ); + await app.init(); + + const response = await request(app.getHttpServer()) + .post('/auth/sign-in') + .send({ + username: E2E_CONFIG.users.basic.aa.username, + password: E2E_CONFIG.users.basic.aa.password, + }) + .expect(201); + + jwtToken = response.body.accessToken; + + anOrganization = await OrganizationsTestUtils.createOrganization( + app, + jwtToken, + E2E_CONFIG.organizations.valid.minimal(), + ).then(async (response) => { + return await Deserializer.deserialize(response); + }); + + aProjectWithCountryAsPlanningArea = await ProjectsTestUtils.createProject( + app, + jwtToken, + { + ...E2E_CONFIG.projects.valid.minimalInGivenAdminArea({ + countryCode: country, + adminAreaLevel1Id: l1AdminArea, + adminAreaLevel2Id: l2AdminArea, + }), + organizationId: anOrganization.id, + }, + ).then(async (response) => await Deserializer.deserialize(response)); + + aScenario = await ScenariosTestUtils.createScenario( + app, + jwtToken, + { + ...E2E_CONFIG.scenarios.valid.minimal(), + projectId: aProjectWithCountryAsPlanningArea.id, + wdpaIucnCategories: [IUCNCategory.NotReported], + }, + ).then(async (response) => await Deserializer.deserialize(response)); + }); afterAll(async () => { + await ScenariosTestUtils.deleteScenario( + app, + jwtToken, + aScenario.id, + ); + + await ProjectsTestUtils.deleteProject( + app, + jwtToken, + aProjectWithCountryAsPlanningArea.id, + ); + await OrganizationsTestUtils.deleteOrganization( + app, + jwtToken, + anOrganization.id, + ); await Promise.all([app.close()]); }); - describe('Proxy administrative areas', () => { - // Make sure we have GADM data for this country in the test data which - // is used to populate the geodb in CI pipelines. + describe('Vector Layers', () => { + /** + * https://www.figma.com/file/hq0BZNB9fzyFSbEUgQIHdK/Marxan-Visual_V02?node-id=2991%3A2492 + */ + describe('Admin-areas layers', () => { + test.todo( + 'we should test that the response is a valid mvt', + ); + test('Should give back a valid request for preview', + async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/1/preview/tiles/6/30/25.mvt') + .set('Accept-Encoding', 'gzip, deflate') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK) + }) + describe('Filter by guid',() => { + test.skip('guid country level', + async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); + }); + test.skip('guid adm1 level', + async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); + }); + }); - it( - 'Should give back a valid request for admin areas', + test.skip('Filter by bbox', + async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); + }); + + test('Should simulate an error if input is invalid', async () => { - return request(app.getHttpServer()) - .get('/api/v1/administrative-areas/1/preview/tiles/6/30/25.mvt') - .set('Accept-Encoding', 'gzip, deflate') + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.BAD_REQUEST); + }); + + test('Should throw a 400 error if filtering by level other than 0, 1 or 2', + async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/3/preview/tiles/6/30/25.mvt') .set('Authorization', `Bearer ${jwtToken}`) - .expect(200); - }, - 10 * 1000, - ); + .expect(HttpStatus.BAD_REQUEST); + }); - /** - * @todo add error respone in main code - */ - it('Should simulate an error', async () => { - return request(app.getHttpServer()) - .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') - .set('Authorization', `Bearer ${jwtToken}`) - .expect(400); + test('Should throw a 400 error if filtering by z level greater than 20', + async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.BAD_REQUEST); + }); }); - - /** - * @todo add level restriction on endpoint - */ - it('Should throw a 400 error if filtering by level other than 0, 1 or 2', async () => { - return request(app.getHttpServer()) - .get('/api/v1/administrative-areas/3/preview/tiles/6/30/25.mvt') - .set('Authorization', `Bearer ${jwtToken}`) - .expect(400); + describe('WDPA layers', () => { + test.skip('Should give back a valid request for wdpa preview', + async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); + }); }); + describe('Feature layer previews', () => { + test('Should give back a valid request for a feature preview', + async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') + .set('Authorization', `Bearer ${jwtToken}`); - /** - * @todo add zoom level restriction on endpoint - */ - it('Should throw a 400 error if filtering by z level greater than 20', async () => { - return request(app.getHttpServer()) - .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') - .set('Authorization', `Bearer ${jwtToken}`) - .expect(400); + logger.error(typeof(response.body)) + // response.expect(HttpStatus.OK); + }); + }); + describe('PUs layer previews', () => { + test('Should give back a valid request for a PUs preview', + async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); + }); + }); + describe('Scenario PUs layers', () => { + test.skip('Should give back a valid request for a scenario PUs', + async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); + }); }); - - /** - * @todo add invalid tile test - * @todo add mvt format test - */ }); }); diff --git a/api/package.json b/api/package.json index 64b01606a1..e84b115508 100644 --- a/api/package.json +++ b/api/package.json @@ -61,7 +61,11 @@ "http-proxy": "^1.18.1", "jsonapi-serializer": "^3.6.7", "lodash": "^4.17.20", +<<<<<<< HEAD "mapshaper": "0.5.57", +======= + "mapshaper": "0.5.51", +>>>>>>> add decoder for vector layers so we can validate them "ms": "^2.1.3", "multer": "^1.4.2", "nestjs-base-service": "0.6.0", @@ -98,6 +102,7 @@ "@types/node": "^14.14.6", "@types/passport-jwt": "^3.0.3", "@types/passport-local": "^1.0.33", + "@types/pbf": "^3.0.2", "@types/supertest": "^2.0.10", "@types/unzipper": "^0.10.3", "@types/uuid": "8.3.0", @@ -113,6 +118,7 @@ "jest": "^26.6.3", "lint-staged": "^10.5.2", "nodemon": "^2.0.6", + "pbf": "^3.2.1", "prettier": "^2.1.2", "supertest": "^6.0.0", "ts-jest": "^26.4.3", diff --git a/api/yarn.lock b/api/yarn.lock index a3e39f0e32..c551144db6 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -2213,6 +2213,11 @@ dependencies: "@types/express" "*" +"@types/pbf@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.2.tgz#8d291ad68b4b8c533e96c174a2e3e6399a59ed61" + integrity sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ== + "@types/prettier@^2.0.0": version "2.1.6" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.6.tgz#f4b1efa784e8db479cdb8b14403e2144b1e9ff03" @@ -5294,7 +5299,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@^1.1.13: +ieee754@^1.1.12, ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -7526,6 +7531,14 @@ pause@0.0.1: resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= +pbf@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a" + integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ== + dependencies: + ieee754 "^1.1.12" + resolve-protobuf-schema "^2.1.0" + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -7736,6 +7749,11 @@ prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +protocol-buffers-schema@^3.3.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz#8388e768d383ac8cbea23e1280dfadb79f4122ad" + integrity sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw== + proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -8155,6 +8173,13 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-protobuf-schema@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758" + integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ== + dependencies: + protocol-buffers-schema "^3.3.1" + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" From 2c5b79c89b90f38b304ed0febee263b024e81720 Mon Sep 17 00:00:00 2001 From: Alicia Date: Mon, 7 Jun 2021 15:37:03 +0200 Subject: [PATCH 06/21] wip --- .../protected-areas.controller.ts | 2 +- .../protected-areas/protected-areas.module.ts | 2 +- .../api/src/modules/proxy/proxy.controller.ts | 229 ------------------ api/package.json | 6 +- 4 files changed, 3 insertions(+), 236 deletions(-) delete mode 100644 api/apps/api/src/modules/proxy/proxy.controller.ts diff --git a/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts b/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts index 570d981073..e59a944331 100644 --- a/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts +++ b/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts @@ -33,7 +33,7 @@ import { } from './protected-areas.service'; import { IUCNProtectedAreaCategoryResult } from './dto/iucn-protected-area-category.dto'; import { Request, Response } from 'express'; -import { ProxyService } from 'modules/proxy/proxy.service'; +import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; @UseGuards(JwtAuthGuard) @ApiBearerAuth() diff --git a/api/apps/api/src/modules/protected-areas/protected-areas.module.ts b/api/apps/api/src/modules/protected-areas/protected-areas.module.ts index 39b6e73e97..d2ec4cfa8a 100644 --- a/api/apps/api/src/modules/protected-areas/protected-areas.module.ts +++ b/api/apps/api/src/modules/protected-areas/protected-areas.module.ts @@ -5,7 +5,7 @@ import { ProtectedAreasController } from './protected-areas.controller'; import { ProtectedArea } from './protected-area.geo.entity'; import { ProtectedAreasService } from './protected-areas.service'; import { apiConnections } from '../../ormconfig'; -import { ProxyService } from 'modules/proxy/proxy.service'; +import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; @Module({ imports: [ diff --git a/api/apps/api/src/modules/proxy/proxy.controller.ts b/api/apps/api/src/modules/proxy/proxy.controller.ts deleted file mode 100644 index 6d680882bf..0000000000 --- a/api/apps/api/src/modules/proxy/proxy.controller.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { Controller, Req, Res, UseGuards, Get, Param } from '@nestjs/common'; -import { - ApiBearerAuth, - ApiForbiddenResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { JwtAuthGuard } from '@marxan-api/guards/jwt-auth.guard'; -import { apiGlobalPrefixes } from '@marxan-api/api.config'; -import { ProxyService } from './proxy.service'; -import { Request, Response } from 'express'; - -@UseGuards(JwtAuthGuard) -@ApiBearerAuth() -@ApiTags('Vector tile proxy') -@Controller(`${apiGlobalPrefixes.v1}`) -export class ProxyController { - constructor(private readonly proxyService: ProxyService) {} - - @ApiOperation({ - description: - 'Find administrative areas within a given country in mvt format.', - }) - /** - *@todo Change ApiOkResponse mvt type - */ - @ApiOkResponse({ - type: 'mvt', - }) - @ApiUnauthorizedResponse() - @ApiForbiddenResponse() - @ApiParam({ - name: 'z', - description: 'The zoom level ranging from 0 - 20', - type: Number, - required: true, - }) - @ApiParam({ - name: 'x', - description: 'The tile x offset on Mercator Projection', - type: Number, - required: true, - }) - @ApiParam({ - name: 'y', - description: 'The tile y offset on Mercator Projection', - type: Number, - required: true, - }) - @ApiParam({ - name: 'level', - description: - 'Specific level to filter the administrative areas (0, 1 or 2)', - type: Number, - required: true, - example: '1', - }) - @ApiQuery({ - name: 'guid', - description: 'Parent country of administrative areas in ISO code', - type: String, - required: false, - example: 'BRA.1_1', - }) - @ApiQuery({ - name: 'bbox', - description: 'Bounding box of the project', - type: [Number], - required: false, - example: [-1, 40, 1, 42], - }) - @Get('/administrative-areas/:level/preview/tiles/:z/:x/:y.mvt') - async proxyAdminAreaTile(@Req() request: Request, @Res() response: Response) { - return this.proxyService.proxyTileRequest(request, response); - } - - @ApiOperation({ - description: 'Get tile for protected areas.', - }) - /** - *@todo Change ApiOkResponse mvt type - */ - @ApiOkResponse({ - type: 'mvt', - }) - @ApiUnauthorizedResponse() - @ApiForbiddenResponse() - @ApiParam({ - name: 'z', - description: 'The zoom level ranging from 0 - 20', - type: Number, - required: true, - }) - @ApiParam({ - name: 'x', - description: 'The tile x offset on Mercator Projection', - type: Number, - required: true, - }) - @ApiParam({ - name: 'y', - description: 'The tile y offset on Mercator Projection', - type: Number, - required: true, - }) - @ApiQuery({ - name: 'id', - description: 'Id of WDPA area', - type: String, - required: false, - example: 'e5c3b978-908c-49d3-b1e3-89727e9f999c', - }) - @Get('/protected-areas/preview/tiles/:z/:x/:y.mvt') - async proxyProtectedAreaTile( - @Req() request: Request, - @Res() response: Response, - ) { - return this.proxyService.proxyTileRequest(request, response); - } - - @ApiOperation({ - description: 'Get tile for a feature by id.', - }) - /** - *@todo Change ApiOkResponse mvt type - */ - @ApiOkResponse({ - type: 'mvt', - }) - @ApiUnauthorizedResponse() - @ApiForbiddenResponse() - @ApiParam({ - name: 'z', - description: 'The zoom level ranging from 0 - 20', - type: Number, - required: true, - }) - @ApiParam({ - name: 'x', - description: 'The tile x offset on Mercator Projection', - type: Number, - required: true, - }) - @ApiParam({ - name: 'y', - description: 'The tile y offset on Mercator Projection', - type: Number, - required: true, - }) - @ApiParam({ - name: 'id', - description: 'Specific id of the feature', - type: String, - required: true, - }) - @ApiQuery({ - name: 'bbox', - description: 'Bounding box of the project', - type: Array, - required: false, - example: [-1, 40, 1, 42], - }) - @Get('/features/:id/preview/tiles/:z/:x/:y.mvt') - async proxyFeaturesTile(@Req() request: Request, @Res() response: Response) { - return this.proxyService.proxyTileRequest(request, response); - } - - @ApiOperation({ - description: 'Get planning unit geometries.', - }) - /** - *@todo Change ApiOkResponse mvt type - */ - @ApiOkResponse({ - type: 'mvt', - }) - @ApiUnauthorizedResponse() - @ApiForbiddenResponse() - @ApiParam({ - name: 'z', - description: 'The zoom level ranging from 0 - 20', - type: Number, - required: true, - }) - @ApiParam({ - name: 'x', - description: 'The tile x offset on Mercator Projection', - type: Number, - required: true, - }) - @ApiParam({ - name: 'y', - description: 'The tile y offset on Mercator Projection', - type: Number, - required: true, - }) - @ApiParam({ - name: 'planningUnitGridShape', - description: 'Planning unit grid shape', - type: String, - required: true, - }) - @ApiParam({ - name: 'planningUnitAreakm2', - description: 'Planning unit area in km2', - type: Number, - required: true, - }) - @ApiQuery({ - name: 'bbox', - description: 'Bounding box of the project', - type: Array, - required: false, - example: [-1, 40, 1, 42], - }) - @Get( - '/planning-units/preview/regular/:planningUnitGridShape/:planningUnitAreakm2/tiles/:z/:x/:y.mvt', - ) - async proxyProtectedAreasTile( - @Req() request: Request, - @Res() response: Response, - ) { - return this.proxyService.proxyTileRequest(request, response); - } -} diff --git a/api/package.json b/api/package.json index e84b115508..e76a4cf372 100644 --- a/api/package.json +++ b/api/package.json @@ -61,11 +61,7 @@ "http-proxy": "^1.18.1", "jsonapi-serializer": "^3.6.7", "lodash": "^4.17.20", -<<<<<<< HEAD "mapshaper": "0.5.57", -======= - "mapshaper": "0.5.51", ->>>>>>> add decoder for vector layers so we can validate them "ms": "^2.1.3", "multer": "^1.4.2", "nestjs-base-service": "0.6.0", @@ -162,4 +158,4 @@ "@marxan/api-events": "/libs/api-events/src" } } -} \ No newline at end of file +} From cf700f8cb99b418bcf5c5f9ccf564c05d7461269 Mon Sep 17 00:00:00 2001 From: Alicia Date: Mon, 7 Jun 2021 16:11:29 +0200 Subject: [PATCH 07/21] fixed link to marxan-api --- api/apps/api/src/app.module.ts | 6 +++--- .../src/modules/admin-areas/admin-areas.controller.ts | 2 +- .../api/src/modules/admin-areas/admin-areas.module.ts | 2 +- .../modules/geo-features/geo-features.controller.ts | 2 +- .../src/modules/geo-features/geo-features.module.ts | 2 +- .../planning-units/planning-units.controller.ts | 6 +++--- .../modules/planning-units/planning-units.module.ts | 2 +- api/apps/api/test/proxy.vector-tiles.e2e-spec.ts | 10 +++++----- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/api/apps/api/src/app.module.ts b/api/apps/api/src/app.module.ts index fe828aabc3..d9485b4d5f 100644 --- a/api/apps/api/src/app.module.ts +++ b/api/apps/api/src/app.module.ts @@ -26,9 +26,9 @@ import { ApiEventsModule } from '@marxan-api/modules/api-events/api-events.modul import { ProtectedAreasModule } from '@marxan-api/modules/protected-areas/protected-areas.module'; import { ProxyModule } from '@marxan-api/modules/proxy/proxy.module'; import { ScenariosPlanningUnitModule } from './modules/scenarios-planning-unit/scenarios-planning-unit.module'; -import { PlanningUnitsProtectionLevelModule } from './modules/planning-units-protection-level'; -import { AnalysisModule } from './modules/analysis/analysis.module'; -import { PlanningUnitsModule } from 'modules/planning-units/planning-units.module'; +import { PlanningUnitsProtectionLevelModule } from '@marxan-api/modules/planning-units-protection-level'; +import { AnalysisModule } from '@marxan-api/modules/analysis/analysis.module'; +import { PlanningUnitsModule } from '@marxan-api/modules/planning-units/planning-units.module'; @Module({ imports: [ diff --git a/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts b/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts index 140d57e8e4..a0803a3809 100644 --- a/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts +++ b/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts @@ -18,7 +18,7 @@ import { FetchSpecification, ProcessFetchSpecification, } from 'nestjs-base-service'; -import { ProxyService } from 'modules/proxy/proxy.service'; +import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; import { Request, Response } from 'express'; @UseGuards(JwtAuthGuard) diff --git a/api/apps/api/src/modules/admin-areas/admin-areas.module.ts b/api/apps/api/src/modules/admin-areas/admin-areas.module.ts index 8c02ebae19..adafc6aece 100644 --- a/api/apps/api/src/modules/admin-areas/admin-areas.module.ts +++ b/api/apps/api/src/modules/admin-areas/admin-areas.module.ts @@ -5,7 +5,7 @@ import { AdminArea } from '@marxan/admin-regions'; import { AdminAreasController } from './admin-areas.controller'; import { AdminAreasService } from './admin-areas.service'; import { apiConnections } from '../../ormconfig'; -import { ProxyService } from 'modules/proxy/proxy.service'; +import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; @Module({ imports: [ diff --git a/api/apps/api/src/modules/geo-features/geo-features.controller.ts b/api/apps/api/src/modules/geo-features/geo-features.controller.ts index 6b9968f962..17e743df81 100644 --- a/api/apps/api/src/modules/geo-features/geo-features.controller.ts +++ b/api/apps/api/src/modules/geo-features/geo-features.controller.ts @@ -22,7 +22,7 @@ import { ProcessFetchSpecification, } from 'nestjs-base-service'; import { Request, Response } from 'express'; -import { ProxyService } from 'modules/proxy/proxy.service'; +import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; import { BBox} from 'geojson'; @UseGuards(JwtAuthGuard) diff --git a/api/apps/api/src/modules/geo-features/geo-features.module.ts b/api/apps/api/src/modules/geo-features/geo-features.module.ts index af20b90dd0..c450bb3f24 100644 --- a/api/apps/api/src/modules/geo-features/geo-features.module.ts +++ b/api/apps/api/src/modules/geo-features/geo-features.module.ts @@ -10,7 +10,7 @@ import { import { GeoFeaturesController } from './geo-features.controller'; import { GeoFeaturesService } from './geo-features.service'; import { apiConnections } from '../../ormconfig'; -import { ProxyService } from 'modules/proxy/proxy.service'; +import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; @Module({ imports: [ diff --git a/api/apps/api/src/modules/planning-units/planning-units.controller.ts b/api/apps/api/src/modules/planning-units/planning-units.controller.ts index a5bff83085..50b7d79b74 100644 --- a/api/apps/api/src/modules/planning-units/planning-units.controller.ts +++ b/api/apps/api/src/modules/planning-units/planning-units.controller.ts @@ -1,8 +1,8 @@ import { Controller, UseGuards,Req, Res, Get} from "@nestjs/common"; import { ApiBearerAuth, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger"; -import { apiGlobalPrefixes } from "api.config"; -import { JwtAuthGuard } from "guards/jwt-auth.guard"; -import { ProxyService } from "modules/proxy/proxy.service"; +import { apiGlobalPrefixes } from "@marxan-api/api.config"; +import { JwtAuthGuard } from "@marxan-api/guards/jwt-auth.guard"; +import { ProxyService } from "@marxan-api/modules/proxy/proxy.service"; import { Request, Response } from 'express'; @UseGuards(JwtAuthGuard) diff --git a/api/apps/api/src/modules/planning-units/planning-units.module.ts b/api/apps/api/src/modules/planning-units/planning-units.module.ts index d66739a194..72e1de82c5 100644 --- a/api/apps/api/src/modules/planning-units/planning-units.module.ts +++ b/api/apps/api/src/modules/planning-units/planning-units.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { ProxyService } from 'modules/proxy/proxy.service'; +import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; import { PlanningUnitsController } from './planning-units.controller'; import { PlanningUnitsService } from './planning-units.service'; diff --git a/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts b/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts index 08e60066c6..cd5e8c1cdd 100644 --- a/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts +++ b/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts @@ -3,16 +3,16 @@ import * as request from 'supertest'; import * as JSONAPISerializer from 'jsonapi-serializer'; import PBF from 'pbf'; import { tearDown } from './utils/tear-down'; -import { Scenario } from 'modules/scenarios/scenario.api.entity'; -import { Organization } from 'modules/organizations/organization.api.entity'; -import { Project } from 'modules/projects/project.api.entity'; -import { AppModule } from 'app.module'; +import { Scenario } from '@marxan-api/modules/scenarios/scenario.api.entity'; +import { Organization } from '@marxan-api/modules/organizations/organization.api.entity'; +import { Project } from '@marxan-api/modules/projects/project.api.entity'; +import { AppModule } from '@marxan-api/app.module'; import { Test, TestingModule } from '@nestjs/testing'; import { E2E_CONFIG } from './e2e.config'; import { OrganizationsTestUtils } from './utils/organizations.test.utils'; import { ProjectsTestUtils } from './utils/projects.test.utils'; import { ScenariosTestUtils } from './utils/scenarios.test.utils'; -import { IUCNCategory } from 'modules/protected-areas/protected-area.geo.entity'; +import { IUCNCategory } from '@marxan-api/modules/protected-areas/protected-area.geo.entity'; const logger = new Logger('test-vtiles') From 3264ad0d80134880b7694d0150b2b3f478ca1b1e Mon Sep 17 00:00:00 2001 From: Alicia Date: Tue, 8 Jun 2021 09:55:34 +0200 Subject: [PATCH 08/21] wip --- .../modules/planning-units/planning-units.controller.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/apps/api/src/modules/planning-units/planning-units.controller.ts b/api/apps/api/src/modules/planning-units/planning-units.controller.ts index 50b7d79b74..ae9418892b 100644 --- a/api/apps/api/src/modules/planning-units/planning-units.controller.ts +++ b/api/apps/api/src/modules/planning-units/planning-units.controller.ts @@ -4,6 +4,8 @@ import { apiGlobalPrefixes } from "@marxan-api/api.config"; import { JwtAuthGuard } from "@marxan-api/guards/jwt-auth.guard"; import { ProxyService } from "@marxan-api/modules/proxy/proxy.service"; import { Request, Response } from 'express'; +import { Binary } from "typeorm"; +import { string } from "purify-ts"; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @@ -18,10 +20,11 @@ export class PlanningUnitsController { description: 'Get planning unit geometries.', }) /** - *@todo Change ApiOkResponse mvt type + *@reference https://docs.ogc.org/per/19-069.html#_openapi_example_for_vector_tiles */ @ApiOkResponse({ - type: 'mvt', + description: 'Binary tile succesful retrieve', + type: String }) @ApiUnauthorizedResponse() @ApiForbiddenResponse() From 18fd0edc49af325682918195a9137a9f46a7b6b3 Mon Sep 17 00:00:00 2001 From: Alicia Date: Tue, 8 Jun 2021 10:40:11 +0200 Subject: [PATCH 09/21] adjust bbox description --- .../api/src/modules/admin-areas/admin-areas.controller.ts | 2 +- .../src/modules/geo-features/geo-features.controller.ts | 2 +- .../modules/planning-units/planning-units.controller.ts | 2 +- .../modules/protected-areas/protected-areas.controller.ts | 7 +++++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts b/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts index a0803a3809..dfc199ba31 100644 --- a/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts +++ b/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts @@ -112,7 +112,7 @@ export class AdminAreasController { }) @ApiQuery({ name: 'bbox', - description: 'Bounding box of the project', + description: 'Bounding box of the project [xMin, xMax, yMin, yMax]', type: [Number], required: false, example: [-1, 40, 1, 42], diff --git a/api/apps/api/src/modules/geo-features/geo-features.controller.ts b/api/apps/api/src/modules/geo-features/geo-features.controller.ts index 17e743df81..092a8c0b34 100644 --- a/api/apps/api/src/modules/geo-features/geo-features.controller.ts +++ b/api/apps/api/src/modules/geo-features/geo-features.controller.ts @@ -89,7 +89,7 @@ export class GeoFeaturesController { }) @ApiQuery({ name: 'bbox', - description: 'Bounding box of the project', + description: 'Bounding box of the project [xMin, xMax, yMin, yMax]', type: [Number], required: false, example: [-1, 40, 1, 42], diff --git a/api/apps/api/src/modules/planning-units/planning-units.controller.ts b/api/apps/api/src/modules/planning-units/planning-units.controller.ts index ae9418892b..118843f5cc 100644 --- a/api/apps/api/src/modules/planning-units/planning-units.controller.ts +++ b/api/apps/api/src/modules/planning-units/planning-units.controller.ts @@ -60,7 +60,7 @@ export class PlanningUnitsController { }) @ApiQuery({ name: 'bbox', - description: 'Bounding box of the project', + description: 'Bounding box of the project [xMin, xMax, yMin, yMax]', type: [Number], required: false, example: [-1, 40, 1, 42], diff --git a/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts b/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts index e59a944331..dddf486d6d 100644 --- a/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts +++ b/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts @@ -104,6 +104,13 @@ export class ProtectedAreasController { required: false, example: 'e5c3b978-908c-49d3-b1e3-89727e9f999c', }) + @ApiQuery({ + name: 'bbox', + description: 'Bounding box of the project [xMin, xMax, yMin, yMax]', + type: [Number], + required: false, + example: [-1, 40, 1, 42], + }) @Get('/preview/tiles/:z/:x/:y.mvt') async proxyProtectedAreaTile( @Req() request: Request, From f5cb20e30fa2f71636f677fc7a96ef1edd97b3a6 Mon Sep 17 00:00:00 2001 From: Alicia Date: Tue, 8 Jun 2021 11:06:40 +0200 Subject: [PATCH 10/21] fixed rebase issues with queue import module in planning units --- .../api/src/modules/planning-units/planning-units.module.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/apps/api/src/modules/planning-units/planning-units.module.ts b/api/apps/api/src/modules/planning-units/planning-units.module.ts index 72e1de82c5..389d1a6636 100644 --- a/api/apps/api/src/modules/planning-units/planning-units.module.ts +++ b/api/apps/api/src/modules/planning-units/planning-units.module.ts @@ -2,8 +2,14 @@ import { Module } from '@nestjs/common'; import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; import { PlanningUnitsController } from './planning-units.controller'; import { PlanningUnitsService } from './planning-units.service'; +import { QueueModule } from '@marxan-api/modules/queue/queue.module'; @Module({ + imports: [ + QueueModule.register({ + name: 'planning-units', + }), + ], providers: [PlanningUnitsService, ProxyService], exports: [PlanningUnitsService], controllers: [PlanningUnitsController], From 810333f1a5f9e9d94bcd5e1650b5cd89b2143d4b Mon Sep 17 00:00:00 2001 From: Alicia Date: Tue, 8 Jun 2021 12:04:32 +0200 Subject: [PATCH 11/21] fixed some issues with openapi spec that were hurting my eyes --- .../api/src/decorators/shapefile.decorator.ts | 3 +- api/apps/api/src/main.ts | 10 +- .../api/src/modules/users/users.controller.ts | 5 +- api/apps/api/src/swagger.json | 4201 +++++++++++++++++ .../src/decoratos/shapefile.decorator.ts | 3 +- api/package.json | 2 +- api/yarn.lock | 32 +- 7 files changed, 4230 insertions(+), 26 deletions(-) create mode 100644 api/apps/api/src/swagger.json diff --git a/api/apps/api/src/decorators/shapefile.decorator.ts b/api/apps/api/src/decorators/shapefile.decorator.ts index c650fe9cb7..d021b64b32 100644 --- a/api/apps/api/src/decorators/shapefile.decorator.ts +++ b/api/apps/api/src/decorators/shapefile.decorator.ts @@ -22,7 +22,8 @@ export function ApiConsumesShapefile(withGeoJsonResponse = true) { type: 'object', properties: { file: { - type: 'Zip file containing .shp, .dbj, .prj and .shx files', + description: 'Zip file containing .shp, .dbj, .prj and .shx files', + type: 'string', format: 'binary', }, }, diff --git a/api/apps/api/src/main.ts b/api/apps/api/src/main.ts index ba0af386da..f0e4b58a8c 100644 --- a/api/apps/api/src/main.ts +++ b/api/apps/api/src/main.ts @@ -7,6 +7,7 @@ import { CorsUtils } from './utils/cors.utils'; import { AppConfig } from '@marxan-api/utils/config.utils'; import { ValidationPipe } from '@nestjs/common'; import { AllExceptionsFilter } from '@marxan-api/filters/all-exceptions.exception.filter'; +import {writeFileSync} from "fs"; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -32,12 +33,13 @@ async function bootstrap() { .setDescription('MarxanCloud is a conservation planning platform.') .setVersion(process.env.npm_package_version || 'development') .addBearerAuth({ - type: 'apiKey', - in: 'header', - name: 'Authorization', - }) + type: 'http', + }, 'BearerAuth') .build(); const swaggerDocument = SwaggerModule.createDocument(app, swaggerOptions); + + writeFileSync("./swagger.json", JSON.stringify(swaggerDocument)); + SwaggerModule.setup('/swagger', app, swaggerDocument); app.useGlobalPipes( diff --git a/api/apps/api/src/modules/users/users.controller.ts b/api/apps/api/src/modules/users/users.controller.ts index bc9c52f9eb..f57e860cdc 100644 --- a/api/apps/api/src/modules/users/users.controller.ts +++ b/api/apps/api/src/modules/users/users.controller.ts @@ -16,7 +16,6 @@ import { ApiForbiddenResponse, ApiOkResponse, ApiOperation, - ApiResponse, ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; @@ -41,7 +40,7 @@ export class UsersController { @ApiOperation({ description: 'Find all users', }) - @ApiResponse({ + @ApiOkResponse({ type: User, }) @ApiUnauthorizedResponse({ @@ -95,7 +94,7 @@ export class UsersController { @ApiOperation({ description: 'Retrieve attributes of the current user', }) - @ApiResponse({ + @ApiOkResponse({ type: UserResult, }) @ApiUnauthorizedResponse({ diff --git a/api/apps/api/src/swagger.json b/api/apps/api/src/swagger.json new file mode 100644 index 0000000000..d0bc6ad193 --- /dev/null +++ b/api/apps/api/src/swagger.json @@ -0,0 +1,4201 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "MarxanCloud API", + "description": "MarxanCloud is a conservation planning platform.", + "version": "0.4.0", + "contact": {} + }, + "tags": [], + "servers": [], + "components": { + "securitySchemes": { + "BearerAuth": { + "scheme": "bearer", + "bearerFormat": "JWT", + "type": "http" + } + }, + "schemas": { + "AdminArea": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "gid0": { + "type": "object" + }, + "name0": { + "type": "object" + }, + "gid1": { + "type": "object" + }, + "name1": { + "type": "object" + }, + "gid2": { + "type": "object" + }, + "name2": { + "type": "object" + }, + "iso3": { + "type": "object" + }, + "level": { + "type": "string" + }, + "theGeom": { + "type": "object" + }, + "bbox": { + "type": "object" + } + }, + "required": ["id", "level", "theGeom", "bbox"] + }, + "JSONAPIAdminAreaData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/AdminArea" + } + }, + "required": ["type", "id", "attributes"] + }, + "AdminAreaResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JSONAPIAdminAreaData" + } + }, + "required": ["data"] + }, + "ApiEvent": { + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "topic": { + "type": "string" + } + }, + "required": ["kind", "topic"] + }, + "JSONAPIApiEventData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/ApiEvent" + } + }, + "required": ["type", "id", "attributes"] + }, + "ApiEventResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JSONAPIApiEventData" + } + }, + "required": ["data"] + }, + "CreateApiEventDTO": { + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "topic": { + "type": "string" + }, + "data": { + "type": "object" + } + }, + "required": ["kind", "topic"] + }, + "Country": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "gid0": { + "type": "string" + }, + "name0": { + "type": "string" + }, + "theGeom": { + "type": "object" + }, + "bbox": { + "type": "object" + } + }, + "required": ["id", "gid0", "name0", "theGeom", "bbox"] + }, + "JSONAPICountryData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/Country" + } + }, + "required": ["type", "id", "attributes"] + }, + "CountryResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JSONAPICountryData" + } + }, + "required": ["data"] + }, + "GeoFeatureGeometry": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"] + }, + "JSONAPIGeoFeaturesGeometryData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/GeoFeatureGeometry" + } + }, + "required": ["type", "id", "attributes"] + }, + "GeoFeatureResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JSONAPIGeoFeaturesData" + } + }, + "required": ["data"] + }, + "OrganizationResultPlural": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["data"] + }, + "ScenarioType": { + "type": "string", + "enum": ["marxan", "marxan-with-zones"] + }, + "JobStatus": { + "type": "string", + "enum": ["created", "running", "done", "failure"] + }, + "Scenario": { + "type": "object", + "properties": { + "createdAt": { + "format": "date-time", + "type": "string" + }, + "createdByUser": { + "$ref": "#/components/schemas/User" + }, + "lastModifiedAt": { + "format": "date-time", + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/ScenarioType" + }, + "project": { + "$ref": "#/components/schemas/Project" + }, + "wdpaIucnCategories": { + "type": "array", + "items": { + "type": "string" + } + }, + "protectedAreaFilterByIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "wdpaThreshold": { + "type": "object" + }, + "numberOfRuns": { + "type": "number" + }, + "boundaryLengthModifier": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "status": { + "$ref": "#/components/schemas/JobStatus" + }, + "parentScenarioId": { + "type": "string" + }, + "parentScenario": { + "$ref": "#/components/schemas/Scenario" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": [ + "createdAt", + "createdByUser", + "lastModifiedAt", + "id", + "name", + "type", + "project", + "status", + "users" + ] + }, + "Project": { + "type": "object", + "properties": { + "createdAt": { + "format": "date-time", + "type": "string" + }, + "createdByUser": { + "$ref": "#/components/schemas/User" + }, + "lastModifiedAt": { + "format": "date-time", + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "organization": { + "$ref": "#/components/schemas/Organization" + }, + "adminAreaLevel1Id": { + "type": "string" + }, + "adminAreaLevel2Id": { + "type": "string" + }, + "planningUnitGridShape": { + "type": "string" + }, + "planningUnitAreakm2": { + "type": "number" + }, + "extent": { + "type": "object" + }, + "bbox": { + "type": "object" + }, + "metadata": { + "type": "object" + }, + "scenarios": { + "$ref": "#/components/schemas/Scenario" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "planningAreaId": { + "type": "string", + "description": "ID of Country / Gid1 / Gid2 of project's area" + }, + "planningAreaName": { + "type": "string", + "description": "Display name of Country / Gid1 / Gid2 of project's area" + } + }, + "required": [ + "createdAt", + "createdByUser", + "lastModifiedAt", + "id", + "name", + "organization", + "adminAreaLevel1Id", + "adminAreaLevel2Id", + "planningUnitGridShape", + "planningUnitAreakm2", + "bbox", + "users" + ] + }, + "User": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "displayName": { + "type": "object" + }, + "fname": { + "type": "object" + }, + "lname": { + "type": "object" + }, + "avatarDataUrl": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "isActive": { + "type": "boolean" + }, + "isDeleted": { + "type": "boolean" + }, + "projects": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + } + }, + "scenarios": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Scenario" + } + } + }, + "required": ["email", "isActive", "isDeleted", "projects", "scenarios"] + }, + "Organization": { + "type": "object", + "properties": { + "createdAt": { + "format": "date-time", + "type": "string" + }, + "createdByUser": { + "$ref": "#/components/schemas/User" + }, + "lastModifiedAt": { + "format": "date-time", + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "projects": { + "$ref": "#/components/schemas/Project" + } + }, + "required": [ + "createdAt", + "createdByUser", + "lastModifiedAt", + "id", + "name" + ] + }, + "JSONAPIOrganizationData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/Organization" + }, + "relationships": { + "type": "object" + } + }, + "required": ["type", "id", "attributes"] + }, + "OrganizationResultSingular": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JSONAPIOrganizationData" + } + }, + "required": ["data"] + }, + "CreateOrganizationDTO": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": ["name"] + }, + "UpdateOrganizationDTO": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + } + } + }, + "UpdateUserPasswordDTO": { + "type": "object", + "properties": { + "currentPassword": { + "type": "string" + }, + "newPassword": { + "type": "string" + } + }, + "required": ["currentPassword", "newPassword"] + }, + "JSONAPIUserData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/User" + } + }, + "required": ["type", "id", "attributes"] + }, + "UserResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JSONAPIUserData" + } + }, + "required": ["data"] + }, + "UpdateUserDTO": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "displayName": { + "type": "object" + }, + "fname": { + "type": "object" + }, + "lname": { + "type": "object" + }, + "password": { + "type": "string" + }, + "avatarDataUrl": { + "type": "string" + }, + "metadata": { + "type": "object" + } + } + }, + "LoginDto": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["username", "password"] + }, + "SignUpDto": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["email", "password"] + }, + "GeoFeature": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "featureClassName": { + "type": "string" + }, + "description": { + "type": "object" + }, + "source": { + "type": "string" + }, + "alias": { + "type": "object" + }, + "propertyName": { + "type": "string" + }, + "intersection": { + "type": "array", + "items": { + "type": "string" + } + }, + "tag": { + "type": "string" + }, + "properties": { + "type": "array", + "items": { + "type": "string" + } + }, + "projectId": { + "type": "string" + } + }, + "required": ["id", "tag"] + }, + "JSONAPIGeoFeaturesData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/GeoFeature" + } + }, + "required": ["type", "id", "attributes"] + }, + "ProjectResultPlural": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["data"] + }, + "JSONAPIProjectData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/Project" + }, + "relationships": { + "type": "object" + } + }, + "required": ["type", "id", "attributes"] + }, + "ProjectResultSingular": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JSONAPIProjectData" + } + }, + "required": ["data"] + }, + "CreateProjectDTO": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "countryId": { + "type": "string", + "description": "ISO 3166-1 alpha3 country code (uppercase)", + "example": "ESP" + }, + "adminAreaLevel1Id": { + "type": "string" + }, + "adminAreaLevel2Id": { + "type": "string" + }, + "planningUnitGridShape": { + "type": "string" + }, + "planningUnitAreakm2": { + "type": "number" + }, + "extent": { + "type": "object", + "description": "Geometry part of GeoJson; MultiPolygon or Polygon" + }, + "metadata": { + "type": "object" + } + }, + "required": ["name", "organizationId"] + }, + "UpdateProjectDTO": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "countryId": { + "type": "string", + "description": "ISO 3166-1 alpha3 country code (uppercase)", + "example": "ESP" + }, + "adminAreaLevel1Id": { + "type": "string" + }, + "adminAreaLevel2Id": { + "type": "string" + }, + "planningUnitGridShape": { + "type": "string" + }, + "planningUnitAreakm2": { + "type": "number" + }, + "extent": { + "type": "object", + "description": "Geometry part of GeoJson; MultiPolygon or Polygon" + }, + "metadata": { + "type": "object" + } + } + }, + "JSONAPIScenarioData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/Scenario" + } + }, + "required": ["type", "id", "attributes"] + }, + "ScenarioResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JSONAPIScenarioData" + } + }, + "required": ["data"] + }, + "CreateScenarioDTO": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "wdpaIucnCategories": { + "type": "array", + "items": { + "type": "string" + } + }, + "customProtectedAreaIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "wdpaThreshold": { + "type": "number" + }, + "numberOfRuns": { + "type": "number" + }, + "boundaryLengthModifier": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "status": { + "$ref": "#/components/schemas/JobStatus" + } + }, + "required": ["name", "type", "projectId", "status"] + }, + "ShapefileGeoJSONResponseDTO": { + "type": "object", + "properties": { + "data": { + "type": "object" + } + }, + "required": ["data"] + }, + "UpdateScenarioDTO": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "wdpaIucnCategories": { + "type": "array", + "items": { + "type": "string" + } + }, + "customProtectedAreaIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "wdpaThreshold": { + "type": "number" + }, + "numberOfRuns": { + "type": "number" + }, + "boundaryLengthModifier": { + "type": "number" + }, + "metadata": { + "type": "object" + }, + "status": { + "$ref": "#/components/schemas/JobStatus" + } + } + }, + "PlanningUnitsByIdUpdateDto": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PlanningUnitsByGeoJsonUpdateDto": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "UpdateScenarioPlanningUnitLockStatusDto": { + "type": "object", + "properties": { + "byId": { + "$ref": "#/components/schemas/PlanningUnitsByIdUpdateDto" + }, + "byGeoJson": { + "$ref": "#/components/schemas/PlanningUnitsByGeoJsonUpdateDto" + } + } + }, + "RemoteScenarioFeaturesData": { + "type": "object", + "properties": { + "fpf": { + "type": "number", + "description": "Feature Penalty Factor for this feature run." + }, + "target": { + "type": "number", + "description": "Total area space, expressed in m^2" + }, + "coverageTarget": { + "type": "number", + "description": "0-100 (%) value of target protection coverage of all available species." + }, + "coverageTargetArea": { + "type": "number", + "description": "Equivalent of `target` percentage in covered area, expressed in m^2" + }, + "met": { + "type": "number", + "description": "0-100 (%) value of how many species % is protected currently." + }, + "metArea": { + "type": "number", + "description": "Equivalent of `met` percentage in covered area, expressed in m^2" + }, + "onTarget": { + "type": "boolean", + "description": "Shorthand value if current `met` is good enough compared to `target`." + }, + "tag": { + "type": "string", + "enum": ["bioregional", "species"] + }, + "featureId": { + "type": "string" + }, + "name": { + "type": "object", + "description": "Name of the feature, for example `Lion in Deserts`." + }, + "description": { + "type": "object", + "description": "Description of the feature." + } + }, + "required": [ + "fpf", + "target", + "coverageTarget", + "coverageTargetArea", + "met", + "metArea", + "onTarget", + "tag", + "featureId" + ] + }, + "ProtectedArea": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "wdpaId": { + "type": "number" + }, + "fullName": { + "type": "string" + }, + "iucnCategory": { + "type": "string" + }, + "shapeLength": { + "type": "number" + }, + "shapeArea": { + "type": "number" + }, + "countryId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "designation": { + "type": "string" + }, + "theGeom": { + "type": "object" + } + }, + "required": ["id", "wdpaId"] + }, + "JSONAPIProtectedAreaData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/ProtectedArea" + } + }, + "required": ["type", "id", "attributes"] + }, + "ProtectedAreaResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JSONAPIProtectedAreaData" + } + }, + "required": ["data"] + }, + "IUCNProtectedAreaCategoryDTO": { + "type": "object", + "properties": { + "iucnCategory": { + "type": "string" + } + } + }, + "JSONAPIIUCNProtectedAreaCategoryData": { + "type": "object", + "properties": { + "type": { + "type": "object" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/IUCNProtectedAreaCategoryDTO" + } + }, + "required": ["type", "id", "attributes"] + }, + "IUCNProtectedAreaCategoryResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JSONAPIIUCNProtectedAreaCategoryData" + } + }, + "required": ["data"] + } + } + }, + "paths": { + "/ping": { + "get": { + "operationId": "PingController_ping", + "parameters": [], + "responses": { + "200": { + "description": "" + } + } + } + }, + "/api/v1/countries/{countryId}/administrative-areas": { + "get": { + "operationId": "AdminAreasController_findAllAdminAreasInGivenCountry", + "summary": "", + "description": "Find administrative areas within a given country.", + "parameters": [ + { + "name": "countryId", + "required": true, + "in": "path", + "description": "Parent country of administrative areas", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "required": false, + "in": "query", + "description": "Whether to filter for areas of a specific level (1 or 2). By default areas of both level 1 and level 2 areas may be included in the response, if present in the search results.", + "example": "2", + "schema": { + "type": "number" + } + }, + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminAreaResult" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["AdminArea"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/administrative-areas/{level}/preview/tiles/{z}/{x}/{y}.mvt": { + "get": { + "operationId": "AdminAreasController_proxyAdminAreaTile", + "summary": "", + "description": "Find administrative areas within a given country in mvt format.", + "parameters": [ + { + "name": "bbox", + "required": false, + "in": "query", + "description": "Bounding box of the project [xMin, xMax, yMin, yMax]", + "example": [-1, 40, 1, 42], + "schema": { + "type": "array", + "items": { + "type": "number" + } + } + }, + { + "name": "guid", + "required": false, + "in": "query", + "description": "Parent country of administrative areas in ISO code", + "example": "BRA.1_1", + "schema": { + "type": "string" + } + }, + { + "name": "level", + "required": true, + "in": "path", + "description": "Specific level to filter the administrative areas (0, 1 or 2)", + "example": "1", + "schema": { + "type": "number" + } + }, + { + "name": "y", + "required": true, + "in": "path", + "description": "The tile y offset on Mercator Projection", + "schema": { + "type": "number" + } + }, + { + "name": "x", + "required": true, + "in": "path", + "description": "The tile x offset on Mercator Projection", + "schema": { + "type": "number" + } + }, + { + "name": "z", + "required": true, + "in": "path", + "description": "The zoom level ranging from 0 - 20", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["AdminArea"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/administrative-areas/{areaId}/subdivisions": { + "get": { + "operationId": "AdminAreasController_findAllChildrenAdminAreas", + "summary": "", + "description": "Find administrative areas that are children of a given one.", + "parameters": [ + { + "name": "areaId", + "required": true, + "in": "path", + "description": "Parent admin area (gid)", + "schema": { + "type": "string" + } + }, + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminAreaResult" + } + } + } + } + }, + "tags": ["AdminArea"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/administrative-areas/{areaId}": { + "get": { + "operationId": "AdminAreasController_findOne", + "summary": "", + "description": "Find administrative area by id", + "parameters": [ + { + "name": "areaId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminAreaResult" + } + } + } + } + }, + "tags": ["AdminArea"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/api-events": { + "get": { + "operationId": "ApiEventsController_findAll", + "summary": "", + "description": "Find all API events", + "parameters": [ + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiEventResult" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["ApiEvents"], + "security": [ + { + "bearer": [] + } + ] + }, + "post": { + "operationId": "ApiEventsController_create", + "summary": "", + "description": "Create an API event", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiEventDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiEvent" + } + } + } + } + }, + "tags": ["ApiEvents"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/api-events/kind/{kind}/topic/{topic}/latest": { + "get": { + "operationId": "ApiEventsController_findLatestEventByKindAndTopic", + "summary": "", + "description": "Find latest API event by kind for a given topic", + "parameters": [ + { + "name": "kind", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "topic", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiEvent" + } + } + } + } + }, + "tags": ["ApiEvents"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/api-events/kind/{kind}/topic/{topic}": { + "delete": { + "operationId": "ApiEventsController_deleteEventSeriesByKindAndTopic", + "summary": "", + "description": "Delete API event series by kind for a given topic", + "parameters": [ + { + "name": "kind", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "topic", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiEvent" + } + } + } + } + }, + "tags": ["ApiEvents"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/countries": { + "get": { + "operationId": "CountriesController_findAll", + "summary": "", + "description": "Find all countries", + "parameters": [ + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CountryResult" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["Country"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/countries/{id}": { + "get": { + "operationId": "CountriesController_findOne", + "summary": "", + "description": "Find country by id", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CountryResult" + } + } + } + } + }, + "tags": ["Country"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/geo-features": { + "get": { + "operationId": "GeoFeaturesController_findAll", + "summary": "", + "description": "Find all geo features", + "parameters": [ + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeoFeatureResult" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["GeoFeature"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/geo-features/{id}/preview/tiles/{z}/{x}/{y}.mvt": { + "get": { + "operationId": "GeoFeaturesController_proxyFeaturesTile", + "summary": "", + "description": "Get tile for a feature by id.", + "parameters": [ + { + "name": "bbox", + "required": false, + "in": "query", + "description": "Bounding box of the project [xMin, xMax, yMin, yMax]", + "example": [-1, 40, 1, 42], + "schema": { + "type": "array", + "items": { + "type": "number" + } + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "Specific id of the feature", + "schema": { + "type": "string" + } + }, + { + "name": "y", + "required": true, + "in": "path", + "description": "The tile y offset on Mercator Projection", + "schema": { + "type": "number" + } + }, + { + "name": "x", + "required": true, + "in": "path", + "description": "The tile x offset on Mercator Projection", + "schema": { + "type": "number" + } + }, + { + "name": "z", + "required": true, + "in": "path", + "description": "The zoom level ranging from 0 - 20", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["GeoFeature"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/geo-features/{id}": { + "get": { + "operationId": "GeoFeaturesController_findOne", + "summary": "", + "description": "Find geo feature by id", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeoFeatureResult" + } + } + } + } + }, + "tags": ["GeoFeature"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/organizations": { + "get": { + "operationId": "OrganizationsController_findAll", + "summary": "", + "description": "Find all organizations", + "parameters": [ + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `projects`.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationResultPlural" + } + } + } + }, + "401": { + "description": "Unauthorized." + }, + "403": { + "description": "The current user does not have suitable permissions for this request." + } + }, + "tags": ["Organization"], + "security": [ + { + "bearer": [] + } + ] + }, + "post": { + "operationId": "OrganizationsController_create", + "summary": "", + "description": "Create organization", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationDTO" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationResultSingular" + } + } + } + } + }, + "tags": ["Organization"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/organizations/{id}": { + "get": { + "operationId": "OrganizationsController_findOne", + "summary": "", + "description": "Find organization by id", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `projects`.", + "schema": { + "type": "string" + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationResultSingular" + } + } + } + } + }, + "tags": ["Organization"], + "security": [ + { + "bearer": [] + } + ] + }, + "patch": { + "operationId": "OrganizationsController_update", + "summary": "", + "description": "Update organization", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationResultSingular" + } + } + } + } + }, + "tags": ["Organization"], + "security": [ + { + "bearer": [] + } + ] + }, + "delete": { + "operationId": "OrganizationsController_delete", + "summary": "", + "description": "Delete organization", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": ["Organization"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/users": { + "get": { + "operationId": "UsersController_findAll", + "summary": "", + "description": "Find all users", + "parameters": [ + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `projects`.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "401": { + "description": "Unauthorized." + }, + "403": { + "description": "The current user does not have suitable permissions for this request." + } + }, + "tags": ["User"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/users/me/password": { + "patch": { + "operationId": "UsersController_updateOwnPassword", + "summary": "", + "description": "Update the password of a user, if they can present the current one.", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserPasswordDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResult" + } + } + } + } + }, + "tags": ["User"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/users/me": { + "patch": { + "operationId": "UsersController_update", + "summary": "", + "description": "Update a user.", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResult" + } + } + } + } + }, + "tags": ["User"], + "security": [ + { + "bearer": [] + } + ] + }, + "get": { + "operationId": "UsersController_userMetadata", + "summary": "", + "description": "Retrieve attributes of the current user", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResult" + } + } + } + }, + "401": { + "description": "Unauthorized." + }, + "403": { + "description": "The current user does not have suitable permissions for this request." + } + }, + "tags": ["User"], + "security": [ + { + "bearer": [] + } + ] + }, + "delete": { + "operationId": "UsersController_deleteOwnUser", + "summary": "", + "description": "Mark user as deleted.", + "parameters": [], + "responses": { + "200": { + "description": "" + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["User"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/auth/sign-in": { + "post": { + "operationId": "sign-in", + "summary": "Sign user in", + "description": "Sign user in, issuing a JWT token.", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/" + } + } + } + } + }, + "tags": ["Authentication"] + } + }, + "/auth/sign-out": { + "post": { + "operationId": "sign-out", + "summary": "Sign user out", + "description": "Sign user out of all their current sessions by invalidating all the JWT tokens issued to them", + "parameters": [], + "responses": { + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["Authentication"] + } + }, + "/auth/sign-up": { + "post": { + "operationId": "AuthenticationController_signUp", + "summary": "", + "description": "Sign up for a MarxanCloud account.", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignUpDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + }, + "400": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["Authentication"] + } + }, + "/auth/validate-account/{sub}/{validationToken}": { + "get": { + "operationId": "AuthenticationController_confirm", + "summary": "", + "description": "Confirm an activation token for a new user.", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": ["Authentication"] + } + }, + "/auth/refresh-token": { + "post": { + "operationId": "refresh-token", + "summary": "Refresh JWT token", + "description": "Request a fresh JWT token, given a still-valid one for the same user; no request payload is required: the user id is read from the JWT token presented.", + "parameters": [], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/" + } + } + } + }, + "401": { + "description": "" + } + }, + "tags": ["Authentication"] + } + }, + "/api/v1/projects/{projectId}/features": { + "get": { + "operationId": "ProjectsController_findAllGeoFeaturesForProject", + "summary": "", + "description": "Find all geo features", + "parameters": [ + { + "name": "q", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeoFeatureResult" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["Project"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/projects/legacy": { + "post": { + "operationId": "ProjectsController_importLegacyProject", + "summary": "Import a Marxan project", + "description": "Import a Marxan project via file upload", + "parameters": [], + "responses": { + "201": { + "description": "" + } + }, + "tags": ["Project"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/projects": { + "get": { + "operationId": "ProjectsController_findAll", + "summary": "", + "description": "Find all projects", + "parameters": [ + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `scenarios`, `users`.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `organizationId`, `countryId`, `adminAreaLevel1Id`, `adminAreaLevel21Id`.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResultPlural" + } + } + } + } + }, + "tags": ["Project"], + "security": [ + { + "bearer": [] + } + ] + }, + "post": { + "operationId": "ProjectsController_create", + "summary": "", + "description": "Create project", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProjectDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResultSingular" + } + } + } + } + }, + "tags": ["Project"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/projects/{id}": { + "get": { + "operationId": "ProjectsController_findOne", + "summary": "", + "description": "Find project by id", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `scenarios`, `users`.", + "schema": { + "type": "string" + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResultSingular" + } + } + } + } + }, + "tags": ["Project"], + "security": [ + { + "bearer": [] + } + ] + }, + "patch": { + "operationId": "ProjectsController_update", + "summary": "", + "description": "Update project", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProjectDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResultSingular" + } + } + } + } + }, + "tags": ["Project"], + "security": [ + { + "bearer": [] + } + ] + }, + "delete": { + "operationId": "ProjectsController_delete", + "summary": "", + "description": "Delete project", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": ["Project"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/projects/{id}/protected-areas/shapefile": { + "post": { + "operationId": "ProjectsController_shapefileForProtectedArea", + "summary": "", + "description": "Upload Zip file containing .shp, .dbj, .prj and .shx files", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "description": "Zip file containing .shp, .dbj, .prj and .shx files", + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "204": { + "description": "" + } + }, + "tags": ["Project"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/scenarios": { + "get": { + "operationId": "ScenariosController_findAll", + "summary": "", + "description": "Find all scenarios", + "parameters": [ + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `project`, `users`.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `type`, `projectId`, `status`.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScenarioResult" + } + } + } + } + }, + "tags": ["Scenario"], + "security": [ + { + "bearer": [] + } + ] + }, + "post": { + "operationId": "ScenariosController_create", + "summary": "", + "description": "Create scenario", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScenarioDTO" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScenarioResult" + } + } + } + } + }, + "tags": ["Scenario"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/scenarios/{id}": { + "get": { + "operationId": "ScenariosController_findOne", + "summary": "", + "description": "Find scenario by id", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `project`, `users`.", + "schema": { + "type": "string" + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScenarioResult" + } + } + } + } + }, + "tags": ["Scenario"], + "security": [ + { + "bearer": [] + } + ] + }, + "patch": { + "operationId": "ScenariosController_update", + "summary": "", + "description": "Update scenario", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScenarioDTO" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScenarioResult" + } + } + } + } + }, + "tags": ["Scenario"], + "security": [ + { + "bearer": [] + } + ] + }, + "delete": { + "operationId": "ScenariosController_delete", + "summary": "", + "description": "Delete scenario", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": ["Scenario"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/scenarios/{id}/cost-surface/shapefile": { + "post": { + "operationId": "ScenariosController_processCostSurfaceShapefile", + "summary": "", + "description": "Upload Zip file containing .shp, .dbj, .prj and .shx files", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "description": "Zip file containing .shp, .dbj, .prj and .shx files", + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "204": { + "description": "" + } + }, + "tags": ["Scenario"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/scenarios/{id}/planning-unit-shapefile": { + "post": { + "operationId": "ScenariosController_uploadLockInShapeFile", + "summary": "", + "description": "Upload Zip file containing .shp, .dbj, .prj and .shx files", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "description": "Zip file containing .shp, .dbj, .prj and .shx files", + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShapefileGeoJSONResponseDTO" + } + } + } + } + }, + "tags": ["Scenario"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/scenarios/{id}/planning-units": { + "patch": { + "operationId": "ScenariosController_changePlanningUnits", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScenarioPlanningUnitLockStatusDto" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "tags": ["Scenario"], + "security": [ + { + "bearer": [] + } + ] + }, + "get": { + "operationId": "ScenariosController_planningUnitsStatus", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": ["Scenario"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/scenarios/{id}/features": { + "get": { + "operationId": "ScenariosController_getScenarioFeatures", + "summary": "", + "description": "Resolve scenario's features pre-gap data.", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoteScenarioFeaturesData" + } + } + } + } + }, + "tags": ["Scenario"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/protected-areas": { + "get": { + "operationId": "ProtectedAreasController_findAll", + "summary": "", + "description": "Find all protected areas", + "parameters": [ + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `fullName`, `wdpaId`, `iucnCategory`, `status`, `designation`, `countryId`.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtectedAreaResult" + } + } + } + } + }, + "tags": ["ProtectedArea"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/protected-areas/preview/tiles/{z}/{x}/{y}.mvt": { + "get": { + "operationId": "ProtectedAreasController_proxyProtectedAreaTile", + "summary": "", + "description": "Get tile for protected areas.", + "parameters": [ + { + "name": "bbox", + "required": false, + "in": "query", + "description": "Bounding box of the project [xMin, xMax, yMin, yMax]", + "example": [-1, 40, 1, 42], + "schema": { + "type": "array", + "items": { + "type": "number" + } + } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Id of WDPA area", + "example": "e5c3b978-908c-49d3-b1e3-89727e9f999c", + "schema": { + "type": "string" + } + }, + { + "name": "y", + "required": true, + "in": "path", + "description": "The tile y offset on Mercator Projection", + "schema": { + "type": "number" + } + }, + { + "name": "x", + "required": true, + "in": "path", + "description": "The tile x offset on Mercator Projection", + "schema": { + "type": "number" + } + }, + { + "name": "z", + "required": true, + "in": "path", + "description": "The zoom level ranging from 0 - 20", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["ProtectedArea"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/protected-areas/iucn-categories": { + "get": { + "operationId": "ProtectedAreasController_listIUCNProtectedAreaCategories", + "summary": "", + "description": "Find unique IUCN categories among protected areas in a single given administrative area.", + "parameters": [ + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "description": "An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).", + "schema": { + "type": "string" + } + }, + { + "name": "page[size]", + "required": false, + "in": "query", + "description": "Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.", + "schema": { + "type": "number" + } + }, + { + "name": "page[number]", + "required": false, + "in": "query", + "description": "Page number for pagination. If not supplied, the first page of results will be returned.", + "schema": { + "type": "number" + } + }, + { + "name": "disablePagination", + "required": false, + "in": "query", + "description": "If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.", + "schema": { + "type": "boolean" + } + }, + { + "name": "filter[adminAreaId]", + "required": true, + "in": "query", + "description": "Only protected areas within the given admin area will be considered.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IUCNProtectedAreaCategoryResult" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["ProtectedArea"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/protected-areas/{id}": { + "get": { + "operationId": "ProtectedAreasController_findOne", + "summary": "", + "description": "Get protected area by id", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "include", + "required": false, + "in": "query", + "description": "A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "fields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).", + "schema": { + "type": "string" + } + }, + { + "name": "omitFields", + "required": false, + "in": "query", + "description": "A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtectedAreaResult" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["ProtectedArea"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/api/v1/planning-units/preview/regular/{planningUnitGridShape}/{planningUnitAreakm2}/tiles/{z}/{x}/{y}.mvt": { + "get": { + "operationId": "PlanningUnitsController_proxyProtectedAreasTile", + "summary": "", + "description": "Get planning unit geometries.", + "parameters": [ + { + "name": "bbox", + "required": false, + "in": "query", + "description": "Bounding box of the project [xMin, xMax, yMin, yMax]", + "example": [-1, 40, 1, 42], + "schema": { + "type": "array", + "items": { + "type": "number" + } + } + }, + { + "name": "planningUnitAreakm2", + "required": true, + "in": "path", + "description": "Planning unit area in km2", + "schema": { + "type": "number" + } + }, + { + "name": "planningUnitGridShape", + "required": true, + "in": "path", + "description": "Planning unit grid shape", + "schema": { + "type": "string" + } + }, + { + "name": "y", + "required": true, + "in": "path", + "description": "The tile y offset on Mercator Projection", + "schema": { + "type": "number" + } + }, + { + "name": "x", + "required": true, + "in": "path", + "description": "The tile x offset on Mercator Projection", + "schema": { + "type": "number" + } + }, + { + "name": "z", + "required": true, + "in": "path", + "description": "The zoom level ranging from 0 - 20", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Binary tile succesful retrieve", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["Plannig units"], + "security": [ + { + "bearer": [] + } + ] + } + } + } +} diff --git a/api/apps/geoprocessing/src/decoratos/shapefile.decorator.ts b/api/apps/geoprocessing/src/decoratos/shapefile.decorator.ts index 9da7ffd515..67f9a08b74 100644 --- a/api/apps/geoprocessing/src/decoratos/shapefile.decorator.ts +++ b/api/apps/geoprocessing/src/decoratos/shapefile.decorator.ts @@ -18,7 +18,8 @@ export function ApiConsumesShapefile() { type: 'object', properties: { file: { - type: 'Zip file containing .shp, .dbj, .prj and .shx files', + description: 'Zip file containing .shp, .dbj, .prj and .shx files', + type: 'string', format: 'binary', }, }, diff --git a/api/package.json b/api/package.json index e76a4cf372..21bc0044c8 100644 --- a/api/package.json +++ b/api/package.json @@ -46,7 +46,7 @@ "@nestjs/jwt": "^7.2.0", "@nestjs/passport": "^7.1.5", "@nestjs/platform-express": "^7.5.1", - "@nestjs/swagger": "^4.7.9", + "@nestjs/swagger": "^4.8.0", "@nestjs/typeorm": "^7.1.5", "@turf/turf": "^5.1.6", "bcrypt": "^5.0.0", diff --git a/api/yarn.lock b/api/yarn.lock index c551144db6..b33debee00 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -605,10 +605,10 @@ "@types/jsonwebtoken" "8.5.0" jsonwebtoken "8.5.1" -"@nestjs/mapped-types@0.1.1": - version "0.1.1" - resolved "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-0.1.1.tgz" - integrity sha512-FROYmmZ2F+tLJP/aHasPMX40iUHQPtEAzOAcfAp21baebN5iLUrdyTuphoXjIqubfPFSwtnAGpVm9kLJjQ//ig== +"@nestjs/mapped-types@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-0.4.0.tgz#352b9a661d6d36863cf48b2057616cef1b2c802d" + integrity sha512-TVtd/aTb7EqPhVczdeuvzF9dY0fyE3ivvCstc2eO+AkNqrfzSG1kXYYiUUznKjd0qDa8g2TmPSmHUQ21AXsV1Q== "@nestjs/passport@^7.1.5": version "7.1.5" @@ -636,13 +636,13 @@ fs-extra "9.0.1" pluralize "8.0.0" -"@nestjs/swagger@^4.7.9": - version "4.7.9" - resolved "https://registry.npmjs.org/@nestjs/swagger/-/swagger-4.7.9.tgz" - integrity sha512-5WjtrrbWriHCBN9eDCgr43eTU1S/adlF7RaXjS9YDY553vFABqESfs7riZZy4WhBJ35ldfpzgYoyZv3Z/+DyHQ== +"@nestjs/swagger@^4.8.0": + version "4.8.0" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-4.8.0.tgz#7ebfeb0d59e0c27ff40beb429d7311b752c0dca4" + integrity sha512-YU+ahCOoOTZwSHrODHBiQDCqi7GWEjmSFg3Tot/lwVuQ321/3fIOz/lf+ehVQ5DFr7nVMhB7BRWFJLtE/+NhqQ== dependencies: - "@nestjs/mapped-types" "0.1.1" - lodash "4.17.20" + "@nestjs/mapped-types" "0.4.0" + lodash "4.17.21" path-to-regexp "3.2.0" "@nestjs/testing@^7.5.1": @@ -6560,16 +6560,16 @@ lodash.toarray@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= -lodash@4.17.20, lodash@^4.16.3, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -lodash@^4.17.11, lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.16.3, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + log-symbols@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" From 653bca0a8bf0f0fe86b7267585cdfa9bbd89d001 Mon Sep 17 00:00:00 2001 From: Alicia Date: Tue, 8 Jun 2021 18:31:38 +0200 Subject: [PATCH 12/21] fixed bugs with bbox and admin area filtering for VL also add a new util function to convert from nominatim to bbox --- .../admin-areas/admin-areas.controller.ts | 5 +- .../geo-features/geo-features.controller.ts | 4 +- .../planning-units.controller.ts | 9 +- .../protected-areas.controller.ts | 3 +- .../admin-areas/admin-areas.service.ts | 37 ++- .../src/modules/features/features.service.ts | 7 +- .../planning-units/planning-units.service.ts | 21 +- .../protected-areas.controller.ts | 16 +- .../protected-areas.service.ts | 33 ++- .../src/modules/tile/tile.service.ts | 6 +- .../geoprocessing/src/utils/bbox.utils.ts | 20 ++ api/apps/geoprocessing/test/index.html | 238 +++++++++++------- 12 files changed, 269 insertions(+), 130 deletions(-) create mode 100644 api/apps/geoprocessing/src/utils/bbox.utils.ts diff --git a/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts b/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts index dfc199ba31..9fdd1c5a51 100644 --- a/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts +++ b/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts @@ -73,7 +73,8 @@ export class AdminAreasController { *@todo Change ApiOkResponse mvt type */ @ApiOkResponse({ - type: 'mvt', + description: 'Binary protobuffer mvt tile', + type: String, }) @ApiUnauthorizedResponse() @ApiForbiddenResponse() @@ -105,7 +106,7 @@ export class AdminAreasController { }) @ApiQuery({ name: 'guid', - description: 'Parent country of administrative areas in ISO code', + description: 'Parent country of administrative areas in guid code', type: String, required: false, example: 'BRA.1_1', diff --git a/api/apps/api/src/modules/geo-features/geo-features.controller.ts b/api/apps/api/src/modules/geo-features/geo-features.controller.ts index 092a8c0b34..0c6e00d77c 100644 --- a/api/apps/api/src/modules/geo-features/geo-features.controller.ts +++ b/api/apps/api/src/modules/geo-features/geo-features.controller.ts @@ -23,7 +23,6 @@ import { } from 'nestjs-base-service'; import { Request, Response } from 'express'; import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; -import { BBox} from 'geojson'; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @@ -59,7 +58,8 @@ export class GeoFeaturesController { *@todo Change ApiOkResponse mvt type */ @ApiOkResponse({ - type: 'mvt', + description: 'Binary protobuffer mvt tile', + type: String, }) @ApiUnauthorizedResponse() @ApiForbiddenResponse() diff --git a/api/apps/api/src/modules/planning-units/planning-units.controller.ts b/api/apps/api/src/modules/planning-units/planning-units.controller.ts index 118843f5cc..0a2e438616 100644 --- a/api/apps/api/src/modules/planning-units/planning-units.controller.ts +++ b/api/apps/api/src/modules/planning-units/planning-units.controller.ts @@ -23,8 +23,8 @@ export class PlanningUnitsController { *@reference https://docs.ogc.org/per/19-069.html#_openapi_example_for_vector_tiles */ @ApiOkResponse({ - description: 'Binary tile succesful retrieve', - type: String + description: 'Binary protobuffer mvt tile', + type: String, }) @ApiUnauthorizedResponse() @ApiForbiddenResponse() @@ -33,30 +33,35 @@ export class PlanningUnitsController { description: 'The zoom level ranging from 0 - 20', type: Number, required: true, + example: '5' }) @ApiParam({ name: 'x', description: 'The tile x offset on Mercator Projection', type: Number, required: true, + example: '5' }) @ApiParam({ name: 'y', description: 'The tile y offset on Mercator Projection', type: Number, required: true, + example: '5' }) @ApiParam({ name: 'planningUnitGridShape', description: 'Planning unit grid shape', type: String, required: true, + example: 'square' }) @ApiParam({ name: 'planningUnitAreakm2', description: 'Planning unit area in km2', type: Number, required: true, + example: 100 }) @ApiQuery({ name: 'bbox', diff --git a/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts b/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts index dddf486d6d..71fb304227 100644 --- a/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts +++ b/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts @@ -75,7 +75,8 @@ export class ProtectedAreasController { *@todo Change ApiOkResponse mvt type */ @ApiOkResponse({ - type: 'mvt', + description: 'Binary protobuffer mvt tile', + type: String, }) @ApiUnauthorizedResponse() @ApiForbiddenResponse() 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 1235251b8c..68e8e951f0 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 @@ -18,6 +18,7 @@ import { import { Transform } from 'class-transformer'; import { BBox } from 'geojson'; import { AdminArea } from '@marxan/admin-regions'; +import { BboxUtils } from '@marxan-geoprocessing/utils/bbox.utils'; export class TileSpecification extends TileRequest { @ApiProperty() @@ -51,37 +52,31 @@ export class AdminAreasService { private readonly adminAreasRepository: Repository, @Inject(TileService) private readonly tileService: TileService, + @Inject(BboxUtils) + private readonly bboxUtils: BboxUtils, ) {} buildAdminAreaWhereQuery(level: number, filters?: AdminAreasFilters): string { + /** + * @todo this generation query is a bit... + */ let whereQuery = ''; if (level === 0) { - whereQuery = `gid_0 IS NOT NULL AND gid_1 IS NULL AND gid_2 IS NULL`; - if (filters?.guid) { - whereQuery += ` AND gid_0 = '${filters?.guid}'`; - } - if (filters?.bbox) { - whereQuery += ` AND the_geom && ST_MakeEnvelope(${filters?.bbox}, 4326)`; - } + whereQuery = `gid_0 IS NOT NULL AND gid_1 IS NULL AND gid_2 IS NULL AND gid_0 != 'ATA'`; } if (level === 1) { - whereQuery = `gid_1 IS NOT NULL AND gid_2 IS NULL AND gid_0 != 'ATA'`; - if (filters?.guid) { - whereQuery += ` AND gid_0 = '${filters?.guid}'`; - } - if (filters?.bbox) { - whereQuery += ` AND the_geom && ST_MakeEnvelope(${filters?.bbox}, 4326)`; - } + whereQuery = `gid_1 IS NOT NULL AND gid_2 IS NULL`; } if (level === 2) { whereQuery = `gid_2 IS NOT NULL`; - if (filters?.guid) { - whereQuery += ` AND gid_1 = '${filters?.guid}'`; - } - if (filters?.bbox) { - whereQuery += ` AND the_geom && ST_MakeEnvelope(${filters?.bbox}, 4326)`; - } } + if (filters?.guid && level > 0) { + whereQuery += ` AND gid_${level-1} = '${filters?.guid}'`; + } + if (filters?.bbox) { + whereQuery += ` AND the_geom && ST_MakeEnvelope(${this.bboxUtils.nominatim2bbox(filters?.bbox)}, 4326)`; + } + return whereQuery; } @@ -93,7 +88,7 @@ export class AdminAreasService { filters?: AdminAreasFilters, ): Promise { const { z, x, y, level } = tileSpecification; - const attributes = 'name_0, name_1, name_2'; + const attributes = 'name_0, name_1, name_2, gid_0, gid_1, gid_2'; const table = this.adminAreasRepository.metadata.tableName; const customQuery = this.buildAdminAreaWhereQuery(level, filters); return this.tileService.getTile({ diff --git a/api/apps/geoprocessing/src/modules/features/features.service.ts b/api/apps/geoprocessing/src/modules/features/features.service.ts index bba3c408c3..e2d8c700a9 100644 --- a/api/apps/geoprocessing/src/modules/features/features.service.ts +++ b/api/apps/geoprocessing/src/modules/features/features.service.ts @@ -10,6 +10,7 @@ import { IsArray, IsNumber, IsString, IsOptional } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { BBox } from 'geojson'; +import { BboxUtils } from '@marxan-geoprocessing/utils/bbox.utils'; export class TileSpecification extends TileRequest { @ApiProperty() @@ -37,6 +38,8 @@ export class FeatureService { private readonly featuresRepository: Repository, @Inject(TileService) private readonly tileService: TileService, + @Inject(BboxUtils) + private readonly bboxUtils: BboxUtils, ) {} /** @@ -44,10 +47,12 @@ export class FeatureService { * @todo generate the custom queries using query builder and the entity data. * @todo move the string to int transformation to the AdminAreaLevelFilters class */ + buildFeaturesWhereQuery(id: string, bbox?: BBox): string { let whereQuery = `feature_id = '${id}'`; + if (bbox) { - whereQuery += `AND the_geom && ST_MakeEnvelope(${bbox}, 4326)`; + whereQuery += `AND st_intersects(ST_MakeEnvelope(${this.bboxUtils.nominatim2bbox(bbox)}, 4326), the_geom)`; } return whereQuery; } 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 6c06468858..6a46866754 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 @@ -11,6 +11,7 @@ import { BBox } from 'geojson'; import { Transform } from 'class-transformer'; import { PlanningUnitsGeom } from '@marxan-geoprocessing/modules/planning-units/planning-units.geo.entity'; +import { BboxUtils } from '@marxan-geoprocessing/utils/bbox.utils'; export class tileSpecification extends TileRequest { @ApiProperty() @@ -43,6 +44,8 @@ export class PlanningUnitsService { private readonly planningUnitsRepository: Repository, @Inject(TileService) private readonly tileService: TileService, + @Inject(BboxUtils) + private readonly bboxUtils: BboxUtils, ) {} /** @@ -95,7 +98,7 @@ export class PlanningUnitsService { // If so the shape we are getting is down the optimal to visualize it if ( ratioPixelExtent < 8){ Query = `( SELECT row_number() over() as id, st_centroid((${gridShape}(${gridSize}, \ - ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom ) as the_geom)`; + ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom ) as the_geom )`; } return Query; @@ -108,17 +111,13 @@ export class PlanningUnitsService { * If any value is not provided, 4000 would be the default. */ buildPlanningUnitsWhereQuery( - x: number, - y: number, - z: number, - planningUnitGridShape: PlanningUnitGridShape, - planningUnitAreakm2: number, filters?: PlanningUnitsFilters, ): string { - let whereQuery = `st_intersects(the_geom, ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 4326), 3857)))`; + let whereQuery = ``; if (filters?.bbox) { - whereQuery += ` && ST_Transform(ST_MakeEnvelope(${filters.bbox}), 3857))`; + this.logger.debug('Im a bbox') + whereQuery =`st_intersects(ST_Transform(ST_MakeEnvelope(${this.bboxUtils.nominatim2bbox(filters.bbox)}, 4326), 3857) ,the_geom)`; } return whereQuery; } @@ -149,6 +148,9 @@ export class PlanningUnitsService { planningUnitAreakm2, filters ); + const customQuery = this.buildPlanningUnitsWhereQuery( + filters + ) return this.tileService.getTile({ z, @@ -156,7 +158,8 @@ export class PlanningUnitsService { y, table, attributes, - inputProjection + inputProjection, + customQuery }); } } diff --git a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.controller.ts b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.controller.ts index 2b92a4996b..17d2d36182 100644 --- a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.controller.ts +++ b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.controller.ts @@ -17,6 +17,7 @@ import { ApiParam, ApiQuery, ApiBadRequestResponse, + ApiOkResponse, } from '@nestjs/swagger'; import { TileRequest } from '@marxan-geoprocessing/modules/tile/tile.service'; @@ -55,7 +56,18 @@ export class ProtectedAreasController { required: false, example: 'e5c3b978-908c-49d3-b1e3-89727e9f999c', }) + @ApiQuery({ + name: 'bbox', + description: 'Bounding box of the project [xMin, xMax, yMin, yMax]', + type: [Number], + required: false, + example: [-1, 40, 1, 42], + }) @Get('/preview/tiles/:z/:x/:y.mvt') + @ApiOkResponse({ + description: 'Binary protobuffer mvt tile', + type: String, + }) @ApiBadRequestResponse() @Header('Content-Type', 'application/x-protobuf') @Header('Content-Disposition', 'attachment') @@ -63,10 +75,10 @@ export class ProtectedAreasController { @Header('Content-Encoding', 'gzip') async getTile( @Param() tileRequest: TileRequest, - @Query() { id }: ProtectedAreasFilters, + @Query() protectedAreasFilters: ProtectedAreasFilters, @Res() response: Response, ): Promise { - const tile: Buffer = await this.service.findTile(tileRequest, { id }); + const tile: Buffer = await this.service.findTile(tileRequest, protectedAreasFilters); return response.send(tile); } } diff --git a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts index 499a2bcf7c..8f42c76f09 100644 --- a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts +++ b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts @@ -5,14 +5,22 @@ import { } from '@marxan-geoprocessing/modules/tile/tile.service'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { IsOptional, IsString } from 'class-validator'; +import { IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; import { ProtectedArea } from '@marxan-geoprocessing/modules/protected-areas/protected-areas.geo.entity'; +import { BBox } from 'geojson'; +import { Transform } from 'class-transformer'; +import { BboxUtils } from '@marxan-geoprocessing/utils/bbox.utils'; export class ProtectedAreasFilters { @IsOptional() @IsString() id?: string; + + @IsOptional() + @IsNumber({}, { each: true }) + @Transform((value: string): BBox => JSON.parse(value)) + bbox?: BBox; } @Injectable() @@ -23,8 +31,25 @@ export class ProtectedAreasService { private readonly protectedAreasRepository: Repository, @Inject(TileService) private readonly tileService: TileService, + @Inject(BboxUtils) + private readonly bboxUtils: BboxUtils, ) {} + /** + * @param filters bounding box of the area where the grids would be generated + */ + buildProtectedAreasWhereQuery( + filters?: ProtectedAreasFilters, + ): string | undefined { + let whereQuery = undefined; + whereQuery = filters?.id ? ` id = '${filters?.id}'` : undefined; + if (filters?.bbox) { + const bboxIntersect =`st_intersects(ST_MakeEnvelope(${this.bboxUtils.nominatim2bbox(filters.bbox)}, 4326), the_geom)`; + whereQuery = whereQuery ? `${whereQuery} and ${bboxIntersect}`: bboxIntersect; + } + return whereQuery; + } + /** * @todo get attributes from Entity, based on user selection */ @@ -33,16 +58,16 @@ export class ProtectedAreasService { filters?: ProtectedAreasFilters, ): Promise { const { z, x, y } = tileSpecification; - const attributes = 'full_name, status, wdpaid'; + const attributes = 'full_name, status, wdpaid, iucn_cat'; const table = this.protectedAreasRepository.metadata.tableName; - const customQuery = filters?.id ? ` id = '${filters?.id}'` : undefined; + const customQuery = this.buildProtectedAreasWhereQuery(filters); return this.tileService.getTile({ z, x, y, table, - customQuery, attributes, + customQuery, }); } } diff --git a/api/apps/geoprocessing/src/modules/tile/tile.service.ts b/api/apps/geoprocessing/src/modules/tile/tile.service.ts index 20a282ee27..993744e655 100644 --- a/api/apps/geoprocessing/src/modules/tile/tile.service.ts +++ b/api/apps/geoprocessing/src/modules/tile/tile.service.ts @@ -120,16 +120,18 @@ export class TileService { ST_TileEnvelope(${z}, ${x}, ${y}), ${extent}, ${buffer}, true) AS mvt_geom`, ); - subQuery.from(table, '').where( + subQuery.from(table, 'data').where( `ST_Intersects(ST_Transform(ST_TileEnvelope(:z, :x, :y), ${inputProjection}), ${geometry} )`, { z, x, y }, ); + this.logger.debug(customQuery) if (customQuery) { subQuery.andWhere(customQuery); } return subQuery; }, 'tile'); - const result = await query.printSql().getRawMany(); + this.logger.debug(query.getSql()) + const result = await query.getRawMany(); if (result) { return result; diff --git a/api/apps/geoprocessing/src/utils/bbox.utils.ts b/api/apps/geoprocessing/src/utils/bbox.utils.ts new file mode 100644 index 0000000000..bbdad8b8c6 --- /dev/null +++ b/api/apps/geoprocessing/src/utils/bbox.utils.ts @@ -0,0 +1,20 @@ + +import { BBox } from 'geojson'; +/** + * Utility functions related to lower-level interaction with bbox operations. + * + * @debt This should be moved to a self-standing + */ +export class BboxUtils { + /** + * conversion operation between bbox [xmin, ymin, xmax, ymax] + * to Nominatim bbox [xmin, xmax, ymin, ymax]. + * + */ + public bbox2Nominatim(bbox: BBox): BBox { + return[bbox[0],bbox[2],bbox[1],bbox[3]] + } + public nominatim2bbox(nominatim: BBox): BBox { + return[nominatim[0], nominatim[2], nominatim[1], nominatim[3]] + } +} diff --git a/api/apps/geoprocessing/test/index.html b/api/apps/geoprocessing/test/index.html index 2ff79c3bc8..1eca491ac5 100644 --- a/api/apps/geoprocessing/test/index.html +++ b/api/apps/geoprocessing/test/index.html @@ -33,7 +33,7 @@ var map = new mapboxgl.Map({ container: 'map', - zoom: 5, + zoom: 7, center: [ 22.686767578125, -19.072501451715087 @@ -42,42 +42,58 @@ style: { version: 8, sources: { - // 'postgis-tiles0': { - // type: 'vector', - // maxzoom: 12, - // tiles: [ - // 'http://localhost:3040/api/v1/administrative-areas/0/preview/tiles/{z}/{x}/{y}.mvt', - // ], - // }, - // 'postgis-tiles1': { - // type: 'vector', - // maxzoom: 12, - // tiles: [ - // 'http://localhost:3040/api/v1/administrative-areas/1/preview/tiles/{z}/{x}/{y}.mvt', - // ], - // }, - // 'postgis-tiles2': { - // type: 'vector', - // maxzoom: 12, - // tiles: [ - // 'http://localhost:3040/api/v1/administrative-areas/2/preview/tiles/{z}/{x}/{y}.mvt', - // ], - // }, - // 'postgis-tileswdpa': { - // type: 'vector', - // minzoom: 1, - // maxzoom: 12, - // tiles: [ - // 'http://localhost:3040/api/v1/protected-areas/preview/tiles/{z}/{x}/{y}.mvt', - // ], - // }, + 'postgis-tiles0': { + type: 'vector', + maxzoom: 12, + tiles: [ + 'http://localhost:3040/api/v1/administrative-areas/0/preview/tiles/{z}/{x}/{y}.mvt', + ], + }, + 'postgis-tiles1': { + type: 'vector', + maxzoom: 12, + tiles: [ + 'http://localhost:3040/api/v1/administrative-areas/1/preview/tiles/{z}/{x}/{y}.mvt?guid=BWA', + ], + }, + 'postgis-tiles2': { + type: 'vector', + maxzoom: 12, + tiles: [ + 'http://localhost:3040/api/v1/administrative-areas/2/preview/tiles/{z}/{x}/{y}.mvt?guid=BWA.12_1&bbox=[21.14044189453125,-19.202241064923044,22.980651855468746,-18.22935133838667]', + ], + }, + 'postgis-tileswdpa': { + type: 'vector', + minzoom: 1, + maxzoom: 12, + tiles: [ + 'http://localhost:3040/api/v1/protected-areas/preview/tiles/{z}/{x}/{y}.mvt', + ], + }, 'postgis-tilesPU': { type: 'vector', minzoom: 1, maxzoom: 20, tiles: [ - 'http://localhost:3040/api/v1/planning-units/preview/regular/hexagon/10/tiles/{z}/{x}/{y}.mvt', + 'http://localhost:3040/api/v1/planning-units/preview/regular/hexagon/100/tiles/{z}/{x}/{y}.mvt?bbox=[21.14044189453125,-19.202241064923044,22.980651855468746,-18.22935133838667]', + ], + }, + 'postgis-tilesPU2': { + type: 'vector', + minzoom: 1, + maxzoom: 20, + tiles: [ + 'http://localhost:3040/api/v1/planning-units/preview/regular/hexagon/100/tiles/{z}/{x}/{y}.mvt', + ], + }, + 'postgis-tilesFeatures': { + type: 'vector', + minzoom: 1, + maxzoom: 20, + tiles: [ + 'http://localhost:3040/api/v1/geo-features/127a5016-c697-4a6c-ae2a-4fa29a2729be/preview/tiles/{z}/{x}/{y}.mvt?bbox=[21.14044189453125,-19.202241064923044,22.980651855468746,-18.22935133838667]', ], }, @@ -98,78 +114,132 @@ maxzoom: 22, }, + { + id: 'postgis-tiles-layer', + type: 'fill', + source: 'postgis-tiles2', + 'source-layer': 'layer0', + paint: { + 'fill-outline-color': 'purple', + 'fill-color': 'purple', + 'fill-opacity': 0.1, + }, + }, + { + 'id': 'postgis-tiles-layer1', + 'type': 'fill', + 'source': 'postgis-tiles1', + 'source-layer': 'layer0', + 'paint': { + 'fill-outline-color': 'green', + 'fill-color': 'green', + 'fill-opacity': 0.1 + } + }, + { + 'id': 'postgis-tiles-layer2', + 'type': 'fill', + 'source': 'postgis-tiles0', + 'source-layer': 'layer0', + 'paint': { + 'fill-outline-color': 'red', + 'fill-color': 'red', + 'fill-opacity': 0.1 + } + }, // { - // id: 'postgis-tiles-layer', + // id: 'postgis-tiles-layer-wdpa', // type: 'fill', - // source: 'postgis-tiles2', + // source: 'postgis-tileswdpa', // 'source-layer': 'layer0', // paint: { - // 'fill-outline-color': 'red', - // 'fill-color': 'green', - // 'fill-opacity': 0.2, + // 'fill-outline-color': 'blue', + // 'fill-color': 'blue', + // 'fill-opacity': 0.3, // }, // }, // { - // 'id': 'postgis-tiles-layer1', - // 'type': 'fill', - // 'source': 'postgis-tiles1', - // 'source-layer': 'layer0', - // 'paint': { - // 'fill-outline-color': 'blue', - // 'fill-color': 'green', - // 'fill-opacity': 0.2 - // } - // }, - // { - // 'id': 'postgis-tiles-layer2', - // 'type': 'fill', - // 'source': 'postgis-tiles0', - // 'source-layer': 'layer0', - // 'paint': { - // 'fill-outline-color': 'pink', - // 'fill-color': 'green', - // 'fill-opacity': 0.2 - // } + // id: 'postgis-tiles-layer-features', + // type: 'fill', + // source: 'postgis-tilesFeatures', + // 'source-layer': 'layer0', + // paint: { + // 'fill-outline-color': 'pink', + // 'fill-color': 'pink', + // 'fill-opacity': 0.3, + // }, // }, // { - // id: 'postgis-tiles-layer-wdpa', + // id: 'postgis-tiles-layer-PU', // type: 'fill', - // source: 'postgis-tileswdpa', + // source: 'postgis-tilesPU', // 'source-layer': 'layer0', + // paint: { // 'fill-outline-color': 'yellow', // 'fill-color': 'red', - // 'fill-opacity': 0.7, + // 'fill-opacity': 0.1, + // }, + // }, + // { + // id: 'postgis-tiles-layer-PU-points', + // type: 'circle', + // source: 'postgis-tilesPU2', + // 'source-layer': 'layer0', + // paint: { + // 'circle-color': 'blue', + // 'circle-opacity':[ + // 'interpolate', + // // Set the exponential rate of change to 0.5 + // ['exponential', 0.5], + // ['zoom'], + // // When zoom is 15, buildings will be beige. + // 5, + // 1, + // // When zoom is 18 or higher, buildings will be yellow. + // 12, + // 0 + // ], + // 'circle-stroke-opacity':[ + // 'interpolate', + // // Set the exponential rate of change to 0.5 + // ['exponential', 0.5], + // ['zoom'], + // // When zoom is 15, buildings will be beige. + // 2, + // 1, + // // When zoom is 18 or higher, buildings will be yellow. + // 7, + // 0 + // ], + // 'circle-radius': 1, + // 'circle-stroke-width': 1, + // 'circle-stroke-color': 'blue' // }, // }, - { - id: 'postgis-tiles-layer-PU', - type: 'fill', - source: 'postgis-tilesPU', - 'source-layer': 'layer0', - - paint: { - 'fill-outline-color': 'yellow', - 'fill-color': 'red', - 'fill-opacity': 0.1, - }, - }, - { - id: 'postgis-tiles-layer-PU-points', - type: 'circle', - source: 'postgis-tilesPU', - - 'source-layer': 'layer0', - paint: { - 'circle-color': 'yellow', - 'circle-radius': 1, - 'circle-stroke-width': 1, - 'circle-stroke-color': 'yellow' - }, - }, ], }, }); + map.on('load', function () { + map.on('click', 'postgis-tiles-layer', function (e) { + console.log(e.features[0].properties) + }); + map.on('click', 'postgis-tiles-layer1', function (e) { + console.log(e.features[0].properties) + }); + map.on('click', 'postgis-tiles-layer2', function (e) { + console.log(e.features[0].properties) + }); + // Change the cursor to a pointer when the mouse is over the places layer. + map.on('mouseenter', 'postgis-tiles-layer', function () { + map.getCanvas().style.cursor = 'pointer'; + }); + + // Change it back to a pointer when it leaves. + map.on('mouseleave', 'postgis-tiles-layer', function () { + map.getCanvas().style.cursor = ''; + }); + }); From d396126b05ddfd23cd891444a15427daf7eca6fc Mon Sep 17 00:00:00 2001 From: Alicia Date: Wed, 9 Jun 2021 15:50:19 +0200 Subject: [PATCH 13/21] bbox utilities used in vl bbox filter --- .../src/modules/admin-areas/admin-areas.service.ts | 6 ++---- .../src/modules/features/features.service.ts | 6 ++---- .../planning-units/planning-units.service.ts | 7 ++----- .../protected-areas/protected-areas.service.ts | 6 ++---- .../geoprocessing/src/modules/tile/tile.service.ts | 10 ++++------ api/apps/geoprocessing/src/utils/bbox.utils.ts | 13 ++++++------- 6 files changed, 18 insertions(+), 30 deletions(-) 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 68e8e951f0..8128f44a62 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 @@ -18,7 +18,7 @@ import { import { Transform } from 'class-transformer'; import { BBox } from 'geojson'; import { AdminArea } from '@marxan/admin-regions'; -import { BboxUtils } from '@marxan-geoprocessing/utils/bbox.utils'; +import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils'; export class TileSpecification extends TileRequest { @ApiProperty() @@ -52,8 +52,6 @@ export class AdminAreasService { private readonly adminAreasRepository: Repository, @Inject(TileService) private readonly tileService: TileService, - @Inject(BboxUtils) - private readonly bboxUtils: BboxUtils, ) {} buildAdminAreaWhereQuery(level: number, filters?: AdminAreasFilters): string { @@ -74,7 +72,7 @@ export class AdminAreasService { whereQuery += ` AND gid_${level-1} = '${filters?.guid}'`; } if (filters?.bbox) { - whereQuery += ` AND the_geom && ST_MakeEnvelope(${this.bboxUtils.nominatim2bbox(filters?.bbox)}, 4326)`; + whereQuery += ` AND the_geom && ST_MakeEnvelope(${nominatim2bbox(filters?.bbox)}, 4326)`; } return whereQuery; diff --git a/api/apps/geoprocessing/src/modules/features/features.service.ts b/api/apps/geoprocessing/src/modules/features/features.service.ts index e2d8c700a9..8f819bb3bd 100644 --- a/api/apps/geoprocessing/src/modules/features/features.service.ts +++ b/api/apps/geoprocessing/src/modules/features/features.service.ts @@ -10,7 +10,7 @@ import { IsArray, IsNumber, IsString, IsOptional } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { BBox } from 'geojson'; -import { BboxUtils } from '@marxan-geoprocessing/utils/bbox.utils'; +import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils'; export class TileSpecification extends TileRequest { @ApiProperty() @@ -38,8 +38,6 @@ export class FeatureService { private readonly featuresRepository: Repository, @Inject(TileService) private readonly tileService: TileService, - @Inject(BboxUtils) - private readonly bboxUtils: BboxUtils, ) {} /** @@ -52,7 +50,7 @@ export class FeatureService { let whereQuery = `feature_id = '${id}'`; if (bbox) { - whereQuery += `AND st_intersects(ST_MakeEnvelope(${this.bboxUtils.nominatim2bbox(bbox)}, 4326), the_geom)`; + whereQuery += `AND st_intersects(ST_MakeEnvelope(${nominatim2bbox(bbox)}, 4326), the_geom)`; } return whereQuery; } 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 6a46866754..5a9588919e 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 @@ -11,7 +11,7 @@ import { BBox } from 'geojson'; import { Transform } from 'class-transformer'; import { PlanningUnitsGeom } from '@marxan-geoprocessing/modules/planning-units/planning-units.geo.entity'; -import { BboxUtils } from '@marxan-geoprocessing/utils/bbox.utils'; +import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils'; export class tileSpecification extends TileRequest { @ApiProperty() @@ -44,8 +44,6 @@ export class PlanningUnitsService { private readonly planningUnitsRepository: Repository, @Inject(TileService) private readonly tileService: TileService, - @Inject(BboxUtils) - private readonly bboxUtils: BboxUtils, ) {} /** @@ -116,8 +114,7 @@ export class PlanningUnitsService { let whereQuery = ``; if (filters?.bbox) { - this.logger.debug('Im a bbox') - whereQuery =`st_intersects(ST_Transform(ST_MakeEnvelope(${this.bboxUtils.nominatim2bbox(filters.bbox)}, 4326), 3857) ,the_geom)`; + whereQuery =`st_intersects(ST_Transform(ST_MakeEnvelope(${nominatim2bbox(filters.bbox)}, 4326), 3857) ,the_geom)`; } return whereQuery; } diff --git a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts index 8f42c76f09..0899a80269 100644 --- a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts +++ b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts @@ -10,7 +10,7 @@ import { IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator' import { ProtectedArea } from '@marxan-geoprocessing/modules/protected-areas/protected-areas.geo.entity'; import { BBox } from 'geojson'; import { Transform } from 'class-transformer'; -import { BboxUtils } from '@marxan-geoprocessing/utils/bbox.utils'; +import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils'; export class ProtectedAreasFilters { @IsOptional() @@ -31,8 +31,6 @@ export class ProtectedAreasService { private readonly protectedAreasRepository: Repository, @Inject(TileService) private readonly tileService: TileService, - @Inject(BboxUtils) - private readonly bboxUtils: BboxUtils, ) {} /** @@ -44,7 +42,7 @@ export class ProtectedAreasService { let whereQuery = undefined; whereQuery = filters?.id ? ` id = '${filters?.id}'` : undefined; if (filters?.bbox) { - const bboxIntersect =`st_intersects(ST_MakeEnvelope(${this.bboxUtils.nominatim2bbox(filters.bbox)}, 4326), the_geom)`; + const bboxIntersect =`st_intersects(ST_MakeEnvelope(${nominatim2bbox(filters.bbox)}, 4326), the_geom)`; whereQuery = whereQuery ? `${whereQuery} and ${bboxIntersect}`: bboxIntersect; } return whereQuery; diff --git a/api/apps/geoprocessing/src/modules/tile/tile.service.ts b/api/apps/geoprocessing/src/modules/tile/tile.service.ts index 993744e655..5500673515 100644 --- a/api/apps/geoprocessing/src/modules/tile/tile.service.ts +++ b/api/apps/geoprocessing/src/modules/tile/tile.service.ts @@ -1,5 +1,5 @@ // to-do: work on cache later -import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { getConnection } from 'typeorm'; import * as zlib from 'zlib'; import { Transform } from 'class-transformer'; @@ -124,20 +124,18 @@ export class TileService { `ST_Intersects(ST_Transform(ST_TileEnvelope(:z, :x, :y), ${inputProjection}), ${geometry} )`, { z, x, y }, ); - this.logger.debug(customQuery) if (customQuery) { subQuery.andWhere(customQuery); } return subQuery; }, 'tile'); - this.logger.debug(query.getSql()) const result = await query.getRawMany(); if (result) { return result; } else { - this.logger.debug(query.getSql()); - throw new Error("Property 'mvt' does not exist in res.rows[0]"); + this.logger.error(query.getSql()); + throw new NotFoundException("Property 'mvt' does not exist in res.rows[0]"); } } @@ -174,7 +172,7 @@ export class TileService { >[] = await this.fetchTileFromDatabase(tileInput); // zip data data = await this.zip(queryResult[0].mvt); - } catch (error) { + } catch (error: any) { this.logger.error(`Database error: ${error.message}`); throw new BadRequestException(error.message); } diff --git a/api/apps/geoprocessing/src/utils/bbox.utils.ts b/api/apps/geoprocessing/src/utils/bbox.utils.ts index bbdad8b8c6..3f3caaa89a 100644 --- a/api/apps/geoprocessing/src/utils/bbox.utils.ts +++ b/api/apps/geoprocessing/src/utils/bbox.utils.ts @@ -5,16 +5,15 @@ import { BBox } from 'geojson'; * * @debt This should be moved to a self-standing */ -export class BboxUtils { + /** * conversion operation between bbox [xmin, ymin, xmax, ymax] * to Nominatim bbox [xmin, xmax, ymin, ymax]. * */ - public bbox2Nominatim(bbox: BBox): BBox { - return[bbox[0],bbox[2],bbox[1],bbox[3]] - } - public nominatim2bbox(nominatim: BBox): BBox { - return[nominatim[0], nominatim[2], nominatim[1], nominatim[3]] - } +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]] } From 300e81bf837745547d552e267ffcd0b892a808d0 Mon Sep 17 00:00:00 2001 From: Alicia Date: Wed, 9 Jun 2021 16:22:38 +0200 Subject: [PATCH 14/21] rebase problem detected on proxy tests. already fixed --- .../api/test/proxy.vector-tiles.e2e-spec.ts | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts b/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts index cd5e8c1cdd..46698788b7 100644 --- a/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts +++ b/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts @@ -13,6 +13,8 @@ import { OrganizationsTestUtils } from './utils/organizations.test.utils'; import { ProjectsTestUtils } from './utils/projects.test.utils'; import { ScenariosTestUtils } from './utils/scenarios.test.utils'; import { IUCNCategory } from '@marxan-api/modules/protected-areas/protected-area.geo.entity'; +import { GivenUserIsLoggedIn } from './steps/given-user-is-logged-in'; +import { bootstrapApplication } from 'apps/geoprocessing/test/utils'; const logger = new Logger('test-vtiles') @@ -44,28 +46,9 @@ describe('ProxyVectorTilesModule (e2e)', () => { }; beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - app = moduleFixture.createNestApplication(); - app.useGlobalPipes( - new ValidationPipe({ - transform: true, - whitelist: true, - forbidNonWhitelisted: true, - }), - ); - await app.init(); - - const response = await request(app.getHttpServer()) - .post('/auth/sign-in') - .send({ - username: E2E_CONFIG.users.basic.aa.username, - password: E2E_CONFIG.users.basic.aa.password, - }) - .expect(201); + app = await bootstrapApplication(); - jwtToken = response.body.accessToken; + jwtToken = await GivenUserIsLoggedIn(app); anOrganization = await OrganizationsTestUtils.createOrganization( app, From 29fcd371625addb0b2d3b99b542f308037a3e963 Mon Sep 17 00:00:00 2001 From: Alicia Date: Thu, 10 Jun 2021 09:54:53 +0200 Subject: [PATCH 15/21] split swagger generation as per Dom suggestion --- api/apps/api/src/add-swagger.ts | 22 +++++++++++ api/apps/api/src/bootstrap-app.ts | 40 +++++++++++++++++++ api/apps/api/src/generate-swagger.ts | 14 +++++++ api/apps/api/src/main.ts | 57 +++++----------------------- 4 files changed, 85 insertions(+), 48 deletions(-) create mode 100644 api/apps/api/src/add-swagger.ts create mode 100644 api/apps/api/src/bootstrap-app.ts create mode 100644 api/apps/api/src/generate-swagger.ts diff --git a/api/apps/api/src/add-swagger.ts b/api/apps/api/src/add-swagger.ts new file mode 100644 index 0000000000..4a9c1803ef --- /dev/null +++ b/api/apps/api/src/add-swagger.ts @@ -0,0 +1,22 @@ +import { INestApplication } from '@nestjs/common'; +import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger'; + + + + +export function addSwagger(app: INestApplication): OpenAPIObject { + + const swaggerOptions = new DocumentBuilder() + .setTitle('MarxanCloud API') + .setDescription('MarxanCloud is a conservation planning platform.') + .setVersion(process.env.npm_package_version || 'development') + .addBearerAuth({ + type: 'http', + }, 'BearerAuth') + .build(); + + return SwaggerModule.createDocument(app, swaggerOptions); +} + + + diff --git a/api/apps/api/src/bootstrap-app.ts b/api/apps/api/src/bootstrap-app.ts new file mode 100644 index 0000000000..6b35c10028 --- /dev/null +++ b/api/apps/api/src/bootstrap-app.ts @@ -0,0 +1,40 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +import * as helmet from 'helmet'; +import { CorsUtils } from './utils/cors.utils'; +import { AppConfig } from '@marxan-api/utils/config.utils'; +import { ValidationPipe } from '@nestjs/common'; + +export async function bootstrapSetUp() { + const app = await NestFactory.create(AppModule); + + // We forcibly prevent the app from starting if no `API_AUTH_JWT_SECRET` + // environment variable has been set. + if (!AppConfig.get('auth.jwt.secret')) { + throw new Error( + 'No secret configured for the signing of JWT tokens. Please set the `API_AUTH_JWT_SECRET` environment variable.', + ); + } + + app.use(helmet()); + app.enableCors({ + allowedHeaders: 'Content-Type,Authorization,Content-Disposition', + exposedHeaders: 'Authorization', + origin: CorsUtils.originHandler, + }); + + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + }), + ); + + return app + + + + //await app.listen(3000); +} diff --git a/api/apps/api/src/generate-swagger.ts b/api/apps/api/src/generate-swagger.ts new file mode 100644 index 0000000000..94c7542ca8 --- /dev/null +++ b/api/apps/api/src/generate-swagger.ts @@ -0,0 +1,14 @@ +import { OpenAPIObject } from "@nestjs/swagger"; +import {writeFileSync} from "fs"; +import { addSwagger } from "./add-swagger"; +import { bootstrapSetUp } from "./bootstrap-app"; + +export async function generateSwagger() { + + const app = await bootstrapSetUp() + const swaggerDocument = addSwagger(app) + writeFileSync("./swagger.json", JSON.stringify(swaggerDocument)); + +} +generateSwagger() + diff --git a/api/apps/api/src/main.ts b/api/apps/api/src/main.ts index f0e4b58a8c..4a66b36448 100644 --- a/api/apps/api/src/main.ts +++ b/api/apps/api/src/main.ts @@ -1,55 +1,16 @@ -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { bootstrapSetUp } from '@marxan-api/bootstrap-app' +import { addSwagger } from '@marxan-api/add-swagger' +import { SwaggerModule } from '@nestjs/swagger'; -import * as helmet from 'helmet'; -import { CorsUtils } from './utils/cors.utils'; -import { AppConfig } from '@marxan-api/utils/config.utils'; -import { ValidationPipe } from '@nestjs/common'; -import { AllExceptionsFilter } from '@marxan-api/filters/all-exceptions.exception.filter'; -import {writeFileSync} from "fs"; +export async function bootstrap(){ -async function bootstrap() { - const app = await NestFactory.create(AppModule); +const app = await bootstrapSetUp() +const swaggerDocument = addSwagger(app) - // We forcibly prevent the app from starting if no `API_AUTH_JWT_SECRET` - // environment variable has been set. - if (!AppConfig.get('auth.jwt.secret')) { - throw new Error( - 'No secret configured for the signing of JWT tokens. Please set the `API_AUTH_JWT_SECRET` environment variable.', - ); - } +SwaggerModule.setup('/swagger', app, swaggerDocument); - app.use(helmet()); - app.enableCors({ - allowedHeaders: 'Content-Type,Authorization,Content-Disposition', - exposedHeaders: 'Authorization', - origin: CorsUtils.originHandler, - }); +await app.listen(3000); - // OpenAPI documentation module - setup - const swaggerOptions = new DocumentBuilder() - .setTitle('MarxanCloud API') - .setDescription('MarxanCloud is a conservation planning platform.') - .setVersion(process.env.npm_package_version || 'development') - .addBearerAuth({ - type: 'http', - }, 'BearerAuth') - .build(); - const swaggerDocument = SwaggerModule.createDocument(app, swaggerOptions); - - writeFileSync("./swagger.json", JSON.stringify(swaggerDocument)); - - SwaggerModule.setup('/swagger', app, swaggerDocument); - - app.useGlobalPipes( - new ValidationPipe({ - transform: true, - whitelist: true, - forbidNonWhitelisted: true, - }), - ); - - await app.listen(3000); } + bootstrap(); From 0393b6de0d2b33651d676aed1aa2483fdae916d1 Mon Sep 17 00:00:00 2001 From: Alicia Date: Thu, 10 Jun 2021 10:10:48 +0200 Subject: [PATCH 16/21] manually linted --- api/apps/api/src/add-swagger.ts | 24 ++- api/apps/api/src/bootstrap-app.ts | 4 +- .../api/src/decorators/shapefile.decorator.ts | 3 +- api/apps/api/src/generate-swagger.ts | 19 +-- api/apps/api/src/main.ts | 16 +- .../1619777176256-PropertyListForFeatures.ts | 2 +- .../admin-areas/admin-areas.controller.ts | 16 +- .../modules/admin-areas/admin-areas.module.ts | 1 - .../geo-features/geo-features.controller.ts | 10 +- .../planning-units.controller.ts | 35 ++-- .../protected-areas.controller.ts | 9 +- .../api/test/proxy.vector-tiles.e2e-spec.ts | 149 ++++++++---------- api/apps/geoprocessing/src/dto/info.dto.ts | 3 +- .../admin-areas/admin-areas.service.ts | 6 +- .../src/modules/features/features.service.ts | 4 +- .../planning-units/planning-units.service.ts | 55 +++---- .../protected-areas.controller.ts | 5 +- .../protected-areas.service.ts | 17 +- .../src/modules/tile/tile.service.ts | 37 +++-- .../geoprocessing/src/utils/bbox.utils.ts | 7 +- 20 files changed, 219 insertions(+), 203 deletions(-) diff --git a/api/apps/api/src/add-swagger.ts b/api/apps/api/src/add-swagger.ts index 4a9c1803ef..58d3c04d13 100644 --- a/api/apps/api/src/add-swagger.ts +++ b/api/apps/api/src/add-swagger.ts @@ -1,22 +1,18 @@ import { INestApplication } from '@nestjs/common'; import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger'; - - - export function addSwagger(app: INestApplication): OpenAPIObject { - const swaggerOptions = new DocumentBuilder() - .setTitle('MarxanCloud API') - .setDescription('MarxanCloud is a conservation planning platform.') - .setVersion(process.env.npm_package_version || 'development') - .addBearerAuth({ - type: 'http', - }, 'BearerAuth') - .build(); + .setTitle('MarxanCloud API') + .setDescription('MarxanCloud is a conservation planning platform.') + .setVersion(process.env.npm_package_version || 'development') + .addBearerAuth( + { + type: 'http', + }, + 'BearerAuth', + ) + .build(); return SwaggerModule.createDocument(app, swaggerOptions); } - - - diff --git a/api/apps/api/src/bootstrap-app.ts b/api/apps/api/src/bootstrap-app.ts index 6b35c10028..471fe35490 100644 --- a/api/apps/api/src/bootstrap-app.ts +++ b/api/apps/api/src/bootstrap-app.ts @@ -32,9 +32,7 @@ export async function bootstrapSetUp() { }), ); - return app - - + return app; //await app.listen(3000); } diff --git a/api/apps/api/src/decorators/shapefile.decorator.ts b/api/apps/api/src/decorators/shapefile.decorator.ts index d021b64b32..efd3de7bf0 100644 --- a/api/apps/api/src/decorators/shapefile.decorator.ts +++ b/api/apps/api/src/decorators/shapefile.decorator.ts @@ -22,7 +22,8 @@ export function ApiConsumesShapefile(withGeoJsonResponse = true) { type: 'object', properties: { file: { - description: 'Zip file containing .shp, .dbj, .prj and .shx files', + description: + 'Zip file containing .shp, .dbj, .prj and .shx files', type: 'string', format: 'binary', }, diff --git a/api/apps/api/src/generate-swagger.ts b/api/apps/api/src/generate-swagger.ts index 94c7542ca8..17f8d342fb 100644 --- a/api/apps/api/src/generate-swagger.ts +++ b/api/apps/api/src/generate-swagger.ts @@ -1,14 +1,11 @@ -import { OpenAPIObject } from "@nestjs/swagger"; -import {writeFileSync} from "fs"; -import { addSwagger } from "./add-swagger"; -import { bootstrapSetUp } from "./bootstrap-app"; +import { OpenAPIObject } from '@nestjs/swagger'; +import { writeFileSync } from 'fs'; +import { addSwagger } from './add-swagger'; +import { bootstrapSetUp } from './bootstrap-app'; export async function generateSwagger() { - - const app = await bootstrapSetUp() - const swaggerDocument = addSwagger(app) - writeFileSync("./swagger.json", JSON.stringify(swaggerDocument)); - + const app = await bootstrapSetUp(); + const swaggerDocument = addSwagger(app); + writeFileSync('./swagger.json', JSON.stringify(swaggerDocument)); } -generateSwagger() - +generateSwagger(); diff --git a/api/apps/api/src/main.ts b/api/apps/api/src/main.ts index 4a66b36448..8e0ec35ae9 100644 --- a/api/apps/api/src/main.ts +++ b/api/apps/api/src/main.ts @@ -1,16 +1,14 @@ -import { bootstrapSetUp } from '@marxan-api/bootstrap-app' -import { addSwagger } from '@marxan-api/add-swagger' +import { bootstrapSetUp } from '@marxan-api/bootstrap-app'; +import { addSwagger } from '@marxan-api/add-swagger'; import { SwaggerModule } from '@nestjs/swagger'; -export async function bootstrap(){ +export async function bootstrap() { + const app = await bootstrapSetUp(); + const swaggerDocument = addSwagger(app); -const app = await bootstrapSetUp() -const swaggerDocument = addSwagger(app) - -SwaggerModule.setup('/swagger', app, swaggerDocument); - -await app.listen(3000); + SwaggerModule.setup('/swagger', app, swaggerDocument); + await app.listen(3000); } bootstrap(); diff --git a/api/apps/api/src/migrations/api/1619777176256-PropertyListForFeatures.ts b/api/apps/api/src/migrations/api/1619777176256-PropertyListForFeatures.ts index eba9afbc9d..63b0708beb 100644 --- a/api/apps/api/src/migrations/api/1619777176256-PropertyListForFeatures.ts +++ b/api/apps/api/src/migrations/api/1619777176256-PropertyListForFeatures.ts @@ -10,7 +10,7 @@ export class PropertyListForFeatures1619777176256 } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE features DROP COLUMN list_property_keys; `); diff --git a/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts b/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts index 9fdd1c5a51..f96fe33864 100644 --- a/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts +++ b/api/apps/api/src/modules/admin-areas/admin-areas.controller.ts @@ -1,4 +1,12 @@ -import { Controller, Get, Param, Query, Req, Res, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Param, + Query, + Req, + Res, + UseGuards, +} from '@nestjs/common'; import { adminAreaResource, AdminAreaResult } from './admin-area.geo.entity'; import { AdminAreaLevel, AdminAreasService } from './admin-areas.service'; import { @@ -26,8 +34,10 @@ import { Request, Response } from 'express'; @ApiTags(adminAreaResource.className) @Controller(`${apiGlobalPrefixes.v1}`) export class AdminAreasController { - constructor(public readonly service: AdminAreasService, - private readonly proxyService: ProxyService) {} + constructor( + public readonly service: AdminAreasService, + private readonly proxyService: ProxyService, + ) {} @ApiOperation({ description: 'Find administrative areas within a given country.', diff --git a/api/apps/api/src/modules/admin-areas/admin-areas.module.ts b/api/apps/api/src/modules/admin-areas/admin-areas.module.ts index adafc6aece..13ab2dacee 100644 --- a/api/apps/api/src/modules/admin-areas/admin-areas.module.ts +++ b/api/apps/api/src/modules/admin-areas/admin-areas.module.ts @@ -10,7 +10,6 @@ import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; @Module({ imports: [ TypeOrmModule.forFeature([AdminArea], apiConnections.geoprocessingDB.name), - ], providers: [AdminAreasService, ProxyService], controllers: [AdminAreasController], diff --git a/api/apps/api/src/modules/geo-features/geo-features.controller.ts b/api/apps/api/src/modules/geo-features/geo-features.controller.ts index 0c6e00d77c..08809b05ba 100644 --- a/api/apps/api/src/modules/geo-features/geo-features.controller.ts +++ b/api/apps/api/src/modules/geo-features/geo-features.controller.ts @@ -28,11 +28,13 @@ import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; @ApiBearerAuth() @ApiTags(geoFeatureResource.className) @Controller( - `${apiGlobalPrefixes.v1}/${geoFeatureResource.moduleControllerPrefix}`, + `${apiGlobalPrefixes.v1}/${geoFeatureResource.moduleControllerPrefix}`, ) export class GeoFeaturesController { - constructor(public readonly service: GeoFeaturesService, - private readonly proxyService: ProxyService) {} + constructor( + public readonly service: GeoFeaturesService, + private readonly proxyService: ProxyService, + ) {} @ApiOperation({ description: 'Find all geo features', @@ -106,6 +108,4 @@ export class GeoFeaturesController { async findOne(@Param('id') id: string): Promise { return await this.service.serialize(await this.service.fakeFindOne(id)); } - - } diff --git a/api/apps/api/src/modules/planning-units/planning-units.controller.ts b/api/apps/api/src/modules/planning-units/planning-units.controller.ts index 0a2e438616..6be9568ef3 100644 --- a/api/apps/api/src/modules/planning-units/planning-units.controller.ts +++ b/api/apps/api/src/modules/planning-units/planning-units.controller.ts @@ -1,18 +1,23 @@ -import { Controller, UseGuards,Req, Res, Get} from "@nestjs/common"; -import { ApiBearerAuth, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger"; -import { apiGlobalPrefixes } from "@marxan-api/api.config"; -import { JwtAuthGuard } from "@marxan-api/guards/jwt-auth.guard"; -import { ProxyService } from "@marxan-api/modules/proxy/proxy.service"; +import { Controller, UseGuards, Req, Res, Get } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiForbiddenResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiQuery, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; +import { apiGlobalPrefixes } from '@marxan-api/api.config'; +import { JwtAuthGuard } from '@marxan-api/guards/jwt-auth.guard'; +import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; import { Request, Response } from 'express'; -import { Binary } from "typeorm"; -import { string } from "purify-ts"; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiTags('Plannig units') -@Controller( - `${apiGlobalPrefixes.v1}/planning-units`, -) +@Controller(`${apiGlobalPrefixes.v1}/planning-units`) export class PlanningUnitsController { constructor(private readonly proxyService: ProxyService) {} @@ -33,35 +38,35 @@ export class PlanningUnitsController { description: 'The zoom level ranging from 0 - 20', type: Number, required: true, - example: '5' + example: '5', }) @ApiParam({ name: 'x', description: 'The tile x offset on Mercator Projection', type: Number, required: true, - example: '5' + example: '5', }) @ApiParam({ name: 'y', description: 'The tile y offset on Mercator Projection', type: Number, required: true, - example: '5' + example: '5', }) @ApiParam({ name: 'planningUnitGridShape', description: 'Planning unit grid shape', type: String, required: true, - example: 'square' + example: 'square', }) @ApiParam({ name: 'planningUnitAreakm2', description: 'Planning unit area in km2', type: Number, required: true, - example: 100 + example: 100, }) @ApiQuery({ name: 'bbox', diff --git a/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts b/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts index 71fb304227..a939d0185f 100644 --- a/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts +++ b/api/apps/api/src/modules/protected-areas/protected-areas.controller.ts @@ -4,7 +4,8 @@ import { Param, ParseUUIDPipe, UseGuards, - Req, Res, + Req, + Res, } from '@nestjs/common'; import { ProtectedAreaResult } from './protected-area.geo.entity'; import { @@ -40,8 +41,10 @@ import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; @ApiTags(protectedAreaResource.className) @Controller(`${apiGlobalPrefixes.v1}/protected-areas`) export class ProtectedAreasController { - constructor(public readonly service: ProtectedAreasService, - private readonly proxyService: ProxyService) {} + constructor( + public readonly service: ProtectedAreasService, + private readonly proxyService: ProxyService, + ) {} @ApiOperation({ description: 'Find all protected areas', diff --git a/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts b/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts index 46698788b7..91f868909c 100644 --- a/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts +++ b/api/apps/api/test/proxy.vector-tiles.e2e-spec.ts @@ -1,7 +1,12 @@ -import { HttpStatus, INestApplication, ValidationPipe, Logger } from '@nestjs/common'; +import { + HttpStatus, + INestApplication, + ValidationPipe, + Logger, +} from '@nestjs/common'; import * as request from 'supertest'; import * as JSONAPISerializer from 'jsonapi-serializer'; -import PBF from 'pbf'; +import PBF from 'pbf'; import { tearDown } from './utils/tear-down'; import { Scenario } from '@marxan-api/modules/scenarios/scenario.api.entity'; import { Organization } from '@marxan-api/modules/organizations/organization.api.entity'; @@ -16,7 +21,7 @@ import { IUCNCategory } from '@marxan-api/modules/protected-areas/protected-area import { GivenUserIsLoggedIn } from './steps/given-user-is-logged-in'; import { bootstrapApplication } from 'apps/geoprocessing/test/utils'; -const logger = new Logger('test-vtiles') +const logger = new Logger('test-vtiles'); afterAll(async () => { await tearDown(); @@ -37,10 +42,10 @@ describe('ProxyVectorTilesModule (e2e)', () => { * for tests related to protected areas in a L1 or L2 admin area below. * */ - const country = 'NAM'; - const l1AdminArea = 'NAM.13_1'; - const l2AdminArea = 'NAM.13.5_1'; - const geoFeaturesFilters = { + const country = 'NAM'; + const l1AdminArea = 'NAM.13_1'; + const l2AdminArea = 'NAM.13.5_1'; + const geoFeaturesFilters = { cheeta: { featureClassName: 'iucn_acinonyxjubatus', alias: 'cheetah' }, partialMatches: { us: 'us' }, }; @@ -71,24 +76,15 @@ describe('ProxyVectorTilesModule (e2e)', () => { }, ).then(async (response) => await Deserializer.deserialize(response)); - aScenario = await ScenariosTestUtils.createScenario( - app, - jwtToken, - { - ...E2E_CONFIG.scenarios.valid.minimal(), - projectId: aProjectWithCountryAsPlanningArea.id, - wdpaIucnCategories: [IUCNCategory.NotReported], - }, - ).then(async (response) => await Deserializer.deserialize(response)); - + aScenario = await ScenariosTestUtils.createScenario(app, jwtToken, { + ...E2E_CONFIG.scenarios.valid.minimal(), + projectId: aProjectWithCountryAsPlanningArea.id, + wdpaIucnCategories: [IUCNCategory.NotReported], + }).then(async (response) => await Deserializer.deserialize(response)); }); afterAll(async () => { - await ScenariosTestUtils.deleteScenario( - app, - jwtToken, - aScenario.id, - ); + await ScenariosTestUtils.deleteScenario(app, jwtToken, aScenario.id); await ProjectsTestUtils.deleteProject( app, @@ -107,103 +103,90 @@ describe('ProxyVectorTilesModule (e2e)', () => { /** * https://www.figma.com/file/hq0BZNB9fzyFSbEUgQIHdK/Marxan-Visual_V02?node-id=2991%3A2492 */ - describe('Admin-areas layers', () => { - test.todo( - 'we should test that the response is a valid mvt', - ); - test('Should give back a valid request for preview', - async () => { + describe('Admin-areas layers', () => { + test.todo('we should test that the response is a valid mvt'); + test('Should give back a valid request for preview', async () => { const response = await request(app.getHttpServer()) - .get('/api/v1/administrative-areas/1/preview/tiles/6/30/25.mvt') - .set('Accept-Encoding', 'gzip, deflate') - .set('Authorization', `Bearer ${jwtToken}`) - .expect(HttpStatus.OK) - }) - describe('Filter by guid',() => { - test.skip('guid country level', - async () => { - const response = await request(app.getHttpServer()) - .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') - .set('Authorization', `Bearer ${jwtToken}`) - .expect(HttpStatus.OK); - }); - test.skip('guid adm1 level', - async () => { - const response = await request(app.getHttpServer()) - .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') - .set('Authorization', `Bearer ${jwtToken}`) - .expect(HttpStatus.OK); - }); + .get('/api/v1/administrative-areas/1/preview/tiles/6/30/25.mvt') + .set('Accept-Encoding', 'gzip, deflate') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); }); - - test.skip('Filter by bbox', - async () => { - const response = await request(app.getHttpServer()) - .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') - .set('Authorization', `Bearer ${jwtToken}`) - .expect(HttpStatus.OK); + describe('Filter by guid', () => { + test.skip('guid country level', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); }); - - test('Should simulate an error if input is invalid', - async () => { + test.skip('guid adm1 level', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') .set('Authorization', `Bearer ${jwtToken}`) - .expect(HttpStatus.BAD_REQUEST); + .expect(HttpStatus.OK); + }); }); - test('Should throw a 400 error if filtering by level other than 0, 1 or 2', - async () => { - const response = await request(app.getHttpServer()) + test.skip('Filter by bbox', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); + }); + + test('Should simulate an error if input is invalid', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/1/preview/tiles/100/60/30.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.BAD_REQUEST); + }); + + test('Should throw a 400 error if filtering by level other than 0, 1 or 2', async () => { + const response = await request(app.getHttpServer()) .get('/api/v1/administrative-areas/3/preview/tiles/6/30/25.mvt') .set('Authorization', `Bearer ${jwtToken}`) .expect(HttpStatus.BAD_REQUEST); }); - test('Should throw a 400 error if filtering by z level greater than 20', - async () => { - const response = await request(app.getHttpServer()) + test('Should throw a 400 error if filtering by z level greater than 20', async () => { + const response = await request(app.getHttpServer()) .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') .set('Authorization', `Bearer ${jwtToken}`) .expect(HttpStatus.BAD_REQUEST); }); }); describe('WDPA layers', () => { - test.skip('Should give back a valid request for wdpa preview', - async () => { - const response = await request(app.getHttpServer()) + test.skip('Should give back a valid request for wdpa preview', async () => { + const response = await request(app.getHttpServer()) .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') .set('Authorization', `Bearer ${jwtToken}`) .expect(HttpStatus.OK); }); }); describe('Feature layer previews', () => { - test('Should give back a valid request for a feature preview', - async () => { + test('Should give back a valid request for a feature preview', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') .set('Authorization', `Bearer ${jwtToken}`); - logger.error(typeof(response.body)) + logger.error(typeof response.body); // response.expect(HttpStatus.OK); }); }); describe('PUs layer previews', () => { - test('Should give back a valid request for a PUs preview', - async () => { - const response = await request(app.getHttpServer()) - .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') - .set('Authorization', `Bearer ${jwtToken}`) - .expect(HttpStatus.OK); + test('Should give back a valid request for a PUs preview', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); }); }); describe('Scenario PUs layers', () => { - test.skip('Should give back a valid request for a scenario PUs', - async () => { - const response = await request(app.getHttpServer()) - .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') - .set('Authorization', `Bearer ${jwtToken}`) - .expect(HttpStatus.OK); + test.skip('Should give back a valid request for a scenario PUs', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/administrative-areas/3/preview/tiles/21/30/25.mvt') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); }); }); }); diff --git a/api/apps/geoprocessing/src/dto/info.dto.ts b/api/apps/geoprocessing/src/dto/info.dto.ts index aee6460886..67298abbe1 100644 --- a/api/apps/geoprocessing/src/dto/info.dto.ts +++ b/api/apps/geoprocessing/src/dto/info.dto.ts @@ -1,3 +1,2 @@ - -import { InfoDTO } from "nestjs-base-service"; +import { InfoDTO } from 'nestjs-base-service'; export type AppInfoDTO = InfoDTO; 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 8128f44a62..016130ceaf 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 @@ -69,10 +69,12 @@ export class AdminAreasService { whereQuery = `gid_2 IS NOT NULL`; } if (filters?.guid && level > 0) { - whereQuery += ` AND gid_${level-1} = '${filters?.guid}'`; + whereQuery += ` AND gid_${level - 1} = '${filters?.guid}'`; } if (filters?.bbox) { - whereQuery += ` AND the_geom && ST_MakeEnvelope(${nominatim2bbox(filters?.bbox)}, 4326)`; + whereQuery += ` AND the_geom && ST_MakeEnvelope(${nominatim2bbox( + filters?.bbox, + )}, 4326)`; } return whereQuery; diff --git a/api/apps/geoprocessing/src/modules/features/features.service.ts b/api/apps/geoprocessing/src/modules/features/features.service.ts index 8f819bb3bd..67a0aa10d5 100644 --- a/api/apps/geoprocessing/src/modules/features/features.service.ts +++ b/api/apps/geoprocessing/src/modules/features/features.service.ts @@ -50,7 +50,9 @@ export class FeatureService { let whereQuery = `feature_id = '${id}'`; if (bbox) { - whereQuery += `AND st_intersects(ST_MakeEnvelope(${nominatim2bbox(bbox)}, 4326), the_geom)`; + whereQuery += `AND st_intersects(ST_MakeEnvelope(${nominatim2bbox( + bbox, + )}, 4326), the_geom)`; } return whereQuery; } 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 5a9588919e..60678d404a 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 @@ -53,22 +53,23 @@ export class PlanningUnitsService { * If any value is not provided, 4000 would be the default. */ regularFuncionGridSelector( - planningUnitGridShape: PlanningUnitGridShape + planningUnitGridShape: PlanningUnitGridShape, ): string { - const functEquivalence: { [key in keyof typeof PlanningUnitGridShape]: string} = { - hexagon: 'ST_HexagonGrid', - square: 'ST_SquareGrid', - } + const functEquivalence: { + [key in keyof typeof PlanningUnitGridShape]: string; + } = { + hexagon: 'ST_HexagonGrid', + square: 'ST_SquareGrid', + }; - return functEquivalence[planningUnitGridShape] + return functEquivalence[planningUnitGridShape]; } calculateGridSize( planningUnitGridShape: PlanningUnitGridShape, - planningUnitAreakm2: number - ): number { - - return Math.sqrt(planningUnitAreakm2) * 1000 + planningUnitAreakm2: number, + ): number { + return Math.sqrt(planningUnitAreakm2) * 1000; } /** * @param bbox bounding box of the area where the grids would be generated @@ -85,22 +86,24 @@ export class PlanningUnitsService { planningUnitAreakm2: number, filters?: PlanningUnitsFilters, ): string { - const gridShape = this.regularFuncionGridSelector(planningUnitGridShape) - const gridSize = this.calculateGridSize(planningUnitGridShape, - planningUnitAreakm2) - const ratioPixelExtent = (gridSize / (156412/(2**z))) + const gridShape = this.regularFuncionGridSelector(planningUnitGridShape); + const gridSize = this.calculateGridSize( + planningUnitGridShape, + planningUnitAreakm2, + ); + const ratioPixelExtent = gridSize / (156412 / 2 ** z); let Query = `( SELECT row_number() over() as id, (${gridShape}(${gridSize}, \ ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom as the_geom)`; // 156412 references to m per pixel at z level 0 at the equator in EPSG:3857 // (so we are checking that the pixel ration is < 8 px) // If so the shape we are getting is down the optimal to visualize it - if ( ratioPixelExtent < 8){ + if (ratioPixelExtent < 8) { Query = `( SELECT row_number() over() as id, st_centroid((${gridShape}(${gridSize}, \ ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom ) as the_geom )`; } return Query; -} + } /** * @param bbox bounding box of the area where the grids would be generated * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape @@ -108,13 +111,13 @@ export class PlanningUnitsService { * @param planningUnitAreakm2 area in km2 of the individual grid that would be generated. * If any value is not provided, 4000 would be the default. */ - buildPlanningUnitsWhereQuery( - filters?: PlanningUnitsFilters, - ): string { + buildPlanningUnitsWhereQuery(filters?: PlanningUnitsFilters): string { let whereQuery = ``; if (filters?.bbox) { - whereQuery =`st_intersects(ST_Transform(ST_MakeEnvelope(${nominatim2bbox(filters.bbox)}, 4326), 3857) ,the_geom)`; + whereQuery = `st_intersects(ST_Transform(ST_MakeEnvelope(${nominatim2bbox( + filters.bbox, + )}, 4326), 3857) ,the_geom)`; } return whereQuery; } @@ -134,7 +137,7 @@ export class PlanningUnitsService { planningUnitAreakm2, } = tileSpecification; - const inputProjection = 3857 + const inputProjection = 3857; const attributes = 'id'; const table = this.buildPlanningUnitsCustomQuery( @@ -143,11 +146,9 @@ export class PlanningUnitsService { z, planningUnitGridShape, planningUnitAreakm2, - filters - ); - const customQuery = this.buildPlanningUnitsWhereQuery( - filters - ) + filters, + ); + const customQuery = this.buildPlanningUnitsWhereQuery(filters); return this.tileService.getTile({ z, @@ -156,7 +157,7 @@ export class PlanningUnitsService { table, attributes, inputProjection, - customQuery + customQuery, }); } } diff --git a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.controller.ts b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.controller.ts index 17d2d36182..26f00dfef6 100644 --- a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.controller.ts +++ b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.controller.ts @@ -78,7 +78,10 @@ export class ProtectedAreasController { @Query() protectedAreasFilters: ProtectedAreasFilters, @Res() response: Response, ): Promise { - const tile: Buffer = await this.service.findTile(tileRequest, protectedAreasFilters); + const tile: Buffer = await this.service.findTile( + tileRequest, + protectedAreasFilters, + ); return response.send(tile); } } diff --git a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts index 0899a80269..c3feff43c9 100644 --- a/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts +++ b/api/apps/geoprocessing/src/modules/protected-areas/protected-areas.service.ts @@ -5,7 +5,12 @@ import { } from '@marxan-geoprocessing/modules/tile/tile.service'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { + IsNumber, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; import { ProtectedArea } from '@marxan-geoprocessing/modules/protected-areas/protected-areas.geo.entity'; import { BBox } from 'geojson'; @@ -36,14 +41,18 @@ export class ProtectedAreasService { /** * @param filters bounding box of the area where the grids would be generated */ - buildProtectedAreasWhereQuery( + buildProtectedAreasWhereQuery( filters?: ProtectedAreasFilters, ): string | undefined { let whereQuery = undefined; whereQuery = filters?.id ? ` id = '${filters?.id}'` : undefined; if (filters?.bbox) { - const bboxIntersect =`st_intersects(ST_MakeEnvelope(${nominatim2bbox(filters.bbox)}, 4326), the_geom)`; - whereQuery = whereQuery ? `${whereQuery} and ${bboxIntersect}`: bboxIntersect; + const bboxIntersect = `st_intersects(ST_MakeEnvelope(${nominatim2bbox( + filters.bbox, + )}, 4326), the_geom)`; + whereQuery = whereQuery + ? `${whereQuery} and ${bboxIntersect}` + : bboxIntersect; } return whereQuery; } diff --git a/api/apps/geoprocessing/src/modules/tile/tile.service.ts b/api/apps/geoprocessing/src/modules/tile/tile.service.ts index 5500673515..d4805bdf89 100644 --- a/api/apps/geoprocessing/src/modules/tile/tile.service.ts +++ b/api/apps/geoprocessing/src/modules/tile/tile.service.ts @@ -1,5 +1,10 @@ // to-do: work on cache later -import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; import { getConnection } from 'typeorm'; import * as zlib from 'zlib'; import { Transform } from 'class-transformer'; @@ -85,10 +90,10 @@ export class TileService { */ private readonly logger: Logger = new Logger(TileService.name); - simplification(z:number, geometry: string): string { - return (z > 7) ? `${geometry}` : `ST_RemoveRepeatedPoints(${geometry}, ${ - 0.1 / (z * 2) - })` + simplification(z: number, geometry: string): string { + return z > 7 + ? `${geometry}` + : `ST_RemoveRepeatedPoints(${geometry}, ${0.1 / (z * 2)})`; } /** * All database interaction is encapsulated in this function. The design-goal is to keep the time where a database- @@ -114,16 +119,20 @@ export class TileService { .createQueryBuilder() .select(`ST_AsMVT(tile, 'layer0', ${extent}, 'mvt_geom')`, 'mvt') .from((subQuery) => { - - subQuery.select( - `${attributes}, ST_AsMVTGeom(ST_Transform(${this.simplification(z, geometry)}, 3857), + subQuery.select( + `${attributes}, ST_AsMVTGeom(ST_Transform(${this.simplification( + z, + geometry, + )}, 3857), ST_TileEnvelope(${z}, ${x}, ${y}), ${extent}, ${buffer}, true) AS mvt_geom`, - ); + ); - subQuery.from(table, 'data').where( - `ST_Intersects(ST_Transform(ST_TileEnvelope(:z, :x, :y), ${inputProjection}), ${geometry} )`, + subQuery + .from(table, 'data') + .where( + `ST_Intersects(ST_Transform(ST_TileEnvelope(:z, :x, :y), ${inputProjection}), ${geometry} )`, { z, x, y }, - ); + ); if (customQuery) { subQuery.andWhere(customQuery); } @@ -135,7 +144,9 @@ export class TileService { return result; } else { this.logger.error(query.getSql()); - throw new NotFoundException("Property 'mvt' does not exist in res.rows[0]"); + throw new NotFoundException( + "Property 'mvt' does not exist in res.rows[0]", + ); } } diff --git a/api/apps/geoprocessing/src/utils/bbox.utils.ts b/api/apps/geoprocessing/src/utils/bbox.utils.ts index 3f3caaa89a..d95e062f1a 100644 --- a/api/apps/geoprocessing/src/utils/bbox.utils.ts +++ b/api/apps/geoprocessing/src/utils/bbox.utils.ts @@ -1,4 +1,3 @@ - import { BBox } from 'geojson'; /** * Utility functions related to lower-level interaction with bbox operations. @@ -6,14 +5,14 @@ import { BBox } from 'geojson'; * @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]] + return [bbox[0], bbox[2], bbox[1], bbox[3]]; } export function nominatim2bbox(nominatim: BBox): BBox { -return[nominatim[0], nominatim[2], nominatim[1], nominatim[3]] + return [nominatim[0], nominatim[2], nominatim[1], nominatim[3]]; } From 64520a2785328e590e4e1c28b9e53d5326f47e35 Mon Sep 17 00:00:00 2001 From: Alicia Date: Thu, 10 Jun 2021 11:37:24 +0200 Subject: [PATCH 17/21] instead of filter by parent filter by its level --- .../src/modules/admin-areas/admin-areas.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 016130ceaf..4e61872115 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 @@ -68,8 +68,8 @@ export class AdminAreasService { if (level === 2) { whereQuery = `gid_2 IS NOT NULL`; } - if (filters?.guid && level > 0) { - whereQuery += ` AND gid_${level - 1} = '${filters?.guid}'`; + if (filters?.guid) { + whereQuery += ` AND gid_${level} = '${filters?.guid}'`; } if (filters?.bbox) { whereQuery += ` AND the_geom && ST_MakeEnvelope(${nominatim2bbox( From dd2d691c2482aceffee05d6f3bb3f7ef19c29346 Mon Sep 17 00:00:00 2001 From: Alicia Date: Thu, 10 Jun 2021 11:57:15 +0200 Subject: [PATCH 18/21] fixed how to manage planning grids unused params as per dom request --- .../planning-units/planning-units.service.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) 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 60678d404a..67a1b796bf 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 @@ -49,30 +49,29 @@ export class PlanningUnitsService { /** * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape * can be square or hexagon. If any grid shape is provided, square would be the default. - * @param planningUnitAreakm2 area in km2 of the individual grid that would be generated. - * If any value is not provided, 4000 would be the default. */ - regularFuncionGridSelector( + regularFunctionGridSelector( planningUnitGridShape: PlanningUnitGridShape, ): string { - const functEquivalence: { + const functionEquivalence: { [key in keyof typeof PlanningUnitGridShape]: string; } = { hexagon: 'ST_HexagonGrid', square: 'ST_SquareGrid', }; - return functEquivalence[planningUnitGridShape]; + return functionEquivalence[planningUnitGridShape]; } calculateGridSize( - planningUnitGridShape: PlanningUnitGridShape, planningUnitAreakm2: number, ): number { return Math.sqrt(planningUnitAreakm2) * 1000; } /** - * @param bbox bounding box of the area where the grids would be generated + * @param x bounding box of the area where the grids would be generated + * @param y bounding box of the area where the grids would be generated + * @param z bounding box of the area where the grids would be generated * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape * can be square or hexagon. If any grid shape is provided, square would be the default. * @param planningUnitAreakm2 area in km2 of the individual grid that would be generated. @@ -84,11 +83,9 @@ export class PlanningUnitsService { z: number, planningUnitGridShape: PlanningUnitGridShape, planningUnitAreakm2: number, - filters?: PlanningUnitsFilters, ): string { - const gridShape = this.regularFuncionGridSelector(planningUnitGridShape); + const gridShape = this.regularFunctionGridSelector(planningUnitGridShape); const gridSize = this.calculateGridSize( - planningUnitGridShape, planningUnitAreakm2, ); const ratioPixelExtent = gridSize / (156412 / 2 ** z); @@ -146,7 +143,6 @@ export class PlanningUnitsService { z, planningUnitGridShape, planningUnitAreakm2, - filters, ); const customQuery = this.buildPlanningUnitsWhereQuery(filters); From f733e3269905776d35f60655d4d4d4187a5a6de5 Mon Sep 17 00:00:00 2001 From: Alicia Date: Thu, 10 Jun 2021 11:59:08 +0200 Subject: [PATCH 19/21] uncommented code used for testing --- api/apps/geoprocessing/test/index.html | 138 ++++++++++++------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/api/apps/geoprocessing/test/index.html b/api/apps/geoprocessing/test/index.html index 1eca491ac5..6d87947257 100644 --- a/api/apps/geoprocessing/test/index.html +++ b/api/apps/geoprocessing/test/index.html @@ -147,76 +147,76 @@ 'fill-opacity': 0.1 } }, - // { - // id: 'postgis-tiles-layer-wdpa', - // type: 'fill', - // source: 'postgis-tileswdpa', - // 'source-layer': 'layer0', - // paint: { - // 'fill-outline-color': 'blue', - // 'fill-color': 'blue', - // 'fill-opacity': 0.3, - // }, - // }, - // { - // id: 'postgis-tiles-layer-features', - // type: 'fill', - // source: 'postgis-tilesFeatures', - // 'source-layer': 'layer0', - // paint: { - // 'fill-outline-color': 'pink', - // 'fill-color': 'pink', - // 'fill-opacity': 0.3, - // }, - // }, - // { - // id: 'postgis-tiles-layer-PU', - // type: 'fill', - // source: 'postgis-tilesPU', - // 'source-layer': 'layer0', + { + id: 'postgis-tiles-layer-wdpa', + type: 'fill', + source: 'postgis-tileswdpa', + 'source-layer': 'layer0', + paint: { + 'fill-outline-color': 'blue', + 'fill-color': 'blue', + 'fill-opacity': 0.3, + }, + }, + { + id: 'postgis-tiles-layer-features', + type: 'fill', + source: 'postgis-tilesFeatures', + 'source-layer': 'layer0', + paint: { + 'fill-outline-color': 'pink', + 'fill-color': 'pink', + 'fill-opacity': 0.3, + }, + }, + { + id: 'postgis-tiles-layer-PU', + type: 'fill', + source: 'postgis-tilesPU', + 'source-layer': 'layer0', - // paint: { - // 'fill-outline-color': 'yellow', - // 'fill-color': 'red', - // 'fill-opacity': 0.1, - // }, - // }, - // { - // id: 'postgis-tiles-layer-PU-points', - // type: 'circle', - // source: 'postgis-tilesPU2', - // 'source-layer': 'layer0', - // paint: { - // 'circle-color': 'blue', - // 'circle-opacity':[ - // 'interpolate', - // // Set the exponential rate of change to 0.5 - // ['exponential', 0.5], - // ['zoom'], - // // When zoom is 15, buildings will be beige. - // 5, - // 1, - // // When zoom is 18 or higher, buildings will be yellow. - // 12, - // 0 - // ], - // 'circle-stroke-opacity':[ - // 'interpolate', - // // Set the exponential rate of change to 0.5 - // ['exponential', 0.5], - // ['zoom'], - // // When zoom is 15, buildings will be beige. - // 2, - // 1, - // // When zoom is 18 or higher, buildings will be yellow. - // 7, - // 0 - // ], - // 'circle-radius': 1, - // 'circle-stroke-width': 1, - // 'circle-stroke-color': 'blue' - // }, - // }, + paint: { + 'fill-outline-color': 'yellow', + 'fill-color': 'red', + 'fill-opacity': 0.1, + }, + }, + { + id: 'postgis-tiles-layer-PU-points', + type: 'circle', + source: 'postgis-tilesPU2', + 'source-layer': 'layer0', + paint: { + 'circle-color': 'blue', + 'circle-opacity':[ + 'interpolate', + // Set the exponential rate of change to 0.5 + ['exponential', 0.5], + ['zoom'], + // When zoom is 15, buildings will be beige. + 5, + 1, + // When zoom is 18 or higher, buildings will be yellow. + 12, + 0 + ], + 'circle-stroke-opacity':[ + 'interpolate', + // Set the exponential rate of change to 0.5 + ['exponential', 0.5], + ['zoom'], + // When zoom is 15, buildings will be beige. + 2, + 1, + // When zoom is 18 or higher, buildings will be yellow. + 7, + 0 + ], + 'circle-radius': 1, + 'circle-stroke-width': 1, + 'circle-stroke-color': 'blue' + }, + }, ], }, }); From c4fbf9240b04727e2c249b5abd067c40efcb12cf Mon Sep 17 00:00:00 2001 From: Alicia Date: Thu, 10 Jun 2021 12:09:42 +0200 Subject: [PATCH 20/21] arange a bit the code as requested by dom --- .../planning-units/planning-units.service.ts | 127 +++++++++--------- 1 file changed, 66 insertions(+), 61 deletions(-) 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 67a1b796bf..e9031a31a9 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 @@ -47,37 +47,56 @@ export class PlanningUnitsService { ) {} /** - * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape - * can be square or hexagon. If any grid shape is provided, square would be the default. + * @todo findTile for entity:(already created grid for a scenario with join options of other entities) + * + * @param tileSpecification + * @param filters so far only bbox is accepted + * @returns vector tile */ - regularFunctionGridSelector( - planningUnitGridShape: PlanningUnitGridShape, - ): string { - const functionEquivalence: { - [key in keyof typeof PlanningUnitGridShape]: string; - } = { - hexagon: 'ST_HexagonGrid', - square: 'ST_SquareGrid', - }; + public findPreviewTile( + tileSpecification: tileSpecification, + filters?: PlanningUnitsFilters, + ): Promise { + const { + z, + x, + y, + planningUnitGridShape, + planningUnitAreakm2, + } = tileSpecification; - return functionEquivalence[planningUnitGridShape]; - } + const inputProjection = 3857; - calculateGridSize( - planningUnitAreakm2: number, - ): number { - return Math.sqrt(planningUnitAreakm2) * 1000; + const attributes = 'id'; + const table = this.buildPlanningUnitsCustomQuery( + x, + y, + z, + planningUnitGridShape, + planningUnitAreakm2, + ); + const customQuery = this.buildPlanningUnitsWhereQuery(filters); + + return this.tileService.getTile({ + z, + x, + y, + table, + attributes, + inputProjection, + customQuery, + }); } /** - * @param x bounding box of the area where the grids would be generated - * @param y bounding box of the area where the grids would be generated - * @param z bounding box of the area where the grids would be generated + * @param x x param of a tiler system + * @param y y param of a tiler system + * @param z z param of a tiler system * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape * can be square or hexagon. If any grid shape is provided, square would be the default. * @param planningUnitAreakm2 area in km2 of the individual grid that would be generated. * If any value is not provided, 4000 would be the default. */ - buildPlanningUnitsCustomQuery( + private buildPlanningUnitsCustomQuery( x: number, y: number, z: number, @@ -88,10 +107,10 @@ export class PlanningUnitsService { const gridSize = this.calculateGridSize( planningUnitAreakm2, ); + // 156412 references to m per pixel at z level 0 at the equator in EPSG:3857 const ratioPixelExtent = gridSize / (156412 / 2 ** z); let Query = `( SELECT row_number() over() as id, (${gridShape}(${gridSize}, \ ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom as the_geom)`; - // 156412 references to m per pixel at z level 0 at the equator in EPSG:3857 // (so we are checking that the pixel ration is < 8 px) // If so the shape we are getting is down the optimal to visualize it if (ratioPixelExtent < 8) { @@ -102,13 +121,9 @@ export class PlanningUnitsService { return Query; } /** - * @param bbox bounding box of the area where the grids would be generated - * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape - * can be square or hexagon. If any grid shape is provided, square would be the default. - * @param planningUnitAreakm2 area in km2 of the individual grid that would be generated. - * If any value is not provided, 4000 would be the default. + * @param filters including only bounding box of the area where the grids would be generated */ - buildPlanningUnitsWhereQuery(filters?: PlanningUnitsFilters): string { + private buildPlanningUnitsWhereQuery(filters?: PlanningUnitsFilters): string { let whereQuery = ``; if (filters?.bbox) { @@ -119,41 +134,31 @@ export class PlanningUnitsService { return whereQuery; } + /** - * @todo get attributes from Entity, based on user selection + * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape + * can be square or hexagon. If any grid shape is provided, square would be the default. */ - public findPreviewTile( - tileSpecification: tileSpecification, - filters?: PlanningUnitsFilters, - ): Promise { - const { - z, - x, - y, - planningUnitGridShape, - planningUnitAreakm2, - } = tileSpecification; - - const inputProjection = 3857; - - const attributes = 'id'; - const table = this.buildPlanningUnitsCustomQuery( - x, - y, - z, - planningUnitGridShape, - planningUnitAreakm2, - ); - const customQuery = this.buildPlanningUnitsWhereQuery(filters); + private regularFunctionGridSelector( + planningUnitGridShape: PlanningUnitGridShape, + ): string { + const functionEquivalence: { + [key in keyof typeof PlanningUnitGridShape]: string; + } = { + hexagon: 'ST_HexagonGrid', + square: 'ST_SquareGrid', + }; - return this.tileService.getTile({ - z, - x, - y, - table, - attributes, - inputProjection, - customQuery, - }); + return functionEquivalence[planningUnitGridShape]; + } + /** + * + * @param planningUnitAreakm2 + * @returns grid h size in m + */ + private calculateGridSize( + planningUnitAreakm2: number, + ): number { + return Math.sqrt(planningUnitAreakm2) * 1000; } } From d1124726cff024ba33493952dbcb8f874d3bbacb Mon Sep 17 00:00:00 2001 From: Alicia Date: Thu, 10 Jun 2021 12:25:09 +0200 Subject: [PATCH 21/21] addressed comments --- .../planning-units/planning-units.service.ts | 17 ++++++----------- .../src/modules/tile/tile.service.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 13 deletions(-) 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 e9031a31a9..e9b0ae9473 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 @@ -53,7 +53,7 @@ export class PlanningUnitsService { * @param filters so far only bbox is accepted * @returns vector tile */ - public findPreviewTile( + public findPreviewTile( tileSpecification: tileSpecification, filters?: PlanningUnitsFilters, ): Promise { @@ -104,21 +104,19 @@ export class PlanningUnitsService { planningUnitAreakm2: number, ): string { const gridShape = this.regularFunctionGridSelector(planningUnitGridShape); - const gridSize = this.calculateGridSize( - planningUnitAreakm2, - ); + const gridSize = this.calculateGridSize(planningUnitAreakm2); // 156412 references to m per pixel at z level 0 at the equator in EPSG:3857 const ratioPixelExtent = gridSize / (156412 / 2 ** z); - let Query = `( SELECT row_number() over() as id, (${gridShape}(${gridSize}, \ + let query = `( SELECT row_number() over() as id, (${gridShape}(${gridSize}, \ ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom as the_geom)`; // (so we are checking that the pixel ration is < 8 px) // If so the shape we are getting is down the optimal to visualize it if (ratioPixelExtent < 8) { - Query = `( SELECT row_number() over() as id, st_centroid((${gridShape}(${gridSize}, \ + query = `( SELECT row_number() over() as id, st_centroid((${gridShape}(${gridSize}, \ ST_Transform(ST_TileEnvelope(${z}, ${x}, ${y}), 3857))).geom ) as the_geom )`; } - return Query; + return query; } /** * @param filters including only bounding box of the area where the grids would be generated @@ -134,7 +132,6 @@ export class PlanningUnitsService { return whereQuery; } - /** * @param planningUnitGridShape the grid shape that would be use for generating the grid. This grid shape * can be square or hexagon. If any grid shape is provided, square would be the default. @@ -156,9 +153,7 @@ export class PlanningUnitsService { * @param planningUnitAreakm2 * @returns grid h size in m */ - private calculateGridSize( - planningUnitAreakm2: number, - ): number { + private calculateGridSize(planningUnitAreakm2: number): number { return Math.sqrt(planningUnitAreakm2) * 1000; } } diff --git a/api/apps/geoprocessing/src/modules/tile/tile.service.ts b/api/apps/geoprocessing/src/modules/tile/tile.service.ts index d4805bdf89..121e48374e 100644 --- a/api/apps/geoprocessing/src/modules/tile/tile.service.ts +++ b/api/apps/geoprocessing/src/modules/tile/tile.service.ts @@ -90,7 +90,13 @@ export class TileService { */ private readonly logger: Logger = new Logger(TileService.name); - simplification(z: number, geometry: string): string { + /** + * Simplification based in zoom level + * @param z + * @param geometry + * @returns + */ + geometrySimplification(z: number, geometry: string): string { return z > 7 ? `${geometry}` : `ST_RemoveRepeatedPoints(${geometry}, ${0.1 / (z * 2)})`; @@ -120,7 +126,7 @@ export class TileService { .select(`ST_AsMVT(tile, 'layer0', ${extent}, 'mvt_geom')`, 'mvt') .from((subQuery) => { subQuery.select( - `${attributes}, ST_AsMVTGeom(ST_Transform(${this.simplification( + `${attributes}, ST_AsMVTGeom(ST_Transform(${this.geometrySimplification( z, geometry, )}, 3857),