Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump moment from 2.24.0 to 2.29.4 #110

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
11 changes: 5 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
FROM node:12-alpine
FROM node:12-bullseye
MAINTAINER [email protected]

ENV NAME gfw-geostore-api
ENV USER microservice

RUN apk update && apk upgrade && \
apk add --no-cache --update bash git openssh python alpine-sdk
RUN apt-get update -y && apt-get upgrade -y && \
apt-get install -y bash git ssh python3 make

RUN addgroup $USER && adduser -s /bin/bash -D -G $USER $USER

RUN yarn global add grunt-cli bunyan
RUN addgroup $USER && useradd -ms /bin/bash $USER -g $USER
RUN yarn global add bunyan grunt

RUN mkdir -p /opt/$NAME
COPY package.json /opt/$NAME/package.json
Expand Down
160 changes: 149 additions & 11 deletions app/src/services/geoStoreService.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const GeoJSONConverter = require('converters/geoJSONConverter');
const md5 = require('md5');
const CartoDB = require('cartodb');
const IdConnection = require('models/idConnection');
const turf = require('turf');
const turf = require('@turf/turf');
const ProviderNotFound = require('errors/providerNotFound');
const GeoJSONNotFound = require('errors/geoJSONNotFound');
const UnknownGeometry = require('errors/unknownGeometry');
Expand Down Expand Up @@ -48,6 +48,18 @@ class GeoStoreService {

logger.debug('Repair geoJSON geometry');
logger.debug('Generating query');
/**
* Geometry repair tries to follow this steps:
* st_MakeValid: create a valid representation of a given invalid geometry
* https://postgis.net/docs/manual-dev/ST_MakeValid.html
*
* ST_CollectionExtract: ensure that the geometry is not a collection of different geom types
*
* In order to ensure a valid geojson representation based on rfc7946, we need to:
* ST_ForcePolygonCCW: ensure that the exterior ring is counterclockwise as per spec
* @todo: The geometry needs to enforce the antimeridian split rule over [-180,180] epsg:4326
* geometries.
*/
const sql = `SELECT ST_AsGeoJson(ST_CollectionExtract(st_MakeValid(ST_GeomFromGeoJSON('${JSON.stringify(geojson)}')),${geometryType})) as geojson`;

if (process.env.NODE_ENV !== 'test' || sql.length < 2000) {
Expand All @@ -73,7 +85,10 @@ class GeoStoreService {
static async obtainGeoJSONOfCarto(table, user, filter) {
logger.debug('Obtaining geojson with params: table %s, user %s, filter %s', table, user, filter);
logger.debug('Generating query');
const sql = `SELECT ST_AsGeoJson(the_geom) as geojson, (ST_Area(geography(the_geom))/10000) as area_ha FROM ${table} WHERE ${filter}`;
const sql = `SELECT ST_AsGeoJson(the_geom) as geojson,
(ST_Area(geography(the_geom)) / 10000) as area_ha
FROM ${table}
WHERE ${filter}`;
logger.debug('SQL to obtain geojson: %s', sql);
const client = new CartoDB.SQL({
user
Expand Down Expand Up @@ -130,13 +145,11 @@ class GeoStoreService {
}

static async getGeostoreByInfoProps(infoQuery) {
const geoStore = await GeoStore.findOne(infoQuery);
return geoStore;
return GeoStore.findOne(infoQuery);
}

static async getGeostoreByInfo(info) {
const geoStore = await GeoStore.findOne({ info });
return geoStore;
return GeoStore.findOne({ info });
}

static async obtainGeoJSON(provider) {
Expand All @@ -152,9 +165,129 @@ class GeoStoreService {
}
}

// @TODO: Extract bbox handling to its own class
/**
* @name overflowsAntimeridian
* @description check if the geometry overflows the [-180, -90, 180, 90] box
* @param {Array} bbox
* @returns {boolean}
*/
static overflowsAntimeridian(bbox) {
return bbox[0] > 180 || bbox[2] > 180;
}

/**
* @name bboxToPolygon
* @description converts a bbox to a polygon
* @param {Array} bbox
* @returns {Polygon}
*/
static bboxToPolygon(bbox) {
return turf.polygon([[[bbox[2], bbox[3]], [bbox[2], bbox[1]],
[bbox[0], bbox[1]], [bbox[0], bbox[3]],
[bbox[2], bbox[3]]]]);
}

/**
* @name: crossAntiMeridian
* @description: checks if a bbox crosses the antimeridian
* this is a mirror of https://github.com/mapbox/carmen/blob/03fac2d7397ecdfcb4f0828fcfd9d8a54c845f21/lib/util/bbox.js#L59
* @param {Array} bbox A bounding box array in the format [minX, minY, maxX, maxY]
* @returns {Array}
*
*/
static crossAntimeridian(feature, bbox) {
logger.info('Checking antimeridian');

const geomTypes = ['Point', 'MultiPoint'];
const bboxTotal = bbox || turf.bbox(feature);
const westHemiBBox = [-180, -90, 0, 90];
const eastHemiBBox = [0, -90, 180, 90];
const overflowsAntimeridian = this.overflowsAntimeridian(bbox);

if (geomTypes.includes(turf.getType(feature))) {
/**
* if the geometry is a triangle geometry length is 4 and
* the points are spread among hemispheres bbox calc over each
* hemisphere will be wrong
* This will need its own development
*/
logger.debug('Multipoint or point geometry');
return bboxTotal;
}

if (overflowsAntimeridian) {
logger.debug('BBOX crosses antimeridian but is in [0, 360º]');
return bboxTotal;
}

if (
turf.booleanIntersects(feature, this.bboxToPolygon(eastHemiBBox))
&& turf.booleanIntersects(feature, this.bboxToPolygon(westHemiBBox))
) {
logger.debug('Geometry that is contained in both hemispheres');

const clippedEastGeom = turf.bboxClip(feature, eastHemiBBox);
const clippedWestGeom = turf.bboxClip(feature, westHemiBBox);
const bboxEast = turf.bbox(clippedEastGeom);
const bboxWest = turf.bbox(clippedWestGeom);

const amBBox = [bboxEast[0], bboxTotal[1], bboxWest[2], bboxTotal[3]];
const pmBBox = [bboxWest[0], bboxTotal[1], bboxEast[2], bboxTotal[3]];

const pmBBoxWidth = (bboxEast[2]) + Math.abs(bboxWest[0]);
const amBBoxWidth = (180 - bboxEast[0]) + (180 - Math.abs(bboxWest[2]));

return (pmBBoxWidth > amBBoxWidth) ? amBBox : pmBBox;
}

return bboxTotal;

}

/**
* @name: translateBBox
* @description: This function translates a bbox that crosses the antimeridian
* @param {Array} bbox
* @returns {Array} bbox with the antimeridian corrected
*/
static translateBBox(bbox) {
logger.debug('Converting bbox from [-180,180] to [0,360] for representation');
return [bbox[0], bbox[1], 360 - Math.abs(bbox[2]), bbox[3]];
}

/**
* @name: swapBBox
* @description: swap a bbox. If a bbox crosses
* the antimeridian will be transformed its
* latitudes from [-180, 180] to [0, 360]
* @param {GeoStore} geoStore
* @returns {Array}
*
* */
static swapBBox(geoStore) {
const orgBbox = turf.bbox(geoStore.geojson);
const bbox = turf.featureReduce(
geoStore.geojson,
(previousValue, currentFeature) => GeoStoreService.crossAntimeridian(currentFeature, previousValue),
orgBbox
);

return bbox[0] > bbox[2] ? GeoStoreService.translateBBox(bbox) : bbox;
}

/**
* @name: calculateBBox
* @description: Calculates a bbox.
* If a bbox that crosses the antimeridian will be transformed its
* latitudes from [-180, 180] to [0, 360]
* @param {GeoStore} geoStore
* @returns {geoStore}
*
* */
static async calculateBBox(geoStore) {
logger.debug('Calculating bbox');
geoStore.bbox = turf.bbox(geoStore.geojson);
geoStore.bbox = GeoStoreService.swapBBox(geoStore);
await geoStore.save();
return geoStore;
}
Expand Down Expand Up @@ -203,18 +336,24 @@ class GeoStoreService {

logger.debug('Repaired geometry', JSON.stringify(geoStore.geojson));
logger.debug('Make Feature Collection');

geoStore.geojson = GeoJSONConverter.makeFeatureCollection(geoStore.geojson, props);

logger.debug('Result', JSON.stringify(geoStore.geojson));
logger.debug('Creating hash from geojson md5');

geoStore.hash = md5(JSON.stringify(geoStore.geojson));

if (geoStore.areaHa === undefined) {
geoStore.areaHa = turf.area(geoStore.geojson) / 10000; // convert to ha2
}
await GeoStore.findOne({
hash: geoStore.hash
});
logger.debug('bbox geostore');
logger.debug('geojson', JSON.stringify(geoStore.bbox));
if (!geoStore.bbox) {
geoStore.bbox = turf.bbox(geoStore.geojson);
geoStore.bbox = GeoStoreService.swapBBox(geoStore);
}

return GeoStore.findOneAndUpdate({ hash: geoStore.hash }, geoStore, {
Expand All @@ -240,12 +379,11 @@ class GeoStoreService {
geoStore.areaHa = geoJsonObtained.area_ha;
}

logger.debug('Converting geojson');
logger.debug('Converting', JSON.stringify(geoStore.geojson));
logger.debug('Converting geojson', JSON.stringify(geoStore.geojson));
geoStore.geojson = GeoJSONConverter.makeFeatureCollection(geoStore.geojson);
logger.debug('Result', JSON.stringify(geoStore.geojson));
geoStore.areaHa = turf.area(geoStore.geojson) / 10000; // convert to ha2
geoStore.bbox = turf.bbox(geoStore.geojson);
geoStore.bbox = await GeoStoreService.swapBBox(geoStore); // calculate bbox

return geoStore;

Expand Down
Loading