Skip to content

Commit

Permalink
Get Features and FeatureCollection
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Mar 6, 2024
1 parent afe0fd5 commit fcc74d5
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 49 deletions.
27 changes: 11 additions & 16 deletions api/src/modules/eudr-alerts/eudr.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { Response } from 'express';
import { Writable } from 'stream';

import { ApiOkTreeResponse } from 'decorators/api-tree-response.decorator';
import { Supplier } from 'modules/suppliers/supplier.entity';
import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor';
Expand All @@ -35,6 +34,8 @@ import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto';
import { EudrService } from 'modules/eudr-alerts/eudr.service';
import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto';
import { EUDRAlertDates } from 'modules/eudr-alerts/eudr.repositoty.interface';
import { GetEUDRFeaturesGeoJSONDto } from 'modules/geo-regions/dto/get-features-geojson.dto';
import { Feature, FeatureCollection } from 'geojson';

@ApiTags('EUDR')
@Controller('/api/v1/eudr')
Expand Down Expand Up @@ -163,19 +164,13 @@ export class EudrController {
return this.eudrAlertsService.getAlerts(dto);
}

streamResponse(response: Response, stream: Writable): any {
stream.on('data', (data: any) => {
const json: string = JSON.stringify(data);
response.write(json + '\n');
});

stream.on('end', () => {
response.end();
});

stream.on('error', (error: any) => {
console.error('Stream error:', error);
response.status(500).send('Error processing stream');
});
@ApiOperation({
description: 'Get a Feature List or Feature Collection by GeoRegion Ids',
})
@Get('/geo-features')
async getGeoJson(
@Query(ValidationPipe) dto: GetEUDRFeaturesGeoJSONDto,
): Promise<Feature[] | FeatureCollection> {
return this.geoRegionsService.getGeoJson(dto);
}
}
30 changes: 30 additions & 0 deletions api/src/modules/geo-regions/dto/get-features-geojson.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsBoolean, IsOptional, IsUUID } from 'class-validator';

export class GetFeaturesGeoJsonDto {
@ApiPropertyOptional()
@IsOptional()
@IsUUID('4', { each: true })
geoRegionIds!: string[];

@ApiPropertyOptional({
description:
'If true, it will return a FeatureCollection instead of a Feature List. Default is false.',
default: false,
})
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
collection: boolean = false;

isEUDRRequested(): boolean {
return 'eudr' in this;
}
}

