Skip to content

Commit

Permalink
Merge pull request #1115 from Vizzuality/fix/db/bbox_calc
Browse files Browse the repository at this point in the history
Antimeridian and bbox calc fix
  • Loading branch information
aagm authored Jun 1, 2022
2 parents 4380867 + 20610a6 commit 0daef9e
Show file tree
Hide file tree
Showing 19 changed files with 207 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class MemoryExportRepo implements ExportRepository {

async findLatestExportsFor(
projectId: string,
limit: number = 5,
limit = 5,
options?: {
isStandalone?: boolean;
isFinished?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ class FakeScenarioRepository {

class FakeExportRepository implements ExportRepository {
public returnUnfinishedExport = false;
public importResourceId: string = '';
public importResourceId = '';

async save(exportInstance: Export): Promise<Either<SaveError, Success>> {
throw new Error('Method not implemented.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { GeoFeature } from './geo-feature.api.entity';
import { GeoFeaturePropertySet } from './geo-feature.geo.entity';
import { DbConnections } from '@marxan-api/ormconfig.connections';
import { BBox } from 'geojson';
import { antimeridianBbox } from '@marxan/utils/geo';

@Injectable()
export class GeoFeaturePropertySetService {
Expand All @@ -32,16 +33,25 @@ export class GeoFeaturePropertySetService {
.where(`propertySets.featureId IN (:...ids)`, { ids: geoFeatureIds });

if (withinBBox) {
const { westBbox, eastBbox } = antimeridianBbox([
withinBBox[1],
withinBBox[3],
withinBBox[0],
withinBBox[2],
]);
query.andWhere(
`st_intersects(
st_makeenvelope(:xmin, :ymin, :xmax, :ymax, 4326),
"propertySets".bbox
)`,
`(st_intersects(
st_intersection(st_makeenvelope(:...westBbox, 4326),
ST_MakeEnvelope(0, -90, 180, 90, 4326)),
"propertySets".bbox)
or
st_intersects(
st_intersection(st_makeenvelope(:...eastBbox, 4326),
ST_MakeEnvelope(-180, -90, 0, 90, 4326)),
"propertySets".bbox))`,
{
xmin: withinBBox[1],
ymin: withinBBox[3],
xmax: withinBBox[0],
ymax: withinBBox[2],
westBbox: westBbox,
eastBbox: eastBbox,
},
);
}
Expand Down
19 changes: 12 additions & 7 deletions api/apps/api/src/modules/geo-features/geo-features.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { DbConnections } from '@marxan-api/ormconfig.connections';
import { v4 } from 'uuid';
import { UploadShapefileDTO } from '../projects/dto/upload-shapefile.dto';
import { GeoFeaturesRequestInfo } from './geo-features-request-info';
import { antimeridianBbox, nominatim2bbox } from '@marxan/utils/geo';

const geoFeatureFilterKeyNames = [
'featureClassName',
Expand Down Expand Up @@ -167,20 +168,24 @@ export class GeoFeaturesService extends AppBaseService<
*
*/
if (projectId && info?.params?.bbox) {
const { westBbox, eastBbox } = antimeridianBbox(nominatim2bbox(info.params.bbox));
const geoFeaturesWithinProjectBbox = await this.geoFeaturesGeometriesRepository
.createQueryBuilder('geoFeatureGeometries')
.select('"geoFeatureGeometries"."feature_id"', 'featureId')
.distinctOn(['"geoFeatureGeometries"."feature_id"'])
.where(
`st_intersects(
st_makeenvelope(:xmin, :ymin, :xmax, :ymax, 4326),
`(st_intersects(
st_intersection(st_makeenvelope(:...eastBbox, 4326),
ST_MakeEnvelope(0, -90, 180, 90, 4326)),
"geoFeatureGeometries".the_geom
)`,
) or st_intersects(
st_intersection(st_makeenvelope(:...westBbox, 4326),
ST_MakeEnvelope(-180, -90, 0, 90, 4326)),
"geoFeatureGeometries".the_geom
))`,
{
xmin: info.params.bbox[1],
ymin: info.params.bbox[3],
xmax: info.params.bbox[0],
ymax: info.params.bbox[2],
westBbox: westBbox,
eastBbox: eastBbox,
},
)
.getRawMany()
Expand Down
2 changes: 1 addition & 1 deletion api/apps/api/src/utils/json.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export interface JSONObject {
[x: string]: JSONValue;
}

export interface JSONArray extends Array<JSONValue> {}
export type JSONArray = Array<JSONValue>;
5 changes: 5 additions & 0 deletions api/apps/api/test/fixtures/test-init-apidb.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ VALUES
('[email protected]', 'b', 'b', 'User B B', true, false, crypt('bbuserpassword', gen_salt('bf'))),
('[email protected]', 'c', 'c', 'User C C', true, false, crypt('ccuserpassword', gen_salt('bf'))),
('[email protected]', 'd', 'd', 'User D D', true, false, crypt('dduserpassword', gen_salt('bf')));

INSERT INTO organizations (name, created_by)
VALUES
('Example Org 1', (SELECT id FROM users WHERE email = '[email protected]')),
('Example Org 2', (SELECT id FROM users WHERE email = '[email protected]'));
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class ModifyBboxCalculationTakingAntimeridian1653565019335
implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE OR REPLACE FUNCTION mx_bbox2json(geom geometry)
returns jsonb
language plpgsql
as
$function$
DECLARE
-- variable declaration
both_hemispheres RECORD;
BEGIN
-- logic https://github.com/mapbox/carmen/blob/03fac2d7397ecdfcb4f0828fcfd9d8a54c845f21/lib/util/bbox.js#L59
-- json form of the bbox should be in Nominatim bbox [xmin, xmax, ymin, ymax] [W, E, S, N].
execute 'with region as (select st_intersection($1, geom) as the_geom,
st_intersects($1, geom) intersects, pos
from (values (ST_MakeEnvelope(-180, -90, 0, 90, 4326), ''west''),
(ST_MakeEnvelope(0, -90, 180, 90, 4326), ''east'')) as t(geom, pos)),
data as (select ST_XMax(the_geom), ST_XMin(the_geom),
ST_YMax(the_geom),ST_YMin(the_geom), pos, intersects,
ST_XMax(the_geom) + ABS(lag(ST_XMin(the_geom), 1) OVER ()) >
(180 - ST_XMin(the_geom)) + (180 - ABS(lag(ST_XMax(the_geom), 1) OVER ())) as pm_am
from region)
select bool_and(intersects) and bool_and(pm_am) result,
jsonb_build_array(max(st_xmax), min(st_xmin), max(st_ymax), min(st_ymin)) if_false,
jsonb_build_array(min(st_xmax), max(st_xmin), max(st_ymax), min(st_ymin))if_true from data;'
into both_hemispheres
using geom;
if both_hemispheres.result then
return both_hemispheres.if_true;
else
return both_hemispheres.if_false;
end if;
end;
$function$;
CREATE OR REPLACE FUNCTION public.tr_getbbox()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
NEW.bbox := mx_bbox2json(NEW.the_geom);
RETURN NEW;
END;
$function$;
UPDATE admin_regions SET id = id;
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE OR REPLACE FUNCTION public.tr_getbbox()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
NEW.bbox := jsonb_build_array(ST_XMax(NEW.the_geom), ST_XMin(NEW.the_geom),
ST_YMax(NEW.the_geom), ST_YMin(NEW.the_geom));
RETURN NEW;
END;
$function$;
Drop FUNCTION mx_bbox2json(geom geometry);
UPDATE admin_regions SET id = id;
`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { Transform } from 'class-transformer';
import { BBox } from 'geojson';
import { AdminArea } from '@marxan/admin-regions';
import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils';
import { nominatim2bbox } from '@marxan/utils/geo';
import { TileRequest } from '@marxan/tiles';

export class TileSpecification extends TileRequest {
Expand Down
20 changes: 15 additions & 5 deletions api/apps/geoprocessing/src/modules/features/features.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { IsArray, IsNumber, IsString, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { BBox } from 'geojson';
import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils';
import { antimeridianBbox, nominatim2bbox } from '@marxan/utils/geo';

import { TileRequest } from '@marxan/tiles';

Expand Down Expand Up @@ -49,23 +49,32 @@ export class FeatureService {
let whereQuery = `feature_id = '${id}'`;

if (bbox) {
whereQuery += `AND st_intersects(ST_MakeEnvelope(${nominatim2bbox(
bbox,
)}, 4326), the_geom)`;
const { westBbox, eastBbox } = antimeridianBbox(nominatim2bbox(bbox));
whereQuery += `AND
(st_intersects(
st_intersection(st_makeenvelope(${eastBbox}, 4326),
ST_MakeEnvelope(0, -90, 180, 90, 4326)),
the_geom
) or st_intersects(
st_intersection(st_makeenvelope(${westBbox}, 4326),
ST_MakeEnvelope(-180, -90, 0, 90, 4326)),
the_geom
))`;
}
return whereQuery;
}

/**
* @todo get attributes from Entity, based on user selection
* @todo simplification level based on zoom level
*/
public findTile(
tileSpecification: TileSpecification,
bbox?: BBox,
): Promise<Buffer> {
const { z, x, y, id } = tileSpecification;
const attributes = 'feature_id, properties';
const table = `(select (st_dump(the_geom)).geom as the_geom, properties, feature_id from "${this.featuresRepository.metadata.tableName}")`;
const table = `(select ST_RemoveRepeatedPoints((st_dump(the_geom)).geom, 0.1) as the_geom, properties, feature_id from "${this.featuresRepository.metadata.tableName}")`;
const customQuery = this.buildFeaturesWhereQuery(id, bbox);
return this.tileService.getTile({
z,
Expand All @@ -76,4 +85,5 @@ export class FeatureService {
attributes,
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class PlanningAreaTilesService {
/**
* @todo this generation query is a bit...
*/
let whereQuery = `project_id = '${planningAreaId}'`;
const whereQuery = `project_id = '${planningAreaId}'`;

return whereQuery;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,16 @@ export class PlanningUnitsJobProcessor {
SELECT ST_Transform(the_geom, 3410) as geom
FROM planning_areas
WHERE id = '${data.planningAreaId}'
), grid AS (
SELECT (${gridFn}(${size}, geom)).*
FROM region
),
bboxes as (select * from (values (st_transform(ST_MakeEnvelope(-180, -90, 0, 90, 4326), 3410), 'west'),
(st_transform(ST_MakeEnvelope(0, -90, 180, 90, 4326),3410), 'east')) as t(geom, pos)),
grid AS (
SELECT ST_ClipByBox2D((${gridFn}(${size}, st_intersection(region.geom, bboxes.geom))).geom,
st_transform(ST_MakeEnvelope(-180, -90, 180, 90, 4326), 3410)) as geom
FROM region, bboxes
)
SELECT grid.geom
SELECT distinct
grid.geom
FROM grid, region
WHERE ST_Intersects(grid.geom, region.geom)
`;
Expand All @@ -145,11 +150,16 @@ export class PlanningUnitsJobProcessor {
SELECT ST_Transform(the_geom, 3410) as geom
FROM admin_regions
WHERE ${whereConditions.join(' AND ')}
), grid AS (
SELECT (${gridFn}(${size}, geom)).*
FROM region
),
bboxes as (select * from (values (st_transform(ST_MakeEnvelope(-180, -90, 0, 90, 4326), 3410), 'west'),
(st_transform(ST_MakeEnvelope(0, -90, 180, 90, 4326),3410), 'east')) as t(geom, pos)),
grid AS (
SELECT ST_ClipByBox2D((${gridFn}(${size}, st_intersection(region.geom, bboxes.geom))).geom,
st_transform(ST_MakeEnvelope(-180, -90, 180, 90, 4326), 3410)) as geom
FROM region, bboxes
)
SELECT grid.geom
SELECT distinct
grid.geom
FROM grid, region
WHERE ST_Intersects(grid.geom, region.geom)
`;
Expand Down Expand Up @@ -196,7 +206,7 @@ export class PlanningUnitsJobProcessor {
const geometries: { id: string }[] = await em.query(
`
INSERT INTO planning_units_geom (the_geom, type, size)
SELECT st_transform(geom, 4326) AS the_geom, $1::planning_unit_grid_shape AS type, $2 AS size
SELECT ST_MakeValid(st_transform(geom, 4326)) AS the_geom, $1::planning_unit_grid_shape AS type, $2 AS size
FROM (${subquery}) grid
ON CONFLICT (the_geom_hash, type) DO UPDATE SET type = $1::planning_unit_grid_shape
RETURNING id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { TileService } from '@marxan-geoprocessing/modules/tile/tile.service';
import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils';
import { nominatim2bbox, antimeridianBbox } from '@marxan/utils/geo';
import { PlanningUnitGridShape } from '@marxan/scenarios-planning-unit';
import { TileRequest } from '@marxan/tiles';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsArray, IsIn, IsNumber, IsOptional, IsString } from 'class-validator';
import { IsArray, IsIn, IsNumber, IsOptional } from 'class-validator';
import { BBox } from 'geojson';
import {
calculateGridSize,
Expand Down Expand Up @@ -110,6 +110,7 @@ export class PlanningUnitsService {
* Because we want to reduce the overhead for the db if an uncontroled area requests
* a large area.
* If so the shape we are getting is down the optimal to visualize it as points
*
*/
const query =
ratioPixelExtent < 8 && !filters?.bbox
Expand All @@ -122,14 +123,20 @@ export class PlanningUnitsService {
}
/**
* @param filters including only bounding box of the area where the grids would be generated
*
*/
private buildPlanningUnitsWhereQuery(filters?: PlanningUnitsFilters): string {
let whereQuery = ``;

if (filters?.bbox) {
whereQuery = `st_intersects(ST_Transform(ST_MakeEnvelope(${nominatim2bbox(
filters.bbox,
)}, 4326), 3857) ,the_geom)`;
const { westBbox, eastBbox } = antimeridianBbox(
nominatim2bbox(filters.bbox),
);
whereQuery = `st_intersects(ST_Transform(st_intersection(ST_MakeEnvelope(${eastBbox}, 4326),
ST_MakeEnvelope(0, -90, 180, 90, 4326)), 3857), the_geom)
OR
st_intersects(ST_Transform(st_intersection(ST_MakeEnvelope(${westBbox}, 4326),
ST_MakeEnvelope(-180, -90, 0, 90, 4326)), 3857), the_geom)`;
}
return whereQuery;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';

import { InjectRepository } from '@nestjs/typeorm';
import { Brackets, Repository } from 'typeorm';
import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils';
import { nominatim2bbox, antimeridianBbox } from '@marxan/utils/geo';
import { TileService } from '@marxan-geoprocessing/modules/tile/tile.service';

import { ProtectedArea } from '@marxan/protected-areas';
Expand Down Expand Up @@ -73,10 +73,14 @@ export class ProtectedAreasTilesService {
}

if (bbox) {
const { westBbox, eastBbox } = antimeridianBbox(nominatim2bbox(bbox));
subQuery.andWhere(
`st_intersects(ST_MakeEnvelope(:...bbox, 4326), the_geom)`,
`(st_intersects(ST_MakeEnvelope(:...westBbox, 4326), the_geom)
or
st_intersects(ST_MakeEnvelope(:...eastBbox, 4326), the_geom))`,
{
bbox: nominatim2bbox(bbox),
westBbox: westBbox,
eastBbox: eastBbox,
},
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';

import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { nominatim2bbox } from '@marxan-geoprocessing/utils/bbox.utils';
import { TileService } from '@marxan-geoprocessing/modules/tile/tile.service';

import { ScenariosPuPaDataGeo } from '@marxan/scenarios-planning-unit';
Expand Down
Loading

2 comments on commit 0daef9e

@vercel
Copy link

@vercel vercel bot commented on 0daef9e Jun 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

marxan-storybook – ./app

marxan-storybook-git-main-vizzuality1.vercel.app
marxan-storybook.vercel.app
marxan-storybook-vizzuality1.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 0daef9e Jun 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

marxan – ./app

marxan-git-main-vizzuality1.vercel.app
marxan-production.vercel.app
marxan-vizzuality1.vercel.app

Please sign in to comment.