diff --git a/src/components/InventoryList.tsx b/src/components/InventoryList.tsx
index 0e590e5..26e31d3 100644
--- a/src/components/InventoryList.tsx
+++ b/src/components/InventoryList.tsx
@@ -6,10 +6,12 @@ import {
IonLabel,
IonList,
IonListHeader,
+ IonSpinner,
IonThumbnail,
} from "@ionic/react";
import {
bookmarkOutline,
+ banOutline,
navigate,
swapVerticalOutline,
arrowDownOutline,
@@ -24,8 +26,13 @@ import { useSettings } from "../context/settings";
import { InventoryFeature } from "../context/data.model";
import "./InventoryList.css";
+import { useSelection } from "../context/selection";
+import { useState } from "react";
const InventoryList: React.FC = () => {
+ // add a state for processing selections
+ const [selectionProcessing, setSelectionProcessing] = useState(false)
+
// load the filtered inventory list
const {
filteredInventory,
@@ -35,6 +42,9 @@ const InventoryList: React.FC = () => {
setSortDirection
} = useData();
+ // get selection functions
+ const { addToActiveSelection, removeFromActiveSelection, activeSelection } = useSelection()
+
// get a history context
const history = useHistory();
@@ -76,10 +86,23 @@ const InventoryList: React.FC = () => {
};
const addToBookmarksHandler = (
- event: React.MouseEvent
+ event: React.MouseEvent,
+ treeId: number
) => {
+ // set selection processing
+ setSelectionProcessing(true)
+
+ // stop event propagation
event.stopPropagation();
- console.log("add to bookmarks");
+
+ // check if this treeId is already in the selection
+ if (activeSelection?.selection.treeIds.includes(treeId)) {
+ removeFromActiveSelection(treeId).finally(() => setSelectionProcessing(false))
+ } else {
+ addToActiveSelection(treeId).finally(() => setSelectionProcessing(false))
+ }
+
+ //console.log("add to bookmarks");
};
return (
@@ -145,8 +168,13 @@ const InventoryList: React.FC = () => {
{distString(f)}
- addToBookmarksHandler(e)}>
-
+ addToBookmarksHandler(e, Number(f.properties.treeid))}
+ >
+ { selectionProcessing ? : (
+
+ )}
);
diff --git a/src/components/TreeDetails.tsx b/src/components/TreeDetails.tsx
index 6df1a4f..519a184 100644
--- a/src/components/TreeDetails.tsx
+++ b/src/components/TreeDetails.tsx
@@ -1,4 +1,6 @@
import {
+ IonButton,
+ IonButtons,
IonCard,
IonCardContent,
IonCardHeader,
@@ -6,6 +8,7 @@ import {
IonCardTitle,
IonCol,
IonGrid,
+ IonIcon,
IonItem,
IonLabel,
IonNote,
@@ -13,12 +16,14 @@ import {
IonSegment,
IonSegmentButton,
} from "@ionic/react";
+import { bookmarkOutline } from "ionicons/icons";
import { Data, Layout } from "plotly.js";
import { useEffect, useState } from "react";
import Plot from "react-plotly.js";
import { useData } from "../context/data";
import { InventoryFeature } from "../context/data.model";
import { useOffline } from "../context/offline";
+import { useSelection } from "../context/selection";
import * as plot from "../util/plot";
import "./TreeDetails.css";
@@ -61,6 +66,9 @@ const TreeDetails: React.FC = ({ treeID }) => {
// load all inventory data
const { filteredInventory, allInventory } = useData();
+ // get selection functions
+ const { addToActiveSelection } = useSelection()
+
// compnent state to store this feature
const [feature, setFeature] = useState();
const [currentImg, setCurrentImg] = useState();
diff --git a/src/components/map-components/InventorySource.tsx b/src/components/map-components/InventorySource.tsx
index 235a2eb..bb22419 100644
--- a/src/components/map-components/InventorySource.tsx
+++ b/src/components/map-components/InventorySource.tsx
@@ -6,18 +6,8 @@ import { Source, Layer, useMap } from "react-map-gl";
import { useData } from "../../context/data";
import { InventoryData, InventoryFeature } from "../../context/data.model";
import { useLayers } from "../../context/layers";
-import {
- IonCard,
- IonCardContent,
- IonCardHeader,
- IonCardTitle,
- IonItem,
- IonLabel,
- IonPopover,
-} from "@ionic/react";
import { useHistory } from "react-router";
import { useOffline } from "../../context/offline";
-import bbox from "@turf/bbox";
const InventoryLayer: React.FC = () => {
const {
diff --git a/src/components/map-components/MainMapMaplibre.tsx b/src/components/map-components/MainMapMaplibre.tsx
index 3b82c2a..7154127 100644
--- a/src/components/map-components/MainMapMaplibre.tsx
+++ b/src/components/map-components/MainMapMaplibre.tsx
@@ -8,6 +8,7 @@ import InventorySource from "./InventorySource";
import BaseLayerSource from "./BaseLayerSource";
import UserLocationSource from "./UserLocationSource";
import LayerInteraction from "./LayerInteraction";
+import SelectionSource from "./SelectionSource";
const MainMap: React.FC = () => {
// onload callback handler
@@ -75,6 +76,7 @@ const MainMap: React.FC = () => {
>
+
diff --git a/src/components/map-components/SelectionSource.tsx b/src/components/map-components/SelectionSource.tsx
new file mode 100644
index 0000000..2e28cf1
--- /dev/null
+++ b/src/components/map-components/SelectionSource.tsx
@@ -0,0 +1,45 @@
+import cloneDeep from "lodash.clonedeep"
+import { useEffect, useState } from "react"
+import { Layer, Source } from "react-map-gl"
+import { InventoryData } from "../../context/data.model"
+import { useSelection } from "../../context/selection"
+
+const SelectionSource: React.FC = () => {
+ // define a component state for the selection GeoJSON
+ const [src, setSrc] = useState()
+
+ // subscribe to the current active selection
+ const { activeSelection } = useSelection()
+
+ // update component state when activeSelection changes
+ useEffect(() => {
+ if (activeSelection) {
+ setSrc(cloneDeep(activeSelection.geoJSON))
+ } else {
+ setSrc(undefined)
+ }
+ }, [activeSelection])
+
+ // if there is no source, return null
+ if (!src) {
+ return (
+ null
+ )
+ } else {
+ return (
+
+
+
+ )
+ }
+}
+
+export default SelectionSource
\ No newline at end of file
diff --git a/src/context/offline.tsx b/src/context/offline.tsx
index 67d57ef..96fa273 100644
--- a/src/context/offline.tsx
+++ b/src/context/offline.tsx
@@ -41,8 +41,8 @@ interface OfflineState {
remoteChecksums: Checksums | null;
getImageData: (name: string) => Promise
getBaselayer: (name: string) => Promise
- createSelection: (treeIds: number[], title?: string) => Promise
- updateSelection: (selection: InventorySelection) => Promise
+ createSelection: (treeIds: number[], title?: string) => Promise
+ updateSelection: (selection: InventorySelection) => Promise
dropSelection: (selectionId: string) => Promise
}
@@ -289,7 +289,7 @@ export const OfflineProvider: React.FC = ({ children })
}
- const createSelection = async (treeIds: number[], title?: string): Promise => {
+ const createSelection = async (treeIds: number[], title?: string): Promise => {
// create a random 16 character string as id
const id = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
@@ -304,7 +304,7 @@ export const OfflineProvider: React.FC = ({ children })
return updateSelection(selection)
}
- const updateSelection = async (selection: InventorySelection): Promise => {
+ const updateSelection = async (selection: InventorySelection): Promise => {
// create the selections folder if it does not exist
if (!fileInfos?.map(i => i.name).includes('selections')) {
await Filesystem.mkdir({path: '/selections', directory: Directory.Data})
@@ -323,16 +323,16 @@ export const OfflineProvider: React.FC = ({ children })
// create the new array of selections
let newSelections: InventorySelection[] = []
if (selections) {
- newSelections = cloneDeep([...selections, selection])
+ newSelections = cloneDeep([...selections.filter(s => s.id !== selection.id), {...selection}])
} else {
- newSelections = [selection]
+ newSelections = [{...selection}]
}
// update the selections state variable
setSelections(newSelections)
// return the local path
- return path
+ return selection
})
}
diff --git a/src/context/selection.tsx b/src/context/selection.tsx
index 1d72ffa..02fe23d 100644
--- a/src/context/selection.tsx
+++ b/src/context/selection.tsx
@@ -1,7 +1,6 @@
import bbox from "@turf/bbox";
import cloneDeep from "lodash.clonedeep";
-import { createContext, useContext, useEffect, useState } from "react";
-import { useData } from "./data";
+import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { InventoryData } from "./data.model";
import { InventorySelection } from "./inventory-selection.model";
import { useOffline } from "./offline";
@@ -15,12 +14,16 @@ interface SelectionState {
selections: InventorySelection[];
activeSelection: ActiveSelection | null;
setActiveSelection: (selectionId: string | null) => void
+ addToActiveSelection: (treeId: number | string) => Promise
+ removeFromActiveSelection: (treeID: number | string) => Promise
}
const initialState: SelectionState = {
selections: [],
activeSelection: null,
- setActiveSelection: (selectionId: string | null) => {}
+ setActiveSelection: (selectionId: string | null) => {},
+ addToActiveSelection: (treeId: number | string) => Promise.reject('Not implemented.'),
+ removeFromActiveSelection: (treeID: number | string) => Promise.reject('Not implemented.'),
}
// add the context
@@ -30,11 +33,13 @@ const SelectionContext = createContext(initialState);
export const SelectionProvider: React.FC = ({ children }) => {
// create the state
const [selections, setSelections] = useState(initialState.selections)
+ const [activeSelectionId, setActiveSelectionId] = useState(null)
const [activeSelection, setActiveSelectionState] = useState(initialState.activeSelection)
// get a reference to all inventory data
- const { filteredInventory } = useData()
- const {selections: offlineSelections} = useOffline()
+ const { inventory } = useOffline()
+
+ const {selections: offlineSelections, createSelection, updateSelection} = useOffline()
// subscribe to changes in offline selections
useEffect(() => {
@@ -45,40 +50,103 @@ export const SelectionProvider: React.FC = ({ children
}
}, [offlineSelections])
- // define the context functions
+ // setActiveSelection handler
const setActiveSelection = (selectionId: string | null) => {
- if (selectionId === null || !filteredInventory) {
- setActiveSelectionState(null)
+ if (!selectionId) {
+ setActiveSelectionId(null)
+ } else {
+ setActiveSelectionId(selectionId)
+ }
+ }
+
+ const addToActiveSelection = async (treeId: number | string): Promise => {
+ // skip if the treeId is already selection
+ if (activeSelection && activeSelection.selection.treeIds.includes(Number(treeId))) {
+ return Promise.reject(`The tree ID=${treeId} is already part of the current selection`)
+ }
+ // get the selection representation from the current data or create a new one
+ let selection: InventorySelection;
+ if (!activeSelection) {
+ // create a new selection
+ selection = await createSelection([Number(treeId)], `New Selection ${selections.length}`)
} else {
- const selection = selections.find(s => s.id === selectionId)
+ // update
+ activeSelection.selection.treeIds.push(Number(treeId))
+ selection = await updateSelection(activeSelection.selection)
+ }
+
+ // build the geojson
+ const newActiveSelection = getSelectionWithGeoJSON(selection)
+ setActiveSelectionState(newActiveSelection)
+
+ // set the selection active if it was not
+ if (activeSelectionId !== selection.id) {
+ setActiveSelectionId(selection.id)
+ }
+ }
+
+ const removeFromActiveSelection = async (treeId: number | string): Promise => {
+ // skip if the treeId is not in selection
+ if (activeSelection && !activeSelection.selection.treeIds.includes(Number(treeId))) {
+ return Promise.reject(`Tree ID=${treeId} is not in the active selection.`)
+ }
+
+ // remove the treeId
+ const newTreeIds = activeSelection?.selection.treeIds.filter(id => id !== treeId) || []
+
+ // update the selection
+ const selection = await updateSelection({...activeSelection!.selection, treeIds: newTreeIds})
+
+ // build the geoJSON
+ const newActiveSelection = getSelectionWithGeoJSON(selection)
+ setActiveSelectionState(newActiveSelection)
+
+ // set the selection as the new active selection
+ if (activeSelectionId !== selection.id) {
+ setActiveSelectionId(selection.id)
+ }
+ }
+
+ const getSelectionWithGeoJSON = useCallback((selection: InventorySelection): ActiveSelection => {
+ // filter the full inventory for any selected tree
+ const features = inventory?.features.filter(f => selection.treeIds.includes(Number(f.properties.treeid))) || []
+
+ // create the geojson
+ const data = {
+ type: 'FeatureCollection',
+ features: cloneDeep(features),
+ } as InventoryData
+
+ // add the bounding box
+ data.bbox = bbox(data)
+
+ return {selection: cloneDeep(selection), geoJSON: cloneDeep(data)}
+ }, [inventory])
+
+ // use effect to update the current active selection, when the activeSelectionId or the filteredInventory changes
+ useEffect(() => {
+ if (!inventory) return
+ if (activeSelectionId) {
+ const selection = selections.find(s => s.id === activeSelectionId)
if (selection) {
- // extract the features from the inventory data that match the selection
- const features = filteredInventory.features.filter(f => selection.treeIds.includes(Number(f.properties.treeid)))
-
- // create the geojson
- const data = {
- type: 'FeatureCollection',
- features: cloneDeep(features),
- } as InventoryData
-
- // add the bounding box
- data.bbox = bbox(data)
-
- setActiveSelectionState({
- selection: cloneDeep(selection),
- geoJSON: cloneDeep(data)
- })
+ const newActiveSelection = getSelectionWithGeoJSON(selection)
+ setActiveSelectionState(newActiveSelection)
} else {
setActiveSelectionState(null)
}
+ } else {
+ setActiveSelectionState(null)
}
- }
+ }, [activeSelectionId, inventory, selections, getSelectionWithGeoJSON])
+
// create the context value
const value = {
selections,
activeSelection,
- setActiveSelection
+ setActiveSelection,
+ addToActiveSelection,
+ removeFromActiveSelection
}
// return the provider