Skip to content

Commit

Permalink
Exploration page (E&A merge)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielfdsilva authored Dec 11, 2023
2 parents afe7133 + eb37147 commit 404043e
Show file tree
Hide file tree
Showing 108 changed files with 11,929 additions and 117 deletions.
1 change: 0 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ [email protected]

API_RASTER_ENDPOINT='https://staging-raster.delta-backend.com'
API_STAC_ENDPOINT='https://staging-stac.delta-backend.com'
API_XARRAY_ENDPOINT='https://dev-titiler-xarray.delta-backend.com/tilejson.json'

# If the app is being served in from a subfolder, the domain url must be set.
# For example, if the app is served from /mysite:
Expand Down
Binary file added app/graphics/content/tour-analysis.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/graphics/content/tour-comparison.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
28 changes: 14 additions & 14 deletions app/scripts/components/common/browse-controls/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ function BrowseControls(props: BrowseControlsProps) {

return (
<BrowseControlsWrapper {...rest}>
<TaxonomyWrapper>
{taxonomiesOptions.map(({ name, values }) => (
<DropdownOptions
key={name}
prefix={name}
items={[optionAll].concat(values)}
currentId={taxonomies?.[name] ?? 'all'}
onChange={(v) => {
onAction(Actions.TAXONOMY, { key: name, value: v });
}}
size={isLargeUp ? 'large' : 'medium'}
/>
))}
</TaxonomyWrapper>
<SearchWrapper>
<SearchField
size={isLargeUp ? 'large' : 'medium'}
Expand Down Expand Up @@ -142,20 +156,6 @@ function BrowseControls(props: BrowseControlsProps) {
</DropMenu>
</DropdownScrollable>
</SearchWrapper>
<TaxonomyWrapper>
{taxonomiesOptions.map(({ name, values }) => (
<DropdownOptions
key={name}
prefix={name}
items={[optionAll].concat(values)}
currentId={taxonomies?.[name] ?? 'all'}
onChange={(v) => {
onAction(Actions.TAXONOMY, { key: name, value: v });
}}
size={isLargeUp ? 'large' : 'medium'}
/>
))}
</TaxonomyWrapper>
</BrowseControlsWrapper>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import useQsStateCreator from 'qs-state-hook';
import { set, omit } from 'lodash';

export enum Actions {
CLEAR = 'clear',
SEARCH = 'search',
SORT_FIELD = 'sfield',
SORT_DIR = 'sdir',
TAXONOMY = 'taxonomy'
}

export type BrowserControlsAction = (what: Actions, value: any) => void;
export type BrowserControlsAction = (what: Actions, value?: any) => void;

export interface FilterOption {
id: string;
Expand Down Expand Up @@ -85,6 +86,10 @@ export function useBrowserControls({ sortOptions }: BrowseControlsHookParams) {
const onAction = useCallback<BrowserControlsAction>(
(what, value) => {
switch (what) {
case Actions.CLEAR:
setSearch('');
setTaxonomies({});
break;
case Actions.SEARCH:
setSearch(value);
break;
Expand Down
8 changes: 5 additions & 3 deletions app/scripts/components/common/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ interface CardComponentProps {
parentTo?: string;
footerContent?: ReactNode;
onCardClickCapture?: MouseEventHandler;
onLinkClick?: MouseEventHandler;
}

function CardComponent(props: CardComponentProps) {
Expand All @@ -349,13 +350,14 @@ function CardComponent(props: CardComponentProps) {
parentName,
parentTo,
footerContent,
onCardClickCapture
onCardClickCapture,
onLinkClick
} = props;

const isExternalLink = linkTo.match(/^https?:\/\//);
const linkProps = isExternalLink
? { href: linkTo }
: { as: Link, to: linkTo };
? { href: linkTo, onClick: onLinkClick }
: { as: Link, to: linkTo, onClick: onLinkClick };

return (
<ElementInteractive
Expand Down
26 changes: 14 additions & 12 deletions app/scripts/components/common/empty-hub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@ import { themeVal } from '@devseed-ui/theme-provider';

import { variableGlsp } from '$styles/variable-utils';

const EmptyHubWrapper = styled.div`
function EmptyHub(props: { children: ReactNode }) {
const theme = useTheme();

const { children, ...rest } = props;

return (
<div {...rest}>
<CollecticonPage size='xxlarge' color={theme.color!['base-400']} />
{children}
</div>
);
}

export default styled(EmptyHub)`
max-width: 100%;
grid-column: 1/-1;
display: flex;
Expand All @@ -16,14 +29,3 @@ const EmptyHubWrapper = styled.div`
border: 1px dashed ${themeVal('color.base-300')};
gap: ${variableGlsp(1)};
`;

export default function EmptyHub(props: { children: ReactNode }) {
const theme = useTheme();

return (
<EmptyHubWrapper>
<CollecticonPage size='xxlarge' color={theme.color!['base-400']} />
{props.children}
</EmptyHubWrapper>
);
}
21 changes: 21 additions & 0 deletions app/scripts/components/common/icons/calendar-minus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { createCollecticon } from '@devseed-ui/collecticons';
import styled from 'styled-components';

export const CollecticonCalendarMinus = styled(
createCollecticon((props: any) => (
<svg {...props}>
<title>{props.title || 'Calendar with minus icon'}</title>
<path
d='M11.5 0H10V3H9V1H5V0H3.5V3H2.5V1H1C0.447715 1 0 1.44772 0 2V12C0 12.5523 0.447716 13 1 13H5.34141C5.12031 12.3744 5 11.7013 5 11H2V5H12C13 5 13.3926 5.36838 14 5.71974V2C14 1.44772 13.5523 1 13 1H11.5V0Z'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M16 11C16 13.7614 13.7614 16 11 16C8.23858 16 6 13.7614 6 11C6 8.23858 8.23858 6 11 6C13.7614 6 16 8.23858 16 11ZM8.5 11.625V10.375H13.5V11.625H8.5Z'
/>
</svg>
))
)`
/* icons must be styled-components */
`;
21 changes: 21 additions & 0 deletions app/scripts/components/common/icons/calendar-plus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { createCollecticon } from '@devseed-ui/collecticons';
import styled from 'styled-components';

export const CollecticonCalendarPlus = styled(
createCollecticon((props: any) => (
<svg {...props}>
<title>{props.title || 'Calendar with plus icon'}</title>
<path
d='M11.5 0H10V3H9V1H5V0H3.5V3H2.5V1H1C0.447715 1 0 1.44772 0 2V12C0 12.5523 0.447716 13 1 13H5.34141C5.12031 12.3744 5 11.7013 5 11H2V5H12C13 5 13.3926 5.36838 14 5.71974V2C14 1.44772 13.5523 1 13 1H11.5V0Z'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M16 11C16 13.7614 13.7614 16 11 16C8.23858 16 6 13.7614 6 11C6 8.23858 8.23858 6 11 6C13.7614 6 16 8.23858 16 11ZM10.375 8.5V10.375H8.5V11.625H10.375V13.5H11.625V11.625H13.5V10.375H11.625V8.5H10.375Z'
/>
</svg>
))
)`
/* icons must be styled-components */
`;
19 changes: 19 additions & 0 deletions app/scripts/components/common/icons/magnifier-minus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { createCollecticon } from '@devseed-ui/collecticons';
import styled from 'styled-components';

export const CollecticonMagnifierMinus = styled(
createCollecticon((props: any) => (
<svg {...props}>
<title>{props.title || 'Magnifier with minus icon'}</title>
<path d='M9.5 5.5V7.5L3.5 7.5V5.5H9.5Z' />
<path
fillRule='evenodd'
clipRule='evenodd'
d='M12.033 9.912L15.708 13.587C16.097 13.976 16.097 14.612 15.708 15.001L15.001 15.708C14.612 16.097 13.976 16.097 13.587 15.708L9.912 12.033C8.92 12.646 7.751 13 6.5 13C2.91 13 0 10.09 0 6.5C0 2.91 2.91 0 6.5 0C10.09 0 13 2.91 13 6.5C13 7.751 12.646 8.92 12.033 9.912ZM3.318 9.682C2.468 8.832 2 7.702 2 6.5C2 5.298 2.468 4.168 3.318 3.318C4.168 2.468 5.298 2 6.5 2C7.702 2 8.832 2.468 9.682 3.318C10.532 4.168 11 5.298 11 6.5C11 7.702 10.532 8.832 9.682 9.682C8.832 10.532 7.702 11 6.5 11C5.298 11 4.168 10.532 3.318 9.682Z'
/>
</svg>
))
)`
/* icons must be styled-components */
`;
21 changes: 21 additions & 0 deletions app/scripts/components/common/icons/magnifier-plus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { createCollecticon } from '@devseed-ui/collecticons';
import styled from 'styled-components';

export const CollecticonMagnifierPlus = styled(
createCollecticon((props: any) => (
<svg {...props}>
<title>{props.title || 'Magnifier with plus icon'}</title>
<path
d='M7.5 7.5H9.5V5.5H7.5V3.5H5.5V5.5H3.5V7.5H5.5V9.5H7.5V7.5Z'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M15.708 13.587L12.033 9.912C12.646 8.92 13 7.751 13 6.5C13 2.91 10.09 0 6.5 0C2.91 0 0 2.91 0 6.5C0 10.09 2.91 13 6.5 13C7.751 13 8.92 12.646 9.912 12.033L13.587 15.708C13.976 16.097 14.612 16.097 15.001 15.708L15.708 15.001C16.097 14.612 16.097 13.976 15.708 13.587ZM2 6.5C2 7.702 2.468 8.832 3.318 9.682C4.168 10.532 5.298 11 6.5 11C7.702 11 8.832 10.532 9.682 9.682C10.532 8.832 11 7.702 11 6.5C11 5.298 10.532 4.168 9.682 3.318C8.832 2.468 7.702 2 6.5 2C5.298 2 4.168 2.468 3.318 3.318C2.468 4.168 2 5.298 2 6.5Z'
/>
</svg>
))
)`
/* icons must be styled-components */
`;
2 changes: 1 addition & 1 deletion app/scripts/components/common/loading-skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const pulse = keyframes`
}
`;

const pulsingAnimation = css`
export const pulsingAnimation = css`
animation: ${pulse} 0.8s ease 0s infinite alternate;
`;

Expand Down
68 changes: 68 additions & 0 deletions app/scripts/components/common/map/controls/aoi/atoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { atom } from 'jotai';
import { Feature, Polygon } from 'geojson';
import { AoIFeature } from '../../types';
import { decodeAois, encodeAois } from '$utils/polygon-url';
import { atomWithUrlValueStability } from '$utils/params-location-atom/atom-with-url-value-stability';

// This is the atom acting as a single source of truth for the AOIs.
export const aoisSerialized = atomWithUrlValueStability<string>({
initialValue: new URLSearchParams(window.location.search).get('aois') ?? '',
urlParam: 'aois',
hydrate: (v) => v ?? '',
dehydrate: (v) => v,
});

// Getter atom to get AoiS as GeoJSON features from the hash.
export const aoisFeaturesAtom = atom<AoIFeature[]>((get) => {
const hash = get(aoisSerialized);
if (!hash) return [];
return decodeAois(hash);
});

// Setter atom to update AOIs geometries, writing directly to the hash atom.
export const aoisUpdateGeometryAtom = atom(
null,
(get, set, updates: Feature<Polygon>[]) => {
let newFeatures = [...get(aoisFeaturesAtom)];
updates.forEach(({ id, geometry }) => {
const existingFeature = newFeatures.find((feature) => feature.id === id);
if (existingFeature) {
existingFeature.geometry = geometry;
} else {
const newFeature: AoIFeature = {
type: 'Feature',
id: id as string,
geometry,
selected: true,
properties: {}
};
newFeatures = [...newFeatures, newFeature];
}
});
set(aoisSerialized, encodeAois(newFeatures));
}
);

// Setter atom to update AOIs selected state, writing directly to the hash atom.
export const aoisSetSelectedAtom = atom(null, (get, set, ids: string[]) => {
const features = get(aoisFeaturesAtom);
const newFeatures = features.map((feature) => {
return { ...feature, selected: ids.includes(feature.id as string) };
});
set(aoisSerialized, encodeAois(newFeatures));
});

// Setter atom to delete AOIs, writing directly to the hash atom.
export const aoisDeleteAtom = atom(null, (get, set, ids: string[]) => {
const features = get(aoisFeaturesAtom);
const newFeatures = features.filter(
(feature) => !ids.includes(feature.id as string)
);
set(aoisSerialized, encodeAois(newFeatures));
});

export const aoiDeleteAllAtom = atom(null, (get, set) => {
set(aoisSerialized, encodeAois([]));
});

export const isDrawingAtom = atom(false);
Loading

0 comments on commit 404043e

Please sign in to comment.