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

Basic AoIs #690

Merged
merged 10 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { atomWithLocation } from "jotai-location";
import { Polygon } from "geojson";
import { AoIFeature } from "../../types";
import { decodeAois, encodeAois } from "$utils/polygon-url";

// This is the atom acting as a single source of truth for the AOIs.
export const aoisAtom = atomWithLocation();

const aoisSerialized = atom(
(get) => get(aoisAtom).searchParams?.get("aois"),
(get, set, aois) => {
set(aoisAtom, (prev) => ({
...prev,
searchParams: new URLSearchParams([["aois", aois as string]])
}));
}
);


// 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 geoometries, writing directly to the hash atom.
export const aoisUpdateGeometryAtom = atom(
null,
(get, set, updates: { id: string; geometry: 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,
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));
});
56 changes: 56 additions & 0 deletions app/scripts/components/common/map/controls/aoi/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import { createGlobalStyle } from 'styled-components';
import { useAtomValue } from 'jotai';
import { useRef } from 'react';
import { useControl } from 'react-map-gl';
import { aoisFeaturesAtom } from './atoms';

type DrawControlProps = {
onCreate?: (evt: { features: object[] }) => void;
onUpdate?: (evt: { features: object[]; action: string }) => void;
onDelete?: (evt: { features: object[] }) => void;
onSelectionChange?: (evt: { selectedFeatures: object[] }) => void;
} & MapboxDraw.DrawOptions;

const Css = createGlobalStyle`
.mapbox-gl-draw_trash {
opacity: .5;
pointer-events: none !important;
}
`;

export default function DrawControl(props: DrawControlProps) {
const control = useRef<MapboxDraw>();
const aoisFeatures = useAtomValue(aoisFeaturesAtom);

useControl<MapboxDraw>(
() => {
control.current = new MapboxDraw(props);
return control.current;
},
({ map }: { map: any }) => {
map.on('draw.create', props.onCreate);
map.on('draw.update', props.onUpdate);
map.on('draw.delete', props.onDelete);
map.on('draw.selectionchange', props.onSelectionChange);
map.on('load', () => {
control.current?.set({
type: 'FeatureCollection',
features: aoisFeatures
});
});
},
({ map }: { map: any }) => {
map.off('draw.create', props.onCreate);
map.off('draw.update', props.onUpdate);
map.off('draw.delete', props.onDelete);
map.off('draw.selectionchange', props.onSelectionChange);
},
{
position: 'top-left'
}
);

return aoisFeatures.length ? null : <Css />;
}
40 changes: 40 additions & 0 deletions app/scripts/components/common/map/controls/hooks/use-aois.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { Polygon } from 'geojson';
import { toAoIid } from '../../utils';
import { aoisDeleteAtom, aoisFeaturesAtom, aoisSetSelectedAtom, aoisUpdateGeometryAtom } from '../aoi/atoms';

export default function useAois() {
const features = useAtomValue(aoisFeaturesAtom);

const aoisUpdateGeometry = useSetAtom(aoisUpdateGeometryAtom);
const onUpdate = useCallback(
(e) => {
const updates = e.features.map((f) => ({
id: toAoIid(f.id),
geometry: f.geometry as Polygon
}));
aoisUpdateGeometry(updates);
},
[aoisUpdateGeometry]
);

const aoiDelete = useSetAtom(aoisDeleteAtom);
const onDelete = useCallback(
(e) => {
const selectedIds = e.features.map((f) => toAoIid(f.id));
aoiDelete(selectedIds);
},
[aoiDelete]
);

const aoiSetSelected = useSetAtom(aoisSetSelectedAtom);
const onSelectionChange = useCallback(
(e) => {
const selectedIds = e.features.map((f) => toAoIid(f.id));
aoiSetSelected(selectedIds);
},
[aoiSetSelected]
);
return { features, onUpdate, onDelete, onSelectionChange };
}
2 changes: 1 addition & 1 deletion app/scripts/components/common/map/controls/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ export function NavigationControl() {

export function ScaleControl() {
return <MapboxGLScaleControl position='bottom-left' />;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import { Button, createButtonStyles } from '@devseed-ui/button';
import { FormSwitch } from '@devseed-ui/form';
import { Subtitle } from '@devseed-ui/typography';

import useThemedControl from '../hooks/use-themed-control';
import {
ProjectionItemConic,
ProjectionItemCustom,
ProjectionItemSimple
} from './map-options/projection-items';
import { MapOptionsProps } from './map-options/types';
import { projectionsList } from './map-options/projections';
import { BASEMAP_STYLES } from './map-options/basemap';
import useThemedControl from './hooks/use-themed-control';
} from './projection-items';
import { MapOptionsProps } from './types';
import { projectionsList } from './projections';
import { BASEMAP_STYLES } from './basemap';
import { ShadowScrollbarImproved as ShadowScrollbar } from '$components/common/shadow-scrollbar-improved';

const DropHeader = styled.div`
Expand Down
32 changes: 31 additions & 1 deletion app/scripts/components/common/map/mapbox-style-override.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
CollecticonPlusSmall,
CollecticonMinusSmall,
CollecticonMagnifierLeft,
CollecticonXmarkSmall
CollecticonXmarkSmall,
CollecticonPencil,
CollecticonTrashBin
} from '@devseed-ui/collecticons';
import { glsp, themeVal } from '@devseed-ui/theme-provider';
import { variableGlsp } from '$styles/variable-utils';
Expand Down Expand Up @@ -178,6 +180,34 @@ const MapboxStyleOverride = css`
background-color: ${themeVal('color.base-400a')};
}