export class GetEUDRFeaturesGeoJSONDto extends GetFeaturesGeoJsonDto {
@IsOptional()
@IsBoolean()
eudr: boolean = true;
}
84 changes: 62 additions & 22 deletions api/src/modules/geo-regions/geo-features.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { DataSource, Repository, SelectQueryBuilder } from 'typeorm';
import { GeoRegion } from 'modules/geo-regions/geo-region.entity';
import { FeatureCollection, Feature } from 'geojson';
Expand All @@ -13,35 +13,75 @@ import {

@Injectable()
export class GeoFeaturesService extends Repository<GeoRegion> {
logger: Logger = new Logger(GeoFeaturesService.name);

constructor(private dataSource: DataSource) {
super(GeoRegion, dataSource.createEntityManager());
}

async getGeoFeatures(): Promise<Feature[] | FeatureCollection> {
return null as any;
}

async getGeoJson(
dto?: GetEUDRFeaturesGeoJSONDto | GetFeaturesGeoJsonDto,
): Promise<any> {
async getGeoFeatures(
dto: GetFeaturesGeoJsonDto | GetEUDRFeaturesGeoJSONDto,
): Promise<Feature[] | FeatureCollection> {
const queryBuilder: SelectQueryBuilder<GeoRegion> =
this.createQueryBuilder('gr');
queryBuilder
.select(
'json_build_object(type, FeatureCollection, features, json_agg(ST_AsGeoJSON(gr.theGeom)::json))',
'geojson',
)
.innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id');
if (dto?.geoRegionIds) {
queryBuilder.where('gr.id IN (:...ids)', { ids: dto.geoRegionIds });
queryBuilder.innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id');
if (dto.isEUDRRequested()) {
queryBuilder.where('sl.locationType = :locationType', {
locationType: LOCATION_TYPES.EUDR,
});
}

if (dto?.isEUDRRequested()) {
queryBuilder.andWhere('sl.locationType = :type', {
type: LOCATION_TYPES.EUDR,
if (dto.geoRegionIds) {
queryBuilder.andWhere('gr.id IN (:...geoRegionIds)', {
geoRegionIds: dto.geoRegionIds,
});
}
const [qury, params] = queryBuilder.getQueryAndParameters();
return queryBuilder.getRawMany();
if (dto?.collection) {
return this.selectAsFeatureCollection(queryBuilder);
}
return this.selectAsFeatures(queryBuilder);
}

private async selectAsFeatures(
queryBuilder: SelectQueryBuilder<GeoRegion>,
): Promise<Feature[]> {
queryBuilder.select(
`
json_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(gr.theGeom)::json,
'properties', json_build_object('id', gr.id)
)`,
'geojson',
);
const result: Feature[] | undefined = await queryBuilder.getRawMany();
if (!result.length) {
throw new NotFoundException(`Could not retrieve geo features`);
}
return result;
}

private async selectAsFeatureCollection(
queryBuilder: SelectQueryBuilder<GeoRegion>,
): Promise<FeatureCollection> {
queryBuilder.select(
`
json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(gr.theGeom)::json,
'properties', json_build_object('id', gr.id)
)
)
)`,
'geojson',
);
const result: FeatureCollection | undefined =
await queryBuilder.getRawOne<FeatureCollection>();
if (!result) {
throw new NotFoundException(`Could not retrieve geo features`);
}
return result;
}
}
3 changes: 2 additions & 1 deletion api/src/modules/geo-regions/geo-region.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AdminRegion } from 'modules/admin-regions/admin-region.entity';
import { BaseServiceResource } from 'types/resource.interface';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity';
import { Geometry } from 'geojson';

export const geoRegionResource: BaseServiceResource = {
className: 'GeoRegion',
Expand Down Expand Up @@ -47,7 +48,7 @@ export class GeoRegion extends BaseEntity {
nullable: true,
})
@ApiPropertyOptional()
theGeom?: JSON;
theGeom?: Geometry;

// TODO: It might be interesting to add a trigger to calculate the value in case it's not provided. We are considering that EUDR will alwaus provide the value
// but not the regular ingestion
Expand Down
8 changes: 3 additions & 5 deletions api/src/modules/geo-regions/geo-region.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ import {
import { GeoRegion } from 'modules/geo-regions/geo-region.entity';
import { LocationGeoRegionDto } from 'modules/geo-regions/dto/location.geo-region.dto';
import { Injectable } from '@nestjs/common';
import { GetAdminRegionTreeWithOptionsDto } from '../admin-regions/dto/get-admin-region-tree-with-options.dto';
import { AdminRegion } from '../admin-regions/admin-region.entity';
import { SourcingLocation } from '../sourcing-locations/sourcing-location.entity';
import { BaseQueryBuilder } from '../../utils/base.query-builder';
import { GetEUDRGeoRegions } from './dto/get-geo-region.dto';
import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity';
import { BaseQueryBuilder } from 'utils/base.query-builder';
import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto';

@Injectable()
export class GeoRegionRepository extends Repository<GeoRegion> {
Expand Down
2 changes: 0 additions & 2 deletions api/src/modules/geo-regions/geo-regions.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
Param,
Patch,
Post,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
Expand Down Expand Up @@ -36,7 +35,6 @@ import {
import { CreateGeoRegionDto } from 'modules/geo-regions/dto/create.geo-region.dto';
import { UpdateGeoRegionDto } from 'modules/geo-regions/dto/update.geo-region.dto';
import { PaginationMeta } from 'utils/app-base.service';
import { GetEUDRGeoRegions } from './dto/get-geo-region.dto';

@Controller(`/api/v1/geo-regions`)
@ApiTags(geoRegionResource.className)
Expand Down
4 changes: 2 additions & 2 deletions api/src/modules/geo-regions/geo-regions.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import {
AppBaseService,
JSONAPISerializerConfig,
Expand Down Expand Up @@ -122,6 +122,6 @@ export class GeoRegionsService extends AppBaseService<
async getGeoJson(
dto: GetFeaturesGeoJsonDto | GetEUDRFeaturesGeoJSONDto,
): Promise<any> {
return this.geoFeatures.getGeoJson(dto);
return this.geoFeatures.getGeoFeatures(dto);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as wellknown from 'wellknown';
import { DataSource, QueryRunner, Repository } from 'typeorm';
import { GeoCodingError } from 'modules/geo-coding/errors/geo-coding.error';
import { AdminRegion } from 'modules/admin-regions/admin-region.entity';
import { Geometry } from 'geojson';

/**
* @debt: Define a more accurate DTO / Interface / Class for API-DB trades
Expand Down Expand Up @@ -87,7 +88,7 @@ export class EUDRDTOProcessor {
const geoRegion: GeoRegion = new GeoRegion();
let savedGeoRegion: GeoRegion;
geoRegion.totalArea = row.total_area_ha;
geoRegion.theGeom = wellknown.parse(row.geometry) as unknown as JSON;
geoRegion.theGeom = wellknown.parse(row.geometry) as Geometry;
geoRegion.isCreatedByUser = true;
geoRegion.name = row.plot_name;
const foundGeoRegion: GeoRegion | null =
Expand Down

0 comments on commit fcc74d5

Please sign in to comment.