Skip to content

Commit

Permalink
Basic AoIs
Browse files Browse the repository at this point in the history
  • Loading branch information
nerik committed Oct 4, 2023
1 parent 2d8b371 commit 5c227a7
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 16 deletions.
34 changes: 34 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,34 @@
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import { useControl } from 'react-map-gl';

// import type { MapRef } from 'react-map-gl';

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

export default function DrawControl(props: DrawControlProps) {
useControl<MapboxDraw>(
() => new MapboxDraw(props),
({ 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 }: { 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 null;
}
51 changes: 51 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,51 @@
import { useAtom } from "jotai";
import { useCallback } from "react";
import { aoiFeaturesAtom } from "$components/exploration/atoms/atoms";

export default function useAois() {
const [features, setFeatures] = useAtom(aoiFeaturesAtom);

const onUpdate = useCallback(
(e) => {
let newFeatures = [...features];
e.features.forEach((f) => {
const existingFeature = newFeatures.find(
(feature) => feature.id === f.id
);
if (existingFeature) {
existingFeature.geometry = f.geometry;
} else {
newFeatures = [...newFeatures, f];
}
});
setFeatures(newFeatures);
},
[features, setFeatures]
);

const onDelete = useCallback(
(e) => {
let newFeatures = [...features];
e.features.forEach((f) => {
newFeatures = newFeatures.filter((feature) => feature.id !== f.id);
});
setFeatures(newFeatures);
},
[features, setFeatures]
);

const onSelectionChange = useCallback(
(e) => {
const newFeatures = features.map((feature) => {
if (e.features.find((f) => f.id === feature.id)) {
return { ...feature, selected: true };
} else {
return { ...feature, selected: false };
}
});
setFeatures(newFeatures);
},
[features, setFeatures]
);
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' />;
}
}
30 changes: 29 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,32 @@ 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_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
})});
}
}
// mapbox-gl-draw_polygon"
/* GEOCODER styles */
.mapboxgl-ctrl.mapboxgl-ctrl-geocoder {
background-color: ${themeVal('color.surface')};
Expand Down
19 changes: 19 additions & 0 deletions app/scripts/components/exploration/atoms/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { atom } from 'jotai';
import { atomWithHash } from 'jotai-location';
import { Feature } from 'geojson';
import {
DataMetric,
dataMetrics
} from '../components/analysis-metrics-dropdown';

import { HEADER_COLUMN_WIDTH, RIGHT_AXIS_SPACE } from '../constants';
import { DateRange, TimelineDataset, ZoomTransformPlain } from '../types.d.ts';
import { decodeAois, encodeAois } from '$utils/polygon-url';

// Datasets to show on the timeline and their settings
export const timelineDatasetsAtom = atom<TimelineDataset[]>([]);
Expand Down Expand Up @@ -43,3 +46,19 @@ export const activeAnalysisMetricsAtom = atom<DataMetric[]>(dataMetrics);

// 🛑 Whether or not an analysis is being performed. Temporary!!!
export const isAnalysisAtom = atom<boolean>(false);

export const aoiHashAtom = atomWithHash('aois','')

export const aoiFeaturesAtom = atom<Feature[]>(
(get) => {
const hash = get(aoiHashAtom);
if (!hash) return [];
return decodeAois(hash);
},
(get, set, features) => {
const hash = encodeAois(features);
set(aoiHashAtom, hash);
}
);

// export const aoiFeaturesAtom = atom<Feature<Polygon>[]>([]);
17 changes: 17 additions & 0 deletions app/scripts/components/exploration/components/map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import MapCoordsControl from '$components/common/map/controls/coords';
import MapOptionsControl from '$components/common/map/controls/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, features } = useAois();
console.log(features);

return (
<Map id='exploration' projection={projection}>
{/* Map layers */}
Expand Down Expand Up @@ -81,6 +86,18 @@ export function ExplorationMap(props: { comparing: boolean }) {
boundariesOption={boundariesOption}
onOptionChange={onOptionChange}
/>
<DrawControl
displayControlsDefault={false}
controls={{
polygon: true,
trash: true
}}
defaultMode='draw_polygon'
onCreate={onUpdate}
onUpdate={onUpdate}
onDelete={onDelete}
onSelectionChange={onSelectionChange}
/>
{props.comparing && (
// Compare map layers
<Compare>
Expand Down
60 changes: 46 additions & 14 deletions app/scripts/utils/polygon-url.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { FeatureCollection, Polygon } from 'geojson';
import { Feature, FeatureCollection, Polygon } from 'geojson';
import gjv from 'geojson-validation';
import { decode, encode } from 'google-polyline';

function decodeFeature(polygon: string): Feature<Polygon> {
const coords = decode(polygon);
return {
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [[...coords, coords[0]]]
}
} as Feature<Polygon>;
}

/**
* Decodes a multi polygon string converting it into a FeatureCollection of
* Polygons.
Expand All @@ -15,15 +27,7 @@ export function polygonUrlDecode(polygonStr: string) {
const geojson = {
type: 'FeatureCollection',
features: polygonStr.split(';').map((polygon) => {
const coords = decode(polygon);
return {
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [[...coords, coords[0]]]
}
};
return decodeFeature(polygon) as Feature<Polygon>;
})
} as FeatureCollection<Polygon>;

Expand All @@ -33,6 +37,13 @@ export function polygonUrlDecode(polygonStr: string) {
};
}

function encodePolygon(polygon: Polygon) {
const points = polygon.coordinates[0]
// Remove last coordinate since it is repeated.
.slice(0, -1);
return encode(points);
}

/**
* Converts a FeatureCollection of Polygons into a url string.
* Removes the last point of the polygon as it is the same as the first.
Expand All @@ -47,10 +58,31 @@ export function polygonUrlEncode(
) {
return featureCollection.features
.map((feature) => {
const points = feature.geometry.coordinates[0]
// Remove last coordinate since it is repeated.
.slice(0, -1);
return encode(points);
return encodePolygon(feature.geometry);
})
.join(';');
}

export function encodeAois(aois: Feature<Polygon>[]): string {
const encoded = aois.reduce((acc, aoi) => {
const encodedGeom = encodePolygon(aoi.geometry);
return [...acc, encodedGeom, !!aoi.selected];
}, []);
return JSON.stringify(encoded);
}

export function decodeAois(aois: string): Feature<Polygon>[] {
const decoded = JSON.parse(aois) as string[];
const features = decoded.reduce<Feature<Polygon>[]>((acc, current, i) => {
if (i % 2 === 0) {
const decodedFeature = decodeFeature(current);
return [...acc, decodedFeature];
} else {
const lastFeature = acc[acc.length - 1];
const newFeature = { ...lastFeature, selected: current };
acc[acc.length - 1] = newFeature;
return acc;
}
}, []);
return features!;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
"intersection-observer": "^0.12.0",
"jest-environment-jsdom": "^28.1.3",
"jotai": "^2.2.3",
"jotai-location": "^0.5.1",
"jotai-optics": "^0.3.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8308,6 +8308,11 @@ jest@^28.1.3:
import-local "^3.0.2"
jest-cli "^28.1.3"

jotai-location@^0.5.1:
version "0.5.1"
resolved "http://verdaccio.ds.io:4873/jotai-location/-/jotai-location-0.5.1.tgz#1a08b683cd7823ce57f7fef8b98335f1ce5c7105"
integrity sha512-6b34X6PpUaXmHCcyxdMFUHgRLUEp+SFHq9UxHbg5HxHC1LddVyVZbPJI+P15+SOQJcUTH3KrsIeKmeLko+Vw/A==

jotai-optics@^0.3.1:
version "0.3.1"
resolved "http://verdaccio.ds.io:4873/jotai-optics/-/jotai-optics-0.3.1.tgz#7ff38470551429460cc41d9cd1320193665354e0"
Expand Down

0 comments on commit 5c227a7

Please sign in to comment.