.mapbox-gl-draw_ctrl-draw-btn {
${createButtonStyles({ variation: 'primary-fill', fitting: 'skinny' })}
}

.mapbox-gl-draw_ctrl-draw-btn.active {
background-color: ${themeVal('color.base-400a')};
}

.mapbox-gl-draw_ctrl-draw-btn:not(:disabled):hover {
background-color: ${themeVal('color.base-400a')};
}

.mapbox-gl-draw_polygon.mapbox-gl-draw_polygon::before {
background-image: url(${({ theme }) =>
iconDataURI(CollecticonPencil, {
color: theme.color?.surface
})});
}
}

.mapbox-gl-draw_trash.mapbox-gl-draw_trash::before {
background-image: url(${({ theme }) =>
iconDataURI(CollecticonTrashBin, {
color: theme.color?.surface
})});
}
}

/* GEOCODER styles */
.mapboxgl-ctrl.mapboxgl-ctrl-geocoder {
background-color: ${themeVal('color.surface')};
Expand Down
10 changes: 10 additions & 0 deletions app/scripts/components/common/map/maps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ const MapsContainer = styled.div`
.mapboxgl-map {
position: absolute !important;
inset: 0;

&.mouse-add .mapboxgl-canvas-container {
cursor: crosshair;
}
&.mouse-pointer .mapboxgl-canvas-container {
cursor: pointer;
}
&.mouse-move .mapboxgl-canvas-container {
cursor: move;
}
}

.mapboxgl-compare .compare-swiper-vertical {
Expand Down
12 changes: 9 additions & 3 deletions app/scripts/components/common/map/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AnyLayer, AnySourceImpl } from "mapbox-gl";
import { Feature, Polygon } from 'geojson';
import { AnyLayer, AnySourceImpl } from 'mapbox-gl';

export interface ExtendedMetadata {
layerOrderPosition?: LayerOrderPosition;
Expand Down Expand Up @@ -29,10 +30,15 @@ export type LayerOrderPosition =
| 'vector'
| 'basemap-foreground';

export type MapId = 'main' | 'compared'
export type MapId = 'main' | 'compared';

export interface StacFeature {
bbox: [number, number, number, number];
}

export type OptionalBbox = number[] | undefined | null;
export type OptionalBbox = number[] | undefined | null;

export type AoIFeature = Feature<Polygon> & {
selected: boolean;
id: string;
};
4 changes: 4 additions & 0 deletions app/scripts/components/common/map/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,7 @@ export function resolveConfigFunctions(

return datum;
}

export function toAoIid(drawId: string) {
return drawId.slice(-6);
}
18 changes: 17 additions & 1 deletion app/scripts/components/exploration/components/map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import {
ScaleControl
} from '$components/common/map/controls';
import MapCoordsControl from '$components/common/map/controls/coords';
import MapOptionsControl from '$components/common/map/controls/options';
import MapOptionsControl from '$components/common/map/controls/map-options';
import { projectionDefault } from '$components/common/map/controls/map-options/projections';
import { useBasemap } from '$components/common/map/controls/hooks/use-basemap';
import DrawControl from '$components/common/map/controls/aoi';
import useAois from '$components/common/map/controls/hooks/use-aois';

export function ExplorationMap(props: { comparing: boolean }) {
const [projection, setProjection] = useState(projectionDefault);
Expand Down Expand Up @@ -49,6 +51,9 @@ export function ExplorationMap(props: { comparing: boolean }) {
.slice()
.reverse();

const { onUpdate, onDelete, onSelectionChange } = useAois();
// console.log(features);

return (
<Map id='exploration' projection={projection}>
{/* Map layers */}
Expand Down Expand Up @@ -81,6 +86,17 @@ export function ExplorationMap(props: { comparing: boolean }) {
boundariesOption={boundariesOption}
onOptionChange={onOptionChange}
/>
<DrawControl
displayControlsDefault={false}
controls={{
polygon: true,
trash: true
} as any}
onCreate={onUpdate}
onUpdate={onUpdate}
onDelete={onDelete}
onSelectionChange={onSelectionChange}
/>
{props.comparing && (
// Compare map layers
<Compare>
Expand Down
Loading
Loading