Skip to content

Commit

Permalink
Ensure polygons are inside -180/180 range before making stac request (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
danielfdsilva authored Nov 9, 2023
2 parents d2e0e9c + 268e089 commit 2c3f46d
Show file tree
Hide file tree
Showing 7 changed files with 509 additions and 9 deletions.
30 changes: 24 additions & 6 deletions app/scripts/components/analysis/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { endOfDay, startOfDay, format } from 'date-fns';
import { Feature, FeatureCollection, MultiPolygon, Polygon } from 'geojson';
import { userTzDate2utcString } from '$utils/date';
import { fixAntimeridian } from '$utils/antimeridian';

/**
* Creates the appropriate filter object to send to STAC.
Expand All @@ -16,6 +17,8 @@ export function getFilterPayload(
aoi: FeatureCollection<Polygon>,
collections: string[]
) {
const aoiMultiPolygon = fixAoiFcForStacSearch(aoi);

const filterPayload = {
op: 'and',
args: [
Expand All @@ -31,11 +34,9 @@ export function getFilterPayload(
}
]
},
// Stac search spatial intersect needs to be done on a single feature.
// Using a Multipolygon
{
op: 's_intersects',
args: [{ property: 'geometry' }, combineFeatureCollection(aoi).geometry]
args: [{ property: 'geometry' }, aoiMultiPolygon.geometry]
},
{
op: 'in',
Expand All @@ -50,9 +51,9 @@ export function getFilterPayload(
* Converts a MultiPolygon to a Feature Collection of polygons.
*
* @param feature MultiPolygon feature
*
*
* @see combineFeatureCollection() for opposite
*
*
* @returns Feature Collection of Polygons
*/
export function multiPolygonToPolygons(feature: Feature<MultiPolygon>) {
Expand All @@ -75,7 +76,7 @@ export function multiPolygonToPolygons(feature: Feature<MultiPolygon>) {
* Converts a Feature Collection of polygons into a MultiPolygon
*
* @param featureCollection Feature Collection of Polygons
*
*
* @see multiPolygonToPolygons() for opposite
*
* @returns MultiPolygon Feature
Expand All @@ -95,6 +96,23 @@ export function combineFeatureCollection(
};
}

/**
* Fixes the AOI feature collection for a STAC search by converting all polygons
* to a single multipolygon and ensuring that every polygon is inside the
* -180/180 range.
* @param aoi The AOI feature collection
* @returns AOI as a multipolygon with every polygon inside the -180/180 range
*/
export function fixAoiFcForStacSearch(aoi: FeatureCollection<Polygon>) {
// Stac search spatial intersect needs to be done on a single feature.
// Using a Multipolygon
const singleMultiPolygon = combineFeatureCollection(aoi);
// And every polygon must be inside the -180/180 range.
// See: https://github.com/NASA-IMPACT/veda-ui/issues/732
const aoiMultiPolygon = fixAntimeridian(singleMultiPolygon);
return aoiMultiPolygon;
}

export function getDateRangeFormatted(startDate, endDate) {
const dFormat = 'yyyy-MM-dd';
const startDateFormatted = format(startDate, dFormat);
Expand Down
8 changes: 7 additions & 1 deletion app/scripts/components/common/map/map-component.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import React, { useCallback, ReactElement, useMemo } from 'react';
import ReactMapGlMap from 'react-map-gl';
import ReactMapGlMap, { LngLatBoundsLike } from 'react-map-gl';
import { ProjectionOptions } from 'veda';
import 'mapbox-gl/dist/mapbox-gl.css';
import 'mapbox-gl-compare/dist/mapbox-gl-compare.css';
import { convertProjectionToMapbox } from '../mapbox/map-options/utils';
import useMapStyle from './hooks/use-map-style';
import { useMapsContext } from './hooks/use-maps';

const maxMapBounds: LngLatBoundsLike = [
[-540, -90], // SW
[540, 90] // NE
];

export default function MapComponent({
controls,
isCompared,
Expand Down Expand Up @@ -52,6 +57,7 @@ export default function MapComponent({
mapStyle={style as any}
onMove={onMove}
projection={mapboxProjection}
maxBounds={maxMapBounds}
>
{controls}
</ReactMapGlMap>
Expand Down
4 changes: 2 additions & 2 deletions app/scripts/components/exploration/analysis-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from './types.d.ts';
import { ExtendedError } from './data-utils';
import {
combineFeatureCollection,
fixAoiFcForStacSearch,
getFilterPayload
} from '$components/analysis/utils';

Expand Down Expand Up @@ -163,7 +163,7 @@ export async function requestDatasetTimeseriesData({
const { data } = await axios.post(
`${process.env.API_RASTER_ENDPOINT}/cog/statistics?url=${url}`,
// Making a request with a FC causes a 500 (as of 2023/01/20)
combineFeatureCollection(aoi),
fixAoiFcForStacSearch(aoi),
{ signal }
);
return {
Expand Down
243 changes: 243 additions & 0 deletions app/scripts/utils/antimeridian.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { Feature, MultiPolygon, Polygon } from 'geojson';
import { fixAntimeridian } from './antimeridian';

describe('Antimeridian', () => {
it('Should move a feature in the first map east (180/540) to the -180/180 range', () => {
const feature: Feature<Polygon> = {
type: 'Feature',
properties: {},
geometry: {
coordinates: [
[
[190, 20],
[190, -20],
[200, -20],
[200, 20],
[190, 20]
]
],
type: 'Polygon'
}
};

const expected: Feature<MultiPolygon> = {
type: 'Feature',
properties: {},
geometry: {
coordinates: [
[
[
[-170, -20],
[-160, -20],
[-160, 20],
[-170, 20],
[-170, -20]
]
]
],
type: 'MultiPolygon'
}
};

const result = fixAntimeridian(feature);
expect(result).toEqual(expected);
});

it('Should split a feature crossing the 540 antimeridian into 2 that fall within the -180/180 range', () => {
const feature: Feature<Polygon> = {
type: 'Feature',
properties: {},
geometry: {
coordinates: [
[
[500, 20],
[500, -20],
[580, -20],
[580, 20],
[500, 20]
]
],
type: 'Polygon'
}
};

const expected: Feature<MultiPolygon> = {
type: 'Feature',
properties: {},
geometry: {
coordinates: [
[
[
[-180, -20],
[-140, -20],
[-140, 20],
[-180, 20],
[-180, -20]
]
],
[
[
[140, -20],
[180, -20],
[180, 20],
[140, 20],
[140, -20]
]
]
],
type: 'MultiPolygon'
}
};

const result = fixAntimeridian(feature);
expect(result).toEqual(expected);
});

it('Should calculate the resulting feature in the -180/180 range when the input spans several maps', () => {
const feature: Feature<Polygon> = {
type: 'Feature',
properties: {},
geometry: {
coordinates: [
[
[-360, 10],
[500, 10],
[500, -10],
[-360, -10],
[-360, 10]
]
],
type: 'Polygon'
}
};

const expected: Feature<MultiPolygon> = {
type: 'Feature',
properties: {},
geometry: {
coordinates: [
[
[
[-180, -10],
[180, -10],
[180, 10],
[-180, 10],
[-180, -10]
]
]
],
type: 'MultiPolygon'
}
};

const result = fixAntimeridian(feature);
expect(result).toEqual(expected);
});

it('Should be able to process multipolygons, correctly resolving each polygon to the -180/180 range', () => {
const feature: Feature<MultiPolygon> = {
type: 'Feature',
properties: {},
geometry: {
coordinates: [
[
[
[170, 30],
[190, 30],
[190, 20],
[170, 20],
[170, 30]
]
],
[
[
[-530, 10],
[-530, 0],
[-550, 0],
[-550, 10],
[-530, 10]
]
],
[
[
[530, -10],
[530, -20],
[550, -20],
[550, -10],
[530, -10]
]
]
],

type: 'MultiPolygon'
}
};

const expected: Feature<MultiPolygon> = {
type: 'Feature',
properties: {},
geometry: {
coordinates: [
[
[
[-180, -20],
[-170, -20],
[-170, -10],
[-180, -10],
[-180, -20]
]
],
[
[
[-180, 0],
[-170, 0],
[-170, 10],
[-180, 10],
[-180, 0]
]
],
[
[
[-180, 20],
[-170, 20],
[-170, 30],
[-180, 30],
[-180, 20]
]
],
[
[
[170, -20],
[180, -20],
[180, -10],
[170, -10],
[170, -20]
]
],
[
[
[170, 0],
[180, 0],
[180, 10],
[170, 10],
[170, 0]
]
],
[
[
[170, 20],
[180, 20],
[180, 30],
[170, 30],
[170, 20]
]
]
],
type: 'MultiPolygon'
}
};

const result = fixAntimeridian(feature);
expect(result).toEqual(expected);
});
});
Loading

0 comments on commit 2c3f46d

Please sign in to comment.