From 133175f62d7955c7cabe34639b5ded27427702ba Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 30 Aug 2024 12:46:05 -0700 Subject: [PATCH 001/306] chore(sentinel2explorer): add sentinel-2-explorer data to config.json add folder and entrypoint for sentinel-2-explorer app --- package.json | 6 +++- src/config.json | 12 +++++++ src/sentinel-2-explorer/index.tsx | 55 +++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/sentinel-2-explorer/index.tsx diff --git a/package.json b/package.json index 2a3f911a..6a7289f0 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,13 @@ "start:landsat": "webpack serve --mode development --open --env app=landsat", "start:landcover": "webpack serve --mode development --open --env app=landcover-explorer", "start:sentinel1": "webpack serve --mode development --open --env app=sentinel1-explorer", + "start:sentinel2": "webpack serve --mode development --open --env app=sentinel2-explorer", "start:spectral-sampling-tool": "webpack serve --mode development --open --env app=spectral-sampling-tool", "start:surface-temp": "webpack serve --mode development --open --env app=landsat-surface-temp", "build:landsat": "webpack --mode production --env app=landsat", "build:landcover": "webpack --mode production --env app=landcover-explorer", "build:sentinel1": "webpack --mode production --env app=sentinel1-explorer", + "build:sentinel2": "webpack --mode production --env app=sentinel2-explorer", "build:spectral-sampling-tool": "webpack --mode production --env app=spectral-sampling-tool", "build:surface-temp": "webpack --mode production --env app=landsat-surface-temp", "prepare": "husky install", @@ -35,9 +37,11 @@ "keywords": [ "landsat", "remote-sensing", + "sentinel-1", "sentinel-2", "landcover", - "remote-sensing", + "esri", + "imagery-explorer-apps", "living-atlas" ], "author": "ArcGIS Living Atlas of the World", diff --git a/src/config.json b/src/config.json index ab494a57..a6354838 100644 --- a/src/config.json +++ b/src/config.json @@ -24,6 +24,14 @@ "pathname": "/sentinel1explorer", "entrypoint": "/src/sentinel-1-explorer/index.tsx" }, + "sentinel2-explorer": { + "title": "Esri | Sentinel-2 Explorer", + "webmapId": "81609bbe235942919ad27c77e42c600e", + "description": "", + "animationMetadataSources": "Esri, European Space Agency", + "pathname": "/sentinel2explorer", + "entrypoint": "/src/sentinel-2-explorer/index.tsx" + }, "spectral-sampling-tool": { "title": "Spectral Sampling Tool", "webmapId": "81609bbe235942919ad27c77e42c600e", @@ -47,6 +55,10 @@ "sentinel-1": { "development": "https://utility.arcgis.com/usrsvcs/servers/ae20c269388b4ea3acac60b9bf2a319f/rest/services/Sentinel1RTC/ImageServer", "production": "https://utility.arcgis.com/usrsvcs/servers/6cd9e108e0b84784afb476269296fdd7/rest/services/Sentinel1RTC/ImageServer" + }, + "sentinel-2": { + "development": "https://utility.arcgis.com/usrsvcs/servers/3f0ca149c08f4746bc0d0fef2cee1624/rest/services/Sentinel2L2A/ImageServer", + "production": "https://utility.arcgis.com/usrsvcs/servers/3f0ca149c08f4746bc0d0fef2cee1624/rest/services/Sentinel2L2A/ImageServer" } } } diff --git a/src/sentinel-2-explorer/index.tsx b/src/sentinel-2-explorer/index.tsx new file mode 100644 index 00000000..d68bfe70 --- /dev/null +++ b/src/sentinel-2-explorer/index.tsx @@ -0,0 +1,55 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// import '@arcgis/core/assets/esri/themes/dark/main.css'; +import '@shared/styles/index.css'; +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { Provider as ReduxProvider } from 'react-redux'; +import ErrorBoundary from '@shared/components/ErrorBoundary/ErrorBoundary'; +import { ErrorPage } from '@shared/components/ErrorPage'; +import AppContextProvider from '@shared/contexts/AppContextProvider'; + +(async () => { + const root = createRoot(document.getElementById('root')); + + try { + // const store = await getSentinel1ExplorerStore(); + + // const timeExtent = await getTimeExtentOfSentinel1Service(); + // // console.log(timeExtent); + + // root.render( + // + // + // + // + // + // + // + // + // + // + // ); + + root.render(

Sentinel2 Explorer

); + } catch (err) { + console.log(err); + root.render(); + } +})(); From b98fbecb8a36f8302eca212e5404596cf8c81a06 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 30 Aug 2024 12:58:18 -0700 Subject: [PATCH 002/306] feat(sentinel2explorer): add getSentinel2ExplorerStore helper function --- src/sentinel-2-explorer/index.tsx | 3 +- .../getPreloadedState4Sentinel2Explorer.ts | 211 ++++++++++++++++++ src/sentinel-2-explorer/store/index.ts | 22 ++ 3 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 src/sentinel-2-explorer/store/getPreloadedState4Sentinel2Explorer.ts create mode 100644 src/sentinel-2-explorer/store/index.ts diff --git a/src/sentinel-2-explorer/index.tsx b/src/sentinel-2-explorer/index.tsx index d68bfe70..a023e49d 100644 --- a/src/sentinel-2-explorer/index.tsx +++ b/src/sentinel-2-explorer/index.tsx @@ -21,12 +21,13 @@ import { Provider as ReduxProvider } from 'react-redux'; import ErrorBoundary from '@shared/components/ErrorBoundary/ErrorBoundary'; import { ErrorPage } from '@shared/components/ErrorPage'; import AppContextProvider from '@shared/contexts/AppContextProvider'; +import { getSentinel2ExplorerStore } from './store'; (async () => { const root = createRoot(document.getElementById('root')); try { - // const store = await getSentinel1ExplorerStore(); + const store = await getSentinel2ExplorerStore(); // const timeExtent = await getTimeExtentOfSentinel1Service(); // // console.log(timeExtent); diff --git a/src/sentinel-2-explorer/store/getPreloadedState4Sentinel2Explorer.ts b/src/sentinel-2-explorer/store/getPreloadedState4Sentinel2Explorer.ts new file mode 100644 index 00000000..34899f1f --- /dev/null +++ b/src/sentinel-2-explorer/store/getPreloadedState4Sentinel2Explorer.ts @@ -0,0 +1,211 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// import { PartialRootState } from './configureStore'; + +import { initialMapState, MapState } from '@shared/store/Map/reducer'; +import { + getAnimationSpeedFromHashParams, + getChangeCompareToolDataFromHashParams, + getHashParamValueByKey, + getMapCenterFromHashParams, + getMaskToolDataFromHashParams, + getQueryParams4MainSceneFromHashParams, + getListOfQueryParamsFromHashParams, + getQueryParams4SecondarySceneFromHashParams, + getSpectralProfileToolDataFromHashParams, + getTemporalProfileToolDataFromHashParams, +} from '@shared/utils/url-hash-params'; +import { MAP_CENTER, MAP_ZOOM } from '@shared/constants/map'; +// import { initialUIState, UIState } from './UI/reducer'; +import { + AnalysisTool, + AppMode, + DefaultQueryParams4ImageryScene, + initialImagerySceneState, + ImageryScenesState, + QueryParams4ImageryScene, + // QueryParams4ImageryScene, +} from '@shared/store/ImageryScene/reducer'; +import { IS_MOBILE_DEVICE } from '@shared/constants/UI'; +import { initialUIState, UIState } from '@shared/store/UI/reducer'; +import { PartialRootState } from '@shared/store/configureStore'; +import { + ChangeCompareToolState, + initialChangeCompareToolState, +} from '@shared/store/ChangeCompareTool/reducer'; +import { + TrendToolState, + initialTrendToolState, +} from '@shared/store/TrendTool/reducer'; +import { + MaskToolState, + initialMaskToolState, +} from '@shared/store/MaskTool/reducer'; +import { getRandomElement } from '@shared/utils/snippets/getRandomElement'; + +/** + * Map location info that contains center and zoom info from URL Hash Params + */ +const mapLocationFromHashParams = getMapCenterFromHashParams(); + +/** + * Use the location of a randomly selected interesting place if there is no map location info + * found in the URL hash params. + */ +// const randomInterestingPlace = !mapLocationFromHashParams +// ? getRandomElement(sentinel1InterestingPlaces) +// : null; + +const getPreloadedMapState = (): MapState => { + const mapLocation = mapLocationFromHashParams; + + // if (!mapLocation) { + // mapLocation = randomInterestingPlace?.location; + // } + + // show map labels if there is no `hideMapLabels` in hash params + const showMapLabel = getHashParamValueByKey('hideMapLabels') === null; + + // show terrain if there is no `hideTerrain` in hash params + const showTerrain = getHashParamValueByKey('hideTerrain') === null; + + const showBasemap = getHashParamValueByKey('hideBasemap') === null; + + return { + ...initialMapState, + center: mapLocation?.center || MAP_CENTER, + zoom: mapLocation?.zoom || MAP_ZOOM, + showMapLabel, + showTerrain, + showBasemap, + }; +}; + +const getPreloadedImageryScenesState = (): ImageryScenesState => { + let mode: AppMode = + (getHashParamValueByKey('mode') as AppMode) || 'dynamic'; + + // user is only allowed to use the "dynamic" mode when using mobile device + if (IS_MOBILE_DEVICE) { + mode = 'dynamic'; + } + + // const defaultRasterFunction: Sentinel1FunctionName = + // 'False Color dB with DRA'; + + // Attempt to extract query parameters from the URL hash. + // If not found, fallback to using the default values along with the raster function from a randomly selected interesting location, + // which will serve as the map center. + const queryParams4MainScene = getQueryParams4MainSceneFromHashParams() || { + ...DefaultQueryParams4ImageryScene, + // rasterFunctionName: + // randomInterestingPlace?.renderer || defaultRasterFunction, + }; + + const queryParams4SecondaryScene = + getQueryParams4SecondarySceneFromHashParams() || { + ...DefaultQueryParams4ImageryScene, + rasterFunctionName: null, + }; + + const listOfQueryParams = getListOfQueryParamsFromHashParams() || []; + + const queryParamsById: { + [key: string]: QueryParams4ImageryScene; + } = {}; + + const tool = getHashParamValueByKey('tool') as AnalysisTool; + + for (const queryParams of listOfQueryParams) { + queryParamsById[queryParams.uniqueId] = queryParams; + } + + return { + ...initialImagerySceneState, + mode, + tool: tool || 'mask', + queryParams4MainScene, + queryParams4SecondaryScene, + queryParamsList: { + byId: queryParamsById, + ids: listOfQueryParams.map((d) => d.uniqueId), + selectedItemID: listOfQueryParams[0] + ? listOfQueryParams[0].uniqueId + : null, + }, + }; +}; + +const getPreloadedUIState = (): UIState => { + const animationSpeed = getAnimationSpeedFromHashParams(); + + const proloadedUIState: UIState = { + ...initialUIState, + // nameOfSelectedInterestingPlace: randomInterestingPlace?.name || '', + }; + + if (animationSpeed) { + proloadedUIState.animationSpeed = animationSpeed; + proloadedUIState.animationStatus = 'loading'; + } + + return proloadedUIState; +}; + +const getPreloadedChangeCompareToolState = (): ChangeCompareToolState => { + const changeCompareToolData = getChangeCompareToolDataFromHashParams(); + + const initalState: ChangeCompareToolState = { + ...initialChangeCompareToolState, + }; + + return { + ...initalState, + ...changeCompareToolData, + }; +}; + +const getPreloadedTrendToolState = (): TrendToolState => { + // const maskToolData = getMaskToolDataFromHashParams(); + const trendToolData = getTemporalProfileToolDataFromHashParams(); + + return { + ...initialTrendToolState, + ...trendToolData, + }; +}; + +const getPreloadedMaskToolState = (): MaskToolState => { + const maskToolData = getMaskToolDataFromHashParams(); + + return { + ...initialMaskToolState, + ...maskToolData, + }; +}; + +export const getPreloadedState = async (): Promise => { + // get default raster function and location and pass to the getPreloadedMapState, getPreloadedUIState and getPreloadedImageryScenesState + + return { + Map: getPreloadedMapState(), + UI: getPreloadedUIState(), + ImageryScenes: getPreloadedImageryScenesState(), + ChangeCompareTool: getPreloadedChangeCompareToolState(), + TrendTool: getPreloadedTrendToolState(), + MaskTool: getPreloadedMaskToolState(), + } as PartialRootState; +}; diff --git a/src/sentinel-2-explorer/store/index.ts b/src/sentinel-2-explorer/store/index.ts new file mode 100644 index 00000000..323de262 --- /dev/null +++ b/src/sentinel-2-explorer/store/index.ts @@ -0,0 +1,22 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import configureAppStore from '@shared/store/configureStore'; +import { getPreloadedState } from './getPreloadedState4Sentinel2Explorer'; + +export const getSentinel2ExplorerStore = async () => { + const preloadedState = await getPreloadedState(); + return configureAppStore(preloadedState); +}; From ca253dd080347063e22ed6f6d1e9893c1d0c661c Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 30 Aug 2024 13:34:20 -0700 Subject: [PATCH 003/306] feat(sentinel2explorer): add sentinel-2 services folder and sentinel-2 layer --- .../components/Layout/Layout.tsx | 137 ++++++++++++++++ .../components/Map/Map.tsx | 84 ++++++++++ .../Sentinel2Layer/Sentinel2Layer.tsx | 50 ++++++ .../components/Sentinel2Layer/index.ts | 16 ++ src/sentinel-2-explorer/index.tsx | 38 ++--- src/shared/services/sentinel-2/config.ts | 151 ++++++++++++++++++ .../services/sentinel-2/getTimeExtent.ts | 49 ++++++ 7 files changed, 506 insertions(+), 19 deletions(-) create mode 100644 src/sentinel-2-explorer/components/Layout/Layout.tsx create mode 100644 src/sentinel-2-explorer/components/Map/Map.tsx create mode 100644 src/sentinel-2-explorer/components/Sentinel2Layer/Sentinel2Layer.tsx create mode 100644 src/sentinel-2-explorer/components/Sentinel2Layer/index.ts create mode 100644 src/shared/services/sentinel-2/config.ts create mode 100644 src/shared/services/sentinel-2/getTimeExtent.ts diff --git a/src/sentinel-2-explorer/components/Layout/Layout.tsx b/src/sentinel-2-explorer/components/Layout/Layout.tsx new file mode 100644 index 00000000..caecba27 --- /dev/null +++ b/src/sentinel-2-explorer/components/Layout/Layout.tsx @@ -0,0 +1,137 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import BottomPanel from '@shared/components/BottomPanel/BottomPanel'; +import { Calendar } from '@shared/components/Calendar'; +import { AppHeader } from '@shared/components/AppHeader'; +import { + ContainerOfSecondaryControls, + ModeSelector, +} from '@shared/components/ModeSelector'; + +import { useSelector } from 'react-redux'; +import { + selectActiveAnalysisTool, + selectAppMode, +} from '@shared/store/ImageryScene/selectors'; +import { AnimationControl } from '@shared/components/AnimationControl'; + +// import { TrendTool } from '../TemporalProfileTool'; +// import { MaskTool } from '../MaskTool'; +import { SwipeLayerSelector } from '@shared/components/SwipeLayerSelector'; +import { useSaveAppState2HashParams } from '@shared/hooks/useSaveAppState2HashParams'; +import { IS_MOBILE_DEVICE } from '@shared/constants/UI'; +// import { DynamicModeInfo } from '@shared/components/DynamicModeInfo'; +// import { SpectralTool } from '../SpectralTool'; +import { ChangeCompareLayerSelector } from '@shared/components/ChangeCompareLayerSelector'; +import { appConfig } from '@shared/config'; +// import { useQueryAvailableLandsatScenes } from '@landsat-explorer/hooks/useQueryAvailableLandsatScenes'; +// import { LandsatRasterFunctionSelector } from '../RasterFunctionSelector'; +// import { LandsatInterestingPlaces } from '../InterestingPlaces'; +// import { AnalyzeToolSelector4Landsat } from '../AnalyzeToolSelector/AnalyzeToolSelector'; +import { useShouldShowSecondaryControls } from '@shared/hooks/useShouldShowSecondaryControls'; +import { CloudFilter } from '@shared/components/CloudFilter'; +// import { LandsatDynamicModeInfo } from '../LandsatDynamicModeInfo/LandsatDynamicModeInfo'; + +const Layout = () => { + const mode = useSelector(selectAppMode); + + const analysisTool = useSelector(selectActiveAnalysisTool); + + const dynamicModeOn = mode === 'dynamic'; + + const shouldShowSecondaryControls = useShouldShowSecondaryControls(); + + // /** + // * This custom hook gets invoked whenever the acquisition year, map center, or selected landsat missions + // * changes, it will dispatch the query that finds the available landsat scenes. + // */ + // useQueryAvailableLandsatScenes(); + + useSaveAppState2HashParams(); + + if (IS_MOBILE_DEVICE) { + return ( + <> + + +
+ {/* + + */} +
+
+ + ); + } + + return ( + <> + + +
+ + + {shouldShowSecondaryControls && ( + + + + {/* */} + + )} + + {mode === 'analysis' && analysisTool === 'change' && ( + + + + )} +
+ +
+ {dynamicModeOn ? ( + <> + {/* + */} + + ) : ( + <> +
+ + + +
+ + {mode === 'analysis' && ( +
+ {/* + + + */} +
+ )} + + {/* */} + + )} + + {/* */} +
+
+ + ); +}; + +export default Layout; diff --git a/src/sentinel-2-explorer/components/Map/Map.tsx b/src/sentinel-2-explorer/components/Map/Map.tsx new file mode 100644 index 00000000..91f260d9 --- /dev/null +++ b/src/sentinel-2-explorer/components/Map/Map.tsx @@ -0,0 +1,84 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import MapViewContainer from '@shared/components/MapView/MapViewContainer'; +// import { SwipeWidget } from '../SwipeWidget'; +import { AnimationLayer } from '@shared/components/AnimationLayer'; +// import { MaskLayer } from '../MaskLayer'; +import { GroupLayer } from '@shared/components/GroupLayer'; +import { AnalysisToolQueryLocation } from '@shared/components/AnalysisToolQueryLocation'; +import { Zoom2NativeScale } from '@shared/components/Zoom2NativeScale/Zoom2NativeScale'; +// import { Popup } from '../PopUp'; +import { MapPopUpAnchorPoint } from '@shared/components/MapPopUpAnchorPoint'; +import { HillshadeLayer } from '@shared/components/HillshadeLayer/HillshadeLayer'; +import { ScreenshotWidget } from '@shared/components/ScreenshotWidget/ScreenshotWidget'; +import { MapMagnifier } from '@shared/components/MapMagnifier'; +import CustomMapArrtribution from '@shared/components/CustomMapArrtribution/CustomMapArrtribution'; +import { MapActionButtonsGroup } from '@shared/components/MapActionButton'; +import { CopyLinkWidget } from '@shared/components/CopyLinkWidget'; +import { useDispatch } from 'react-redux'; +import { updateQueryLocation4TrendTool } from '@shared/store/TrendTool/thunks'; +import { updateQueryLocation4SpectralProfileTool } from '@shared/store/SpectralProfileTool/thunks'; +import { SwipeWidget4ImageryLayers } from '@shared/components/SwipeWidget/SwipeWidget4ImageryLayers'; +import { ZoomToExtent } from '@shared/components/ZoomToExtent'; +import { SENTINEL_2_SERVICE_URL } from '@shared/services/sentinel-2/config'; +import { Sentinel2Layer } from '../Sentinel2Layer'; + +const Map = () => { + const dispatch = useDispatch(); + + return ( + { + dispatch(updateQueryLocation4TrendTool(point)); + dispatch(updateQueryLocation4SpectralProfileTool(point)); + }} + > + + + {/* */} + + + + + + + + + + + + + + + {/* */} + + + + ); +}; + +export default Map; diff --git a/src/sentinel-2-explorer/components/Sentinel2Layer/Sentinel2Layer.tsx b/src/sentinel-2-explorer/components/Sentinel2Layer/Sentinel2Layer.tsx new file mode 100644 index 00000000..8d15cb06 --- /dev/null +++ b/src/sentinel-2-explorer/components/Sentinel2Layer/Sentinel2Layer.tsx @@ -0,0 +1,50 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MapView from '@arcgis/core/views/MapView'; +import React, { FC, useEffect, useMemo } from 'react'; +import GroupLayer from '@arcgis/core/layers/GroupLayer'; +import ImageryLayerByObjectID from '@shared/components/ImageryLayer/ImageryLayerByObjectID'; +import MosaicRule from '@arcgis/core/layers/support/MosaicRule'; +import { + SENTINEL2_SERVICE_SORT_FIELD, + SENTINEL2_SERVICE_SORT_VALUE, + SENTINEL_2_SERVICE_URL, +} from '@shared/services/sentinel-2/config'; + +type Props = { + mapView?: MapView; + groupLayer?: GroupLayer; +}; + +export const Sentinel2Layer: FC = ({ mapView, groupLayer }: Props) => { + const defaultMosaicRule = useMemo(() => { + return new MosaicRule({ + ascending: true, + method: 'attribute', + operation: 'first', + sortField: SENTINEL2_SERVICE_SORT_FIELD, + sortValue: SENTINEL2_SERVICE_SORT_VALUE, + }); + }, []); + + return ( + + ); +}; diff --git a/src/sentinel-2-explorer/components/Sentinel2Layer/index.ts b/src/sentinel-2-explorer/components/Sentinel2Layer/index.ts new file mode 100644 index 00000000..abfad4b9 --- /dev/null +++ b/src/sentinel-2-explorer/components/Sentinel2Layer/index.ts @@ -0,0 +1,16 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { Sentinel2Layer } from './Sentinel2Layer'; diff --git a/src/sentinel-2-explorer/index.tsx b/src/sentinel-2-explorer/index.tsx index a023e49d..3cd189f4 100644 --- a/src/sentinel-2-explorer/index.tsx +++ b/src/sentinel-2-explorer/index.tsx @@ -22,6 +22,10 @@ import ErrorBoundary from '@shared/components/ErrorBoundary/ErrorBoundary'; import { ErrorPage } from '@shared/components/ErrorPage'; import AppContextProvider from '@shared/contexts/AppContextProvider'; import { getSentinel2ExplorerStore } from './store'; +import { getTimeExtentOfSentinel2Service } from '@shared/services/sentinel-2/getTimeExtent'; +import { SENTINEL2_RASTER_FUNCTION_INFOS } from '@shared/services/sentinel-2/config'; +import Map from './components/Map/Map'; +import Layout from './components/Layout/Layout'; (async () => { const root = createRoot(document.getElementById('root')); @@ -29,26 +33,22 @@ import { getSentinel2ExplorerStore } from './store'; try { const store = await getSentinel2ExplorerStore(); - // const timeExtent = await getTimeExtentOfSentinel1Service(); - // // console.log(timeExtent); + const timeExtent = await getTimeExtentOfSentinel2Service(); + // console.log(timeExtent); - // root.render( - // - // - // - // - // - // - // - // - // - // - // ); - - root.render(

Sentinel2 Explorer

); + root.render( + + + + + + + + + ); } catch (err) { console.log(err); root.render(); diff --git a/src/shared/services/sentinel-2/config.ts b/src/shared/services/sentinel-2/config.ts new file mode 100644 index 00000000..7f9384ba --- /dev/null +++ b/src/shared/services/sentinel-2/config.ts @@ -0,0 +1,151 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// import { TIER } from '@shared/constants'; +import { TIER, getServiceConfig } from '@shared/config'; + +const serviceConfig = getServiceConfig('sentinel-2'); +console.log('sentinel-2 service config', serviceConfig); + +/** + * + * Sentinel-2 multispectral and multitemporal atmospherically corrected imagery with on-the-fly visual renderings and indices for visualization and analysis. + * This imagery layer is sourced from the Microsoft Planetary Computer and is updated daily with new imagery. This layer is in beta release. + * @see https://www.arcgis.com/home/item.html?id=255af1ceee844d6da8ef8440c8f90d00 + */ +export const SENTINEL_2_ITEM_ID = `255af1ceee844d6da8ef8440c8f90d00`; + +/** + * URL of the Sentinel-2 Item on ArcGIS Online + */ +export const SENTINEL_2_ITEM_URL = `https://www.arcgis.com/home/item.html?id=${SENTINEL_2_ITEM_ID}`; + +/** + * This is the original service URL, which will prompt user to sign in by default as it requires subscription + */ +const SENTINEL_2_ORIGINAL_SERVICE_URL = + 'https://sentinel.imagery1.arcgis.com/arcgis/rest/services/Sentinel2L2A/ImageServer'; + +/** + * Service URL to be used in PROD enviroment + */ +export const SENTINEL_2_SERVICE_URL_PROD = + serviceConfig.production || SENTINEL_2_ORIGINAL_SERVICE_URL; + +/** + * Service URL to be used in DEV enviroment + * + */ +export const SENTINEL_2_SERVICE_URL_DEV = + serviceConfig.development || SENTINEL_2_ORIGINAL_SERVICE_URL; + +/** + * A proxy imagery service which has embedded credential that points to the sentinel-2 imagery service + * @see https://landsat.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer + */ +export const SENTINEL_2_SERVICE_URL = + TIER === 'development' + ? SENTINEL_2_SERVICE_URL_DEV + : SENTINEL_2_SERVICE_URL_PROD; + +/** + * Field Names Look-up table for Sentinel2L2A (ImageServer) + * @see https://sentinel.imagery1.arcgis.com/arcgis/rest/services/Sentinel2L2A/ImageServer + */ +export const FIELD_NAMES = { + OBJECTID: 'objectid', + NAME: 'name', + ACQUISITION_DATE: 'acquisitiondate', + CLOUD_COVER: 'cloudcover', +}; + +/** + * List of Raster Functions for the Sentinel-1 service + */ +const SENTINEL2_RASTER_FUNCTIONS = [ + 'Natural Color with DRA', + 'Agriculture with DRA', + 'Bathymetric with DRA', + 'Color Infrared with DRA', + 'Short-wave Infrared with DRA', + 'Geology with DRA', + 'NDVI Colormap', + 'NDMI Colorized', +] as const; + +export type Sentinel2FunctionName = (typeof SENTINEL2_RASTER_FUNCTIONS)[number]; + +/** + * Sentinel-2 Raster Function Infos + * @see https://sentinel.imagery1.arcgis.com/arcgis/rest/services/Sentinel2L2A/ImageServer/rasterFunctionInfos + */ +export const SENTINEL2_RASTER_FUNCTION_INFOS: { + name: Sentinel2FunctionName; + description: string; + label: string; +}[] = [ + { + name: 'Natural Color with DRA', + description: + 'Natural Color bands red, green, blue (4, 3, 2) displayed with dynamic range adjustment applied.', + label: 'Natural Color', + }, + { + name: 'Agriculture with DRA', + description: + 'Bands shortwave IR-1, near-IR, blue (11, 8, 2) with dynamic range adjustment applied. Vigorous veg. is bright green, stressed veg. dull green and bare areas as brown.', + label: 'Agriculture', + }, + { + name: 'Bathymetric with DRA', + description: + 'Bands red, green, coastal/aerosol(4, 3, 1) with dynamic range adjustment applied. Useful in bathymetric mapping applicaitons.', + label: 'Bathymetric', + }, + { + name: 'Color Infrared with DRA', + description: + 'Bands near-infrared, red, green (8,4,3) with dynamic range adjustment applied. Healthy vegetation is bright red while stressed vegetation is dull red.', + label: 'Color IR', + }, + { + name: 'Short-wave Infrared with DRA', + description: + 'Bands shortwave infrared-2, shortwave infrared-1, red (12, 11, 4) with dynamic range adjustment applied.', + label: 'Short-wave IR', + }, + { + name: 'Geology with DRA', + description: + 'Bands shortwave IR-2, shortwave IR-1, blue (12, 11, 2) with dynamic range adjustment applied. Highlights geological features.', + label: 'Geology', + }, + { + name: 'NDVI Colormap', + description: + 'Normalized difference vegetation index (NDVI) with colormap. Dark green represents vigorous vegetation and brown represents sparse vegetation. It is computed as (b8 - b4) / (b8 + b4) and is suitable for vegetation, land cover and plant health monitoring.', + label: 'NDVI', + }, + { + name: 'NDMI Colorized', + description: + 'Normalized Difference Moisture Index with color map. Wetlands and moist areas appear blue whereas dry areas are represented by deep yellow and brown color. It is computed as NIR(B8)-SWIR1(B11)/NIR(B8)+SWIR1(B11).', + label: 'NDMI', + }, +]; + +export const SENTINEL2_SERVICE_SORT_FIELD = 'best'; + +export const SENTINEL2_SERVICE_SORT_VALUE = '0'; diff --git a/src/shared/services/sentinel-2/getTimeExtent.ts b/src/shared/services/sentinel-2/getTimeExtent.ts new file mode 100644 index 00000000..f3522dbe --- /dev/null +++ b/src/shared/services/sentinel-2/getTimeExtent.ts @@ -0,0 +1,49 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ImageryServiceTimeExtentData } from '@typing/imagery-service'; +import { getTimeExtent } from '../helpers/getTimeExtent'; +import { SENTINEL_2_SERVICE_URL } from './config'; + +let timeExtentData: ImageryServiceTimeExtentData = null; + +/** + * Get Sentinel-2 layer's time extent + * @returns TimeExtentData + * + * @example + * Usage + * ``` + * getTimeExtent() + * ``` + * + * Returns + * ``` + * { + * start: 1363622294000, + * end: 1683500585000 + * } + * ``` + */ +export const getTimeExtentOfSentinel2Service = + async (): Promise => { + if (timeExtentData) { + return timeExtentData; + } + + const data = await getTimeExtent(SENTINEL_2_SERVICE_URL); + + return (timeExtentData = data); + }; From e39647e6e255e7c7abf7ac3b6033edac5a41246e Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 30 Aug 2024 13:39:56 -0700 Subject: [PATCH 004/306] feat(sentinel2explorer): add Sentinel2DynamicModeInfo --- .../components/Layout/Layout.tsx | 11 ++++----- .../Sentinel2DynamicModeInfo.tsx | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 src/sentinel-2-explorer/components/Sentinel2DynamicModeInfo/Sentinel2DynamicModeInfo.tsx diff --git a/src/sentinel-2-explorer/components/Layout/Layout.tsx b/src/sentinel-2-explorer/components/Layout/Layout.tsx index caecba27..75843336 100644 --- a/src/sentinel-2-explorer/components/Layout/Layout.tsx +++ b/src/sentinel-2-explorer/components/Layout/Layout.tsx @@ -34,7 +34,6 @@ import { AnimationControl } from '@shared/components/AnimationControl'; import { SwipeLayerSelector } from '@shared/components/SwipeLayerSelector'; import { useSaveAppState2HashParams } from '@shared/hooks/useSaveAppState2HashParams'; import { IS_MOBILE_DEVICE } from '@shared/constants/UI'; -// import { DynamicModeInfo } from '@shared/components/DynamicModeInfo'; // import { SpectralTool } from '../SpectralTool'; import { ChangeCompareLayerSelector } from '@shared/components/ChangeCompareLayerSelector'; import { appConfig } from '@shared/config'; @@ -44,7 +43,7 @@ import { appConfig } from '@shared/config'; // import { AnalyzeToolSelector4Landsat } from '../AnalyzeToolSelector/AnalyzeToolSelector'; import { useShouldShowSecondaryControls } from '@shared/hooks/useShouldShowSecondaryControls'; import { CloudFilter } from '@shared/components/CloudFilter'; -// import { LandsatDynamicModeInfo } from '../LandsatDynamicModeInfo/LandsatDynamicModeInfo'; +import { Sentinel2DynamicModeInfo } from '../Sentinel2DynamicModeInfo/Sentinel2DynamicModeInfo'; const Layout = () => { const mode = useSelector(selectAppMode); @@ -69,8 +68,8 @@ const Layout = () => {
- {/* - + + {/* */}
@@ -103,8 +102,8 @@ const Layout = () => {
{dynamicModeOn ? ( <> - {/* - */} + + {/* */} ) : ( <> diff --git a/src/sentinel-2-explorer/components/Sentinel2DynamicModeInfo/Sentinel2DynamicModeInfo.tsx b/src/sentinel-2-explorer/components/Sentinel2DynamicModeInfo/Sentinel2DynamicModeInfo.tsx new file mode 100644 index 00000000..32d849fd --- /dev/null +++ b/src/sentinel-2-explorer/components/Sentinel2DynamicModeInfo/Sentinel2DynamicModeInfo.tsx @@ -0,0 +1,23 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DynamicModeInfo } from '@shared/components/DynamicModeInfo'; +import React from 'react'; + +export const Sentinel2DynamicModeInfo = () => { + return ( + + ); +}; From 3afec0f31f06bb69ba44c3d0d8bbe44a7e298716 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 30 Aug 2024 13:54:03 -0700 Subject: [PATCH 005/306] feat(sentinel2explorer): add RasterFunctionSelector for Sentinel2 --- .../components/Layout/Layout.tsx | 7 +- .../RasterFunctionSelectorContainer.tsx | 24 +++++ .../RasterFunctionSelector/index.ts | 16 ++++ .../thumbnails/Imagery_Agriculture.png | Bin 0 -> 8277 bytes .../thumbnails/Imagery_ColorIR.png | Bin 0 -> 7729 bytes .../thumbnails/Imagery_NDMI.png | Bin 0 -> 9143 bytes .../thumbnails/Imagery_NDVI.png | Bin 0 -> 8812 bytes .../thumbnails/Imagery_NaturalColor.png | Bin 0 -> 8360 bytes .../thumbnails/Imagery_SWIR.png | Bin 0 -> 8586 bytes .../useSentinel2RasterFunctions.tsx | 82 ++++++++++++++++++ src/shared/services/sentinel-2/config.ts | 7 ++ 11 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 src/sentinel-2-explorer/components/RasterFunctionSelector/RasterFunctionSelectorContainer.tsx create mode 100644 src/sentinel-2-explorer/components/RasterFunctionSelector/index.ts create mode 100644 src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_Agriculture.png create mode 100644 src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_ColorIR.png create mode 100644 src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_NDMI.png create mode 100644 src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_NDVI.png create mode 100644 src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_NaturalColor.png create mode 100644 src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_SWIR.png create mode 100644 src/sentinel-2-explorer/components/RasterFunctionSelector/useSentinel2RasterFunctions.tsx diff --git a/src/sentinel-2-explorer/components/Layout/Layout.tsx b/src/sentinel-2-explorer/components/Layout/Layout.tsx index 75843336..867d5f30 100644 --- a/src/sentinel-2-explorer/components/Layout/Layout.tsx +++ b/src/sentinel-2-explorer/components/Layout/Layout.tsx @@ -44,6 +44,7 @@ import { appConfig } from '@shared/config'; import { useShouldShowSecondaryControls } from '@shared/hooks/useShouldShowSecondaryControls'; import { CloudFilter } from '@shared/components/CloudFilter'; import { Sentinel2DynamicModeInfo } from '../Sentinel2DynamicModeInfo/Sentinel2DynamicModeInfo'; +import { Sentinel2RasterFunctionSelector } from '../RasterFunctionSelector'; const Layout = () => { const mode = useSelector(selectAppMode); @@ -69,8 +70,8 @@ const Layout = () => {
- {/* - */} + {/* */} +
@@ -126,7 +127,7 @@ const Layout = () => { )} - {/* */} +
diff --git a/src/sentinel-2-explorer/components/RasterFunctionSelector/RasterFunctionSelectorContainer.tsx b/src/sentinel-2-explorer/components/RasterFunctionSelector/RasterFunctionSelectorContainer.tsx new file mode 100644 index 00000000..78e2b75a --- /dev/null +++ b/src/sentinel-2-explorer/components/RasterFunctionSelector/RasterFunctionSelectorContainer.tsx @@ -0,0 +1,24 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RasterFunctionSelector } from '@shared/components/RasterFunctionSelector'; +import React from 'react'; +import { useSentinel2RasterFunctions } from './useSentinel2RasterFunctions'; + +export const RasterFunctionSelectorContainer = () => { + const data = useSentinel2RasterFunctions(); + + return ; +}; diff --git a/src/sentinel-2-explorer/components/RasterFunctionSelector/index.ts b/src/sentinel-2-explorer/components/RasterFunctionSelector/index.ts new file mode 100644 index 00000000..69a44939 --- /dev/null +++ b/src/sentinel-2-explorer/components/RasterFunctionSelector/index.ts @@ -0,0 +1,16 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { RasterFunctionSelectorContainer as Sentinel2RasterFunctionSelector } from './RasterFunctionSelectorContainer'; diff --git a/src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_Agriculture.png b/src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_Agriculture.png new file mode 100644 index 0000000000000000000000000000000000000000..7237d7f3b53b012d278b45409fe4cfe000d024d7 GIT binary patch literal 8277 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 Tl~Nf_00000NkvXXu0mjf3LBk2 literal 0 HcmV?d00001 diff --git a/src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_ColorIR.png b/src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_ColorIR.png new file mode 100644 index 0000000000000000000000000000000000000000..8793b40f6975469b8ce6e8b704fee1e0204899e5 GIT binary patch literal 7729 zcmV-19?s#3P)5O;2mzPtM>}GefTT;s{*^(nglAUw}ISCL1PLMYvUnEcY z8UX?aNMZzr62pdVz)&1nvK-qQtbt^+n=|ix?wR&jQyvbUv zRrO#0wMxVoBQM*FuHYAHFGPh{^~H197ybBiWM8zGvOoLGzfbt%_y5ejKTq`KbAFM@ z{r%6^F9!3rBZf!}0i_f|$)774f#}2Pf)uL@VWqf8Tq(szMELk*#p?IxLklTEN@4`5 zBmzMstmI#`$j|+rKc&VP2|j=i&*cP2@p1^M6bJ}J#LID4Z}ajS#-ADY?|imEY6A!~ zG2(2?xBv5hp#08lZvWX|^Uhb_;nqL-B3Z9T;~QGn5@iG;iP8*;A@!{0{XhIB!K8>Q z+dTaGw>bE#JNUlh6E|L=n^vr~5`VCyJ+Ii+1=OAnM{#M)a!;}StM4-S#3y)WYX%qh z$%cl?KBM$!G);|`2}UOnJ<$i0mZ%pN6eFDlrJ=7f^rGR%fACGd`G?;}G>V;#UH0~O z$b#Vf=_%K)ULt$@5_iKfaU(^C0R4nF$NF{5dzM7q)^1%u&kF%{`9*X{qbFPj|J|Pi?}!TY0D-4$Ip0ow8t>( z@t5EH3)(!g{q`$V`x#D4bQBn+kxEi^C0<%YFC`%G5~L4=7>S)H`iK%4A*h$8s$Upo zDkQOP*h~us<1t8qbsn8*v`Gjm;JTKwp5s(S-PBY~i}ISJ6Wn~|2JTLYJze4@q=ONs zy@B&XQzC6Jm8f#ukq~o8MAxO5W@n!S(1)3 zN?#L4f+!STMC!66wl%(Pa6%%pj8rM`3WNmb@mQR6FI`=~bn?*RWyg4LM0R0|E=v$T zB2-F55E@5YIh^%WeTZ#GUuVduY1@{AFlQEK3|)sx6m~wPj3eZ0Yt#o7#p9gywnpPP z#jz&(*!O1~-hD<|LVK+w?5yS@RYW9?7zMJE#4h4}pw$6^ClMMcK}HW&aQK%W;y-9X zkEr*D#5}O`gGaooQ!c;v8O{>J=?68+*9u zsBA+N5*1Ti<7h*N6Ck2QMoDxI(bJSi3nt$`W>L31z1U}Gug{K^h>ZbBp3n_)s*@XE6$;O2> zV&`Z&Pl`dI5Gc-kN2@Jzf6O?M96Y#B`glUIwaFrjkVym*AqCn%bQW(ti8dr82&q`i zD<+D`wr#FEsZ(pO;iYcDO`JB+U7^x{#LQ~Z=La;qr!%a(i{nc`@ zaE{4(N$?(I#N-N{ zWF%<1>WK5xbNc(+3^zCFPC8VY;D{KY_q3%O%vrncd3gJP@a%+flHofAMj(x*^^Ur! z*}ia*<>@i~pwK{PAQ(yF3Ah2$^#Rv!-lTLb?V4u3 zolv!)FYHrj#o}y2nM9aOxjgPOGKMlU_>G*?tYY)pRm>>k_$OybWiZ76+f_K z=kis~P8-@`flX4zon#ZmPT40rbUb)4Lv#T%>VYuS(c(l$Y#nPs!toSQHdxzWGf6u# z1RoGitu_)9$xIL38ltVJ&rg|t<9&utfwi2i&s?YI3I-ETe}9j9P;>UoQH&(ryyCo` zVzvjg*3xAHsd@~z`ZObr-7x5_0T_WPGGy%-iXnrTu&B@Iv}SW{gJf-uU=jj`7}QHV zsGs8lq7s-iL!}8xrdZk~o$v78;)6qKMU;Uc1;Tr>QJ?WBXD}SFzO}=o_kd+zvCKWu z!fsR)X-e9GsvN!Xkm2=998Zq9H(Me*k7@+!a7tdctPL_4rr4e$7=elgqZC34q7207 zNmNcEQx>NOqz4j~J+d$FGkWa`^3R_zy12=3XP-%Tz-+xijx^=blx%xUu|2?VWXwxR zQ(Nk-Ax1>jWR9QCnVp>xpUv=3Cd6Ra?2RczpF=!}$tj8+%V4RS1;MN5U_X9r4Rj6} zpr7<1)(hD1eLB{ZO zhhCPm+#T{a?PHEI&31pp_{<_^4T)B$98TPnOjvqtf(jBLS5b}gmKX)fOBRPS4u5cf zi3Ovte3`*J3Dx8YS1*kbLqV)(_-V_;E}3`t*iCZ!m$z8B8I9?&yfjx17C529*mIHwF1b+CA^KMrG_HwCe=ghffGOAyeu4T(u` z-Vs7XN`X)cX%s#Y=sW@Im>fQ3el(%KwZ+xX4UxlwDp#B|HJZqk_dd`0vqSD2J)zp~ zlW%U|w1N+pr0s%%Ou2n>z{!J#?d2P6_cv$-G}016hYt>+4AuFP``^FKpoL6Uf-5|BVx(1aSp`M+we*G4s3l}*#Kc>heG83py;dMa9K1mbMuEqBtI!O^gyMXQ)l2{`> z6h%TX9hGdcxyL$4qD3d3Oc+vf8sTa5i(vj^3JlKTrNgHIrBjrIs1#{dtOD)ei7_Hg zf))y*0_!<_{20fQ^W!rf>RM_A#-r{0~4;@|zq|)eMNUWix!HT%rwAv12q=*SBg6JBYEitJi&r`~( z=D3+c=h?_N5y6wC1=ck$9rq&z%0TNaK|)$&tB4@j73LNKp)?RcOCpe?Sf53nKDa}( zwuxBN-1**pM!;J@20VIp#@)y7^VVRCSfrGLoX}Z}wQRW**-IIZGLG*bB2u`L^qKtM z4>5R>%OiL=q5E*b^c&x$`@qrLzQL{c{u%vV#iI}YoS!^?$m`cGbN$vWWTNS8gFs@P z$61dLVFfpW#d}AHfe<`WfRBzS1v)jDM6H&m=y9$=I!{s*IO(WeO^_WTT7nB;8j|Q& zY}3bGSZIyIQTrA}3Phw-L?qD%w9yD5ky2ArQW(K*x<=Z~5jNq6ckZ)pTmC%>iUAMP zg#RuMxclTWuU;xB_P3ag44ls?4k{||2@2AFk8=@Gouiy|^yiKYN#jfI|F3VOrc*Ya zv^4uyx%6*-lif=@-2cY^U>WYSe`%e!e)%)>2V=UfSs?}=aGfWHK#&pfJnZt`;esbd zN16%pUIrTg>hD}~~m#XC!uW+*98P8&K77ipQ1B*W0tvS5*v?@q%}_R=qGYhPg>4PX zs$!}fMVc{NE-@{vc|%cKax_b3$Vs4BdXlbVwlQGqOP{6K+v11+_4hbB`VPH+H0I`) zKF4@t1KV`8-qCaoZQCJ)M5_cLKs+ZMR=C#@L?BNLX`T>Mfma^s12RPLjwDS{sz)T? zVn7nnD$*_;0Z;83R5UMnS^ZphIA;-7AW;~ZAiV?^&`E*_(3CTdo;*Pb#o*>e%1K37 z%vox~N7GeJ9Cm&MS4rtND^w*S8QMa`NN}VX#TsNQ_D829_i-%;rl})l$tn zMo4s$;JcI**zYvc-WKz(y}{bM*O~nG?=rjl12#Ut#Veox45RH`oc9PL(WydOjdXSu z8wkImN`oLekM|a5JDlyX0-S9yMMe-F5j-+Pl+tJ|@k-(dL=g~DUi^DkpAZqfQ0BT`hR&=Hii7%UJ(9z)~$++NL4PG-;Zm(WD|l zNk!BepGrt1L4)riPDW&A2+E*yMZmB4{}(VO#z^d1M!g=!Nf4g4Et%IfU2Sol$Hs`4 z5+5Lqih<5)O~;^)e3~JzrbD(`#||)Q41ZmfT)s47XJ>=#(i(E7N31-vY0b3k$d(inU-RMlW16NVZ=j7U-cad)Xgl0wM*Zj%BgV`& z6ZB_qG03+${rc}STi#{&-K*^X>X%sN1xaX8lVi?qpO80_Vt0)sFNj%&t1O)rgvQgv z4v)i$0Eq@85lWHu1lmF80wQ!MX%M+a8I6<@8wAb@Xaq_bWcqx~6A`+VQhB+FJYNk} zT*baw(Wjv!DK$oDx)_jg<=(i6krLYk9>fK&zk3~#;(*%`CNjMz9#BQYIVy-Kz8A6djt79cYoZ>3!P+zqZ?f0ZBn;h!-%dcdn! zw|M2(K8p>K#e+i%Gh~w?_V5{J?OoIt-e8_pxYRIOAF`aabahFH5u+4F7`hTnBN-$G zrK{{gNtz>sK#557A7u?c!ht?M-|hvyULPkm z(MiI|*xtM7e*SHAK^-v8~d^W^(Kx@1Pg~lZb*f23uJ=gxdI$$GXxe4P!#h93PfPZ<_Q;Msm9v;c5w)V7 zl}IA}v`4$BXzCeyZAi8;K#G>mI$|P;okzqDBa&56@v)4ret`jJ{R7&!g6CyvayCP3XcEcd;ImczDv@2wr_ostzM6lnI&d{ zGPX2LgDG}c%eSe;oOwG%brD}TD++kJ8I@2U9Wy&Q;#@jH&v5mv zYg~Tg6BK!YS?Co`>>@NBI#CoR$0LzWAlpa`EmDBg{-+ff0Z*h878@)zEqT93mgcm6 zO0#rGCxM7g6lKqGcyfaJ@Gf~$Vfq6apHOENnOLW4miXC}`Me}mj>Ct?q*8EYe`l5N zH40?}i^ByU9G}AeCcpk$Ut|5+1&)8`Pw452y>~97U)^JwM!Xd4UcN>$_dGj1qPqKp zjkO)F-g=X(o4YKM0RtyMMGl^w62p*mL$X$6^alf8-M_?ObBE=l`y_*mO(nT_?Fw7( zzRU3HRs4KLT%4oPxT?a2fKLP>F~lHntwVXhdSsdqd)eQ%z+eQ?Xmq0JWrDQG@WEm6 zm^>woigH@6avqsrSP1;ljIQy7{Y~oa9&Xzq=#wWJ*-NlfNn3Vk7pba>yUY6|LX+n? z)pEka(<$Xfk2l_Xm#t5~L3jH;esN0gEBn~jUt`gic-Nv`L00sT##2vg&X#AKefXHY z{x;kDyKH^_9s1*f4}SDFJh6w|y0Xi>Dyb6B=Kdy&vEa0MOfw9`qR+-?oz1JSz@W#u z>*&fU&1_C=R!ODkEv^f+A>vw~^MuwDy{C(wVsrGj303CW?lx`B;%FO zzRvzv-$tg9<%9e5HgeSGU*W8m(w;SFmyp`9N{c3%Y-f$$<#m4agAbUL$F!kg`1&Qv zHHH1;KHF=7g^T>n2Tw@14E4I={^SY$@ql!gvR-V^c)>FBXyGWAXOyRhOdj2*M`SSQ zAp+>eQ72GMz3MuMhEkO`EK+^G{n~2q>{C?`c{~s9H<~z7+^OzIDiHi+eg@ z`fP%KJfj}>@WYhXvjH-Wc&H@x{ERT`F`u4MJw8Jgk$Nuy85vfBfzA2x$%MmM$}4Z) zVYDd5k_D1ae@(rpVpYXUk=T>%O zwISqvl3u}dxkPUkWW$V1r|fU8ldoqKJ8Q%&&uQ| z%o&+J;-sNIJSRDwGYT!cqjlQN0qsuuF&y~3Woe33E6a1U3Tnqt<)krkGC9XK7I(g2 za&(3~UUEGa;0xS%8#m05Auu?B7%ioT=slcQ3`34;C6$0))9a?;sl~g94VENo@+6}yTYS?JWJ_~? zPQJ7hPO`nWk1|7^Ok0vG>mN^Ru9Vr!3y2!$8+79dihSab_Sfw6nmEfBck1<+%QpJ$kQ?APv;B z33A!8H{QV@@JT>xgNhy-CBX%xk|-e&NkWlSN6g-iIVG+#GQ>4~RODOdC?#n-k2Z>^Q)1fDxQ0cnNj6h-r|5#q&%h5U$-7WlX-AzM~0XXOHE9f^P>)u=otq!NT6w6JVivR}E5`=wj_)p!0c z_a8rD_tGv^Rnw~^TdiR_3Y@M7mKzD~!*j%kHH)()n>#za{oZHUy0{It;_3bO8A?T- z*$BG6zPi;xLg zNrLtGuER%9ONR&=#ELyJTO&lZ#d`OTw&cNk_ZS=qY@Bn30+^|9VDX^wXpcs{d94W5CVjxwH9RzX_{bb zhY1qz<0>a2HAYDm=L?Gd7CV=&ar=+|l;K&)`dY^0pFAX4wrp=*V4*F?vnA7cL%$8| z9eYSp(wi5#{_ZC*6L|WQ`^@EncmC-Y2{tmXmKc?iW+`>!XzB)q!epB0BLabnlHkGG zmS|g)G9Wa?_BtbJSzF&C9S$kGC3W3U3(wld8q2c79i3r^g0-z7$%Qp4->_WH7zBw9 zJ<8J)+O{Lh47q_+KvPvn>5;>PMnT&T^oAKM?aNy0m%DJ+u9#cr0S5^J*#`Q;FfJ0T z5ojT>aY?ETv)hlEe)D~S~BwA)jl^|?|Z#}}WvJ4=^fRc(J4KkIux05}dH0EhQ4f9aVJKX&8B z9S3AZVB0p1{Xn1uLP$SU-ubZ}6GDLWqYn`P5;*q#<2_|<(+2y;&=Jo4Qvt#e_w{xh zkQ@|E#%PuHx_`VU&$b*K+qp0Eujl~Bb{@$7-z~xGmJk9I#{t81_~_RKxOFDa zx$uT1n)@=_`x3=V4Swq9yS(%-MiiI4i~{@6^f1z!Bv0jN3~OZ09p}&miP+c%Tbpql zBh1d-B|h+InwvMh9K36iItS_b0NZa>7}gZ3)hgCVCwcM|Y7LAYNC^dxBp4b!oT!GX zKqRQp@4~=nQ(M;=tPi2C-0#$^!-xZN#})|5K{j45(`&C0J{@N9=_6czdx`1!lQfqN z>g5hDFLd?~kc5gf^57o%=|7*N@j7!LYq$$dQ83G{M>(1|D;S5{EFv&80( z(6aB+(}xU}O-98{CO>(Iq1&N-&?Xm6(^~0qS9$$W0A3 z7^Lv-O_XDgBF8o8+4TE5OE>Q^fA$o?xPw*o(yNZ>?{_&gf1K)8o7#RE-D)AZCG1fb z6c>`ug)8mC3<~CF2aA8bvcue8W@-Ci`4c$7lyuAAD zJ}!BjXex&LP?&bpB%GOH?&D(&S`~s37bh?3B(va|O)&E5%v^NiowlhRG;oF8sKF@h z^(OrTonE(3y;+9P5L?#q1%l)z=aIA^#e*_qC&Kt<$8cwI#8WwDPeP|ESifoRLdv0Vo{QK|paF2NygmePad3vEqZrK?=ksET|h*DUMFx*m*=M8@0@92cQ ztK2E>F&xUQzC1ug(nL=LDIXNMa6}=RGdURbiDc54hC;ihQZG38LP4^N60KsF$%{3< zbp9PSD`A?187LkG>VQZt#;^}^q)#{$qPbmVq_@E5q2F!b%Ox0g`&ha_Z}q_E#%#Co zCgO};2-9v1upEcRfkJJyiLp~bmVB7)F4e6DrL}F;c!bGE(sBaMxl~_ea!AU52AD`Hu{cewmq%&NFQHD6dzj-rl3JTtQCSc*jD7qCrG5Nq^|jYgOrW zhA8O>qM@M;JP2K)czNail7c)Seq@3F^3U3gh7gXK0!y5@LG2n0tV2pLf?i? zeTUga8FwBmUkQ6)GuU-nyXd42q+p)C{kCMXU9RN7bBlFm?dOXTz@l4a1DQxI{L zJ_7<{ekM;$V0r?mL6vY`rFhk5^$%`gR~kqI4U%ch!zRD-kr1kQ@C%Pj`1L>Y;6R%^xZCsWYI6mSgj0^ zBT?+|kdf};#+xPjP6kW%;7SPMF&EN|!lOrBc-=k>d4#MT;B@l5bG1gZR$%eNDKc@v z?d2X3t%ly&WojV<-Y$E$_hG1D3_3`|FxmNWTFntoGeG@%iCWn}xCbQX!gw4v-Fr5- zF1OgZVzcTxh87Ny3JW^-1e=8+l~EtPYNCt@PW?obQAegxX)|*?guVFvMfW zaHgVYPicJWOM5)yOCTZL2hc+BwcI58&1HUSwt$x=w@rnX;_!>(HNJN+%ah3<+OWg5 z!4baudj^&o!|8{(`$7R47Gs%dLjDBp9R;&zVmB?~Q%S^(0=7o^l^vXxA3LjXEOUpa zjjwRnRfd3q$77I{{+yfZ0d#Mc+(Zb?Xb|xRG5Qf~tndf10BHRQev zyW=LLx~LT-h9i)L7vUDvZ;t4#baAI-?C~D{$Fh9p^CoB8|CSTJrx22Ie++bN*#Fil zGZ8npcD_d4^~tH8*tfY!wb)L49KoXD9WQ=d7+&Pz8L-6-(z;0H`K-axwiJJ>yyjc=~dy}H8q zq>u5mg1ZEr6+hL1pzZO{_snsrr4bCLSv)^M=II2P4`on7U<^lu@@WPhgOR6;Bs7L= zBksLeV)d;(M&@1S`Y*6vzDL^oH7w;WjqbZl#Nq55JmzV0Vq(Am8B-czCv5^p1tZg< zymy)4Vgh9$OZb^QU-%+~q+R;PJebapHWQE_A5Rk;1zE3WdF`v$S$xDH)DQ97+vnJd z!o*aHgpjDLxG7bK1miuDp%j07?FPDDrGHC8Cc}slBWr|g`5E?o)GIRcANG+CUSs=P z65Fru(RpH#-O;0rpI>0@`z=!aA)EW3Kt6g1d2h(uFEl7zx<~f!9%bZj<1#`V_@=2P zQy7X1S(V7lkC6RMYArv}*)W}g&fcqUp*mx@@_BZ42Y6=!{H?$JMQ(T66ub_PxnG3I z6~27hBB);B`Q9m(ItHIT(&F*i7G?7ow`!-^tHmka)$#aG5j*ce@`45|-MdC+dXfrH z{m_+ru#ADrGcIo4>~ZktUK&5>&@5Lu{d5|IKEa^M_LU&JI~xAkFoVc<`D-6-aph)= zz5SyM8ZpdmFT;vKAemwA{34B(iBM%SXG0`sw)mX#M_ezQ%`i za#rD&pYc-;1^C{j0i)_F*)vn<6|j~i^y_^(>rM7Idqn2KbniK=U%$a;o@#RFP@h#j z&Y2S$J6F4O_Ir3HW7xwXn$O47On{};161W*K6WU;d3%*3{ntsWYeZrlu5gw_C&(vU z|Aj*JDq7-WxDy4+JNIxcP0?Gm(DfqSQjddXmi?NG>4?h5A{s(c9smgZi3EGEZqRwB ziAx@%*DA1lYY+FB%Gm{#P|0Dpst_J~ho3r|px%k1R&P;L7qIe&IeD&vscoVte#WPtq?kL4SJb%M(#R&_%$}D}509a$ z9+ZI#qfy7|Td1-h$C8;mE8~x6xqWkqQ-?y_sEz29En?vr^iGn{m`1x;LGk1;g9ASN z=rvBqlPv3x(2=JoMe@{?Lqwe+p{T@A*U6>F>1K}d{g#YBHp!?WQF?bsw^E{OUqSK* z2xvLF4M8T<|yuDJ@&q{jj4I5x@4@# zVQ{Lkc>P_k`la9K>WTwpz?moe?bB9sKRA(@loD0YzTdo}7F4@PZ{>wns#ZN1AQ;jc4i{vID%91;!LY*l?oaUZ3(_E2RR z-{BOC3mr~|Z&P*0u@#5!zO1qTx{uWzjan;(<;s!>xA=Ja9U>#ah0J9hP5m*Cm@7

WR#k%W*>~_vD@7pHUdxNTx;jOzFDupJt5J*8m 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 BjMe}E literal 0 HcmV?d00001 diff --git a/src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_NDVI.png b/src/sentinel-2-explorer/components/RasterFunctionSelector/thumbnails/Imagery_NDVI.png new file mode 100644 index 0000000000000000000000000000000000000000..535f40838d34f330401faa2b0ceebc20cc003237 GIT binary patch literal 8812 zcmV-yB9q;TP)dNN=E_Jbiq zei2^bTu;-4#jyzZCL|3&hP^o5@< z{Nh1Ayz_T;|98o$?y0s6rytMu9YhJDFeZ-0pBrC55Kb?EC<;M%)*uFAfjEwU2m}EE z5kWlFJZ&m~aQZ8V5rPmC#}Gw8EI!kX5hV#h5QxHvI6Cc1lte@^g812xKo~+C38xhT z&;HyPK@fRn4L|!j5eTAy^laSIw!&!>5rk(i`C;F245BDJ8z)8(;8~F<{#hGUP5=UO z9LG3;%U8eqFS+x^4!XWVc~&O$L`H)uk@_yP=|iTX97bB^Nzi6G zUE}Jj>ukN*XZy}0Qd6rGmvfZo9x|Ph$;J_dS)K2`C34c3#ghY+q>ChbIK6;u`YNM? znAYAVk=&qsMj)MXxv=2!g%|srsfrwJbs5`fPR1ckr%p~(NlF5oF(Y9v99% z8G&zCO4B6dy8@Jg4r^V7|FY?l_oX4JQkd5wR8aAV*fG`#iA_v0? zP+XOKLMCsBRFVp@>#?|)LGS|vx5cksSfL*eX*3;5L6THbMM|g$BN0jXTT>WBQRK|E zJfiZL?oppFU(`vMKjqAvj8RWApOG<>5++5yXTC;|tT7%q2#!Jjc#Bs32u2Rm&yUEP zAwT^734Sn#m9C&>G-AI4fj}l-W2x9iFl^Q?TihLZBnnxgxQ2VD%j26*`2Bx-h0gJq z_ddu{+uR`>FOW!%8QCND?u0aZ1N5177ME9$QZY~7+QMzygj${AnGx5&coxf$dGgL( zav7ToUoh#`2Dq+DHmMUtE@I%L`!RDVkxOSP%xV@feWEC0IC9BY3iX41(o+fMQU>mp zLhq?fXpD%G0+ddt^XWR(!)TIKy^LuG}O_QU?dFIcp<2gEZKcKl85KjtRyx3-X<|zip zWc4mfYckhgyNnRcP`o@tXIJ5S-|X_G&l#*H0e_T2lgi|B zK68sB%uF9OXLIfIXIXvW47(4TeCxkI;&}6ja|;dfr9MHZ(Chg;+-gxQs4N#P7Sjgv zd6ijB#EDu6h{%43scFczPuWVK%LZ1QCT@suGR1gHChCYE?Wyv!RhQ}IA^vcSO18|T zs<*a-|tDU5L1|zt5YyGIx&iNTN+;>JheY(AhX0*^uA(}AhpHR*#Fm@x;24f zMx#`auo5yw1*||IVMsW5=%Nhx2$F!NN*o>ZF%5%cMjKkpeNfLD?#2W`6?a1=! z?ig`#;Sq5(0l&+R{1Dr>&=v!XY?im*wt0KEfR|0MkUu00a%?^6U=%W3`ezrJj7Nl? zO8?m78()9Q{K7S&z{JcsM43H;p+e+r5{{2Zi4_JX3Tu~ABp1d^jbfVP9(#{%8uzLU z25<9kz7`?aNtTrfpZPTW#h=!B_*Xh=(!?yP*bfJh`mEDMxZrr6uB5f1xg zER~t{41*^wNvp{GCykGGdFcU~xu3|VN^w|M_@lHrrS|*vE zAdCh~SJHgjk4D&|0*YB87Hu@uW_`NOtkA@a6)G76S&s>ni221Vz1jrP69|-; zDDW9{$4H9IiS2P~uSZ{Y5Ysa4ARtma@=IB|`!4oaBEPJH{!anrbQ2K;m`_L?ZMB%1 z5&7zC1M2O7jhf2I);cGbN{^&jhc&VWH=g}Md-Kax_u_j0CQ1gY**O0wFgdz z-~abna+5w+er2BZ>La#=Wga#q=8A2S1BD-dN1`=}sV-EpavkibjclGE2wl8!jch8W zR7tV-WJLRL%<5%}zBfkjB4$cDk{>gPMf{P^Vll(m4e7fff+lisWK+oK=%@t1kR#?_ ztZ?(kH3skaT>s*!35o(B3Nab6$6QWgUN^azP4bC_0kW#9GE>8H;#VS)u!2SnqJU_)7Z;#m7 zGKs_tk*1*J1=4c{#kmyK#S%r!ppX+ORu#^@kfuDR(yqH4-1P{W0+&jQtj(kuxekE< zDP2X@1d_UnDhWhxNbA_a?~lkDGFIAz8G(h*NEpi>`XkSdF@hkVI4+qWg?wC~{lsP% z_OWIpOyNGST+}$3sLZ|4=bJwe8Mmu=^+R61+Gn`fWhR|K9K-DgSx!0-rQSd^Ok{sT zb}l5JPt!dfp{{yl(+-lW(B96|b&j!SlT^>kD9MmWQZaLVoI{IPT;)%1OO#%A@rOg& zL!BSoQn~ZNgifR}89z_!?jO1G%KksOw+{E(P5pqK%kUQG*9WU4;leTe!9!|eZjwENQE|tXz?`#e5q%uc?fG|1Xa4#lhRB&Sl z&#SY`1^)8~H%UY>TQ^NwCnl3{%;LpolTqgH@0 zm*DZEZO*UffIiuYgqin|(>;<_PUW`tF2mStXe(6Am0o8H>SjkC(5TTwl>S?%F7ULi@l*H&qfT7>6OUGr}|y z>~Hkg`bmeCl>*i4Rhq3XvKp~;brEoA4tvOs&ZHxuNGhh~eS}_#!ZTppKS2kpA7i&ddQ25bu9fp zzUQOqGkDH<_UaYxKR#mp+$?q{Aa)##IAkWDLhvWV!jM3-kW!GSEFkJ>hC93TdhehW z6l7B;%8XH~J?!KIx^9G1J4+&X0m094wE2LO!?&3^7xK!dvYb0>aj@OQC!ml}=(l>P zx{o6`s437S2~m-W6^%%O^Dj&j=@D{HCOc(d!1CH$q7YqA7*H%GJ_4%b z4^2_^b;7Yjn3b{H4x%uj9|%OU%;N)>NJ?_O3Y+W;3sd7dEAs(dL@e#=^VUk zQQLBG#sNxaM0T!FXxDPQb0&KEVT!I;)KX16uQwhL&9Wn8a;n%E|v+9R6`2_zR;2*|2a6r>53CvjmxXU`5e zQ$5egD9c;_W0&sz1b&!D8c7U=I>luR)sk`gd3?vj?}IrN5o>Mox`FKqjGGQt)?{uu z&p4Vel}`W`?x=@o#u$N2C7lPxG~ON3y?Kps-zU*I;qttItb|NzGEpQ_IIq!-9eS~g z8;AH4*g5JW%Q7M1N5>cuAPNP>Ck|8j9Fj6bs`z~8af@%=++*`TY;0TDsUo%%@%ral z{Qf`DNq7mSvU6loAqWmK0#n%{%asJOze~Z~z?7OOwu~?uVyZ1xmJ|#JlunMl?+3Ks z%#saDoLf}*;+GZLjZL21fshIl+XknFn)X2z=(dhf5?~byq^B%2OK0NBMDaBELlSh+ zCj!p?nC78Jedq!Oy6Ws;x z;DwhYoRh!d@)?s!&!%F@2xFB{RFSO|mek@)pPr`kMwi3>Eyyg8GmltW3Hav4M+#*cbvOyP|~_HH*=yMB#G+GJ4Q zXL@ZKF|sKpcgc!v#KH{1xW#b4i9MxZ3oUl;w#hFE>+neG#p%jZ8s z`m6r{I&bnf|L=Es8JWzg1^C?F@6wGENCA|@6k{!-)0;4~Bd*Tnp21ijPGJcumvi(* z55e_mMlqK!W(n#cks}iv+8owPboDuok2YD7rYYu+uvCR)2=>TjD70ua2RMOC_Dq3{ z+9t|o2<$r3MVUgvWLh%tdot5gNup>UBhx39&l1qaZE1+!BK>+DNs%aDcnLv_*njv2 z$r-3VcZQ>fx2SDzP+GKT?K$KNk6D--^5v`W;!=VK?>`~113vM51w-`ErI<6HvdAo} z1oY88oy4Tf$*mNZF1?QHj5zwUza-M1kZ>1h)Ou7`Jwgw1y$50t?d#0s=(Ap^)ygY) z$;9y(H9T6wh`Q^Mp9|<|0fmnTm52p|(4^7rvNH(K6agik&};hCPC}OFG|uLFNVdXo z*CUyXxHz9cuw}Y^A6JRdFyX|8jL6)QiRtw5$3tQzP2cgUHDeClJ}Waa){%B$?$ zsL}apo#L4x>OzkfS88N(4RSL%GFG1(Sk#Xc0>k9s-40fvOu!zQoW!+XsSrmQgsy;# z#1HpRkeHBm1!_V-IB_`k$MoC@^kXhB7RjGWqosP7i#i|N`a0vzgtN;AdO;zR_&jEL z8e6!5s@W9glR$(TDdhB!%&gAQ{X?GajTkEvzOeSpl>>;Pn8+E^X-udMB2r}$-4Us^ z6$W;Qu{Gg0UaOKShHOqkYD(0itsY9>Zh`g<2{Sd~E1%1;J8%$#0kwk~4{~!XoWG1?@6&#g zLl9L~pUcoaYEa6K7-|apodn;1SZ22|p_p?B-H23jNJ@$c;xvaXpXqdfQ5aA)2B6yH zr;4N&1Lm()z!7pMcWhUk;FD)XqCn)}eg|fg*+QcD7Tva%_-yl>29B+W0iAfc6 z+<0)r{`L$tWffWNP)xd9y#lXJm&iCVx|O1v5()hW%)OE&N{tBtr#?UtB6jzNJlK$M z#1>Q2NsjiD2+0O!KEcInm-)-PZ}8WDx{X$x!c4x1zzlD03|U)?n4GjQbPZW9FbXUT zX+$Q`rDSLn936euCw3;3Dmv-KklXJZa_!eH6Nz`hxzEzY8J@OXZr*jNHG6#anuV#U zXvqj|1W)#CghlORi1afs;q8qEo}glgK0!OAC)%WCk?TuE=>65OMDar!^H$vBB z#*U3qNn#m3k`t4gmC)&i(oodc8LKe1mb; zh1ej|R8U2M(PEXSw>+-CGRv)7kJ$Zb$Y8urqCCyb8y)&5Ni@?!IO%fa`~blTC!WNxjxU%d33ye3gf03-{J-yvAwVp(uzr zhj91LUCQ$*LT`gWmWX79iDT0n_sNwC>~1xPRFAc*CXWwV>}<&Rb{aY!UOT2c9#g$~ zjVK)83pa_8P=zUGrZn{4k4Y!GynH#$=N2O@NhjqcsN|=4@B3S%vj2!yv&Wm?tPvkR<#Ecx$y1=-f#8Bq> zpud6L8?rRxkSdA1RLwH0t4Ov+#(kdl#tF^IO;%quk*%1+ZJXm3#Pv1}+v4UXvnE)$}=-4o`4?TA>}l?5ld(45R zFmJrZ;%mRbxBlcCy!70^X5wz}_||`=Y}x1&8L?-PvMgjbCaZ-M#XS3WCZ`uiq-H>y z_OL{FWg+u(ap6$lvrAWyjR?bWSue%JaUUaAD41C~{Q+jC#K}RDT6oO$mjpar;9%IN zaXg{AWRh7BdHAC~$45tyQ3#JCs`IcmZLlJw89f*?ydR>aTsqw`XRlqsTJCZ>gq2XxtS;575;L=}Q``JcL~AaJ95P=_U}0hJ$GrH(S=`)&gZ&}-7!qTjMT3v4y|95-!-LO`Zk;D_&2X$KWjXH!_yQ}pUbGiQDdLY}=r3pHy}sOFf20kP93zcLF_g5v{?&=K$>jp0Z|bcf70 zTyja5xoL&+Y@6xV0wgiV_P1}M#c*=t&roB7^(()@!Or(k=0_~O1e4?l*)XuI41>b~ zO^#7SlYVUR_}va?mJ_60jeO?x^df!jYk&B)SnyFw{}Fx7CAYfBcuS)5zJ}-MtgcPd z*s^*0?wHb4NOpPvl1gYaNhC#lH=vlkOzlYq-!0HTZlSt4gfWz+L=#O)0ICb*J&1F@hEHU_V96uFyS+aM}UkC?a9nbbH54#sWbMaok{LIfbDb zY(L&&>+U{A`gt1NWuEMp=!?rZ<}9^A3c3y#W_+ZyKp+bACL_wrDFn%9^T)?L_rh;t zUimT)Z+;zp`2m%eJbH#loK@(HBhs@8#AJl33Ji$ZKb|nE2h3?XQn}wo5S0&IIf2NP zkTjd#!y|&;MHmgBm1XODK3=F%J$uZ|8I_YKJLIRP$joFpsdvfb3e=7sbN$o5MYi}a zIC}pZBxRq65C4jbb1@yKN8fQdvo7O`Jqo=Nj=O`QR@ixKhvBip^d}OWbOR=X3c2Yn zk=|y!SE5w5NzdwRKJ22X0&3NQphe)As6nVN46Z--BoJJ4|M4m!Ev4j@& z>1+)NhCYH7;*4WfW}$js;hfv#_FMmf^v>s*xhS#ti5y+e#>e8MHA0kQ0(C%DO9BdK zk_OpVz-x~Qo4ubqNCS;Q>sY%aM4gj63)#jpGpWOvuz{OSpHB7(H1aT$o0$ zcJOP996xHHgfHO7O*ZfT0lB3lzLI8P^&)O>gZ4>-#nn8b9a7sGFf(6B-LwUtq8| zA{2MYEEtT(DaOYGyvlvl^&U_9DZCAXgt3pAh!LX{ZtjE&7Z+)7XJ~BxZ!+im^iD!@ zixnCN$H;=m#pyf{6A&O{AR2`H`w$A1~pQY4`oL%^J+TJ0D`#+@Lzr~r)_6Sq(@aZ8}tLNx%&5%s{ zv~KMp43?-aoMmh8f3ftb6rnyqa1t2#5KmMXJZ++6o7m%*Na{X=mcxw)MW$C)DbIg^ zRaBV_J%$aHV04}z-P|NlVBFv1$~Bq!;wCX|n(-1X`DNOV;ag3YMCKA1{|r;YA5$`R znY2boy2z~Q)AHV7`Dzj`TSqMF9JnWR1BZMrK|Auviwb-70Z}k!Wu`KY|XOPo5?1LtSxpN$DzQ@c9K694}M7B#}T4yD7fs5vEp{`W$IyUKk0VRw zuoYVa#wv7pwD%1%_E-7*@BI;zyYJ&NAUh=xGomqc=xmHg zMCVw3c?AR$)fy4|V=lb(8o~CrIBp&hp;7O6w7es<*#X(abKh4%^#*K}8UmQv0lB4CVq2|J@NM^$hA{k;f1A zsJxsZHWP@x$0!JxD(WN=7!N}nCnA>w9VntA1)~LreYF^~xmzVh98*kIt_%?d?%M6-hW-kad?%l&O zkEu7C6bmT^p~$w=qP%R;nA~BfejBAEGSnyBc)G{!?Xw(=ze14NN0wd0e#mg>BIquG z4V`g-kf@Q8Zj&vk>}`LFeDN%@nZb?(%&g4b(+{|?>TtGvfbI3!Z{;~Wxy1DBCUNa1 zbDG90^A#4gCZ_LkWj0G$HR#nmB&$OAp}^te0ZvwDY&AIh`ZQx9;;_+29K`hE2{WZU zaisA6tvxh3;?kvQY$xKqdv)ULryo!_W$DL6fdjD!!YN@~5MxB~bg7Ku7*RasrN<$} z@hQPjk`&@7K3!0S0EAOwco@Y9B0S@cp9Wt<;ThRKCX66LB!0&JmxNPqBZ^KbW~cFn z5K)LgfGCmxkthx!7Jf!8;~8OHfEbZD77>I9fe7(KLO39bKjWN>!ZUJu{Osnl_^u!d z#BuyA-Ya~>p%22Jz59pARTRfa;wi~o_%O}km(N}J1@iaI8TdtNAAgFU&yx6grC&B{ i;_sWz@OR5;um2y&fK%OBOMA@#00004!r}ukqo4I!i$r+9$P@+{WTP1Le*pA^v77nt=E}Q%(K^EB< z$RdGbAa;~QmIEbq^1yeJR_0R#a-5GeBeWuui~!gJ}0)%TBf{z9PQ`4pbND2klo`NHLSjwlM`dBKY< z6a)}O@p-q!YV1#}#tRt)K_Jg_07Ov&R{a)54h0CJxN2796nU}w{d^!n5T6UQ>NCqS z@=u(Tq9_oA=Ry@P6ZjgxZtv~$$RW*=b9PG#s zR=cT)XjYl^^(~St<>GjsBF|o)E?HI(M2X&JAK!D?``{hAn>!?NgeZ#WhKV3bKuVJO zP=ts;mZzv@728soI5WDf2HtXs>v(K7n~;r}O_um!fLOrpja!In8OyFBC^GFvmCc=7 zOizx<;tbzeGCDs-ktNJhjq{@?7-osB>$j=Z+x+;)f6dY1A-6yM2HnjaDy<%I=%c6x zi^+h%^-*M*S>W;d?QND>!pUfc6Na3ej@WKCNYa$q+{ZL6n)Nba9Fxm19?f3f8u`U_ z&~2M6P4JgX%JnME?duFr4=M7DB#!8K2aF60OBMm?>2d`cZ{M) zyf7W|i<2b@638IQ3clxH$93{Nr`6p;6h&^_`5d}s^WYahAqoO6j-L?)0j5>u;Ne{~ z(_nMwRW!pu)ine~AX78+LPVAY(kvxQBbwblcRsbt2M_k>wwg5C>-2BDime!2jz$!D zhNc?~PtFO#1xLpxWO>GBbqz(;nGDWIqKMh(oaKCsYFNl>jlg$_;*7U`y2sIEf>kkC@3k=36aqIw5)@3! zU^t$mmvrJR1w=GUf2oVb7n*eI#%-FNbtFYYH!Li>LYBt3izxtc909O>{TAETZ_(;) z5k(%ND6+rzgp>Wp1fEN|(nM0!)ut5MY6w74pwa4XaO2fGwCgou z*GHEWRI7?8t@ifhctpEeB})@R*QH#kAj=|lrG~qha(48TgNN_aU%$dX`h$N-5=Yb; zEw-=SUKztYC-fYm$U{+e8trvV!(u)evslhZ3P^;4EQ8rHz=;yvzm?qW)k1h|VSS(>2e8k%XWtaqAYY9_e?ih7@%t^ z)49uX=@SMqrlq~~4pur)6qrwjOh=ahSk($?o*;-a2sxrCBFi#}B9M_p0nJVivs5K? z9d2IV1>oX%pYG-k?d@F(B$Ob)l1w~jjysK;*KJ@FkW!GQDb=dY z#CMs@oL?H%mFpvLwD%#)$&z}tj#NlwZh|6cZ0+2k)7vKT6Xp-@QLZ&{mJZ|dW6HG# zwbnX$o+1k}7pKoqRgEG@8J(ZtEG9G?O~?yG)g(hC4t$~@!ZuWdG(nVQtWt%`^ApYo z6P!#$HWe08gfHZXs>0F>h@1qal;Jo_ywpJwp|!S+APQ`6c38iD6K^p?k|peVjo`$k zw|NzJG3A4M4_LT9k}R=3amk7lK@xa+c*)TsK$B$}^)j-i5Jee&oZ#mfRoSL&8%*a* zWI-fJGa4lu&DK6{M2aGZFs3Z1)N4&LHvv_qT<#KtJ_ipT(z&ujxv_!2Tri#vNmU88 z+F%)YRH|Kq=?uHvMpr6$3kOM-s5aU-SwQG439}H{G&nze#x$Jr`5RX$i2|ef9IH`b z6lA1k30qOQoGf|fjCd91zqr(pn zBmqB~QLeQyl@h5NP|*#pudlOoJcg4QhN080mbu#PF!GK`A`r5aJS>nEiTTo_cC}AX z)4+$4Xb`0diI-q08q>4o$0L!V0Hac&y3s@yWq$hB_b@sQ`d4?koE&jEIAQ$$4|wI) z4O-m|{^oCg2obc)?@?{mX~i*#=b}gov5=!{22t!WpHF%6@aMdJ_d@_$-5O7i&qzd( zqNI?d0DH1snyUV8}b~xQpYw7M$;LJEYYl$F$|r0t3$ahAleEigE0#) ze@0Z}Lu1`$b?U~4+l=^Q7Hei;!JMNt%4%J=^5zoKkakrkOI(>*SV zkilR|A&YG6cDZ-|kh|~hgCwAfGN0PrV05vdzuqKreAe1cZe819=f-DnW;6c$JAXl@ zi0mJp@$6(kr{6>rVd8i+nq@XSRSGv@;(17_f??=*Va#yq;(G~=^$P8VL95XC{Q3@7 zu0x*k-oZmk?Gh)`32Ss7KM zXW{rLl8j8^Q(PMsm_dZKMVr{*L;|7fVglUqaL>Y#ynuim=O`DjKiz+~a$S!I+|Vx!k& z=>=p_il-~w*zAAY3Kw}!OoUi6$dV9SvAAY+FxnEjsInne+0w4?&;PN+`DcerlZbIt z5Fp~FIVdtERi)Ca^VVArktCUxCb56)FfYKWR2a+x3OA(Iu2S9Uar*3xQpE;QCIs}7 zMwTQj$4k7?jQ(bW^;(&YtzFvfEyj-@GMS#z?)NDNRjO8=fJ3=+$bR4aYorbjp*xQ`WcJWKqW6 z!8vy?=CqnN8|^xm<2k2C6Q<`2rc)P9)A+&ne?iGMu{9ZN4bcyA91lUvNgM}f?y}Y@ z(QDdxqXlsoAZ0147b1lrZQG<Q|P>7QdNs=i196!oXa+!tW zA(xg^N+va1#kLfpIAOAMK5m7xEaRs?`8lR#;Ke=}T{#RXU2v$=pLL$z1Pk^6=Ra{mlxEhK-XXy!rW8`0n@Kkkg zqpqt27ZcQq$!@ns!?u_&mxv{cFZ}*jsnk2{-+#h<>Cvs_Ac}~(h@z`hDiuOK#4~&* z!&8c=KvfjdB0;fCltdtK0@5f!mL;OlC(RPdwI=h)kh6;k!|4pu&=3@hzyt!1!di!*|6yuO&Yq%*?2_LD$%P~2(l1=x@3Rv6iLWv*9>+x znv7=-w{Nb~S4@8K?lW#|_4xAVzCm6@w5lCMIcGi|)BnZ|LU+Pse9n9_#87ks&7)+R zWUdEAjBY77(+R3%qv;Y!xCBztG@&R8WLaXqT%t%ak|=U`c*w%@XxD2$k>Uk-Sb$srX_oVNIA@WM4<{e{sKj@$cltKg#rOtQSqXjrJGQxsyIs@FHX@E9YItGLl2ph zQq@EfB8Jl`hOVK=60#j)>pFs<;v_yvl#!$vf+Qe`zXX~7Z2(zTK+p2MK zGD0YsT&p*@^~zQBLPemU*Q#+a9`HBs{+Q3b_9={5N6Da8>)>BbNQI2WVt}kD$fYt$ zCJ=-Pjg4)3l23#O;tF_3BFICDT#AXZj4aG>lb9&Y$TLW?2+vK(BnjPA31=RrVbbdM z&~=-7yUTQPNuK9)$`;e9%kf}lJPl|XRh(r&F2e542E*~3i{TVqQ>m=kq+vk6StnTr zeC5t9TGcvIAztVW66_AcY|0|Ft( zPz*e0i88xHDH~*Dh=uf0A1SJeB+3+SN}A?)u}2aYXu5)J>qKFK7X@5d>(K45v9o)P zQmKSLUvPACN|qKpJwC(r0_qi;tGynbhCr5NBx#N;h%8)>X081369@uAno%uVTwCk# z?!$d1gE=>DtfT7+hwmKW#1V~p4cS}4TI1ZOr^VzYk$9eCC<>}1QErq7(tzb6pePcW z6`Q^DIit%dEwhf4<`hvtVJ3`5hdg|;kK;L*hD7K&q`64s&oMNWMzw<^%9N{3jt5UT zKHozuM3##&XpkljkYkoA_|5`Jk&r}%SdNI}j3~>Y5K&~6<{BXVSwv}oKNSdYWNb)dX7lLo3CGGtKC3K zGfJAqv-1Ilrm%j!$N79tCW%ZIE`^sMSO$*|E*K8y^fnrxhb#asdyX9@DZcT2>9aR^f7b%!j{t%B$DcdHv1~-qa=WT(UxB8BD1)UZK_91Vbh| zj2O>`?B2LVvwMZ(5ASh&`UG23uu2w5^n5y5j_3O%afm2`v+&qj>(S|S2m*)G;TTKT zFg2aS%K`hN34)*@s{%P9h%%}!)2vpoD<$qfIp*hgKV-Ar@6uW0_?f?oAlY96t5w9cGD7yHzFgG8QwJ zs;1E{mvJT@ahPD}I*KJhMMqg{kW5BgOvZfu>))i)*&toUEJhbh7ekCn3Av;)nqPA8 z{s-KBcAtuEB1jVLW*_Nl%EO=i2u;Wl#S(!xM^QCA*JpAuLJ$S=ET`S7)9G}ORE3M< z8Lk)7td&tziAu?4ymUzbF%wsbFGV8HbCN7!xp0tFfnK*xkVYJwt~?OUQcwitn23mUYwOfYP2xBt&vWu3MGz(OI3dbX+#n!I)0evH{{2Th z+CRm%bYw{;3KL94r(_vuk_355631k5hAcvsWRU07YzsveF?5Za*VY-%=XgQHMz@3T zJpC%Z$ibdoOo+VzKb%vq_4v&^HhHX-9w5YC?`R*_7^T$7WoB!~=A96H1pj5FryBw`> z+Bhe4mSk~)s_Qu6j6{h!TP(0D23{88F2^W^gl$%7Hr61|$)Xs;EE7jAv)LTi4OloH ziXfw@DueNe%kh*TSY-|C6&rwA=#j)3MVcVwDY`0BG9`qpAdMr^Fr=jGXr@BBT4LA*nfW6qBPM z3o`!9C-P#lyg(F1n$0pN!!b_}FR0iSw|gzlE=HJ|&hGX)qtTd!<0D7~lbJ)DMyRqv z8igpbM4m<{l1{tP=JaC1!>1>-o8{Gsk694{UR>c-^1Z+?YgqLb=hFp_=W*l4XZZT< zSGj7Jd9B~WQY2besGbmWV@-ieeLqFbW)yL_|?U5kxH0qSwlq z&XzoRcF5K3K8;$LLP(e`Jep-2QIR=39WYyXY^}A4f|zILiQa5vqZJhrqSqe{_Fw0wOvfzAVt{+#up&FeXY@#4lAx#(n`_ONi}xIm zUM-*~2;6&k$TxmtjiooIS+{A{O_C_#_N^VFwBVg5`rlnAbB8xQPox?-M((r`M9=`QsPEQZ1+I7-2#n5YjLKr3Jnt?N&5CyBr%d(6ph~xs~ zhDcNZnS|px-1~43ThsBQ5L?qZyPTkFDnXJk9L>q{RmeA8x~teZU!`-kLZ;cYu?>@j zyCO>YVe}FY6nXB(hy@&<4p`2{=&C^&IK1`tTm0Fd{&$i%%<;OjX*{P~Zl_cwUu#;XK@LlpUx zTOHQ6uJirx{ssT-+u!E=XpdUG!@Ga|9U^zewQDzUmkTmKT#2k|h`I`L&ZGTPlF)yd z{MSsIQpp5B*HpIF+GwgmmS#LXKBH2y`0VR9={0L4Lc!yMA(E<~n>t~b5+*r0;)+jG zKolovs)`~@xPHKRwj{~Yk6&Ry5U@)YgUOuXY{AB8%$3bG95>*}!8z51Pb}sv{0KK% zA+q}i=S1^>exuH5;xJBPgo?q@bWU}d(ATQmtgZ8dXAju8zRly)33XcoM*R8r|1-b! z)!!s?hwMLikAD9e{WreI?}iC~^__1Mx-MB1@};l-4j}XN!}qv;>kidM8%fr|t`l9J z(`av#=5IY$@+NtnqnRdo0<*y>sj#|WQJfG&5lNB}g$bwU1DdrmrD_Q$PB}ZDkO>8* zqM(^NNs*IetJ9TaDNDyAieqL=7fsh_R?8ocMDn~q5Cyh+EiMKVe*XSrp6u;&ZL80} z_=B(0>(&ru5nWSgcPn(eRc`I9ap%>m_-Vp$xI~jAl9@+MQ&1EMk{r9F@y4fjkdu(j zZk^Hbg!}LB^H)E;&(e3;-hCab-rz64`^O}w$J9Grc5h$j%IkNKG#z_=hl9s=Nuq#P zZoNsZ(?P15xKBS|etCka8PrP}hG~-L8P#^1$oH9!ro>)GvW$8E?o%E-JSNUkY)dE4 zbKbuHn0FpM#q~ovttLn!LMBkE+O!%q$VBo?M3oeDMPcsv491Jqf)yZ(;!Aqliuoge zUe@@@)2CQ8fzQ7eCo~>6j{PHG+fuGtf`opOqeGGaflQZ*prC$RN}L( z4w@|E*K-ci5#=IhOI5k9D>Mp`mQ|utZ;(VWM!CUjZ~O*BXThKR$-g0tT&~=@L%CHY zP6Lv~61&l)+^i$XDQGDOOXe5%5W@kox8Pd0hDeO8=PZ{)mj0YLPN>Nm@ghXZV7=Q! zmsC298e3aEK6Cp9OE2R1Vu&22+*oTJU{PD!PD2g~?NaRKIU)w}*GA_=B?Eh%Mo!4emYZfF{=7@6V@ zNfbyknNFuh5@%FO7QJSTJkK#IHdDuCI9bxF)abXHR4fZkR~XNhY-7nd`JuEX|vo0?(qO0P%fPK7`H z&X1TZJq|A?C?$pF%@)gN1xXY#@&g7lmvlTw$`XWXLi20gmD?~{P_hluB;(}iV-Bhv zG}Ywye&-MIy*cGt6IIn%Oh%mCdk3prrnhkoQ3OR*R_q_uLNzN~oE{+-BS1tdWK6>* ztEU8hOyIdVen_WN!?w!YJ${U->4>7p#dt564_MOL9P!B}7R?5(P9teW?S56-&P$iNlqDBm9z3Ev%^I&sSSOl2%0REH5aY z*Arh5%%79Oi=uc=&=y{32UJBN%d(fdRumvhB7z_w2rr%jFL?V!p1k$pdQlWB9shzb4!=waJtvLxQF%WyhnKwbzeDf;CCOYUJ~FF+DAoTT yT7(w@|6hdqhm}wMFW@8ZieFvp|7Urz;Qt2~!;H<@kPJ-#0000vdPAD89%!vP=W=UGOc=j8ce8AU3O z$HT)hKN@dNepvruc|{Qjp&Yg+1pI~|b8^TJ=PM8(IGp3x#P_Iu@*G|2hXTyWNwS1L zd;d@ICL>(SW@UDPPJfHhxJRShAkARtdst?frMV-NO4EFC`wE^nLX`qPPO#hpS(+nr z6C*V^UOB=_eUYDD{t00|;^nh1kVXNTlJi$T|F6tU&to|bZ~yXbogH{4CJ#7L%WGfo;l*F^jTc^FJm}LN z^tpBaF48eLIOtNIsZpPuL+Uy!v&-xpZ1Leo7kJ~F-$FyO)7eASOiafk&7oRoA%%(; zL=3$S2^r;ri)BlMNYONvSgQ2;Jv`5Qtk}vUwJNz~G}XklO9%vk9}}e+VVp4ZJ>oQD z;C}ZNM zvG_Nz#~#?IMNBz>mqvuniMc7i4)vn2hj(7*FldDlO8i)<}{N*D0W> zDpEjuZ=bWrpJ2CNWWO|^S)1X5FD`Io@iUsEmdaT(f|t2vl9rAM|Mt52%)Ej-Ar+(q*DL@T{m}ucG>}3?*Gx>IVyY_r;Q=`js#NKYIy~5Zz_F>**h+!1 z*JW!zWWF|q?KlVp*0=9aDb}dfJ>od#$ih)(YO`pHPT8vP`7b}<$cZDQ!5G~eqr^V1 zoqL71SANNQ7=R}E@Z$SSqJY`OMYg*;cu9m}Xbgru3T_ci5oiUM{>VooCyQfl-M_^` zYk^#WC=GEOmxen{l7tLK9dtCdJDX&Aj;cv|gCWE5ghru`L{cwS9vdO`k&UrUn>0;{ z;)pa&aLp3AqN3^!LFi-Z5~(UANrDs^8(TM+Z7xu8D@aukB_3HAaM0hSP%4t73AeA` zL#04TFHkR(G00foUq=eiEuGHr06nWxFVA2aE@rIYM>(!#qe+`l*9SkPRw{sNL!<6yT@X<2_J) zWXywbOq!?^EQg|9!nI4JxxvRbZqO)~5P%2++PzJpU_=yo6da4y)=82yWE6~1g^rmUKyG-~>mn)xK~mypnH-%g z)3Njdj;^C1h~o%93?I8(>LVjkt2PhyT1uqU8H{|qaKy@V3ojhg9d)sEiE9_hEt|Eq zTU_|)bLzzf9MeJ-I-~9md+i?GwTL)PnVO&C`>bF2f^*9a{_Quu$N%`N|IFO;FEh$RJdv=ucaQDP2IFCu`a+G9 zudH(Z<<}St2ORV|i09zS=T~VTj2R4lq?8x}G^ybS0ml}fV7YaY&AoMQ-nxQ$OhXe6 zx~>u?K8T!ZsZ5$HB#Fci0}gsyScZavNf18@7#<2xAc&HLAWkSvHHl*la)p8+ux$-H z*64W(BX5ACn@p8!M3V&D=+No+xzfJLLUWekFy;8vGc3Cb=TE*#!KqWL6#3C-|3uT7 zLstbwtHtL27j(KCIF`cJ_C8fvBAx^&x`t-xY#lscyx-ybKlnbUD|H&L{{foga*z+W zcCf}LcfVvZ9`foN&meV!YGn$Mt6aKrjm>*|y!OWH{O||=ioaKXM6W+2hysKzC^<7o zyUAeKMU^qfT18YcCY=tO;T{Fop;jtk87949o1s7E%P;RB6b(HOn44`f*IdEYi-%tJ z*CUbLc9&YEK@z2mM?Q;$d zp<-3=MhbbRGiZ<554H*79wXnRBEQmuh(iL(|catdGZ(9gE&@NZB;0RqJG4%p~;if)LZRQNoBURj4*=ICcpU zANt0Y-o(({!-(hW0vrr|szpf{g69W>S&pIET)TIj;V{543dCtdZ#-h`4{>c9#W2WG zDLZwNEF;YXaW0X%U=WT;V!^GupOQ$AL8#K6Y|$UINz)#-Ris*WXwK^ht;HnpDBA_% zagO7-IOQS-g8|ppu9GA_`@=T4E}wsSpG?!~_kDanK(h^Qe101tRpyT@(I5Abih-e- zoIY}jAR03q4=LJJyvc-Sp~hls36ev{UW$w`$#JYQCG!qf4 zG>g-ix=C+5;ZX#y<_}}e)wyF7-4eawkljv)NqB{=?M*Dr!O#sPf=rrpy@)UkP~(U& z?$XUd(kMd#6rrIhCQ=yKW``(>=!83{ddl|B7D3?S2P0aQGI5%*w!4FBSnT%hv%h;E zMKS3QBXrlKwOC`4j=0|$g07<#%Z$>HiIUSgvB1qQ?^0_uIC5s0FpSvQ*ys9%8(j9U z(^{G1*okADJ$i;}se$x{j6IL7bc2~ni(1iS=U@vI6-7zOvJA_%iL#viUWYWzuyh;S zED@zK)j|y&hl*Xr&@JMu|CJ*Rpa{V;$Dii<{oB0v^IvlQ_1DNmj^XGOR1L#1@nXp! z7;<89mCoP*RpdxTA~*Q#%)T?3bWJG41*DI?z1)AXU1re zNrCHHc)^%7O_5T;)@??Sk8QbV(nJv|c?7B^h_evQ%CK#RC3~5oX%hq!T*o9F_j%>T zm-xHC`CH!o@&9D?*%js*vsikGwVge7w|9t>gm>P3k7v)WqFP1D)oHxZgt6~2=yiGN z)fZ5|rBSpCNNIAaHjk#6kV$%@F%y4`uA5W}MXH4=W8Wu9GA!LfR&)$q1(EV#ug9lX zF7m>2MRIbg#R~nAkEuz*IKb2tEJH_%gwbS=R;`7pe+94|T44dI3g^$g!05QgL4OA? z9+Tw(N$OL`Y{(9cc9LZns*0^+Ot0m$vz)%GDwy%-p2@BKn2qEcuIYU2s44FMPB8_r|{eykdET&j2 zkOdh*6yoXyQev`Ppi2eUun2>Y!DPswKP2-rj?SN8%4(np9Y=TQ#~{r*DG5@E=#0A@ zOgiiy+{1Bf?%&y9GV-`}`6h$DN0caJnZzj-$uo&Jis&7T@xz=+obuGuFLCk4r)=%) zU`WC3OSidq?LN{}ky28dX%PeoQIK)`$VnMF+xqOD<_<+Hv!~M-Q4t59VW|4pY#2^29 zEZpvIk&;lW7TF(mY4^KmGN+&#Sc*ZIW@KrOYU>E0Fcf2&b4`+Vm#v*IIJNo$mTuD< zdFZPB5a`KB;)tRmsniWDq0l;6<;y!4_=msv2ZVq#&z?X;plTYHp|ifeMjofAVTj`z z?2J1+_~dQ+?H);(vwHjpS3bLeu1QXxUS)lK8?QGc&%km`ykM7)uRfsL9#F2v>O z?>_Vc#UlY`JlMQLvo?(?1b*PrAN859HL2JY(kP=~xd=r==8`y#F%%P9cTkHtmo9xy zm`Tp9KF@xy!&H45Rh7g^O1)AhZ2OSqoLPN>-S&L|+>*=5r;cLVMbbbbbIEYjCkaEm z;TX^JNYjW9-@AYxjLG5z+j2Pbe4P{p%`muqbA!fw1H&nD`^&qis$l!>I?eeeyXyxW z?DYB8>&rZM;$^D#H0{AYYx^4rC8trUQLofcRh5^{y~3T1OSn!6Q>~Ez70c!5;wlG& z9oF~OD7saU25|;YpM2^eq?I2c(pi>s`O*cFB*iQ{pxZcZ0b8?BQi;d~jxjO>`~l`$tVgCra`e( zV&(KI_pjY%YOcY`@-nARoaNTt`~2$t_qn&Z$y2M(GhJ^WMNYeaz<%d|zBiy*ZlX&Q z%PrCO#uRK96OAAZ3DcBu5~G<8F)4l+(({IF>~F&<99&2o z{q66u-P`2W&Rt5?3Of&WsZ>e`Ma3{p7F)~MW|76|BeeSiw)Xb8y>WxyXrD@i5tMiEY_fc9HI&i)k99K}ZtjhbaEzNF>iWyLy_1 zxfOEoa&4N}&zNr17*5(;zI2)9VuKgH_cYz% z7&1W^CLEhO&Fwq)$TJCO1Au{gKHbZwSuu|cCa zMV=iR#XOG@LZE6oipVil14UJlsm)Zi$!HYN9gS#@`pAi(m?V@7F3?303Q{VxnyrTi zR9{15GIBO|Hwd!~P1mRt%Sc_vAC1|*x6a?a``1hr>o``CW^IX@JBNbGvH26!OI6nQ zH@UZSoocB>rQ{$rfu>6`CFa5IZ4P$!Svs{q6of=+NTpVzK2-r}(%I`!shh0bTSw~9 z?{+vc|816pWMgNCTQ~31e0`c$c?n&;iRVR#T*V)S+`GHZ(rk;P)61MZd4kVA_>{e^ zeGmdaOj(^f!C>6Ou}us^$JA}o4ALYe&J!k~k0vyNRH1B?C>SNmg(`(Xn@M=+lzZNU zxq1sjm2B*8qoMqEEG!g3p-@K6BD&rP-7wKDow_^6;>iU*{>2A4mdP96`yC9;Kq?BV zQ1OBU(DMjA9tqx1QXK?Z67lfgQV@d{tA?0d=TA|5IeF4jGFjW&>(~+Je4r2<1 z0+wc?n9z$g-TiH$-ZZu|Fcs5^U2(RaJ)JfV0QX z@YWxGn|`Oug-<@C)7wQ6&>Qu+y?&40$ip!m%Eb~=!cKb|$SAoE8@mtq=)-?u>+S}H zl7kT9@Elmz7!Jm4t?zL6#vOKd_NdL284X9Q9Ggeebw2;%0$~ubI&+-iplXt}%}uJsD!=>24=A`rR8?bnW`!5ezQD@t5>KCbn%VjcGxaGn zMbIDh@O&QyLD6yv!VqsfMhcapY2(@jY|Y^K{4qwu0T-`bB#%R)IC^|mt|AT(lQ(wn zu(;6T{eOO&pMve;B_VzsV-_l?j6qS+ zZHIDYmiY|o^+o)^dkiMXKmD^m{j)4j`TI+M$=2=~x4yhXYhj9+`Ya+5NU0*FimqC0 z@9$umCiPa0%QrtmHFeC2i)IRPk>Gi4u3r9voxAJgsmJV0lclL?5Fw4Z89sXFBAyqp za%u%bR|!WWR84UFrPI9h%6X<|7I^Z^3v|WyJa-fU=l_sib9+mhKxdjR5f%}BT6D(Zasw*@~cQhQ6v}ts&IVy1P{C(g`&l!I18-%V5|eN@Jc_Ima_c zpJjdfCM8Sb^5su?`@NsxmK!{C!k}7cP_kY8IK=Zja*-3KAwe9GL@@%vAn4)85ra{J zp&DPoS%O0q{`8lZ2>ck+DX_cICCzj?y&ku|_!3joxN_x7q^3}ws*ouereo0e2GHrS zHSyWHy@@v%h}B!LkZ?xsM+OL`uYd zZwEs+D7pnMT)WClb%xc}2~0gFJ{m9%L!LbGEJ2hq@H>phDu%A1+BOL(Q4}y3cZt)0 zqUE5 zPO*Q`X8XY|&%XK;&DJ#SogV93n|$!uN9=bG2>h68afWiCNt#M*t4bUz2w{+93Q{pq zHIs6&f@&D(x=q0ws#W=hT{If=PrBIjf5+e0cQ( zo;rGlBTpPbvs8p4n1o{-+v3I-y9|c|$|akXBa0M_A|HM5DgA>n4{mN?SthT%_8dVL zap~h*NZle%a-2dLJr3FHZu6_3e8lujgN+A!xXn3^9XW!o8CaT2tzc7h>U2h9);8`^ zC_0$Bfom5J*T6LLBd>T_NR7K^-Kl&MKcMj;cJxZkl zg=ULSFI>YkKs6oCy>f!t=@lH)CDL_1{m09gmc^5=K25VcPn@YNOwXaJ3IYg{9wJB? zdt(ekP_>Gfx=t9UBzc6c8;7j0U__pVXtslCS{MpMlOdL_u{1TyIPgg`ftqLJS;ErP zER{luJXNR`r!eCpYwb<^&;tRQVI8_CX@;#?1YSVdDKS%oB#AK$2j8EdtIA_ap9(oy zmhtrYvt0V{GI5e&+B#o;@d>s0S(=R&p4Vp*$K1Jbmtf>^_SvJPalqD{EhhDtovl94 zefwqhHU^A4BRZWiw=Uga>F5k*slw{fGaQ>g%l-X3d~*F`+)9yu^C!QPl zH}+{%D^#cI{I|b)mzA?eSUtMPGf!3!xxf#HEG(Bv(u95|q&M2-!z&(>c+B;WZ<1#T zliq+z&BeA0gp-)geji0uC^zavX`3ue zHPLh(X7A zalgmv*^@l;{23%6_R11_TU~zsx1S*uLA~K{=jIh`OG65Ui8rCW)1kZ9!!8xMb>$Yv zo;t4~g>$4>mWsb>#rtaJhQx zD&6iLuf6^a=9+aTQHGKp@)Xl7!qH7URmTex8ub=Y5E5n?R+x(E|yE^O63;KRms}*9sco0?_!#VoA=7mBWSM0T%*j|?M;G7#JD>~(=}F3 zEwaBgu&@_Y44~g@H@o0jr+XQKhX*kr% z4dNstj&oeCN)+`m49UXuES90*dp%4|L1sC*mUDmm9-bf2Xe>}IRj~|{z4k7V7a)rY z1-HQNU;{;!zwQU-d5$I|Qj_GW!f@>I+&9kiVDmoruC0?RCTn+h3H=G@UO2+D=bxlm zTSb&>+`M>+-HkR=T9_T4tiD z2BX*~6cL(jkYv#zOpxbj2vpr-Yp}rwpZtp5_Buw%B|Y&csEUfn4-;_~k!6Qe-$z(i znx*7f{y1=kL$bCa6$A=do{=UgLI|W($kId7xvHutit=l0_AJkMWbwZugFhsV!y!ce zH9GvSGx@)|8}Jox|9_v9M+9>5kSL!&>K_#G@MeKH?BS49e%SJ({vHw75#npT9%^Zl z#)mQjEWOPvE!i7kq8}U%UJ8sI&jqs}KJ_q^~ceM-0{f1;iW{s^0=@ QdH?_b07*qoM6N<$f>7gm;s5{u literal 0 HcmV?d00001 diff --git a/src/sentinel-2-explorer/components/RasterFunctionSelector/useSentinel2RasterFunctions.tsx b/src/sentinel-2-explorer/components/RasterFunctionSelector/useSentinel2RasterFunctions.tsx new file mode 100644 index 00000000..fb609703 --- /dev/null +++ b/src/sentinel-2-explorer/components/RasterFunctionSelector/useSentinel2RasterFunctions.tsx @@ -0,0 +1,82 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useMemo } from 'react'; +import { + SENTINEL2_RASTER_FUNCTION_INFOS, + Sentinel2FunctionName, +} from '@shared/services/sentinel-2/config'; + +import { RasterFunctionInfo } from '@typing/imagery-service'; + +import ThumbnailPlaceholder from './thumbnails/Imagery_NaturalColor.png'; +import ThumbnailNatrualColor from './thumbnails/Imagery_NaturalColor.png'; +import ThumbnailAgriculture from './thumbnails/Imagery_Agriculture.png'; +import ThumbnailColorIR from './thumbnails/Imagery_ColorIR.png'; +import ThumbnailNDMI from './thumbnails/Imagery_NDMI.png'; +import ThumbnailNDVI from './thumbnails/Imagery_NDVI.png'; +import ThumbnailSWIR from './thumbnails/Imagery_SWIR.png'; + +const Sentinel2RendererThumbnailByName: Record = + { + 'Agriculture with DRA': ThumbnailAgriculture, + 'Bathymetric with DRA': ThumbnailPlaceholder, + 'Color Infrared with DRA': ThumbnailColorIR, + 'Natural Color with DRA': ThumbnailNatrualColor, + 'Geology with DRA': ThumbnailPlaceholder, + 'Short-wave Infrared with DRA': ThumbnailSWIR, + 'NDVI Colormap': ThumbnailNDVI, + 'NDMI Colorized': ThumbnailNDMI, + 'Normalized Burn Ratio': ThumbnailPlaceholder, + }; + +const Sentinel2RendererLegendByName: Record = { + 'Agriculture with DRA': null, + 'Bathymetric with DRA': null, + 'Color Infrared with DRA': null, + 'Natural Color with DRA': null, + 'Geology with DRA': null, + 'Short-wave Infrared with DRA': null, + 'NDVI Colormap': null, + 'NDMI Colorized': null, + 'Normalized Burn Ratio': null, +}; + +export const getSentinel2RasterFunctionInfo = (): RasterFunctionInfo[] => { + return SENTINEL2_RASTER_FUNCTION_INFOS.map((d) => { + const name: Sentinel2FunctionName = d.name as Sentinel2FunctionName; + + const thumbnail = Sentinel2RendererThumbnailByName[name]; + const legend = Sentinel2RendererLegendByName[name]; + + return { + ...d, + thumbnail, + legend, + } as RasterFunctionInfo; + }); +}; + +/** + * Get raster function information that includes thumbnail and legend + * @returns + */ +export const useSentinel2RasterFunctions = (): RasterFunctionInfo[] => { + const rasterFunctionInfosWithThumbnail = useMemo(() => { + return getSentinel2RasterFunctionInfo(); + }, []); + + return rasterFunctionInfosWithThumbnail; +}; diff --git a/src/shared/services/sentinel-2/config.ts b/src/shared/services/sentinel-2/config.ts index 7f9384ba..e122bb21 100644 --- a/src/shared/services/sentinel-2/config.ts +++ b/src/shared/services/sentinel-2/config.ts @@ -83,6 +83,7 @@ const SENTINEL2_RASTER_FUNCTIONS = [ 'Geology with DRA', 'NDVI Colormap', 'NDMI Colorized', + 'Normalized Burn Ratio', ] as const; export type Sentinel2FunctionName = (typeof SENTINEL2_RASTER_FUNCTIONS)[number]; @@ -144,6 +145,12 @@ export const SENTINEL2_RASTER_FUNCTION_INFOS: { 'Normalized Difference Moisture Index with color map. Wetlands and moist areas appear blue whereas dry areas are represented by deep yellow and brown color. It is computed as NIR(B8)-SWIR1(B11)/NIR(B8)+SWIR1(B11).', label: 'NDMI', }, + { + name: 'Normalized Burn Ratio', + description: + 'The NBR index is computed as NIR(Band08)-SWIR(Band12)/NIR(Band08)+SWIR(Band12) and appropriate for highlighting burned areas and estimating fire severity. It highlights burnt areas in fire zones greater than 500 acres. You will need to generate a differenced NBR image by subtracting a post-fire image from a pre-fire image. The resulting darker pixels represent burned areas.', + label: 'Burn', + }, ]; export const SENTINEL2_SERVICE_SORT_FIELD = 'best'; From a3aa8ef46cb8340808a09f356e0f8ada0bd82e88 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 30 Aug 2024 15:55:56 -0700 Subject: [PATCH 006/306] feat(sentinel2explorer): add queryAvailableSentinel2Scenes thunk function and useQueryAvailableSentinel2Scenes custom hook --- .../components/Layout/Layout.tsx | 11 +- .../hooks/useQueryAvailableLandsatScenes.tsx | 60 ++++ src/shared/services/sentinel-2/config.ts | 3 +- .../services/sentinel-2/getSentinel2Scenes.ts | 266 ++++++++++++++++++ src/shared/store/Sentinel2/reducer.ts | 42 ++- src/shared/store/Sentinel2/selectors.ts | 8 +- src/shared/store/Sentinel2/thunks.ts | 90 +++++- src/types/imagery-service.d.ts | 33 +++ 8 files changed, 491 insertions(+), 22 deletions(-) create mode 100644 src/sentinel-2-explorer/hooks/useQueryAvailableLandsatScenes.tsx create mode 100644 src/shared/services/sentinel-2/getSentinel2Scenes.ts diff --git a/src/sentinel-2-explorer/components/Layout/Layout.tsx b/src/sentinel-2-explorer/components/Layout/Layout.tsx index 867d5f30..182805ad 100644 --- a/src/sentinel-2-explorer/components/Layout/Layout.tsx +++ b/src/sentinel-2-explorer/components/Layout/Layout.tsx @@ -45,6 +45,7 @@ import { useShouldShowSecondaryControls } from '@shared/hooks/useShouldShowSecon import { CloudFilter } from '@shared/components/CloudFilter'; import { Sentinel2DynamicModeInfo } from '../Sentinel2DynamicModeInfo/Sentinel2DynamicModeInfo'; import { Sentinel2RasterFunctionSelector } from '../RasterFunctionSelector'; +import { useQueryAvailableSentinel2Scenes } from '../../hooks/useQueryAvailableLandsatScenes'; const Layout = () => { const mode = useSelector(selectAppMode); @@ -55,11 +56,11 @@ const Layout = () => { const shouldShowSecondaryControls = useShouldShowSecondaryControls(); - // /** - // * This custom hook gets invoked whenever the acquisition year, map center, or selected landsat missions - // * changes, it will dispatch the query that finds the available landsat scenes. - // */ - // useQueryAvailableLandsatScenes(); + /** + * This custom hook gets invoked whenever the acquisition year, map center + * changes, it will dispatch the query that finds the available sentinel-2 scenes. + */ + useQueryAvailableSentinel2Scenes(); useSaveAppState2HashParams(); diff --git a/src/sentinel-2-explorer/hooks/useQueryAvailableLandsatScenes.tsx b/src/sentinel-2-explorer/hooks/useQueryAvailableLandsatScenes.tsx new file mode 100644 index 00000000..5798bd2c --- /dev/null +++ b/src/sentinel-2-explorer/hooks/useQueryAvailableLandsatScenes.tsx @@ -0,0 +1,60 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { selectMapCenter } from '@shared/store/Map/selectors'; +import { useDispatch } from 'react-redux'; +// import { updateObjectIdOfSelectedScene } from '@shared/store/ImageryScene/thunks'; +import { selectIsAnimationPlaying } from '@shared/store/UI/selectors'; +import { selectQueryParams4SceneInSelectedMode } from '@shared/store/ImageryScene/selectors'; +import { queryAvailableSentinel2Scenes } from '@shared/store/Sentinel2/thunks'; +// import { selectAcquisitionYear } from '@shared/store/ImageryScene/selectors'; + +/** + * This custom hook queries the Sentinel-2 service and find Sentinel-2 scenes + * that were acquired within the selected date range and intersect with the center of the map screen + * @returns + */ +export const useQueryAvailableSentinel2Scenes = (): void => { + const dispatch = useDispatch(); + + // const acquisitionYear = useSelector(selectAcquisitionYear); + + const queryParams = useSelector(selectQueryParams4SceneInSelectedMode); + + const acquisitionDateRange = queryParams?.acquisitionDateRange; + + const isAnimationPlaying = useSelector(selectIsAnimationPlaying); + + /** + * current map center + */ + const center = useSelector(selectMapCenter); + + useEffect(() => { + if (!center || !acquisitionDateRange) { + return; + } + + if (isAnimationPlaying) { + return; + } + + dispatch(queryAvailableSentinel2Scenes(acquisitionDateRange)); + }, [center, acquisitionDateRange, isAnimationPlaying]); + + return null; +}; diff --git a/src/shared/services/sentinel-2/config.ts b/src/shared/services/sentinel-2/config.ts index e122bb21..89249246 100644 --- a/src/shared/services/sentinel-2/config.ts +++ b/src/shared/services/sentinel-2/config.ts @@ -53,7 +53,7 @@ export const SENTINEL_2_SERVICE_URL_DEV = /** * A proxy imagery service which has embedded credential that points to the sentinel-2 imagery service - * @see https://landsat.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer + * @see https://sentinel.imagery1.arcgis.com/arcgis/rest/services/Sentinel2L2A/ImageServer */ export const SENTINEL_2_SERVICE_URL = TIER === 'development' @@ -69,6 +69,7 @@ export const FIELD_NAMES = { NAME: 'name', ACQUISITION_DATE: 'acquisitiondate', CLOUD_COVER: 'cloudcover', + CATEGORY: 'category', }; /** diff --git a/src/shared/services/sentinel-2/getSentinel2Scenes.ts b/src/shared/services/sentinel-2/getSentinel2Scenes.ts new file mode 100644 index 00000000..e0dcdea6 --- /dev/null +++ b/src/shared/services/sentinel-2/getSentinel2Scenes.ts @@ -0,0 +1,266 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FIELD_NAMES, SENTINEL_2_SERVICE_URL } from './config'; +import { IExtent, IFeature } from '@esri/arcgis-rest-feature-service'; +import { getFormatedDateString } from '@shared/utils/date-time/formatDateString'; +import { Sentinel2Scene } from '@typing/imagery-service'; +import { DateRange } from '@typing/shared'; +import { Point } from '@arcgis/core/geometry'; +import { getFeatureByObjectId } from '../helpers/getFeatureById'; +import { getExtentByObjectId } from '../helpers/getExtentById'; +import { intersectWithImageryScene } from '../helpers/intersectWithImageryScene'; + +type GetSentinel1ScenesParams = { + /** + * longitude and latitude (e.g. [-105, 40]) + */ + mapPoint: number[]; + /** + * acquisition date range. + * + * @example + * ``` + * { + * startDate: '2023-01-01', + * endDate: '2023-12-31' + * } + * ``` + */ + acquisitionDateRange?: DateRange; + /** + * acquisition month + */ + acquisitionMonth?: number; + /** + * acquisition date in formate of `YYYY-MM-DD` (e.g. `2023-05-26`) + */ + acquisitionDate?: string; + /** + * abortController that will be used to cancel the unfinished requests + */ + abortController: AbortController; +}; + +// let controller:AbortController = null; + +const { OBJECTID, ACQUISITION_DATE, CLOUD_COVER, NAME, CATEGORY } = FIELD_NAMES; + +/** + * A Map that will be used to retrieve Landsat Scene data using the object Id as key + */ +const sentinel2SceneByObjectId: Map = new Map(); + +/** + * Formats the features from Sentinel-2 service and returns an array of Sentinel2Scene objects. + * @param features - An array of IFeature objects from Sentinel-2 service. + * @returns An array of Sentinel2Scene objects containing the acquisition date, formatted acquisition date, name, cloud cover, and best attributes. + */ +export const getFormattedSentinel2Scenes = ( + features: IFeature[] +): Sentinel2Scene[] => { + return features.map((feature) => { + const { attributes } = feature; + + const acquisitionDate = attributes[ACQUISITION_DATE]; + + /** + * formatted aquisition date should be like `2023-05-01` + */ + const formattedAcquisitionDate = getFormatedDateString({ + date: +acquisitionDate, + }); //format(acquisitionDate, 'yyyy-MM-dd'); + + const [acquisitionYear, acquisitionMonth] = formattedAcquisitionDate + .split('-') + .map((d) => +d); + + const sentinel2Scene: Sentinel2Scene = { + objectId: attributes[OBJECTID], + // productId, + acquisitionDate, + formattedAcquisitionDate, + name: attributes[NAME], + cloudCover: attributes[CLOUD_COVER], + formattedCloudCover: attributes[CLOUD_COVER] + ? Math.ceil(attributes[CLOUD_COVER] * 100) + : 0, + acquisitionYear, + acquisitionMonth, + }; + + return sentinel2Scene; + }); +}; + +/** + * Query the Landsat-level-2 imagery service to find a list of scenes for available Landsat data that + * intersect with the input map point or map extent and were acquired during the input year and month. + * + * @param {number} params.year - The year of the desired acquisition dates. + * @param {Object} params.mapPoint - The point geometry to query. + * + * @returns {Promise} A promise that resolves to an array of LandsatScene objects. + * + */ +export const getSentinel2Scenes = async ({ + mapPoint, + // acquisitionYear, + acquisitionDateRange, + acquisitionMonth, + acquisitionDate, + abortController, +}: GetSentinel1ScenesParams): Promise => { + const whereClauses = [`(${CATEGORY} = 1)`]; + + if (acquisitionDateRange) { + whereClauses.push( + `(${ACQUISITION_DATE} BETWEEN timestamp '${acquisitionDateRange.startDate} 00:00:00' AND timestamp '${acquisitionDateRange.endDate} 23:59:59')` + ); + } else if (acquisitionDate) { + // if acquisitionDate is provided, only query scenes that are acquired on this date, + // otherwise, query scenes that were acquired within the acquisitionYear year + whereClauses.push( + `(${ACQUISITION_DATE} BETWEEN timestamp '${acquisitionDate} 00:00:00' AND timestamp '${acquisitionDate} 23:59:59')` + ); + } + + // if (acquisitionMonth) { + // whereClauses.push(`(${MONTH} = ${acquisitionMonth})`); + // } + + const [longitude, latitude] = mapPoint; + + const geometry = JSON.stringify({ + spatialReference: { + wkid: 4326, + }, + x: longitude, + y: latitude, + }); + + const params = new URLSearchParams({ + f: 'json', + spatialRel: 'esriSpatialRelIntersects', + // geometryType: 'esriGeometryEnvelope', + geometryType: 'esriGeometryPoint', + // inSR: '102100', + outFields: [ACQUISITION_DATE, CLOUD_COVER, NAME].join(','), + orderByFields: ACQUISITION_DATE, + resultOffset: '0', + returnGeometry: 'false', + resultRecordCount: '1000', + geometry, + where: whereClauses.join(` AND `), + }); + + const res = await fetch( + `${SENTINEL_2_SERVICE_URL}/query?${params.toString()}`, + { + signal: abortController.signal, + } + ); + + if (!res.ok) { + throw new Error('failed to query Landsat-2 service'); + } + + const data = await res.json(); + + if (data.error) { + throw data.error; + } + + const sentinel2Scenes: Sentinel2Scene[] = getFormattedSentinel2Scenes( + data?.features || [] + ); + + // save the sentinel-2 scenes to `sentinel2SceneByObjectId` map + for (const sentinel2Scene of sentinel2Scenes) { + sentinel2SceneByObjectId.set(sentinel2Scene.objectId, sentinel2Scene); + } + + return sentinel2Scenes; +}; + +/** + * Query a feature from Sentinel-2 service using the input object Id, + * and return the feature as formatted Sentinel-2 Scene. + * @param objectId The unique identifier of the feature + * @returns Sentinel2Scene The formatted Landsat Scene corresponding to the objectId + */ +export const getSentinel2SceneByObjectId = async ( + objectId: number +): Promise => { + // Check if the sentinel-2 scene already exists in the cache + if (sentinel2SceneByObjectId.has(objectId)) { + return sentinel2SceneByObjectId.get(objectId); + } + + const feature = await getSentinel2FeatureByObjectId(objectId); + + if (!feature) { + return null; + } + + const sentinel2Scene = getFormattedSentinel2Scenes([feature])[0]; + + sentinel2SceneByObjectId.set(objectId, sentinel2Scene); + + return sentinel2Scene; +}; + +export const getSentinel2FeatureByObjectId = async ( + objectId: number +): Promise => { + const feature = await getFeatureByObjectId( + SENTINEL_2_SERVICE_URL, + objectId + ); + return feature; +}; + +/** + * Get the extent of a feature from Sentinel-2 service using the object Id as key. + * @param objectId The unique identifier of the feature + * @returns IExtent The extent of the feature from Sentinel-2 service + */ +export const getExtentOfSentinel2SceneByObjectId = async ( + objectId: number +): Promise => { + const extent = await getExtentByObjectId(SENTINEL_2_SERVICE_URL, objectId); + return extent; +}; + +/** + * Check if the input point intersects with the Sentinel-2 scene specified by the input object ID. + * @param point Point geometry representing the location to check for intersection. + * @param objectId Object ID of the Sentinel-2 scene to check intersection with. + * @returns {boolean} Returns true if the input point intersects with the specified Sentinel-2 scene, otherwise false. + */ +export const intersectWithSentinel2Scene = async ( + point: Point, + objectId: number, + abortController?: AbortController +) => { + const res = await intersectWithImageryScene({ + serviceUrl: SENTINEL_2_SERVICE_URL, + objectId, + point, + abortController, + }); + + return res; +}; diff --git a/src/shared/store/Sentinel2/reducer.ts b/src/shared/store/Sentinel2/reducer.ts index d9b6e497..c734cfcd 100644 --- a/src/shared/store/Sentinel2/reducer.ts +++ b/src/shared/store/Sentinel2/reducer.ts @@ -19,30 +19,60 @@ import { PayloadAction, // createAsyncThunk } from '@reduxjs/toolkit'; +import { Sentinel2Scene } from '@typing/imagery-service'; // import { RootState, StoreDispatch, StoreGetState } from '../configureStore'; export type Sentinel2State = { - // ArcGIS Online Webmap Item Id - webmapId?: string; + /** + * Sentinel-2 scenes that intersect with center point of map view and were acquired during the input year. + */ + sentinel2Scenes?: { + byObjectId?: { + [key: number]: Sentinel2Scene; + }; + objectIds?: number[]; + }; }; export const initialSentinel2State: Sentinel2State = { - webmapId: '67372ff42cd145319639a99152b15bc3', // Topographic + sentinel2Scenes: { + byObjectId: {}, + objectIds: [], + }, }; const slice = createSlice({ name: 'Sentinel2', initialState: initialSentinel2State, reducers: { - webmapIdChanged: (state, action: PayloadAction) => { - state.webmapId = action.payload; + sentinel2ScenesUpdated: ( + state, + action: PayloadAction + ) => { + const objectIds: number[] = []; + + const byObjectId: { + [key: number]: Sentinel2Scene; + } = {}; + + for (const scene of action.payload) { + const { objectId } = scene; + + objectIds.push(objectId); + byObjectId[objectId] = scene; + } + + state.sentinel2Scenes = { + objectIds, + byObjectId, + }; }, }, }); const { reducer } = slice; -export const { webmapIdChanged } = slice.actions; +export const { sentinel2ScenesUpdated } = slice.actions; export default reducer; diff --git a/src/shared/store/Sentinel2/selectors.ts b/src/shared/store/Sentinel2/selectors.ts index ac61d9da..6b471032 100644 --- a/src/shared/store/Sentinel2/selectors.ts +++ b/src/shared/store/Sentinel2/selectors.ts @@ -16,7 +16,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from '../configureStore'; -// export const selectWebmapId = createSelector( -// (state: RootState) => state.Map.webmapId, -// (webmapId) => webmapId -// ); +export const selectAvailableScenesByObjectId = createSelector( + (state: RootState) => state.Sentinel2.sentinel2Scenes.byObjectId, + (byObjectId) => byObjectId +); diff --git a/src/shared/store/Sentinel2/thunks.ts b/src/shared/store/Sentinel2/thunks.ts index cdeab802..9261f70f 100644 --- a/src/shared/store/Sentinel2/thunks.ts +++ b/src/shared/store/Sentinel2/thunks.ts @@ -13,15 +13,93 @@ * limitations under the License. */ +import { batch } from 'react-redux'; +import { selectMapCenter } from '../Map/selectors'; import { RootState, StoreDispatch, StoreGetState } from '../configureStore'; -import { webmapIdChanged } from './reducer'; +import { sentinel2ScenesUpdated } from './reducer'; +import { Sentinel2Scene } from '@typing/imagery-service'; +import { + ImageryScene, + availableImageryScenesUpdated, +} from '../ImageryScene/reducer'; +import { DateRange } from '@typing/shared'; +import { selectQueryParams4SceneInSelectedMode } from '../ImageryScene/selectors'; +import { deduplicateListOfImageryScenes } from '@shared/services/helpers/deduplicateListOfScenes'; +import { getSentinel2Scenes } from '@shared/services/sentinel-2/getSentinel2Scenes'; + +let abortController: AbortController = null; + +/** + * Query Sentinel-2 Scenes that intersect with center point of map view that were acquired within the user selected acquisition year. + * @param year use selected acquisition year + * @returns + */ +export const queryAvailableSentinel2Scenes = + (acquisitionDateRange: DateRange) => + async (dispatch: StoreDispatch, getState: StoreGetState) => { + if (!acquisitionDateRange) { + return; + } + + if (abortController) { + abortController.abort(); + } + + abortController = new AbortController(); -// Good resource about what "thunks" are, and why they're used for writing Redux logic: https://redux.js.org/usage/writing-logic-thunks -export const updateWebmap = - () => async (dispatch: StoreDispatch, getState: StoreGetState) => { try { - // do some async work (e.g. check if the new webmap id is an valid ArcGIS Online Item) - // ... + const { objectIdOfSelectedScene } = + selectQueryParams4SceneInSelectedMode(getState()) || {}; + + const center = selectMapCenter(getState()); + + // get scenes that were acquired within the acquisition year + const sentinel2Scenes: Sentinel2Scene[] = await getSentinel2Scenes({ + acquisitionDateRange, + mapPoint: center, + abortController, + }); + + // convert list of Landsat scenes to list of imagery scenes + let imageryScenes: ImageryScene[] = sentinel2Scenes.map( + (sentinel2Scene: Sentinel2Scene) => { + const { + objectId, + name, + formattedAcquisitionDate, + acquisitionDate, + acquisitionYear, + acquisitionMonth, + cloudCover, + } = sentinel2Scene; + + const imageryScene: ImageryScene = { + objectId, + sceneId: name, + formattedAcquisitionDate, + acquisitionDate, + acquisitionYear, + acquisitionMonth, + cloudCover, + satellite: 'Sentinel-2', + customTooltipText: [ + `${Math.ceil(cloudCover * 100)}% Cloudy`, + ], + }; + + return imageryScene; + } + ); + + imageryScenes = deduplicateListOfImageryScenes( + imageryScenes, + objectIdOfSelectedScene + ); + + batch(() => { + dispatch(sentinel2ScenesUpdated(sentinel2Scenes)); + dispatch(availableImageryScenesUpdated(imageryScenes)); + }); } catch (err) { console.error(err); } diff --git a/src/types/imagery-service.d.ts b/src/types/imagery-service.d.ts index 4e1c33f3..aa37eaa5 100644 --- a/src/types/imagery-service.d.ts +++ b/src/types/imagery-service.d.ts @@ -140,6 +140,39 @@ export type LandsatScene = { sunAzimuth: number; }; +export type Sentinel2Scene = { + objectId: number; + /** + * Sentinel-2 product name + * @example Ov_i05_L02_R00000016_C00000004 + */ + name: string; + /** + * acquisitionDate as a string in ISO format (YYYY-MM-DD). + */ + formattedAcquisitionDate: string; + /** + * acquisitionDate in unix timestamp + */ + acquisitionDate: number; + /** + * year when this scene was acquired + */ + acquisitionYear: number; + /** + * month when this scene was acquired + */ + acquisitionMonth: number; + /** + * percent of cloud cover, the value ranges from 0 - 1 + */ + cloudCover: number; + /** + * percent of cloud cover ranges rounded to integers that ranges from 0 - 100 + */ + formattedCloudCover: number; +}; + /** * Temporal Profile/Trend Data sampled at user selected location */ From 159796466965f910bf27c9658befc28432d90e84 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 30 Aug 2024 16:16:39 -0700 Subject: [PATCH 007/306] feat(sentinel2explorer): add SceneInfo component --- .../components/Layout/Layout.tsx | 3 +- .../SceneInfo/SceneInfoContainer.tsx | 61 +++++++++++++ .../components/SceneInfo/index.ts | 16 ++++ .../SceneInfo/useDataFromSelectedScene.tsx | 88 +++++++++++++++++++ .../getPreloadedState4Sentinel2Explorer.ts | 9 +- 5 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 src/sentinel-2-explorer/components/SceneInfo/SceneInfoContainer.tsx create mode 100644 src/sentinel-2-explorer/components/SceneInfo/index.ts create mode 100644 src/sentinel-2-explorer/components/SceneInfo/useDataFromSelectedScene.tsx diff --git a/src/sentinel-2-explorer/components/Layout/Layout.tsx b/src/sentinel-2-explorer/components/Layout/Layout.tsx index 182805ad..3deab9c8 100644 --- a/src/sentinel-2-explorer/components/Layout/Layout.tsx +++ b/src/sentinel-2-explorer/components/Layout/Layout.tsx @@ -46,6 +46,7 @@ import { CloudFilter } from '@shared/components/CloudFilter'; import { Sentinel2DynamicModeInfo } from '../Sentinel2DynamicModeInfo/Sentinel2DynamicModeInfo'; import { Sentinel2RasterFunctionSelector } from '../RasterFunctionSelector'; import { useQueryAvailableSentinel2Scenes } from '../../hooks/useQueryAvailableLandsatScenes'; +import { SceneInfo } from '../SceneInfo'; const Layout = () => { const mode = useSelector(selectAppMode); @@ -124,7 +125,7 @@ const Layout = () => { )} - {/* */} + )} diff --git a/src/sentinel-2-explorer/components/SceneInfo/SceneInfoContainer.tsx b/src/sentinel-2-explorer/components/SceneInfo/SceneInfoContainer.tsx new file mode 100644 index 00000000..b9bf3540 --- /dev/null +++ b/src/sentinel-2-explorer/components/SceneInfo/SceneInfoContainer.tsx @@ -0,0 +1,61 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useMemo } from 'react'; +import { + SceneInfoTable, + SceneInfoTableData, +} from '@shared/components/SceneInfoTable'; +import { useDataFromSelectedLandsatScene } from './useDataFromSelectedScene'; +import { DATE_FORMAT } from '@shared/constants/UI'; +import { useSelector } from 'react-redux'; +import { selectAppMode } from '@shared/store/ImageryScene/selectors'; +import { formatInUTCTimeZone } from '@shared/utils/date-time/formatInUTCTimeZone'; + +export const SceneInfoContainer = () => { + const mode = useSelector(selectAppMode); + + const data = useDataFromSelectedLandsatScene(); + + const tableData: SceneInfoTableData[] = useMemo(() => { + if (!data) { + return []; + } + + const { acquisitionDate, formattedCloudCover, name } = data; + + return [ + { + name: 'Scene ID', + value: name, //name.slice(0, 17), + clickToCopy: true, + }, + { + name: 'Acquired', + value: formatInUTCTimeZone(acquisitionDate, DATE_FORMAT), + }, + { + name: 'Cloud Cover', + value: `${formattedCloudCover}%`, + }, + ]; + }, [data]); + + if (mode === 'dynamic' || mode === 'analysis') { + return null; + } + + return ; +}; diff --git a/src/sentinel-2-explorer/components/SceneInfo/index.ts b/src/sentinel-2-explorer/components/SceneInfo/index.ts new file mode 100644 index 00000000..74d3725a --- /dev/null +++ b/src/sentinel-2-explorer/components/SceneInfo/index.ts @@ -0,0 +1,16 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { SceneInfoContainer as SceneInfo } from './SceneInfoContainer'; diff --git a/src/sentinel-2-explorer/components/SceneInfo/useDataFromSelectedScene.tsx b/src/sentinel-2-explorer/components/SceneInfo/useDataFromSelectedScene.tsx new file mode 100644 index 00000000..e8554b52 --- /dev/null +++ b/src/sentinel-2-explorer/components/SceneInfo/useDataFromSelectedScene.tsx @@ -0,0 +1,88 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + selectAppMode, + selectQueryParams4SceneInSelectedMode, +} from '@shared/store/ImageryScene/selectors'; +import { Sentinel2Scene } from '@typing/imagery-service'; +import { + selectAnimationStatus, + selectIsAnimationPlaying, +} from '@shared/store/UI/selectors'; +import { selectAvailableScenesByObjectId } from '@shared/store/Sentinel2/selectors'; +import { getSentinel2SceneByObjectId } from '@shared/services/sentinel-2/getSentinel2Scenes'; + +// /** +// * Save/cache Landsat scene data using the object ID as the key. +// * Why is it necessary to do this? The reason is that the `availableScenesByObjectId` does not get updated during animation playback. +// * As a result, it may not contain data for the Landsat scene associated with the animation frame. However, we still want to populate +// * the scene information for each animation frame. Therefore, it is a good idea to retrieve the cached data from this map. +// */ +// const landsatSceneByObjectId: Map = new Map(); + +/** + * This custom hook returns the data for the selected Landsat Scene. + * @returns + */ +export const useDataFromSelectedLandsatScene = () => { + const { objectIdOfSelectedScene } = + useSelector(selectQueryParams4SceneInSelectedMode) || {}; + + const availableScenesByObjectId = useSelector( + selectAvailableScenesByObjectId + ); + + const mode = useSelector(selectAppMode); + + const animationPlaying = useSelector(selectIsAnimationPlaying); + + const [sentinel2Scene, setSentinel2Scene] = useState(); + + useEffect(() => { + (async () => { + if ( + !objectIdOfSelectedScene || + animationPlaying || + mode === 'analysis' + ) { + // return null; + setSentinel2Scene(null); + return; + } + + try { + const data = + availableScenesByObjectId[objectIdOfSelectedScene] || + (await getSentinel2SceneByObjectId( + objectIdOfSelectedScene + )); + + setSentinel2Scene(data); + } catch (err) { + console.error(err); + } + })(); + }, [ + objectIdOfSelectedScene, + availableScenesByObjectId, + mode, + animationPlaying, + ]); + + return sentinel2Scene; +}; diff --git a/src/sentinel-2-explorer/store/getPreloadedState4Sentinel2Explorer.ts b/src/sentinel-2-explorer/store/getPreloadedState4Sentinel2Explorer.ts index 34899f1f..be49bb0e 100644 --- a/src/sentinel-2-explorer/store/getPreloadedState4Sentinel2Explorer.ts +++ b/src/sentinel-2-explorer/store/getPreloadedState4Sentinel2Explorer.ts @@ -55,6 +55,7 @@ import { initialMaskToolState, } from '@shared/store/MaskTool/reducer'; import { getRandomElement } from '@shared/utils/snippets/getRandomElement'; +import { Sentinel2FunctionName } from '@shared/services/sentinel-2/config'; /** * Map location info that contains center and zoom info from URL Hash Params @@ -103,16 +104,16 @@ const getPreloadedImageryScenesState = (): ImageryScenesState => { mode = 'dynamic'; } - // const defaultRasterFunction: Sentinel1FunctionName = - // 'False Color dB with DRA'; + const defaultRasterFunction: Sentinel2FunctionName = + 'Natural Color with DRA'; // Attempt to extract query parameters from the URL hash. // If not found, fallback to using the default values along with the raster function from a randomly selected interesting location, // which will serve as the map center. const queryParams4MainScene = getQueryParams4MainSceneFromHashParams() || { ...DefaultQueryParams4ImageryScene, - // rasterFunctionName: - // randomInterestingPlace?.renderer || defaultRasterFunction, + rasterFunctionName: defaultRasterFunction, + // randomInterestingPlace?.renderer || defaultRasterFunction, }; const queryParams4SecondaryScene = From 7a59002befee4a573be17c3a84d4aa921f6197c9 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 30 Aug 2024 16:24:08 -0700 Subject: [PATCH 008/306] feat(sentinel2explorer): add AnalyzeToolSelector --- .../AnalyzeToolSelector.tsx | 45 +++++++++++++++++++ .../components/Layout/Layout.tsx | 4 +- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/sentinel-2-explorer/components/AnalyzeToolSelector/AnalyzeToolSelector.tsx diff --git a/src/sentinel-2-explorer/components/AnalyzeToolSelector/AnalyzeToolSelector.tsx b/src/sentinel-2-explorer/components/AnalyzeToolSelector/AnalyzeToolSelector.tsx new file mode 100644 index 00000000..b77d0e63 --- /dev/null +++ b/src/sentinel-2-explorer/components/AnalyzeToolSelector/AnalyzeToolSelector.tsx @@ -0,0 +1,45 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { AnalysisToolSelector } from '@shared/components/AnalysisToolSelector'; +import { AnalyzeToolSelectorData } from '@shared/components/AnalysisToolSelector/AnalysisToolSelectorContainer'; + +const data: AnalyzeToolSelectorData[] = [ + { + tool: 'mask', + title: 'Index', + subtitle: 'mask', + }, + { + tool: 'trend', + title: 'Temporal', + subtitle: 'profile', + }, + { + tool: 'spectral', + title: 'Spectral', + subtitle: 'profile', + }, + { + tool: 'change', + title: 'Change', + subtitle: 'detection', + }, +]; + +export const Sentinel2AnalyzeToolSelector = () => { + return ; +}; diff --git a/src/sentinel-2-explorer/components/Layout/Layout.tsx b/src/sentinel-2-explorer/components/Layout/Layout.tsx index 3deab9c8..c3daf200 100644 --- a/src/sentinel-2-explorer/components/Layout/Layout.tsx +++ b/src/sentinel-2-explorer/components/Layout/Layout.tsx @@ -37,7 +37,6 @@ import { IS_MOBILE_DEVICE } from '@shared/constants/UI'; // import { SpectralTool } from '../SpectralTool'; import { ChangeCompareLayerSelector } from '@shared/components/ChangeCompareLayerSelector'; import { appConfig } from '@shared/config'; -// import { useQueryAvailableLandsatScenes } from '@landsat-explorer/hooks/useQueryAvailableLandsatScenes'; // import { LandsatRasterFunctionSelector } from '../RasterFunctionSelector'; // import { LandsatInterestingPlaces } from '../InterestingPlaces'; // import { AnalyzeToolSelector4Landsat } from '../AnalyzeToolSelector/AnalyzeToolSelector'; @@ -47,6 +46,7 @@ import { Sentinel2DynamicModeInfo } from '../Sentinel2DynamicModeInfo/Sentinel2D import { Sentinel2RasterFunctionSelector } from '../RasterFunctionSelector'; import { useQueryAvailableSentinel2Scenes } from '../../hooks/useQueryAvailableLandsatScenes'; import { SceneInfo } from '../SceneInfo'; +import { Sentinel2AnalyzeToolSelector } from '../AnalyzeToolSelector/AnalyzeToolSelector'; const Layout = () => { const mode = useSelector(selectAppMode); @@ -91,7 +91,7 @@ const Layout = () => { - {/* */} + )} From ae48aaf64ed903f11dd9f1325a079ce0e40e824c Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 30 Aug 2024 16:50:02 -0700 Subject: [PATCH 009/306] feat(sentinel2explorer): add placeholder Popup component --- .../components/Map/Map.tsx | 4 +- .../components/PopUp/PopupContainer.tsx | 124 ++++++++++++++++++ .../components/PopUp/helper.ts | 29 ++++ .../components/PopUp/index.ts | 16 +++ 4 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 src/sentinel-2-explorer/components/PopUp/PopupContainer.tsx create mode 100644 src/sentinel-2-explorer/components/PopUp/helper.ts create mode 100644 src/sentinel-2-explorer/components/PopUp/index.ts diff --git a/src/sentinel-2-explorer/components/Map/Map.tsx b/src/sentinel-2-explorer/components/Map/Map.tsx index 91f260d9..14fcc45c 100644 --- a/src/sentinel-2-explorer/components/Map/Map.tsx +++ b/src/sentinel-2-explorer/components/Map/Map.tsx @@ -21,7 +21,7 @@ import { AnimationLayer } from '@shared/components/AnimationLayer'; import { GroupLayer } from '@shared/components/GroupLayer'; import { AnalysisToolQueryLocation } from '@shared/components/AnalysisToolQueryLocation'; import { Zoom2NativeScale } from '@shared/components/Zoom2NativeScale/Zoom2NativeScale'; -// import { Popup } from '../PopUp'; +import { Popup } from '../PopUp'; import { MapPopUpAnchorPoint } from '@shared/components/MapPopUpAnchorPoint'; import { HillshadeLayer } from '@shared/components/HillshadeLayer/HillshadeLayer'; import { ScreenshotWidget } from '@shared/components/ScreenshotWidget/ScreenshotWidget'; @@ -74,7 +74,7 @@ const Map = () => { - {/* */} + diff --git a/src/sentinel-2-explorer/components/PopUp/PopupContainer.tsx b/src/sentinel-2-explorer/components/PopUp/PopupContainer.tsx new file mode 100644 index 00000000..9614b13a --- /dev/null +++ b/src/sentinel-2-explorer/components/PopUp/PopupContainer.tsx @@ -0,0 +1,124 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// import './PopUp.css'; +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import MapView from '@arcgis/core/views/MapView'; +import Point from '@arcgis/core/geometry/Point'; +import { useSelector } from 'react-redux'; +import { + // selectActiveAnalysisTool, + selectAppMode, + selectQueryParams4MainScene, + selectQueryParams4SecondaryScene, +} from '@shared/store/ImageryScene/selectors'; +import { getMainContent } from './helper'; +import { formatInUTCTimeZone } from '@shared/utils/date-time/formatInUTCTimeZone'; +import { MapPopup, MapPopupData } from '@shared/components/MapPopup/MapPopup'; +import { identify } from '@shared/services/helpers/identify'; +import { getPixelValuesFromIdentifyTaskResponse } from '@shared/services/helpers/getPixelValuesFromIdentifyTaskResponse'; +import { SENTINEL_2_SERVICE_URL } from '@shared/services/sentinel-2/config'; +import { getFormattedSentinel2Scenes } from '@shared/services/sentinel-2/getSentinel2Scenes'; +// import { canBeConvertedToNumber } from '@shared/utils/snippets/canBeConvertedToNumber'; + +type Props = { + mapView?: MapView; +}; + +let controller: AbortController = null; + +export const PopupContainer: FC = ({ mapView }) => { + const mode = useSelector(selectAppMode); + + const queryParams4MainScene = useSelector(selectQueryParams4MainScene); + + const queryParams4SecondaryScene = useSelector( + selectQueryParams4SecondaryScene + ); + + const [data, setData] = useState(); + + const fetchPopupData = async ( + mapPoint: Point, + clickedOnLeftSideOfSwipeWidget: boolean + ) => { + try { + let queryParams = queryParams4MainScene; + + // in swipe mode, we need to use the query Params based on position of mouse click event + if (mode === 'swipe') { + queryParams = clickedOnLeftSideOfSwipeWidget + ? queryParams4MainScene + : queryParams4SecondaryScene; + } + + if (controller) { + controller.abort(); + } + + controller = new AbortController(); + + const res = await identify({ + serviceURL: SENTINEL_2_SERVICE_URL, + point: mapPoint, + objectIds: + mode !== 'dynamic' + ? [queryParams?.objectIdOfSelectedScene] + : null, + abortController: controller, + maxItemCount: 1, + }); + + // console.log(res) + + const features = res?.catalogItems?.features; + + if (!features.length) { + throw new Error('cannot find sentinel-2 scene'); + } + + const sceneData = getFormattedSentinel2Scenes(features)[0]; + + const bandValues: number[] = + getPixelValuesFromIdentifyTaskResponse(res); + + if (!bandValues) { + throw new Error('identify task does not return band values'); + } + // console.log(bandValues) + + const title = `Sentinel-2 | ${formatInUTCTimeZone( + sceneData.acquisitionDate, + 'MMM dd, yyyy' + )}`; + + setData({ + // Set the popup's title to the coordinates of the location + title: title, + location: mapPoint, // Set the location of the popup to the clicked location + content: getMainContent(bandValues, mapPoint), + }); + } catch (error: any) { + setData({ + title: undefined, + location: undefined, + content: undefined, + error, + }); + } + }; + + return ; +}; diff --git a/src/sentinel-2-explorer/components/PopUp/helper.ts b/src/sentinel-2-explorer/components/PopUp/helper.ts new file mode 100644 index 00000000..db96c78d --- /dev/null +++ b/src/sentinel-2-explorer/components/PopUp/helper.ts @@ -0,0 +1,29 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Point from '@arcgis/core/geometry/Point'; +import { getPopUpContentWithLocationInfo } from '@shared/components/MapPopup/helper'; + +export const getMainContent = (values: number[], mapPoint: Point) => { + const content = ` +

+
+ Index Name: Index Value +
+
+ `; + + return getPopUpContentWithLocationInfo(mapPoint, content); +}; diff --git a/src/sentinel-2-explorer/components/PopUp/index.ts b/src/sentinel-2-explorer/components/PopUp/index.ts new file mode 100644 index 00000000..cb92293f --- /dev/null +++ b/src/sentinel-2-explorer/components/PopUp/index.ts @@ -0,0 +1,16 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { PopupContainer as Popup } from './PopupContainer'; From 095d32843fc4d27b63f48e91be324cff48c86103 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 30 Aug 2024 17:02:43 -0700 Subject: [PATCH 010/306] feat(sentinel2explorer): add parseSentinel2ProductInfo helper function --- src/shared/services/sentinel-2/helpers.ts | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/shared/services/sentinel-2/helpers.ts diff --git a/src/shared/services/sentinel-2/helpers.ts b/src/shared/services/sentinel-2/helpers.ts new file mode 100644 index 00000000..a43b549b --- /dev/null +++ b/src/shared/services/sentinel-2/helpers.ts @@ -0,0 +1,46 @@ +type Sentinel2MissionId = 'S2A' | 'S2B'; + +type Sentinel2ProductInfo = { + /** + * the mission ID + */ + missionID: Sentinel2MissionId; + /** + * Relative Orbit number + */ + relativeOrbit: string; + /** + * Tile Number field + */ + tileNumber: string; + /** + * Processing Baseline number + */ + processingBaselineNumber: string; +}; + +/** + * Parse Info of a Sentinel-2 Scene using its Product ID/Name. + * + * @example S2B_MSIL2A_20240701T182919_N0510_R027_T11SMT_20240702T012050 + * @see https://sentiwiki.copernicus.eu/web/s2-products + */ +export const parseSentinel2ProductInfo = ( + productId: string +): Sentinel2ProductInfo => { + const [ + MMM, // the mission ID(S2A/S2B) + MSIXXX, // MSIL1C denotes the Level-1C product level/ MSIL2A denotes the Level-2A product level + YYYYMMDDHHMMSS, // the datatake sensing start time + Nxxyy, // the Processing Baseline number (e.g. N0204) + ROOO, // Relative Orbit number (R001 - R143), + Txxxxx, // Tile Number field + ] = productId.split('_'); + + return { + missionID: MMM as Sentinel2MissionId, + relativeOrbit: ROOO, + tileNumber: Txxxxx, + processingBaselineNumber: Nxxyy, + }; +}; From 865c75d91c4b488439bfab2a3c099f1e0c60e5cf Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 3 Sep 2024 09:11:26 -0700 Subject: [PATCH 011/306] feat(sentinel2explorer): update native scale --- src/sentinel-2-explorer/components/Map/Map.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentinel-2-explorer/components/Map/Map.tsx b/src/sentinel-2-explorer/components/Map/Map.tsx index 14fcc45c..cd7bb6a2 100644 --- a/src/sentinel-2-explorer/components/Map/Map.tsx +++ b/src/sentinel-2-explorer/components/Map/Map.tsx @@ -66,7 +66,7 @@ const Map = () => { From 114fd9ac55e67394a30d702031f9d1e9352032e4 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 3 Sep 2024 13:23:50 -0700 Subject: [PATCH 012/306] feat(shared): add 'burn' to SpectralIndex --- src/shared/services/landsat-level-2/helpers.ts | 2 +- src/shared/store/MaskTool/reducer.ts | 4 ++++ src/types/imagery-service.d.ts | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/shared/services/landsat-level-2/helpers.ts b/src/shared/services/landsat-level-2/helpers.ts index b5698fd2..da1e2e17 100644 --- a/src/shared/services/landsat-level-2/helpers.ts +++ b/src/shared/services/landsat-level-2/helpers.ts @@ -69,7 +69,7 @@ type LandsatProductInfo = { * @see https://pro.arcgis.com/en/pro-app/3.0/help/analysis/raster-functions/band-arithmetic-function.htm * @see https://www.esri.com/about/newsroom/arcuser/spectral-library/ */ -const BandIndexesLookup: Record = { +const BandIndexesLookup: Partial> = { /** * The Normalized Difference Moisture Index (NDMI) is sensitive to the moisture levels in vegetation. * It is used to monitor droughts as well as monitor fuel levels in fire-prone areas. diff --git a/src/shared/store/MaskTool/reducer.ts b/src/shared/store/MaskTool/reducer.ts index 5950344d..c8b55520 100644 --- a/src/shared/store/MaskTool/reducer.ts +++ b/src/shared/store/MaskTool/reducer.ts @@ -97,6 +97,9 @@ export const DefaultPixelValueRangeBySelectedIndex: MaskToolPixelValueRangeBySpe urban: { selectedRange: [0, 1], }, + burn: { + selectedRange: [0, 1], + }, }; export const initialMaskToolState: MaskToolState = { @@ -113,6 +116,7 @@ export const initialMaskToolState: MaskToolState = { 'water anomaly': [255, 214, 102], ship: [255, 0, 21], urban: [255, 0, 21], + burn: [0, 0, 0], }, // totalVisibleAreaInSqKm: null, // countOfVisiblePixels: 0, diff --git a/src/types/imagery-service.d.ts b/src/types/imagery-service.d.ts index aa37eaa5..b4d9f9ef 100644 --- a/src/types/imagery-service.d.ts +++ b/src/types/imagery-service.d.ts @@ -49,7 +49,9 @@ export type SpectralIndex = | 'vegetation' | 'moisture' | 'temperature farhenheit' - | 'temperature celcius'; + | 'temperature celcius' + | 'urban' + | 'burn'; /** * Name of Radar Index for SAR image (e.g. Sentinel-1) From 67127e96d6fb91207d689bb277fc94af5beeef74 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 3 Sep 2024 13:31:29 -0700 Subject: [PATCH 013/306] feat(sentinel2explorer): add Mask Tool --- .../components/Layout/Layout.tsx | 5 +- .../components/MaskTool/MaskToolContainer.tsx | 123 ++++++++++++++++++ .../components/MaskTool/index.ts | 16 +++ 3 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 src/sentinel-2-explorer/components/MaskTool/MaskToolContainer.tsx create mode 100644 src/sentinel-2-explorer/components/MaskTool/index.ts diff --git a/src/sentinel-2-explorer/components/Layout/Layout.tsx b/src/sentinel-2-explorer/components/Layout/Layout.tsx index c3daf200..c997123f 100644 --- a/src/sentinel-2-explorer/components/Layout/Layout.tsx +++ b/src/sentinel-2-explorer/components/Layout/Layout.tsx @@ -47,6 +47,7 @@ import { Sentinel2RasterFunctionSelector } from '../RasterFunctionSelector'; import { useQueryAvailableSentinel2Scenes } from '../../hooks/useQueryAvailableLandsatScenes'; import { SceneInfo } from '../SceneInfo'; import { Sentinel2AnalyzeToolSelector } from '../AnalyzeToolSelector/AnalyzeToolSelector'; +import { Sentinel2MaskTool } from '../MaskTool'; const Layout = () => { const mode = useSelector(selectAppMode); @@ -118,8 +119,8 @@ const Layout = () => { {mode === 'analysis' && (
- {/* - + + {/* */}
diff --git a/src/sentinel-2-explorer/components/MaskTool/MaskToolContainer.tsx b/src/sentinel-2-explorer/components/MaskTool/MaskToolContainer.tsx new file mode 100644 index 00000000..edd5df00 --- /dev/null +++ b/src/sentinel-2-explorer/components/MaskTool/MaskToolContainer.tsx @@ -0,0 +1,123 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AnalysisToolHeader } from '@shared/components/AnalysisToolHeader'; +import { + MaskLayerRenderingControls, + MaskToolWarnigMessage, +} from '@shared/components/MaskTool'; +import { selectedIndex4MaskToolChanged } from '@shared/store/MaskTool/reducer'; +import { + selectSelectedIndex4MaskTool, + selectMaskLayerPixelValueRange, +} from '@shared/store/MaskTool/selectors'; +import { updateMaskLayerSelectedRange } from '@shared/store/MaskTool/thunks'; +import React, { useEffect, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { + selectActiveAnalysisTool, + selectQueryParams4MainScene, + selectQueryParams4SceneInSelectedMode, +} from '@shared/store/ImageryScene/selectors'; +import classNames from 'classnames'; +import { MASK_TOOL_HEADER_TOOLTIP } from '@shared/components/MaskTool/config'; +import { SpectralIndex } from '@typing/imagery-service'; +import { PixelRangeSlider } from '@shared/components/PixelRangeSlider'; +import { TotalVisibleAreaInfo } from '@shared/components/TotalAreaInfo/TotalAreaInfo'; + +export const MaskToolContainer = () => { + const dispatch = useDispatch(); + + const tool = useSelector(selectActiveAnalysisTool); + + const selectedSpectralIndex = useSelector(selectSelectedIndex4MaskTool); + + const maskOptions = useSelector(selectMaskLayerPixelValueRange); + + const { objectIdOfSelectedScene } = + useSelector(selectQueryParams4SceneInSelectedMode) || {}; + + const shouldBeDisabled = useMemo(() => { + return !objectIdOfSelectedScene; + }, [objectIdOfSelectedScene]); + + if (tool !== 'mask') { + return null; + } + + return ( +
+ { + dispatch( + selectedIndex4MaskToolChanged(val as SpectralIndex) + ); + }} + /> + + {shouldBeDisabled ? ( + + ) : ( + <> +
+
+ +
+ + { + dispatch(updateMaskLayerSelectedRange(values)); + }} + countOfTicks={17} + tickLabels={[-1, -0.5, 0, 0.5, 1]} + showSliderTooltip={true} + /> +
+ + + + )} +
+ ); +}; diff --git a/src/sentinel-2-explorer/components/MaskTool/index.ts b/src/sentinel-2-explorer/components/MaskTool/index.ts new file mode 100644 index 00000000..4675d184 --- /dev/null +++ b/src/sentinel-2-explorer/components/MaskTool/index.ts @@ -0,0 +1,16 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { MaskToolContainer as Sentinel2MaskTool } from './MaskToolContainer'; From d047a7da3748c2a7e04daff81b7a8b91e9d94e9f Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 3 Sep 2024 13:53:58 -0700 Subject: [PATCH 014/306] feat(sentinel2explorer): add Mask Layer --- .../components/Map/Map.tsx | 3 +- .../MaskLayer/MaskLayerContainer.tsx | 125 ++++++++++++++++++ .../components/MaskLayer/index.ts | 16 +++ src/shared/services/sentinel-2/helpers.ts | 78 +++++++++++ 4 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/sentinel-2-explorer/components/MaskLayer/MaskLayerContainer.tsx create mode 100644 src/sentinel-2-explorer/components/MaskLayer/index.ts diff --git a/src/sentinel-2-explorer/components/Map/Map.tsx b/src/sentinel-2-explorer/components/Map/Map.tsx index cd7bb6a2..3ceb4d75 100644 --- a/src/sentinel-2-explorer/components/Map/Map.tsx +++ b/src/sentinel-2-explorer/components/Map/Map.tsx @@ -36,6 +36,7 @@ import { SwipeWidget4ImageryLayers } from '@shared/components/SwipeWidget/SwipeW import { ZoomToExtent } from '@shared/components/ZoomToExtent'; import { SENTINEL_2_SERVICE_URL } from '@shared/services/sentinel-2/config'; import { Sentinel2Layer } from '../Sentinel2Layer'; +import { Sentinel2MaskLayer } from '../MaskLayer'; const Map = () => { const dispatch = useDispatch(); @@ -53,7 +54,7 @@ const Map = () => { index={1} > - {/* */} + diff --git a/src/sentinel-2-explorer/components/MaskLayer/MaskLayerContainer.tsx b/src/sentinel-2-explorer/components/MaskLayer/MaskLayerContainer.tsx new file mode 100644 index 00000000..2e0b016a --- /dev/null +++ b/src/sentinel-2-explorer/components/MaskLayer/MaskLayerContainer.tsx @@ -0,0 +1,125 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MapView from '@arcgis/core/views/MapView'; +import React, { FC, useEffect, useMemo } from 'react'; +// import { MaskLayer } from './MaskLayer'; +import { useSelector } from 'react-redux'; +import { + selectMaskLayerPixelValueRange, + selectShouldClipMaskLayer, + selectMaskLayerOpcity, + selectSelectedIndex4MaskTool, + selectMaskLayerPixelColor, +} from '@shared/store/MaskTool/selectors'; +import { + selectActiveAnalysisTool, + selectAppMode, + selectQueryParams4SceneInSelectedMode, +} from '@shared/store/ImageryScene/selectors'; +import GroupLayer from '@arcgis/core/layers/GroupLayer'; +import { SpectralIndex } from '@typing/imagery-service'; +import { ImageryLayerWithPixelFilter } from '@shared/components/ImageryLayerWithPixelFilter'; +import RasterFunction from '@arcgis/core/layers/support/RasterFunction'; + +import { useCalculateTotalAreaByPixelsCount } from '@shared/hooks/useCalculateTotalAreaByPixelsCount'; +import { useDispatch } from 'react-redux'; +import { countOfVisiblePixelsChanged } from '@shared/store/Map/reducer'; +import { SENTINEL_2_SERVICE_URL } from '@shared/services/sentinel-2/config'; +import { getBandIndexesBySpectralIndex } from '@shared/services/sentinel-2/helpers'; + +type Props = { + mapView?: MapView; + groupLayer?: GroupLayer; +}; + +export const MaskLayerContainer: FC = ({ mapView, groupLayer }) => { + const dispatach = useDispatch(); + + const mode = useSelector(selectAppMode); + + const spectralIndex = useSelector( + selectSelectedIndex4MaskTool + ) as SpectralIndex; + + const { selectedRange } = useSelector(selectMaskLayerPixelValueRange); + + const pixelColor = useSelector(selectMaskLayerPixelColor); + + const opacity = useSelector(selectMaskLayerOpcity); + + const shouldClip = useSelector(selectShouldClipMaskLayer); + + const { objectIdOfSelectedScene } = + useSelector(selectQueryParams4SceneInSelectedMode) || {}; + + const anailysisTool = useSelector(selectActiveAnalysisTool); + + const isVisible = useMemo(() => { + if (mode !== 'analysis' || anailysisTool !== 'mask') { + return false; + } + + if (!objectIdOfSelectedScene) { + return false; + } + + return true; + }, [mode, anailysisTool, objectIdOfSelectedScene]); + + const rasterFunction = useMemo(() => { + if (!spectralIndex) { + return null; + } + + return new RasterFunction({ + functionName: 'BandArithmetic', + outputPixelType: 'f32', + functionArguments: { + Method: 0, + BandIndexes: getBandIndexesBySpectralIndex(spectralIndex) || '', + }, + }); + }, [spectralIndex]); + + const fullPixelValueRange = useMemo(() => { + return [-1, 1]; + }, [spectralIndex]); + + useCalculateTotalAreaByPixelsCount({ + objectId: objectIdOfSelectedScene, + serviceURL: SENTINEL_2_SERVICE_URL, + pixelSize: mapView.resolution, + }); + + return ( + { + dispatach(countOfVisiblePixelsChanged(visiblePixels)); + }} + /> + ); +}; diff --git a/src/sentinel-2-explorer/components/MaskLayer/index.ts b/src/sentinel-2-explorer/components/MaskLayer/index.ts new file mode 100644 index 00000000..87a5e5e9 --- /dev/null +++ b/src/sentinel-2-explorer/components/MaskLayer/index.ts @@ -0,0 +1,16 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { MaskLayerContainer as Sentinel2MaskLayer } from './MaskLayerContainer'; diff --git a/src/shared/services/sentinel-2/helpers.ts b/src/shared/services/sentinel-2/helpers.ts index a43b549b..f94a4b6a 100644 --- a/src/shared/services/sentinel-2/helpers.ts +++ b/src/shared/services/sentinel-2/helpers.ts @@ -1,3 +1,5 @@ +import { SpectralIndex } from '@typing/imagery-service'; + type Sentinel2MissionId = 'S2A' | 'S2B'; type Sentinel2ProductInfo = { @@ -44,3 +46,79 @@ export const parseSentinel2ProductInfo = ( processingBaselineNumber: Nxxyy, }; }; + +/** + * Sentinel-2 Band Index by Spectral Index + * + * Here is the list of Sentinel-2 Bands: + * - Band 1: Coastal aerosol (0.433 - 0.453 µm) + * - Band 2: Blue (0.458 - 0.523 µm) + * - Band 3: Green (0.543 - 0.578 µm) + * - Band 4: Red (0.650 - 0.680 µm) + * - Band 5: Vegetation Red Edge (0.698 - 0.713 µm) + * - Band 6: Vegetation Red Edge (0.733 - 0.748 µm) + * - Band 7: Vegetation Red Edge (0.773 - 0.793 µm) + * - Band 8: Near-Infrared (0.785 - 0.900 µm) + * - Band 8A: Narrow NIR (0.855 - 0.875 µm) + * - Band 9: Water vapour (0.935 - 0.955 µm) + * - Band 10: SWIR – Cirrus (1.365 - 1.385 µm) + * - Band 11: SWIR-1 (1.565 - 1.655 µm) + * - Band 12: SWIR-2 (2.100 - 2.280 µm) + * + * @see https://pro.arcgis.com/en/pro-app/3.0/help/analysis/raster-functions/band-arithmetic-function.htm + * @see https://www.esri.com/about/newsroom/arcuser/spectral-library/ + */ +const BandIndexesLookup: Partial> = { + /** + * The Normalized Difference Moisture Index (NDMI) is sensitive to the moisture levels in vegetation. + * It is used to monitor droughts as well as monitor fuel levels in fire-prone areas. + * It uses NIR and SWIR bands to create a ratio designed to mitigate illumination and atmospheric effects. + * + * NDMI = (NIR - SWIR1)/(NIR + SWIR1) + * - NIR = pixel values from the near-infrared band + * - SWIR1 = pixel values from the first shortwave infrared band + */ + moisture: '(B5-B12)/(B5+B12)', + /** + * The Green Normalized Difference Vegetation Index (GNDVI) method is a vegetation index for estimating photo synthetic activity + * and is a commonly used vegetation index to determine water and nitrogen uptake into the plant canopy. + * + * GNDVI = (NIR-Green)/(NIR+Green) + * - NIR = pixel values from the near-infrared band + * - Green = pixel values from the green band + * + * This index outputs values between -1.0 and 1.0. + */ + vegetation: '(B8-B4)/(B8+B4)', + /** + * The Modified Normalized Difference Water Index (MNDWI) uses green and SWIR bands for the enhancement of open water features. + * + * MNDWI = (Green - SWIR) / (Green + SWIR) + * - Green = pixel values from the green band + * - SWIR = pixel values from the shortwave infrared band + */ + water: '(B3-B12)/(B3+B12)', + /** + * The Normalized Difference Built-up Index (NDBI) uses the NIR and SWIR bands to emphasize man-made built-up areas. + * It is ratio based to mitigate the effects of terrain illumination differences as well as atmospheric effects. + * + * NDBI = (SWIR - NIR) / (SWIR + NIR) + * - SWIR = pixel values from the shortwave infrared band + * - NIR = pixel values from the near-infrared band + */ + urban: '(B12-B8)/(B12+B8)', + /** + * The Normalized Burn Ratio Index (NBRI) uses the NIR and SWIR bands to emphasize burned areas, while mitigating illumination and atmospheric effects. + * + * NBR = (NIR - SWIR) / (NIR+ SWIR) + * - NIR = pixel values from the near-infrared band + * - SWIR = pixel values from the shortwave infrared band + */ + burn: '(B13-B8)/(B13+B8)', +}; + +export const getBandIndexesBySpectralIndex = ( + spectralIndex: SpectralIndex +): string => { + return BandIndexesLookup[spectralIndex]; +}; From 5e49423575d7e7c4e849d0bbcc91d1f33f330e25 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 3 Sep 2024 15:24:48 -0700 Subject: [PATCH 015/306] chore(shared): comment out unused getRasterFunctionBySpectralIndex method --- .../services/landsat-level-2/helpers.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/shared/services/landsat-level-2/helpers.ts b/src/shared/services/landsat-level-2/helpers.ts index da1e2e17..39022b98 100644 --- a/src/shared/services/landsat-level-2/helpers.ts +++ b/src/shared/services/landsat-level-2/helpers.ts @@ -258,15 +258,15 @@ export const getBandIndexesBySpectralIndex = ( return BandIndexesLookup[spectralIndex]; }; -export const getRasterFunctionBySpectralIndex = ( - spectralIndex: SpectralIndex -) => { - return { - rasterFunction: 'BandArithmetic', - rasterFunctionArguments: { - Method: 0, - BandIndexes: getBandIndexesBySpectralIndex(spectralIndex), - }, - outputPixelType: 'F32', - }; -}; +// export const getRasterFunctionBySpectralIndex = ( +// spectralIndex: SpectralIndex +// ) => { +// return { +// rasterFunction: 'BandArithmetic', +// rasterFunctionArguments: { +// Method: 0, +// BandIndexes: getBandIndexesBySpectralIndex(spectralIndex), +// }, +// outputPixelType: 'F32', +// }; +// }; From 906db791d5113a7b820dd3246dcae5a6f1a709d0 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 3 Sep 2024 15:40:56 -0700 Subject: [PATCH 016/306] refactor(shared): add useMaskLayerVisibility custom hook --- .../MaskLayer/MaskLayerContainer.tsx | 17 +----- .../MaskLayer/MaskLayerContainer.tsx | 17 +----- .../MaskLayer/useMaskLayerVisibility.tsx | 56 +++++++++++++++++++ 3 files changed, 60 insertions(+), 30 deletions(-) create mode 100644 src/shared/components/MaskLayer/useMaskLayerVisibility.tsx diff --git a/src/landsat-explorer/components/MaskLayer/MaskLayerContainer.tsx b/src/landsat-explorer/components/MaskLayer/MaskLayerContainer.tsx index cebad5f1..2893233f 100644 --- a/src/landsat-explorer/components/MaskLayer/MaskLayerContainer.tsx +++ b/src/landsat-explorer/components/MaskLayer/MaskLayerContainer.tsx @@ -44,6 +44,7 @@ import { import { useCalculateTotalAreaByPixelsCount } from '@shared/hooks/useCalculateTotalAreaByPixelsCount'; import { useDispatch } from 'react-redux'; import { countOfVisiblePixelsChanged } from '@shared/store/Map/reducer'; +import { useMaskLayerVisibility } from '@shared/components/MaskLayer/useMaskLayerVisibility'; type Props = { mapView?: MapView; @@ -70,8 +71,6 @@ export const getRasterFunctionBySpectralIndex = ( export const MaskLayerContainer: FC = ({ mapView, groupLayer }) => { const dispatach = useDispatch(); - const mode = useSelector(selectAppMode); - const spectralIndex = useSelector( selectSelectedIndex4MaskTool ) as SpectralIndex; @@ -87,19 +86,7 @@ export const MaskLayerContainer: FC = ({ mapView, groupLayer }) => { const { objectIdOfSelectedScene } = useSelector(selectQueryParams4SceneInSelectedMode) || {}; - const anailysisTool = useSelector(selectActiveAnalysisTool); - - const isVisible = useMemo(() => { - if (mode !== 'analysis' || anailysisTool !== 'mask') { - return false; - } - - if (!objectIdOfSelectedScene) { - return false; - } - - return true; - }, [mode, anailysisTool, objectIdOfSelectedScene]); + const isVisible = useMaskLayerVisibility(); const rasterFunction = useMemo(() => { return getRasterFunctionBySpectralIndex(spectralIndex); diff --git a/src/sentinel-2-explorer/components/MaskLayer/MaskLayerContainer.tsx b/src/sentinel-2-explorer/components/MaskLayer/MaskLayerContainer.tsx index 2e0b016a..a62735bc 100644 --- a/src/sentinel-2-explorer/components/MaskLayer/MaskLayerContainer.tsx +++ b/src/sentinel-2-explorer/components/MaskLayer/MaskLayerContainer.tsx @@ -39,6 +39,7 @@ import { useDispatch } from 'react-redux'; import { countOfVisiblePixelsChanged } from '@shared/store/Map/reducer'; import { SENTINEL_2_SERVICE_URL } from '@shared/services/sentinel-2/config'; import { getBandIndexesBySpectralIndex } from '@shared/services/sentinel-2/helpers'; +import { useMaskLayerVisibility } from '@shared/components/MaskLayer/useMaskLayerVisibility'; type Props = { mapView?: MapView; @@ -48,8 +49,6 @@ type Props = { export const MaskLayerContainer: FC = ({ mapView, groupLayer }) => { const dispatach = useDispatch(); - const mode = useSelector(selectAppMode); - const spectralIndex = useSelector( selectSelectedIndex4MaskTool ) as SpectralIndex; @@ -65,19 +64,7 @@ export const MaskLayerContainer: FC = ({ mapView, groupLayer }) => { const { objectIdOfSelectedScene } = useSelector(selectQueryParams4SceneInSelectedMode) || {}; - const anailysisTool = useSelector(selectActiveAnalysisTool); - - const isVisible = useMemo(() => { - if (mode !== 'analysis' || anailysisTool !== 'mask') { - return false; - } - - if (!objectIdOfSelectedScene) { - return false; - } - - return true; - }, [mode, anailysisTool, objectIdOfSelectedScene]); + const isVisible = useMaskLayerVisibility(); const rasterFunction = useMemo(() => { if (!spectralIndex) { diff --git a/src/shared/components/MaskLayer/useMaskLayerVisibility.tsx b/src/shared/components/MaskLayer/useMaskLayerVisibility.tsx new file mode 100644 index 00000000..66203a76 --- /dev/null +++ b/src/shared/components/MaskLayer/useMaskLayerVisibility.tsx @@ -0,0 +1,56 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { FC, useEffect, useMemo } from 'react'; +// import { MaskLayer } from './MaskLayer'; +import { useSelector } from 'react-redux'; +import { + selectMaskLayerPixelValueRange, + selectShouldClipMaskLayer, + selectMaskLayerOpcity, + selectSelectedIndex4MaskTool, + selectMaskLayerPixelColor, +} from '@shared/store/MaskTool/selectors'; +import { + selectActiveAnalysisTool, + selectAppMode, + selectQueryParams4SceneInSelectedMode, +} from '@shared/store/ImageryScene/selectors'; + +/** + * This custom hook determines the visibility of the Mask Layer + * @returns + */ +export const useMaskLayerVisibility = () => { + const mode = useSelector(selectAppMode); + + const { objectIdOfSelectedScene } = + useSelector(selectQueryParams4SceneInSelectedMode) || {}; + + const anailysisTool = useSelector(selectActiveAnalysisTool); + + const isVisible = useMemo(() => { + if (mode !== 'analysis' || anailysisTool !== 'mask') { + return false; + } + + if (!objectIdOfSelectedScene) { + return false; + } + + return true; + }, [mode, anailysisTool, objectIdOfSelectedScene]); + + return isVisible; +}; From 53f1fa141f274f1863137a2e1820471723aa0a3e Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 3 Sep 2024 15:43:02 -0700 Subject: [PATCH 017/306] doc: update CHANGELOG --- CHANGELOG.md | 17 +++++++++++++++++ .../MaskLayer/useMaskLayerVisibility.tsx | 7 ------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c935b9e1..83f59ecf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## Sentinel-2 Explorer + ### Added + ### Changed + ### Fixed + +### Removed + + +## Shared + +### Added + +### Changed +- add `useMaskLayerVisibility` custom hook + +### Fixed + ### Removed ## 2024 July Release diff --git a/src/shared/components/MaskLayer/useMaskLayerVisibility.tsx b/src/shared/components/MaskLayer/useMaskLayerVisibility.tsx index 66203a76..219fa228 100644 --- a/src/shared/components/MaskLayer/useMaskLayerVisibility.tsx +++ b/src/shared/components/MaskLayer/useMaskLayerVisibility.tsx @@ -15,13 +15,6 @@ import React, { FC, useEffect, useMemo } from 'react'; // import { MaskLayer } from './MaskLayer'; import { useSelector } from 'react-redux'; -import { - selectMaskLayerPixelValueRange, - selectShouldClipMaskLayer, - selectMaskLayerOpcity, - selectSelectedIndex4MaskTool, - selectMaskLayerPixelColor, -} from '@shared/store/MaskTool/selectors'; import { selectActiveAnalysisTool, selectAppMode, From 951f59a171042ccf953a30d2b3494dba6af71468 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 3 Sep 2024 16:12:04 -0700 Subject: [PATCH 018/306] refactor(shared): add getChangeCompareLayerRasterFunction helper function --- .../ChangeLayer/ChangeLayerContainer.tsx | 178 +++--------------- .../components/ChangeCompareLayer/helpers.ts | 125 ++++++++++++ 2 files changed, 148 insertions(+), 155 deletions(-) create mode 100644 src/shared/components/ChangeCompareLayer/helpers.ts diff --git a/src/landsat-explorer/components/ChangeLayer/ChangeLayerContainer.tsx b/src/landsat-explorer/components/ChangeLayer/ChangeLayerContainer.tsx index 574676b2..71e8ec39 100644 --- a/src/landsat-explorer/components/ChangeLayer/ChangeLayerContainer.tsx +++ b/src/landsat-explorer/components/ChangeLayer/ChangeLayerContainer.tsx @@ -42,119 +42,15 @@ import { ImageryLayerWithPixelFilter } from '@shared/components/ImageryLayerWith import { useCalculateTotalAreaByPixelsCount } from '@shared/hooks/useCalculateTotalAreaByPixelsCount'; import { useDispatch } from 'react-redux'; import { countOfVisiblePixelsChanged } from '@shared/store/Map/reducer'; +import { getChangeCompareLayerRasterFunction } from '@shared/components/ChangeCompareLayer/helpers'; +import { Polygon } from '@arcgis/core/geometry'; +import { useChangeCompareLayerVisibility } from '@shared/components/ChangeCompareLayer'; type Props = { mapView?: MapView; groupLayer?: GroupLayer; }; -/** - * This function retrieves a raster function that can be used to visualize changes between two input Landsat scenes. - * The output raster function applies an `Arithmetic` operation to calculate the difference of a selected spectral index - * between two input rasters. - * - * @param spectralIndex - The user-selected spectral index to analyze changes. - * @param queryParams4SceneA - Query parameters for the first selected Landsat scene. - * @param queryParams4SceneB - Query parameters for the second selected Landsat scene. - * @returns A Raster Function that contains the `Arithmetic` function to visualize spectral index changes. - * - * @see https://developers.arcgis.com/documentation/common-data-types/raster-function-objects.htm - */ -export const getRasterFunction4ChangeLayer = async ( - /** - * name of selected spectral index - */ - spectralIndex: SpectralIndex, - /** - * query params of the first selected Landsat scene - */ - queryParams4SceneA: QueryParams4ImageryScene, - /** - * query params of the second selected Landsat scene - */ - queryParams4SceneB: QueryParams4ImageryScene -): Promise => { - if (!spectralIndex) { - return null; - } - - if ( - !queryParams4SceneA?.objectIdOfSelectedScene || - !queryParams4SceneB?.objectIdOfSelectedScene - ) { - return null; - } - - // Sort query parameters by acquisition date in ascending order. - const [ - queryParams4SceneAcquiredInEarlierDate, - queryParams4SceneAcquiredInLaterDate, - ] = [queryParams4SceneA, queryParams4SceneB].sort((a, b) => { - return ( - formattedDateString2Unixtimestamp(a.acquisitionDate) - - formattedDateString2Unixtimestamp(b.acquisitionDate) - ); - }); - - try { - // Get the band index for the selected spectral index. - const bandIndex = getBandIndexesBySpectralIndex(spectralIndex); - - // Retrieve the feature associated with the later acquired Landsat scene. - const feature = await getLandsatFeatureByObjectId( - queryParams4SceneAcquiredInLaterDate?.objectIdOfSelectedScene - ); - - return new RasterFunction({ - // the Clip function clips a raster using a rectangular shape according to the extents defined, - // or clips a raster to the shape of an input polygon feature class. - functionName: 'Clip', - functionArguments: { - // a polygon or envelope - ClippingGeometry: feature.geometry, - // use 1 to keep image inside of the geometry - ClippingType: 1, - Raster: { - // The `Arithmetic` function performs an arithmetic operation between two rasters. - rasterFunction: 'Arithmetic', - rasterFunctionArguments: { - Raster: { - rasterFunction: 'BandArithmetic', - rasterFunctionArguments: { - Raster: `$${queryParams4SceneAcquiredInLaterDate.objectIdOfSelectedScene}`, - Method: 0, - BandIndexes: bandIndex, - }, - outputPixelType: 'F32', - }, - Raster2: { - rasterFunction: 'BandArithmetic', - rasterFunctionArguments: { - Raster: `$${queryParams4SceneAcquiredInEarlierDate.objectIdOfSelectedScene}`, - Method: 0, - BandIndexes: bandIndex, - }, - outputPixelType: 'F32', - }, - // 1=esriRasterPlus, 2=esriRasterMinus, 3=esriRasterMultiply, 4=esriRasterDivide, 5=esriRasterPower, 6=esriRasterMode - Operation: 2, - // default 0; 0=esriExtentFirstOf, 1=esriExtentIntersectionOf, 2=esriExtentUnionOf, 3=esriExtentLastOf - ExtentType: 1, - // 0=esriCellsizeFirstOf, 1=esriCellsizeMinOf, 2=esriCellsizeMaxOf, 3=esriCellsizeMeanOf, 4=esriCellsizeLastOf - CellsizeType: 0, - }, - outputPixelType: 'F32', - }, - }, - }); - } catch (err) { - console.error(err); - - // handle any potential errors and return null in case of failure. - return null; - } -}; - export const ChangeLayerContainer: FC = ({ mapView, groupLayer }) => { const dispatach = useDispatch(); @@ -182,60 +78,32 @@ export const ChangeLayerContainer: FC = ({ mapView, groupLayer }) => { const [rasterFunction, setRasterFunction] = useState(); - const isVisible = useMemo(() => { - if (mode !== 'analysis') { - return false; - } - - if (anailysisTool !== 'change') { - return false; - } - - if ( - !queryParams4SceneA?.objectIdOfSelectedScene || - !queryParams4SceneB?.objectIdOfSelectedScene - ) { - return false; - } - - if (!rasterFunction) { - return false; - } - - return changeCompareLayerIsOn; - }, [ - mode, - anailysisTool, - changeCompareLayerIsOn, - queryParams4SceneA, - queryParams4SceneB, - rasterFunction, - ]); + const isVisible = useChangeCompareLayerVisibility(); useEffect(() => { (async () => { - const rasterFunction = await getRasterFunction4ChangeLayer( - spectralIndex, - queryParams4SceneA, - queryParams4SceneB - ); - - setRasterFunction(rasterFunction); + try { + const bandIndex = getBandIndexesBySpectralIndex(spectralIndex); + + const feature = await getLandsatFeatureByObjectId( + queryParams4SceneA?.objectIdOfSelectedScene + ); + + const rasterFunction = + await getChangeCompareLayerRasterFunction({ + bandIndex, + clippingGeometry: feature.geometry as any, + queryParams4SceneA, + queryParams4SceneB, + }); + + setRasterFunction(rasterFunction); + } catch (err) { + setRasterFunction(null); + } })(); }, [spectralIndex, queryParams4SceneA, queryParams4SceneB]); - // return ( - // - // ); - useCalculateTotalAreaByPixelsCount({ objectId: queryParams4SceneA?.objectIdOfSelectedScene || diff --git a/src/shared/components/ChangeCompareLayer/helpers.ts b/src/shared/components/ChangeCompareLayer/helpers.ts new file mode 100644 index 00000000..79e68a2e --- /dev/null +++ b/src/shared/components/ChangeCompareLayer/helpers.ts @@ -0,0 +1,125 @@ +import { Geometry } from '@arcgis/core/geometry'; +import RasterFunction from '@arcgis/core/layers/support/RasterFunction'; +import { QueryParams4ImageryScene } from '@shared/store/ImageryScene/reducer'; +import { formattedDateString2Unixtimestamp } from '@shared/utils/date-time/formatDateString'; +import { SpectralIndex } from '@typing/imagery-service'; + +type GetChangeCompareLayerRasterFunctionParams = { + /** + * band index for the selected spectral index + */ + bandIndex: string; + /** + * geometry that will be used to clip the output imagery + */ + clippingGeometry: Geometry; + /** + * query params of the first selected scene + */ + queryParams4SceneA: QueryParams4ImageryScene; + /** + * query params of the second selected scene + */ + queryParams4SceneB: QueryParams4ImageryScene; +}; + +/** + * This function retrieves a raster function that can be used to visualize changes between two input Imagery scenes. + * The output raster function applies an `Arithmetic` operation to calculate the difference of a selected spectral index + * between two input rasters. + * + * @param {GetChangeCompareLayerRasterFunctionParams} params - The parameters for generating the Change Compare Layer Raster Function. + * - `bandIndex` - band index for the selected spectral index. + * - `clippingGeometry` - geometry that will be used to clip the output imagery + * - `queryParams4SceneA` - Query parameters for the first selected scene. + * - `queryParams4SceneB` - Query parameters for the second selected scene. + * @returns A Raster Function that contains the `Arithmetic` function to visualize spectral index changes. + * + * @see https://developers.arcgis.com/documentation/common-data-types/raster-function-objects.htm + */ +export const getChangeCompareLayerRasterFunction = ({ + bandIndex, + clippingGeometry, + queryParams4SceneA, + queryParams4SceneB, +}: GetChangeCompareLayerRasterFunctionParams): RasterFunction => { + if (!bandIndex) { + return null; + } + + if ( + !queryParams4SceneA?.objectIdOfSelectedScene || + !queryParams4SceneB?.objectIdOfSelectedScene + ) { + return null; + } + + // Sort query parameters by acquisition date in ascending order. + const [ + queryParams4SceneAcquiredInEarlierDate, + queryParams4SceneAcquiredInLaterDate, + ] = [queryParams4SceneA, queryParams4SceneB].sort((a, b) => { + return ( + formattedDateString2Unixtimestamp(a.acquisitionDate) - + formattedDateString2Unixtimestamp(b.acquisitionDate) + ); + }); + + try { + // // Get the band index for the selected spectral index. + // const bandIndex = getBandIndexesBySpectralIndex(spectralIndex); + + // // Retrieve the feature associated with the later acquired Landsat scene. + // const feature = await getLandsatFeatureByObjectId( + // queryParams4SceneAcquiredInLaterDate?.objectIdOfSelectedScene + // ); + + return new RasterFunction({ + // the Clip function clips a raster using a rectangular shape according to the extents defined, + // or clips a raster to the shape of an input polygon feature class. + functionName: 'Clip', + functionArguments: { + // a polygon or envelope + ClippingGeometry: clippingGeometry, + // use 1 to keep image inside of the geometry + ClippingType: 1, + Raster: { + // The `Arithmetic` function performs an arithmetic operation between two rasters. + rasterFunction: 'Arithmetic', + rasterFunctionArguments: { + Raster: { + rasterFunction: 'BandArithmetic', + rasterFunctionArguments: { + Raster: `$${queryParams4SceneAcquiredInLaterDate.objectIdOfSelectedScene}`, + Method: 0, + BandIndexes: bandIndex, + }, + outputPixelType: 'F32', + }, + Raster2: { + rasterFunction: 'BandArithmetic', + rasterFunctionArguments: { + Raster: `$${queryParams4SceneAcquiredInEarlierDate.objectIdOfSelectedScene}`, + Method: 0, + BandIndexes: bandIndex, + }, + outputPixelType: 'F32', + }, + // 1=esriRasterPlus, 2=esriRasterMinus, 3=esriRasterMultiply, 4=esriRasterDivide, 5=esriRasterPower, 6=esriRasterMode + Operation: 2, + // default 0; 0=esriExtentFirstOf, 1=esriExtentIntersectionOf, 2=esriExtentUnionOf, 3=esriExtentLastOf + ExtentType: 1, + // 0=esriCellsizeFirstOf, 1=esriCellsizeMinOf, 2=esriCellsizeMaxOf, 3=esriCellsizeMeanOf, 4=esriCellsizeLastOf + CellsizeType: 0, + }, + outputPixelType: 'F32', + }, + }, + }); + } catch (err) { + console.error(err); + + // handle any potential errors and return null in case of failure. + return null; + } +}; From 420e2ec7d603668dc696e47a64e3405a453aa26b Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 3 Sep 2024 16:21:54 -0700 Subject: [PATCH 019/306] feat(sentinel2explorer): add ChangeCompareLayer --- CHANGELOG.md | 1 + .../ChangeLayerContainer.tsx | 121 ++++++++++++++++++ .../components/ChangeCompareLayer/index.ts | 16 +++ .../ChangeCompareToolContainer.tsx | 77 +++++++++++ .../components/ChangeCompareTool/index.ts | 16 +++ .../components/Layout/Layout.tsx | 5 +- .../components/Map/Map.tsx | 2 + 7 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 src/sentinel-2-explorer/components/ChangeCompareLayer/ChangeLayerContainer.tsx create mode 100644 src/sentinel-2-explorer/components/ChangeCompareLayer/index.ts create mode 100644 src/sentinel-2-explorer/components/ChangeCompareTool/ChangeCompareToolContainer.tsx create mode 100644 src/sentinel-2-explorer/components/ChangeCompareTool/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f59ecf..58256d88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed - add `useMaskLayerVisibility` custom hook +- add `getChangeCompareLayerRasterFunction` helper function ### Fixed diff --git a/src/sentinel-2-explorer/components/ChangeCompareLayer/ChangeLayerContainer.tsx b/src/sentinel-2-explorer/components/ChangeCompareLayer/ChangeLayerContainer.tsx new file mode 100644 index 00000000..3b9966d3 --- /dev/null +++ b/src/sentinel-2-explorer/components/ChangeCompareLayer/ChangeLayerContainer.tsx @@ -0,0 +1,121 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MapView from '@arcgis/core/views/MapView'; +import React, { FC, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + selectActiveAnalysisTool, + selectAppMode, + selectQueryParams4MainScene, + selectQueryParams4SecondaryScene, +} from '@shared/store/ImageryScene/selectors'; +import GroupLayer from '@arcgis/core/layers/GroupLayer'; +// import { ChangeLayer } from './ChangeLayer'; +import { + selectChangeCompareLayerIsOn, + selectFullPixelValuesRangeInChangeCompareTool, + selectSelectedOption4ChangeCompareTool, + selectUserSelectedRangeInChangeCompareTool, +} from '@shared/store/ChangeCompareTool/selectors'; +import { SpectralIndex } from '@typing/imagery-service'; +import RasterFunction from '@arcgis/core/layers/support/RasterFunction'; +import { getPixelColor4ChangeCompareLayer } from '@shared/components/ChangeCompareTool/helpers'; +import { ImageryLayerWithPixelFilter } from '@shared/components/ImageryLayerWithPixelFilter'; +import { useCalculateTotalAreaByPixelsCount } from '@shared/hooks/useCalculateTotalAreaByPixelsCount'; +import { useDispatch } from 'react-redux'; +import { countOfVisiblePixelsChanged } from '@shared/store/Map/reducer'; +import { getChangeCompareLayerRasterFunction } from '@shared/components/ChangeCompareLayer/helpers'; +import { useChangeCompareLayerVisibility } from '@shared/components/ChangeCompareLayer'; +import { getBandIndexesBySpectralIndex } from '@shared/services/sentinel-2/helpers'; +import { getSentinel2FeatureByObjectId } from '@shared/services/sentinel-2/getSentinel2Scenes'; +import { SENTINEL_2_SERVICE_URL } from '@shared/services/sentinel-2/config'; + +type Props = { + mapView?: MapView; + groupLayer?: GroupLayer; +}; + +export const ChangeLayerContainer: FC = ({ mapView, groupLayer }) => { + const dispatach = useDispatch(); + + const spectralIndex = useSelector( + selectSelectedOption4ChangeCompareTool + ) as SpectralIndex; + + const queryParams4SceneA = useSelector(selectQueryParams4MainScene); + + const queryParams4SceneB = useSelector(selectQueryParams4SecondaryScene); + + const selectedRange = useSelector( + selectUserSelectedRangeInChangeCompareTool + ); + + const fullPixelValueRange = useSelector( + selectFullPixelValuesRangeInChangeCompareTool + ); + + const [rasterFunction, setRasterFunction] = useState(); + + const isVisible = useChangeCompareLayerVisibility(); + + useEffect(() => { + (async () => { + try { + const bandIndex = getBandIndexesBySpectralIndex(spectralIndex); + + const feature = await getSentinel2FeatureByObjectId( + queryParams4SceneA?.objectIdOfSelectedScene + ); + + const rasterFunction = + await getChangeCompareLayerRasterFunction({ + bandIndex, + clippingGeometry: feature.geometry as any, + queryParams4SceneA, + queryParams4SceneB, + }); + + setRasterFunction(rasterFunction); + } catch (err) { + setRasterFunction(null); + } + })(); + }, [spectralIndex, queryParams4SceneA, queryParams4SceneB]); + + useCalculateTotalAreaByPixelsCount({ + objectId: + queryParams4SceneA?.objectIdOfSelectedScene || + queryParams4SceneB?.objectIdOfSelectedScene, + serviceURL: SENTINEL_2_SERVICE_URL, + pixelSize: mapView.resolution, + }); + + return ( + { + dispatach(countOfVisiblePixelsChanged(visiblePixels)); + }} + /> + ); +}; diff --git a/src/sentinel-2-explorer/components/ChangeCompareLayer/index.ts b/src/sentinel-2-explorer/components/ChangeCompareLayer/index.ts new file mode 100644 index 00000000..e756b032 --- /dev/null +++ b/src/sentinel-2-explorer/components/ChangeCompareLayer/index.ts @@ -0,0 +1,16 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ChangeLayerContainer as Sentinel2ChangeLayer } from './ChangeLayerContainer'; diff --git a/src/sentinel-2-explorer/components/ChangeCompareTool/ChangeCompareToolContainer.tsx b/src/sentinel-2-explorer/components/ChangeCompareTool/ChangeCompareToolContainer.tsx new file mode 100644 index 00000000..643f4a8d --- /dev/null +++ b/src/sentinel-2-explorer/components/ChangeCompareTool/ChangeCompareToolContainer.tsx @@ -0,0 +1,77 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// import { AnalysisToolHeader } from '@shared/components/AnalysisToolHeader'; +// // import { PixelRangeSlider } from '@shared/components/PixelRangeSlider'; +// import { +// // selectedRangeUpdated, +// selectedOption4ChangeCompareToolChanged, +// } from '@shared/store/ChangeCompareTool/reducer'; +// import { +// selectChangeCompareLayerIsOn, +// selectSelectedOption4ChangeCompareTool, +// selectUserSelectedRangeInChangeCompareTool, +// } from '@shared/store/ChangeCompareTool/selectors'; +import { selectActiveAnalysisTool } from '@shared/store/ImageryScene/selectors'; +import { SpectralIndex } from '@typing/imagery-service'; +import classNames from 'classnames'; +import React from 'react'; +// import { useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; +// import { getChangeCompareLayerColorrampAsCSSGradient } from '../ChangeLayer/helpers'; +import { + ChangeCompareToolHeader, + ChangeCompareToolControls, +} from '@shared/components/ChangeCompareTool'; + +const LEGEND_LABEL_TEXT = ['decrease', 'no change', 'increase']; + +export const ChangeCompareToolContainer = () => { + const tool = useSelector(selectActiveAnalysisTool); + + if (tool !== 'change') { + return null; + } + + return ( +
+ + +
+ ); +}; diff --git a/src/sentinel-2-explorer/components/ChangeCompareTool/index.ts b/src/sentinel-2-explorer/components/ChangeCompareTool/index.ts new file mode 100644 index 00000000..ec6474bb --- /dev/null +++ b/src/sentinel-2-explorer/components/ChangeCompareTool/index.ts @@ -0,0 +1,16 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ChangeCompareToolContainer as Sentinel2ChangeCompareTool } from './ChangeCompareToolContainer'; diff --git a/src/sentinel-2-explorer/components/Layout/Layout.tsx b/src/sentinel-2-explorer/components/Layout/Layout.tsx index c997123f..4624cea5 100644 --- a/src/sentinel-2-explorer/components/Layout/Layout.tsx +++ b/src/sentinel-2-explorer/components/Layout/Layout.tsx @@ -48,6 +48,7 @@ import { useQueryAvailableSentinel2Scenes } from '../../hooks/useQueryAvailableL import { SceneInfo } from '../SceneInfo'; import { Sentinel2AnalyzeToolSelector } from '../AnalyzeToolSelector/AnalyzeToolSelector'; import { Sentinel2MaskTool } from '../MaskTool'; +import { Sentinel2ChangeCompareTool } from '../ChangeCompareTool'; const Layout = () => { const mode = useSelector(selectAppMode); @@ -121,8 +122,8 @@ const Layout = () => {
{/* - - */} + */} +
)} diff --git a/src/sentinel-2-explorer/components/Map/Map.tsx b/src/sentinel-2-explorer/components/Map/Map.tsx index 3ceb4d75..6e93c533 100644 --- a/src/sentinel-2-explorer/components/Map/Map.tsx +++ b/src/sentinel-2-explorer/components/Map/Map.tsx @@ -37,6 +37,7 @@ import { ZoomToExtent } from '@shared/components/ZoomToExtent'; import { SENTINEL_2_SERVICE_URL } from '@shared/services/sentinel-2/config'; import { Sentinel2Layer } from '../Sentinel2Layer'; import { Sentinel2MaskLayer } from '../MaskLayer'; +import { Sentinel2ChangeLayer } from '../ChangeCompareLayer'; const Map = () => { const dispatch = useDispatch(); @@ -55,6 +56,7 @@ const Map = () => { > + From 68ba48a814205956e77bd796947e7962075f925b Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 4 Sep 2024 13:19:16 -0700 Subject: [PATCH 020/306] refactor(landsatexplorer): update SpectralProfileChart component add bottomAxisTickText property --- .../SpectralTool/SpectralProfileChart.tsx | 15 +++++++++++---- .../SpectralTool/SpectralToolContainer.tsx | 2 ++ .../SamplingResults/SamplingResultsContainer.tsx | 6 +++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/landsat-explorer/components/SpectralTool/SpectralProfileChart.tsx b/src/landsat-explorer/components/SpectralTool/SpectralProfileChart.tsx index e0eb9ceb..491f1391 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralProfileChart.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralProfileChart.tsx @@ -16,7 +16,6 @@ import { MultipleLinesChart } from '@vannizhang/react-d3-charts'; import React, { FC, useMemo } from 'react'; import { LineGroupData } from '@vannizhang/react-d3-charts/dist/MultipleLinesChart/types'; -import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; type Props = { /** @@ -24,9 +23,16 @@ type Props = { * for spectral profile tool */ chartData: LineGroupData[]; + /** + * tick lable text for the bottom axis of the chart + */ + bottomAxisTickText: string[]; }; -export const SpectralProfileChart: FC = ({ chartData }) => { +export const SpectralProfileChart: FC = ({ + chartData, + bottomAxisTickText, +}) => { if (!chartData || !chartData.length) { return null; } @@ -59,8 +65,9 @@ export const SpectralProfileChart: FC = ({ chartData }) => { numberOfTicks: 7, tickFormatFunction: (val: number | string, index) => { // console.log(val, index) - const ticks = LANDSAT_BAND_NAMES.slice(0, 7); - return ticks[val as number]; + return ( + bottomAxisTickText[val as number] || val.toString() + ); }, }} margin={{ diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index 4dc030be..11ed7b8d 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -35,6 +35,7 @@ import { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; import { FeatureOfInterests, SpectralProfileFeatureOfInterest } from './config'; import { useSpectralProfileChartData } from './useSpectralProfileChartData'; import { debounce } from '@shared/utils/snippets/debounce'; +import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; export const SpectralToolContainer = () => { const dispatch = useDispatch(); @@ -163,6 +164,7 @@ export const SpectralToolContainer = () => { // featureOfInterest={selectedFeatureOfInterest} // data={spectralProfileData} chartData={chartData} + bottomAxisTickText={LANDSAT_BAND_NAMES.slice(0, 7)} /> diff --git a/src/spectral-sampling-tool/components/SamplingResults/SamplingResultsContainer.tsx b/src/spectral-sampling-tool/components/SamplingResults/SamplingResultsContainer.tsx index caa8c7f9..7795b97a 100644 --- a/src/spectral-sampling-tool/components/SamplingResults/SamplingResultsContainer.tsx +++ b/src/spectral-sampling-tool/components/SamplingResults/SamplingResultsContainer.tsx @@ -19,6 +19,7 @@ import { useChartData } from './useChartData'; import { SpectralProfileChart } from '@landsat-explorer/components/SpectralTool/SpectralProfileChart'; import { Button } from '@shared/components/Button'; import { SaveSamplingResults } from './SaveSamplingResults'; +import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; export const SamplingResultsContainer = () => { const chartData = useChartData(); @@ -45,7 +46,10 @@ export const SamplingResultsContainer = () => { ) : ( <>
- +
From 1a2df7de214385ff5ffda105d748d1879202fae6 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 4 Sep 2024 13:24:27 -0700 Subject: [PATCH 021/306] refactor(shared): move SpectralProfileChart to shared/components --- .../components/SpectralTool/SpectralToolContainer.tsx | 2 +- .../components/SpectralProfileTool}/SpectralProfileChart.tsx | 0 src/shared/components/SpectralProfileTool/index.ts | 1 + .../components/SamplingResults/SamplingResultsContainer.tsx | 4 ++-- 4 files changed, 4 insertions(+), 3 deletions(-) rename src/{landsat-explorer/components/SpectralTool => shared/components/SpectralProfileTool}/SpectralProfileChart.tsx (100%) create mode 100644 src/shared/components/SpectralProfileTool/index.ts diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index 11ed7b8d..24972fba 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -29,7 +29,7 @@ import classNames from 'classnames'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useSelector } from 'react-redux'; -import { SpectralProfileChart } from './SpectralProfileChart'; +import { SpectralProfileChart } from '@shared/components/SpectralProfileTool'; import { findMostSimilarFeatureOfInterest } from './helper'; import { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; import { FeatureOfInterests, SpectralProfileFeatureOfInterest } from './config'; diff --git a/src/landsat-explorer/components/SpectralTool/SpectralProfileChart.tsx b/src/shared/components/SpectralProfileTool/SpectralProfileChart.tsx similarity index 100% rename from src/landsat-explorer/components/SpectralTool/SpectralProfileChart.tsx rename to src/shared/components/SpectralProfileTool/SpectralProfileChart.tsx diff --git a/src/shared/components/SpectralProfileTool/index.ts b/src/shared/components/SpectralProfileTool/index.ts new file mode 100644 index 00000000..afd93ab0 --- /dev/null +++ b/src/shared/components/SpectralProfileTool/index.ts @@ -0,0 +1 @@ +export { SpectralProfileChart } from './SpectralProfileChart'; diff --git a/src/spectral-sampling-tool/components/SamplingResults/SamplingResultsContainer.tsx b/src/spectral-sampling-tool/components/SamplingResults/SamplingResultsContainer.tsx index 7795b97a..345ad6a4 100644 --- a/src/spectral-sampling-tool/components/SamplingResults/SamplingResultsContainer.tsx +++ b/src/spectral-sampling-tool/components/SamplingResults/SamplingResultsContainer.tsx @@ -16,8 +16,8 @@ import classNames from 'classnames'; import React, { useMemo } from 'react'; import { useChartData } from './useChartData'; -import { SpectralProfileChart } from '@landsat-explorer/components/SpectralTool/SpectralProfileChart'; -import { Button } from '@shared/components/Button'; +import { SpectralProfileChart } from '@shared/components/SpectralProfileTool'; +// import { Button } from '@shared/components/Button'; import { SaveSamplingResults } from './SaveSamplingResults'; import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; From d3a5e828647d854729bcca49d24d3abad9f89b40 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 4 Sep 2024 13:57:15 -0700 Subject: [PATCH 022/306] refactor(shared): move SpectralProfileChartLegend to shared/components --- .../SpectralTool/SpectralToolContainer.tsx | 16 ++++++++++---- .../SpectralProfileChartLegend.tsx | 22 ++++++++++++------- .../components/SpectralProfileTool/index.ts | 1 + 3 files changed, 27 insertions(+), 12 deletions(-) rename src/{landsat-explorer/components/SpectralTool => shared/components/SpectralProfileTool}/SpectralProfileChartLegend.tsx (79%) diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index 24972fba..1cabae8d 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -29,9 +29,14 @@ import classNames from 'classnames'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useSelector } from 'react-redux'; -import { SpectralProfileChart } from '@shared/components/SpectralProfileTool'; -import { findMostSimilarFeatureOfInterest } from './helper'; -import { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; +import { + SpectralProfileChart, + SpectralProfileChartLegend, +} from '@shared/components/SpectralProfileTool'; +import { + findMostSimilarFeatureOfInterest, + getFillColorByFeatureOfInterest, +} from './helper'; import { FeatureOfInterests, SpectralProfileFeatureOfInterest } from './config'; import { useSpectralProfileChartData } from './useSpectralProfileChartData'; import { debounce } from '@shared/utils/snippets/debounce'; @@ -169,7 +174,10 @@ export const SpectralToolContainer = () => { )} diff --git a/src/landsat-explorer/components/SpectralTool/SpectralProfileChartLegend.tsx b/src/shared/components/SpectralProfileTool/SpectralProfileChartLegend.tsx similarity index 79% rename from src/landsat-explorer/components/SpectralTool/SpectralProfileChartLegend.tsx rename to src/shared/components/SpectralProfileTool/SpectralProfileChartLegend.tsx index af1e6246..f0f40882 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralProfileChartLegend.tsx +++ b/src/shared/components/SpectralProfileTool/SpectralProfileChartLegend.tsx @@ -14,12 +14,17 @@ */ import React, { FC } from 'react'; -// import { SpectralProfileFeatureOfInterest } from './SpectralToolContainer'; -import { SpectralProfileFeatureOfInterest } from './config'; -import { getFillColorByFeatureOfInterest } from './helper'; type Props = { - featureOfInterest: SpectralProfileFeatureOfInterest; + /** + * Name of the matched or user-selected feature of interest to be displayed along with the + * spectral profile fetched from the selected location. + */ + featureOfInterestName: string; + /** + * Fill color of the matched or user-selected feature of interest. + */ + featureOfInterestFillColor: string; }; type LegendItemProps = { @@ -58,9 +63,10 @@ const LegendItem: FC = ({ label, fill, strokeDasharray }) => { }; export const SpectralProfileChartLegend: FC = ({ - featureOfInterest, + featureOfInterestName, + featureOfInterestFillColor, }) => { - if (!featureOfInterest) { + if (!featureOfInterestName) { return null; } @@ -72,8 +78,8 @@ export const SpectralProfileChartLegend: FC = ({ /> diff --git a/src/shared/components/SpectralProfileTool/index.ts b/src/shared/components/SpectralProfileTool/index.ts index afd93ab0..c86fdb70 100644 --- a/src/shared/components/SpectralProfileTool/index.ts +++ b/src/shared/components/SpectralProfileTool/index.ts @@ -1 +1,2 @@ export { SpectralProfileChart } from './SpectralProfileChart'; +export { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; From 611fdb2812494e0a11058e1ba4c58c3f732327ce Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 4 Sep 2024 14:02:54 -0700 Subject: [PATCH 023/306] refactor(shared): move SpectralProfileFeatureOfInterest to shared/components/SpectralProfileTool --- .../components/SpectralTool/SpectralToolContainer.tsx | 3 ++- .../components/SpectralTool/config.ts | 11 ++++++++--- .../components/SpectralTool/helper.ts | 2 +- .../SpectralTool/useSpectralProfileChartData.tsx | 6 ++---- src/shared/components/SpectralProfileTool/config.ts | 11 +++++++++++ src/shared/components/SpectralProfileTool/index.ts | 1 + 6 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 src/shared/components/SpectralProfileTool/config.ts diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index 1cabae8d..24eb9478 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -32,12 +32,13 @@ import { useSelector } from 'react-redux'; import { SpectralProfileChart, SpectralProfileChartLegend, + SpectralProfileFeatureOfInterest, } from '@shared/components/SpectralProfileTool'; import { findMostSimilarFeatureOfInterest, getFillColorByFeatureOfInterest, } from './helper'; -import { FeatureOfInterests, SpectralProfileFeatureOfInterest } from './config'; +import { FeatureOfInterests } from './config'; import { useSpectralProfileChartData } from './useSpectralProfileChartData'; import { debounce } from '@shared/utils/snippets/debounce'; import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; diff --git a/src/landsat-explorer/components/SpectralTool/config.ts b/src/landsat-explorer/components/SpectralTool/config.ts index 7385c6be..7bf2dd8b 100644 --- a/src/landsat-explorer/components/SpectralTool/config.ts +++ b/src/landsat-explorer/components/SpectralTool/config.ts @@ -13,13 +13,18 @@ * limitations under the License. */ +import { SpectralProfileFeatureOfInterest } from '@shared/components/SpectralProfileTool'; + /** * The typical spectral profiles data from the config file used by the legacy Landsat Explorer app, * and the data prepared using `/scripts/landsat-spectral-signatures`. * * @see https://github.com/Esri/Imagery-Apps/blob/master/Landsat%20Explorer/configs/Identify/config_Identify.json */ -export const SpectralProfileDataByFeatureOfInterest = { +export const SpectralProfileDataByFeatureOfInterest: Record< + SpectralProfileFeatureOfInterest, + number[] +> = { // Cloud: [0.88802, 0.90596, 0.88938, 0.91148, 0.93778, 0.5457, 0.37914], // 'Snow/Ice': [1.0, 1.0, 1.0, 1.0, 0.99766, 0.09434, 0.09942], // Desert: [0.17706, 0.18228, 0.2431, 0.37778, 0.48144, 0.58394, 0.53018], @@ -78,5 +83,5 @@ export const FeatureOfInterests = Object.keys( SpectralProfileDataByFeatureOfInterest ); -export type SpectralProfileFeatureOfInterest = - keyof typeof SpectralProfileDataByFeatureOfInterest; +// export type SpectralProfileFeatureOfInterest = +// keyof typeof SpectralProfileDataByFeatureOfInterest; diff --git a/src/landsat-explorer/components/SpectralTool/helper.ts b/src/landsat-explorer/components/SpectralTool/helper.ts index 488141be..5d787ceb 100644 --- a/src/landsat-explorer/components/SpectralTool/helper.ts +++ b/src/landsat-explorer/components/SpectralTool/helper.ts @@ -18,8 +18,8 @@ import { LineChartDataItem } from '@vannizhang/react-d3-charts/dist/LineChart/ty import { FeatureOfInterests, SpectralProfileDataByFeatureOfInterest, - SpectralProfileFeatureOfInterest, } from './config'; +import { SpectralProfileFeatureOfInterest } from '@shared/components/SpectralProfileTool'; /** * given an array of band values, find the feature of interest that is most similar to it diff --git a/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx b/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx index 89018220..50d92061 100644 --- a/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx +++ b/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx @@ -12,20 +12,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { LineChartDataItem } from '@vannizhang/react-d3-charts/dist/LineChart/types'; - import React, { FC, useMemo } from 'react'; // import { SpectralProfileFeatureOfInterest } from './SpectralToolContainer'; import { SpectralProfileDataByFeatureOfInterest, - SpectralProfileFeatureOfInterest, + // SpectralProfileFeatureOfInterest, } from './config'; import { LineGroupData } from '@vannizhang/react-d3-charts/dist/MultipleLinesChart/types'; import { formatLandsatBandValuesAsLineChartDataItems, getFillColorByFeatureOfInterest, } from './helper'; +import { SpectralProfileFeatureOfInterest } from '@shared/components/SpectralProfileTool'; /** * This is a custom hook that convert the band values from user selected location and diff --git a/src/shared/components/SpectralProfileTool/config.ts b/src/shared/components/SpectralProfileTool/config.ts new file mode 100644 index 00000000..0d1f210c --- /dev/null +++ b/src/shared/components/SpectralProfileTool/config.ts @@ -0,0 +1,11 @@ +export type SpectralProfileFeatureOfInterest = + | 'Trees' + | 'Cloud' + | 'Clear Water' + | 'Turbid Water' + | 'Snow and Ice' + | 'Sand' + | 'Bare Soil' + | 'Paved Surface' + | 'Healthy Vegetation' + | 'Dry Vegetation'; diff --git a/src/shared/components/SpectralProfileTool/index.ts b/src/shared/components/SpectralProfileTool/index.ts index c86fdb70..0a23764a 100644 --- a/src/shared/components/SpectralProfileTool/index.ts +++ b/src/shared/components/SpectralProfileTool/index.ts @@ -1,2 +1,3 @@ export { SpectralProfileChart } from './SpectralProfileChart'; export { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; +export { SpectralProfileFeatureOfInterest } from './config'; From 5fc0c58884a035035732296502ded1d82ec5462e Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 4 Sep 2024 14:30:10 -0700 Subject: [PATCH 024/306] fix(shared): use 'export type' to re-export SpectralProfileFeatureOfInterest --- src/shared/components/SpectralProfileTool/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/SpectralProfileTool/index.ts b/src/shared/components/SpectralProfileTool/index.ts index 0a23764a..2b641ee2 100644 --- a/src/shared/components/SpectralProfileTool/index.ts +++ b/src/shared/components/SpectralProfileTool/index.ts @@ -1,3 +1,3 @@ export { SpectralProfileChart } from './SpectralProfileChart'; export { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; -export { SpectralProfileFeatureOfInterest } from './config'; +export type { SpectralProfileFeatureOfInterest } from './config'; From 87be83484d16bbcd59de4ae898db31ba486202c3 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 4 Sep 2024 14:39:37 -0700 Subject: [PATCH 025/306] refactor: move getFillColorByFeatureOfInterest to @shared/components/SpectralProfileTool/helpers --- .../SpectralTool/SpectralToolContainer.tsx | 6 ++-- .../components/SpectralTool/helper.ts | 30 ------------------ .../useSpectralProfileChartData.tsx | 6 ++-- .../components/SpectralProfileTool/helpers.ts | 31 +++++++++++++++++++ 4 files changed, 35 insertions(+), 38 deletions(-) create mode 100644 src/shared/components/SpectralProfileTool/helpers.ts diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index 24eb9478..3dca7bf0 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -34,14 +34,12 @@ import { SpectralProfileChartLegend, SpectralProfileFeatureOfInterest, } from '@shared/components/SpectralProfileTool'; -import { - findMostSimilarFeatureOfInterest, - getFillColorByFeatureOfInterest, -} from './helper'; +import { findMostSimilarFeatureOfInterest } from './helper'; import { FeatureOfInterests } from './config'; import { useSpectralProfileChartData } from './useSpectralProfileChartData'; import { debounce } from '@shared/utils/snippets/debounce'; import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; +import { getFillColorByFeatureOfInterest } from '@shared/components/SpectralProfileTool/helpers'; export const SpectralToolContainer = () => { const dispatch = useDispatch(); diff --git a/src/landsat-explorer/components/SpectralTool/helper.ts b/src/landsat-explorer/components/SpectralTool/helper.ts index 5d787ceb..f6cea3d4 100644 --- a/src/landsat-explorer/components/SpectralTool/helper.ts +++ b/src/landsat-explorer/components/SpectralTool/helper.ts @@ -91,33 +91,3 @@ export const formatLandsatBandValuesAsLineChartDataItems = ( } as LineChartDataItem; }); }; - -export const FILL_COLOR_BY_FEATURE_OF_INTEREST: Record< - SpectralProfileFeatureOfInterest, - string -> = { - Cloud: '#888888', - 'Clear Water': '#0079F2', - 'Turbid Water': '#76B5E2', - 'Snow and Ice': '#FFFFFF', - Sand: '#EDC692', - 'Bare Soil': '#EE9720', - 'Paved Surface': '#EB7EE0', - Trees: '#0AC5C0', - 'Healthy Vegetation': '#1BE43E', - 'Dry Vegetation': '#CAD728', -}; - -/** - * Get fill color that will be used to render the stroke line by featureOfInterest. - * @param FeatureOfInterests - * @returns hex color string - */ -export const getFillColorByFeatureOfInterest = ( - FeatureOfInterests: SpectralProfileFeatureOfInterest -): string => { - return ( - FILL_COLOR_BY_FEATURE_OF_INTEREST[FeatureOfInterests] || - 'var(--custom-light-blue-90)' - ); -}; diff --git a/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx b/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx index 50d92061..8927c5b4 100644 --- a/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx +++ b/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx @@ -19,11 +19,9 @@ import { // SpectralProfileFeatureOfInterest, } from './config'; import { LineGroupData } from '@vannizhang/react-d3-charts/dist/MultipleLinesChart/types'; -import { - formatLandsatBandValuesAsLineChartDataItems, - getFillColorByFeatureOfInterest, -} from './helper'; +import { formatLandsatBandValuesAsLineChartDataItems } from './helper'; import { SpectralProfileFeatureOfInterest } from '@shared/components/SpectralProfileTool'; +import { getFillColorByFeatureOfInterest } from '@shared/components/SpectralProfileTool/helpers'; /** * This is a custom hook that convert the band values from user selected location and diff --git a/src/shared/components/SpectralProfileTool/helpers.ts b/src/shared/components/SpectralProfileTool/helpers.ts new file mode 100644 index 00000000..cd45b94f --- /dev/null +++ b/src/shared/components/SpectralProfileTool/helpers.ts @@ -0,0 +1,31 @@ +import { SpectralProfileFeatureOfInterest } from './config'; + +export const FILL_COLOR_BY_FEATURE_OF_INTEREST: Record< + SpectralProfileFeatureOfInterest, + string +> = { + Cloud: '#888888', + 'Clear Water': '#0079F2', + 'Turbid Water': '#76B5E2', + 'Snow and Ice': '#FFFFFF', + Sand: '#EDC692', + 'Bare Soil': '#EE9720', + 'Paved Surface': '#EB7EE0', + Trees: '#0AC5C0', + 'Healthy Vegetation': '#1BE43E', + 'Dry Vegetation': '#CAD728', +}; + +/** + * Get fill color that will be used to render the stroke line by featureOfInterest. + * @param FeatureOfInterests + * @returns hex color string + */ +export const getFillColorByFeatureOfInterest = ( + FeatureOfInterests: SpectralProfileFeatureOfInterest +): string => { + return ( + FILL_COLOR_BY_FEATURE_OF_INTEREST[FeatureOfInterests] || + 'var(--custom-light-blue-90)' + ); +}; From 95b71b6a8e4b9b25bf77c9f9fb8296211795bfbe Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 4 Sep 2024 15:35:44 -0700 Subject: [PATCH 026/306] refactor(shared): move normalizeBandValue to @shared/components/SpectralProfileTool/helpers --- .../components/SpectralTool/helper.ts | 11 ++--------- .../components/SpectralProfileTool/helpers.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/landsat-explorer/components/SpectralTool/helper.ts b/src/landsat-explorer/components/SpectralTool/helper.ts index f6cea3d4..21c8a817 100644 --- a/src/landsat-explorer/components/SpectralTool/helper.ts +++ b/src/landsat-explorer/components/SpectralTool/helper.ts @@ -20,6 +20,7 @@ import { SpectralProfileDataByFeatureOfInterest, } from './config'; import { SpectralProfileFeatureOfInterest } from '@shared/components/SpectralProfileTool'; +import { normalizeBandValue } from '@shared/components/SpectralProfileTool/helpers'; /** * given an array of band values, find the feature of interest that is most similar to it @@ -60,14 +61,6 @@ export const findMostSimilarFeatureOfInterest = ( return output; }; -const normalizeBandValue = (value: number): number => { - // band value should never go above 1 - value = Math.min(value, 1); - // band value should never go below 0 - value = Math.max(value, 0); - return value; -}; - /** * Converts an array of Landsat Band Values to an array of LineChartDataItem objects. * @@ -87,7 +80,7 @@ export const formatLandsatBandValuesAsLineChartDataItems = ( return bandValues.slice(0, 7).map((val, index) => { return { x: index, - y: normalizeBandValue(val), + y: normalizeBandValue(val, 0, 1), } as LineChartDataItem; }); }; diff --git a/src/shared/components/SpectralProfileTool/helpers.ts b/src/shared/components/SpectralProfileTool/helpers.ts index cd45b94f..2161f3d7 100644 --- a/src/shared/components/SpectralProfileTool/helpers.ts +++ b/src/shared/components/SpectralProfileTool/helpers.ts @@ -29,3 +29,22 @@ export const getFillColorByFeatureOfInterest = ( 'var(--custom-light-blue-90)' ); }; + +/** + * Get normalized band value to ensure it fits into the range of lower and upper end + * @param value band value to normalize + * @param lowerEnd min value of the value range + * @param upperEnd max value of the value range + * @returns {number} normalized value + */ +export const normalizeBandValue = ( + value: number, + lowerEnd: number, + upperEnd: number +): number => { + // band value should never go above upperEnd + value = Math.min(value, upperEnd); + // band value should never go below lowerEnd + value = Math.max(value, lowerEnd); + return value; +}; From 8257f74b9117e683e3ec3b1128b4804ee6628cc5 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 4 Sep 2024 16:05:14 -0700 Subject: [PATCH 027/306] refactor(shared): rename 'FeatureOfInterest' to 'LandCoverType' in Spectral Profile Tool --- .../SpectralTool/SpectralToolContainer.tsx | 43 ++++++++------- .../components/SpectralTool/config.ts | 17 ++---- .../components/SpectralTool/helper.ts | 33 ++++++------ .../useSpectralProfileChartData.tsx | 52 +++++-------------- .../components/SpectralProfileTool/config.ts | 29 ++++++++++- .../components/SpectralProfileTool/helpers.ts | 17 +++--- .../components/SpectralProfileTool/index.ts | 5 +- 7 files changed, 96 insertions(+), 100 deletions(-) diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index 3dca7bf0..273ff214 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -32,14 +32,15 @@ import { useSelector } from 'react-redux'; import { SpectralProfileChart, SpectralProfileChartLegend, - SpectralProfileFeatureOfInterest, + LandCoverType, } from '@shared/components/SpectralProfileTool'; -import { findMostSimilarFeatureOfInterest } from './helper'; -import { FeatureOfInterests } from './config'; +import { findMostSimilarLandCoverType } from './helper'; import { useSpectralProfileChartData } from './useSpectralProfileChartData'; import { debounce } from '@shared/utils/snippets/debounce'; import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; -import { getFillColorByFeatureOfInterest } from '@shared/components/SpectralProfileTool/helpers'; +import { getFillColorByLandCoverType } from '@shared/components/SpectralProfileTool/helpers'; +import { ListOfLandCoverTypes } from '@shared/components/SpectralProfileTool/config'; +import { LandsatSpectralProfileData } from './config'; export const SpectralToolContainer = () => { const dispatch = useDispatch(); @@ -59,12 +60,12 @@ export const SpectralToolContainer = () => { selectError4SpectralProfileTool ); - const [selectedFeatureOfInterest, setSelectedFeatureOfInterest] = - useState(); + const [selectedLandCoverType, setSelectedLandCoverType] = + useState(); const chartData = useSpectralProfileChartData( spectralProfileData, - selectedFeatureOfInterest + selectedLandCoverType ); const spectralProfileToolMessage = useMemo(() => { @@ -124,10 +125,12 @@ export const SpectralToolContainer = () => { return; } - const mostSimilarFeatureOfInterest = - findMostSimilarFeatureOfInterest(spectralProfileData); + const mostSimilarLandCoverType = findMostSimilarLandCoverType( + spectralProfileData, + LandsatSpectralProfileData + ); - setSelectedFeatureOfInterest(mostSimilarFeatureOfInterest); + setSelectedLandCoverType(mostSimilarLandCoverType); }, [spectralProfileData]); if (tool !== 'spectral') { @@ -145,19 +148,17 @@ export const SpectralToolContainer = () => { > { + dropdownListOptions={ListOfLandCoverTypes.map( + (landCoverType) => { return { - value: featureOfInterest, + value: landCoverType, }; } )} - selectedValue={selectedFeatureOfInterest} + selectedValue={selectedLandCoverType} tooltipText={`The spectral reflectance of different materials on the Earth's surface is variable. Spectral profiles can be used to identify different land cover types.`} dropdownMenuSelectedItemOnChange={(val) => { - setSelectedFeatureOfInterest( - val as SpectralProfileFeatureOfInterest - ); + setSelectedLandCoverType(val as LandCoverType); }} /> @@ -165,17 +166,15 @@ export const SpectralToolContainer = () => { <>
diff --git a/src/landsat-explorer/components/SpectralTool/config.ts b/src/landsat-explorer/components/SpectralTool/config.ts index 7bf2dd8b..66c36a1d 100644 --- a/src/landsat-explorer/components/SpectralTool/config.ts +++ b/src/landsat-explorer/components/SpectralTool/config.ts @@ -13,7 +13,10 @@ * limitations under the License. */ -import { SpectralProfileFeatureOfInterest } from '@shared/components/SpectralProfileTool'; +import { + LandCoverType, + SpectralProfileDataByLandCoverType, +} from '@shared/components/SpectralProfileTool'; /** * The typical spectral profiles data from the config file used by the legacy Landsat Explorer app, @@ -21,10 +24,7 @@ import { SpectralProfileFeatureOfInterest } from '@shared/components/SpectralPro * * @see https://github.com/Esri/Imagery-Apps/blob/master/Landsat%20Explorer/configs/Identify/config_Identify.json */ -export const SpectralProfileDataByFeatureOfInterest: Record< - SpectralProfileFeatureOfInterest, - number[] -> = { +export const LandsatSpectralProfileData: SpectralProfileDataByLandCoverType = { // Cloud: [0.88802, 0.90596, 0.88938, 0.91148, 0.93778, 0.5457, 0.37914], // 'Snow/Ice': [1.0, 1.0, 1.0, 1.0, 0.99766, 0.09434, 0.09942], // Desert: [0.17706, 0.18228, 0.2431, 0.37778, 0.48144, 0.58394, 0.53018], @@ -78,10 +78,3 @@ export const SpectralProfileDataByFeatureOfInterest: Record< 0.1819168888888889, ], }; - -export const FeatureOfInterests = Object.keys( - SpectralProfileDataByFeatureOfInterest -); - -// export type SpectralProfileFeatureOfInterest = -// keyof typeof SpectralProfileDataByFeatureOfInterest; diff --git a/src/landsat-explorer/components/SpectralTool/helper.ts b/src/landsat-explorer/components/SpectralTool/helper.ts index 21c8a817..012a3517 100644 --- a/src/landsat-explorer/components/SpectralTool/helper.ts +++ b/src/landsat-explorer/components/SpectralTool/helper.ts @@ -13,30 +13,31 @@ * limitations under the License. */ -// import { SpectralProfileFeatureOfInterest } from './SpectralToolContainer'; import { LineChartDataItem } from '@vannizhang/react-d3-charts/dist/LineChart/types'; + import { - FeatureOfInterests, - SpectralProfileDataByFeatureOfInterest, -} from './config'; -import { SpectralProfileFeatureOfInterest } from '@shared/components/SpectralProfileTool'; + SpectralProfileDataByLandCoverType, + LandCoverType, +} from '@shared/components/SpectralProfileTool'; import { normalizeBandValue } from '@shared/components/SpectralProfileTool/helpers'; /** - * given an array of band values, find the feature of interest that is most similar to it - * @param bandValues - * @returns + * given an array of band values from user selected location and the spectral data lookup table, + * find the land cover type that is most similar to it. + * + * @param {number[]} bandValues band value from user selected location + * @param {Spectr} spectralProfileData lookup table that provide typical spectral profile data for different land cover types + * @returns {LandCoverType} name of the land cover type (feature of interest) that has the spectral profile that is most similar to the into band values */ -export const findMostSimilarFeatureOfInterest = ( - bandValues: number[] -): SpectralProfileFeatureOfInterest => { +export const findMostSimilarLandCoverType = ( + bandValues: number[], + spectralProfileData: SpectralProfileDataByLandCoverType +): LandCoverType => { // let minSumOfDifferences = Infinity; let minSumOfSquaredDifferences = Infinity; - let output: SpectralProfileFeatureOfInterest = null; + let output: LandCoverType = null; - for (const [key, value] of Object.entries( - SpectralProfileDataByFeatureOfInterest - )) { + for (const [landCoverType, value] of Object.entries(spectralProfileData)) { // let sumOfDiff = 0; let sumOfSquaredDiff = 0; @@ -54,7 +55,7 @@ export const findMostSimilarFeatureOfInterest = ( if (sumOfSquaredDiff < minSumOfSquaredDifferences) { minSumOfSquaredDifferences = sumOfSquaredDiff; - output = key as SpectralProfileFeatureOfInterest; + output = landCoverType as LandCoverType; } } diff --git a/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx b/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx index 8927c5b4..228f8366 100644 --- a/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx +++ b/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx @@ -13,65 +13,41 @@ * limitations under the License. */ import React, { FC, useMemo } from 'react'; -// import { SpectralProfileFeatureOfInterest } from './SpectralToolContainer'; -import { - SpectralProfileDataByFeatureOfInterest, - // SpectralProfileFeatureOfInterest, -} from './config'; import { LineGroupData } from '@vannizhang/react-d3-charts/dist/MultipleLinesChart/types'; import { formatLandsatBandValuesAsLineChartDataItems } from './helper'; -import { SpectralProfileFeatureOfInterest } from '@shared/components/SpectralProfileTool'; -import { getFillColorByFeatureOfInterest } from '@shared/components/SpectralProfileTool/helpers'; +import { LandCoverType } from '@shared/components/SpectralProfileTool'; +import { getFillColorByLandCoverType } from '@shared/components/SpectralProfileTool/helpers'; +import { LandsatSpectralProfileData } from './config'; /** * This is a custom hook that convert the band values from user selected location and - * the matched feature of interest into an array of LineGroupData + * the matched land cover type into an array of LineGroupData * * @param data spectral profile chart data for user selected query location - * @param featureOfInterest selected feature of interest for the spectral profile tool + * @param {LandCoverType} landCoverType matched or user selected land cover type (feature of interest) for the spectral profile tool * @returns */ export const useSpectralProfileChartData = ( data: number[], - featureOfInterest: SpectralProfileFeatureOfInterest + landCoverType: LandCoverType ) => { const chartData: LineGroupData[] = useMemo(() => { if ( !data || !data.length || - !featureOfInterest || - !SpectralProfileDataByFeatureOfInterest[featureOfInterest] + !landCoverType || + !LandsatSpectralProfileData[landCoverType] ) { return []; } - // // only need to plot the first 7 bands in the spectral profile chart - // const bandValues4UserSelectedLocation = data.slice(0, 7); - // const bandValues4SelectedFeatureOfInterest = - // SpectralProfileDataByFeatureOfInterest[featureOfInterest].slice( - // 0, - // 7 - // ); - const spectralProfileData4UserSelectedLocation = formatLandsatBandValuesAsLineChartDataItems(data); - // bandValues4UserSelectedLocation.map((val, index) => { - // return { - // x: index, - // y: normalizeBandValue(val), - // } as LineChartDataItem; - // }); - const spectralProfileData4SelectedFeatureOfInterest = + const spectralProfileData4SelectedLandCoverType = formatLandsatBandValuesAsLineChartDataItems( - SpectralProfileDataByFeatureOfInterest[featureOfInterest] + LandsatSpectralProfileData[landCoverType] ); - // bandValues4SelectedFeatureOfInterest.map((val, index) => { - // return { - // x: index, - // y: normalizeBandValue(val), - // } as LineChartDataItem; - // }); return [ { @@ -80,13 +56,13 @@ export const useSpectralProfileChartData = ( values: spectralProfileData4UserSelectedLocation, } as LineGroupData, { - fill: getFillColorByFeatureOfInterest(featureOfInterest), //'var(--custom-light-blue-70)', - key: featureOfInterest, - values: spectralProfileData4SelectedFeatureOfInterest, + fill: getFillColorByLandCoverType(landCoverType), //'var(--custom-light-blue-70)', + key: landCoverType, + values: spectralProfileData4SelectedLandCoverType, dashPattern: '9 3', // use dash pattern to provide user a hint that the feature of interest is just a reference } as LineGroupData, ]; - }, [data, featureOfInterest]); + }, [data, landCoverType]); return chartData; }; diff --git a/src/shared/components/SpectralProfileTool/config.ts b/src/shared/components/SpectralProfileTool/config.ts index 0d1f210c..93c93c76 100644 --- a/src/shared/components/SpectralProfileTool/config.ts +++ b/src/shared/components/SpectralProfileTool/config.ts @@ -1,4 +1,7 @@ -export type SpectralProfileFeatureOfInterest = +/** + * Land Cover Type that are supported by the Spectral Profile Tool + */ +export type LandCoverType = | 'Trees' | 'Cloud' | 'Clear Water' @@ -9,3 +12,27 @@ export type SpectralProfileFeatureOfInterest = | 'Paved Surface' | 'Healthy Vegetation' | 'Dry Vegetation'; + +/** + * Lookup table that contains spectral profile data for different land cover types. + */ +export type SpectralProfileDataByLandCoverType = Record< + LandCoverType, + number[] +>; + +/** + * List of supported land cover types. + */ +export const ListOfLandCoverTypes: LandCoverType[] = [ + 'Trees', + 'Cloud', + 'Clear Water', + 'Turbid Water', + 'Snow and Ice', + 'Sand', + 'Bare Soil', + 'Paved Surface', + 'Healthy Vegetation', + 'Dry Vegetation', +]; diff --git a/src/shared/components/SpectralProfileTool/helpers.ts b/src/shared/components/SpectralProfileTool/helpers.ts index 2161f3d7..2b4c323f 100644 --- a/src/shared/components/SpectralProfileTool/helpers.ts +++ b/src/shared/components/SpectralProfileTool/helpers.ts @@ -1,9 +1,6 @@ -import { SpectralProfileFeatureOfInterest } from './config'; +import { LandCoverType } from './config'; -export const FILL_COLOR_BY_FEATURE_OF_INTEREST: Record< - SpectralProfileFeatureOfInterest, - string -> = { +const FILL_COLOR_BY_LAND_COVER_TYPE: Record = { Cloud: '#888888', 'Clear Water': '#0079F2', 'Turbid Water': '#76B5E2', @@ -17,15 +14,15 @@ export const FILL_COLOR_BY_FEATURE_OF_INTEREST: Record< }; /** - * Get fill color that will be used to render the stroke line by featureOfInterest. - * @param FeatureOfInterests + * Get fill color that will be used to render the stroke line by land cover type. + * @param {LandCoverType} landCoverType user selected land cover type * @returns hex color string */ -export const getFillColorByFeatureOfInterest = ( - FeatureOfInterests: SpectralProfileFeatureOfInterest +export const getFillColorByLandCoverType = ( + landCoverType: LandCoverType ): string => { return ( - FILL_COLOR_BY_FEATURE_OF_INTEREST[FeatureOfInterests] || + FILL_COLOR_BY_LAND_COVER_TYPE[landCoverType] || 'var(--custom-light-blue-90)' ); }; diff --git a/src/shared/components/SpectralProfileTool/index.ts b/src/shared/components/SpectralProfileTool/index.ts index 2b641ee2..203ef8c9 100644 --- a/src/shared/components/SpectralProfileTool/index.ts +++ b/src/shared/components/SpectralProfileTool/index.ts @@ -1,3 +1,6 @@ export { SpectralProfileChart } from './SpectralProfileChart'; export { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; -export type { SpectralProfileFeatureOfInterest } from './config'; +export type { + LandCoverType, + SpectralProfileDataByLandCoverType, +} from './config'; From 584e415f2a5082f29d2b92b3d5d6ef79b2c336cc Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 4 Sep 2024 16:11:25 -0700 Subject: [PATCH 028/306] refactor(shared): move findMostSimilarLandCoverType to @shared/components/SpectralProfileTool/helpers --- .../SpectralTool/SpectralToolContainer.tsx | 6 ++- .../components/SpectralTool/helper.ts | 45 ------------------ .../components/SpectralProfileTool/helpers.ts | 47 ++++++++++++++++++- .../useAveragedSamplingResults.tsx | 1 - 4 files changed, 50 insertions(+), 49 deletions(-) diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index 273ff214..7158d9fb 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -34,11 +34,13 @@ import { SpectralProfileChartLegend, LandCoverType, } from '@shared/components/SpectralProfileTool'; -import { findMostSimilarLandCoverType } from './helper'; import { useSpectralProfileChartData } from './useSpectralProfileChartData'; import { debounce } from '@shared/utils/snippets/debounce'; import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; -import { getFillColorByLandCoverType } from '@shared/components/SpectralProfileTool/helpers'; +import { + getFillColorByLandCoverType, + findMostSimilarLandCoverType, +} from '@shared/components/SpectralProfileTool/helpers'; import { ListOfLandCoverTypes } from '@shared/components/SpectralProfileTool/config'; import { LandsatSpectralProfileData } from './config'; diff --git a/src/landsat-explorer/components/SpectralTool/helper.ts b/src/landsat-explorer/components/SpectralTool/helper.ts index 012a3517..b61f6513 100644 --- a/src/landsat-explorer/components/SpectralTool/helper.ts +++ b/src/landsat-explorer/components/SpectralTool/helper.ts @@ -15,53 +15,8 @@ import { LineChartDataItem } from '@vannizhang/react-d3-charts/dist/LineChart/types'; -import { - SpectralProfileDataByLandCoverType, - LandCoverType, -} from '@shared/components/SpectralProfileTool'; import { normalizeBandValue } from '@shared/components/SpectralProfileTool/helpers'; -/** - * given an array of band values from user selected location and the spectral data lookup table, - * find the land cover type that is most similar to it. - * - * @param {number[]} bandValues band value from user selected location - * @param {Spectr} spectralProfileData lookup table that provide typical spectral profile data for different land cover types - * @returns {LandCoverType} name of the land cover type (feature of interest) that has the spectral profile that is most similar to the into band values - */ -export const findMostSimilarLandCoverType = ( - bandValues: number[], - spectralProfileData: SpectralProfileDataByLandCoverType -): LandCoverType => { - // let minSumOfDifferences = Infinity; - let minSumOfSquaredDifferences = Infinity; - let output: LandCoverType = null; - - for (const [landCoverType, value] of Object.entries(spectralProfileData)) { - // let sumOfDiff = 0; - let sumOfSquaredDiff = 0; - - const len = Math.min(bandValues.length, value.length); - - for (let i = 0; i < len; i++) { - const diff = Math.abs(bandValues[i] - value[i]); - // sumOfDiff += diff; - - // By squaring the differences, larger deviations from the expected values will have a - // more significant impact on the total difference. - // Therefore, it might provide a more accurate measure of similarity between spectral profiles. - sumOfSquaredDiff += diff * diff; - } - - if (sumOfSquaredDiff < minSumOfSquaredDifferences) { - minSumOfSquaredDifferences = sumOfSquaredDiff; - output = landCoverType as LandCoverType; - } - } - - return output; -}; - /** * Converts an array of Landsat Band Values to an array of LineChartDataItem objects. * diff --git a/src/shared/components/SpectralProfileTool/helpers.ts b/src/shared/components/SpectralProfileTool/helpers.ts index 2b4c323f..ca0c91b9 100644 --- a/src/shared/components/SpectralProfileTool/helpers.ts +++ b/src/shared/components/SpectralProfileTool/helpers.ts @@ -1,4 +1,4 @@ -import { LandCoverType } from './config'; +import { LandCoverType, SpectralProfileDataByLandCoverType } from './config'; const FILL_COLOR_BY_LAND_COVER_TYPE: Record = { Cloud: '#888888', @@ -45,3 +45,48 @@ export const normalizeBandValue = ( value = Math.max(value, lowerEnd); return value; }; + +/** + * Given an array of band values from a user-selected location and a spectral data lookup table, + * find the land cover type that is most similar to the provided band values. + * + * The function compares the band values against typical spectral profiles for different land cover types. + * It uses the sum of squared differences between the band values and each spectral profile to determine similarity. + * The land cover type with the smallest sum of squared differences is returned as the most similar match. + * + * @param {number[]} bandValues - Array of band values from the user-selected location. + * @param {SpectralProfileDataByLandCoverType} spectralProfileData - Lookup table providing typical spectral profiles for different land cover types. + * @returns {LandCoverType} The land cover type that has the spectral profile most similar to the input band values. + */ +export const findMostSimilarLandCoverType = ( + bandValues: number[], + spectralProfileData: SpectralProfileDataByLandCoverType +): LandCoverType => { + // let minSumOfDifferences = Infinity; + let minSumOfSquaredDifferences = Infinity; + let output: LandCoverType = null; + + for (const [landCoverType, value] of Object.entries(spectralProfileData)) { + // let sumOfDiff = 0; + let sumOfSquaredDiff = 0; + + const len = Math.min(bandValues.length, value.length); + + for (let i = 0; i < len; i++) { + const diff = Math.abs(bandValues[i] - value[i]); + // sumOfDiff += diff; + + // By squaring the differences, larger deviations from the expected values will have a + // more significant impact on the total difference. + // Therefore, it might provide a more accurate measure of similarity between spectral profiles. + sumOfSquaredDiff += diff * diff; + } + + if (sumOfSquaredDiff < minSumOfSquaredDifferences) { + minSumOfSquaredDifferences = sumOfSquaredDiff; + output = landCoverType as LandCoverType; + } + } + + return output; +}; diff --git a/src/spectral-sampling-tool/components/SamplingResults/useAveragedSamplingResults.tsx b/src/spectral-sampling-tool/components/SamplingResults/useAveragedSamplingResults.tsx index c2ae2fcf..030127c7 100644 --- a/src/spectral-sampling-tool/components/SamplingResults/useAveragedSamplingResults.tsx +++ b/src/spectral-sampling-tool/components/SamplingResults/useAveragedSamplingResults.tsx @@ -13,7 +13,6 @@ * limitations under the License. */ -import { formatLandsatBandValuesAsLineChartDataItems } from '@landsat-explorer/components/SpectralTool/helper'; import { selectSelectedSpectralSamplingPointData, selectSpectralSamplingPointsData, From 9fa69d7daab9aed3781a699e104e591a21d653ac Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 4 Sep 2024 16:55:45 -0700 Subject: [PATCH 029/306] refactor(shared): move useGenerateSpectralProfileChartData to '@shared/components/SpectralProfileTool' --- .../SpectralTool/SpectralToolContainer.tsx | 5 +- .../components/SpectralTool/helper.ts | 42 --------- .../useSpectralProfileChartData.tsx | 68 -------------- .../components/SpectralProfileTool/helpers.ts | 28 ++++++ .../components/SpectralProfileTool/index.ts | 1 + .../useGenerateSpectralProfileChartData.tsx | 94 +++++++++++++++++++ .../SamplingResults/useChartData.tsx | 8 +- 7 files changed, 129 insertions(+), 117 deletions(-) delete mode 100644 src/landsat-explorer/components/SpectralTool/helper.ts delete mode 100644 src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx create mode 100644 src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index 7158d9fb..d9d2f856 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -33,8 +33,8 @@ import { SpectralProfileChart, SpectralProfileChartLegend, LandCoverType, + useGenerateSpectralProfileChartData, } from '@shared/components/SpectralProfileTool'; -import { useSpectralProfileChartData } from './useSpectralProfileChartData'; import { debounce } from '@shared/utils/snippets/debounce'; import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; import { @@ -65,8 +65,9 @@ export const SpectralToolContainer = () => { const [selectedLandCoverType, setSelectedLandCoverType] = useState(); - const chartData = useSpectralProfileChartData( + const chartData = useGenerateSpectralProfileChartData( spectralProfileData, + LandsatSpectralProfileData[selectedLandCoverType], selectedLandCoverType ); diff --git a/src/landsat-explorer/components/SpectralTool/helper.ts b/src/landsat-explorer/components/SpectralTool/helper.ts deleted file mode 100644 index b61f6513..00000000 --- a/src/landsat-explorer/components/SpectralTool/helper.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { LineChartDataItem } from '@vannizhang/react-d3-charts/dist/LineChart/types'; - -import { normalizeBandValue } from '@shared/components/SpectralProfileTool/helpers'; - -/** - * Converts an array of Landsat Band Values to an array of LineChartDataItem objects. - * - * This function takes an array of numeric band values and processes them to create LineChartDataItem objects. - * It keeps only the first 7 bands and normalizes the values to ensure they fall within the range 0-1. - * - * @param bandValues - An array of numeric values representing Landsat band values. - * @returns An array of LineChartDataItem objects with normalized x and y values. - */ -export const formatLandsatBandValuesAsLineChartDataItems = ( - bandValues: number[] -) => { - if (!bandValues || !bandValues.length) { - return []; - } - - return bandValues.slice(0, 7).map((val, index) => { - return { - x: index, - y: normalizeBandValue(val, 0, 1), - } as LineChartDataItem; - }); -}; diff --git a/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx b/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx deleted file mode 100644 index 228f8366..00000000 --- a/src/landsat-explorer/components/SpectralTool/useSpectralProfileChartData.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { FC, useMemo } from 'react'; -import { LineGroupData } from '@vannizhang/react-d3-charts/dist/MultipleLinesChart/types'; -import { formatLandsatBandValuesAsLineChartDataItems } from './helper'; -import { LandCoverType } from '@shared/components/SpectralProfileTool'; -import { getFillColorByLandCoverType } from '@shared/components/SpectralProfileTool/helpers'; -import { LandsatSpectralProfileData } from './config'; - -/** - * This is a custom hook that convert the band values from user selected location and - * the matched land cover type into an array of LineGroupData - * - * @param data spectral profile chart data for user selected query location - * @param {LandCoverType} landCoverType matched or user selected land cover type (feature of interest) for the spectral profile tool - * @returns - */ -export const useSpectralProfileChartData = ( - data: number[], - landCoverType: LandCoverType -) => { - const chartData: LineGroupData[] = useMemo(() => { - if ( - !data || - !data.length || - !landCoverType || - !LandsatSpectralProfileData[landCoverType] - ) { - return []; - } - - const spectralProfileData4UserSelectedLocation = - formatLandsatBandValuesAsLineChartDataItems(data); - - const spectralProfileData4SelectedLandCoverType = - formatLandsatBandValuesAsLineChartDataItems( - LandsatSpectralProfileData[landCoverType] - ); - - return [ - { - fill: 'var(--custom-light-blue-90)', - key: 'selected-location', - values: spectralProfileData4UserSelectedLocation, - } as LineGroupData, - { - fill: getFillColorByLandCoverType(landCoverType), //'var(--custom-light-blue-70)', - key: landCoverType, - values: spectralProfileData4SelectedLandCoverType, - dashPattern: '9 3', // use dash pattern to provide user a hint that the feature of interest is just a reference - } as LineGroupData, - ]; - }, [data, landCoverType]); - - return chartData; -}; diff --git a/src/shared/components/SpectralProfileTool/helpers.ts b/src/shared/components/SpectralProfileTool/helpers.ts index ca0c91b9..1e3115c1 100644 --- a/src/shared/components/SpectralProfileTool/helpers.ts +++ b/src/shared/components/SpectralProfileTool/helpers.ts @@ -1,3 +1,4 @@ +import { LineChartDataItem } from '@vannizhang/react-d3-charts/dist/LineChart/types'; import { LandCoverType, SpectralProfileDataByLandCoverType } from './config'; const FILL_COLOR_BY_LAND_COVER_TYPE: Record = { @@ -90,3 +91,30 @@ export const findMostSimilarLandCoverType = ( return output; }; + +/** + * Converts an array of band values to an array of LineChartDataItem objects. + * + * This function processes an array of numeric band values, selecting up to a specified maximum number of bands. + * It normalizes the values to ensure they fall within the range of 0 to 1, and then maps each band value to + * a LineChartDataItem object with `x` and `y` coordinates, where `x` represents the band index and `y` the normalized value. + * + * @param {number[]} bandValues - An array of numeric values representing imagery band values. + * @param {number} length - The number of band values to include in the output array. + * @returns {LineChartDataItem[]} An array of LineChartDataItem objects with normalized x and y values. + */ +export const formatBandValuesAsLineChartDataItems = ( + bandValues: number[], + length?: number +) => { + if (!bandValues || !bandValues.length) { + return []; + } + + return bandValues.slice(0, length).map((val, index) => { + return { + x: index, + y: normalizeBandValue(val, 0, 1), + } as LineChartDataItem; + }); +}; diff --git a/src/shared/components/SpectralProfileTool/index.ts b/src/shared/components/SpectralProfileTool/index.ts index 203ef8c9..cff3d9e8 100644 --- a/src/shared/components/SpectralProfileTool/index.ts +++ b/src/shared/components/SpectralProfileTool/index.ts @@ -1,5 +1,6 @@ export { SpectralProfileChart } from './SpectralProfileChart'; export { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; +export { useGenerateSpectralProfileChartData } from './useGenerateSpectralProfileChartData'; export type { LandCoverType, SpectralProfileDataByLandCoverType, diff --git a/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx b/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx new file mode 100644 index 00000000..c7a2e57c --- /dev/null +++ b/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx @@ -0,0 +1,94 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { FC, useMemo } from 'react'; +import { LineGroupData } from '@vannizhang/react-d3-charts/dist/MultipleLinesChart/types'; +import { LandCoverType } from '@shared/components/SpectralProfileTool'; +import { + formatBandValuesAsLineChartDataItems, + getFillColorByLandCoverType, +} from '@shared/components/SpectralProfileTool/helpers'; + +/** + * A custom hook that converts the band values from a user-selected location and + * the matched land cover type into an array of LineGroupData, suitable for use + * in a Spectral Profile Chart. + * + * This hook processes band values from both the user's selected location and a reference land cover type, + * normalizes the data, and formats it for display in the chart. It limits the number of bands used by + * taking the minimum of the available bands from both sources. + * + * @param {number[]} bandValuesFromSelectedLocation - Array of band values from the user-selected location. + * @param {number[]} bandValuesFromSelectedLandCoverType - Array of band values from the matched or user-selected land cover type. + * @param {LandCoverType} landCoverType - The name of the matched or user-selected land cover type to be displayed in the Spectral Profile Chart. + * + * @returns {LineGroupData[]} An array of LineGroupData objects for populating the Spectral Profile Chart. + */ +export const useGenerateSpectralProfileChartData = ( + bandValuesFromSelectedLocation: number[], + bandValuesFromSelectedLandCoverType: number[], + landCoverType: LandCoverType +) => { + const chartData: LineGroupData[] = useMemo(() => { + if ( + !bandValuesFromSelectedLocation || + !bandValuesFromSelectedLandCoverType || + !bandValuesFromSelectedLocation?.length || + !bandValuesFromSelectedLandCoverType?.length || + !landCoverType + ) { + return []; + } + + // The band values from the selected location may contain more data than necessary for display. + // To ensure only the required number of band values are used, calculate the appropriate length + // by taking the minimum of the available band values from both the selected location and the selected land cover type. + const length = Math.min( + bandValuesFromSelectedLocation.length, + bandValuesFromSelectedLandCoverType.length + ); + + const lineChartData4SelectedLocation = + formatBandValuesAsLineChartDataItems( + bandValuesFromSelectedLocation, + length + ); + + const lineChartData4SelectedLandCoverType = + formatBandValuesAsLineChartDataItems( + bandValuesFromSelectedLandCoverType, + length + ); + + return [ + { + fill: 'var(--custom-light-blue-90)', + key: 'selected-location', + values: lineChartData4SelectedLocation, + } as LineGroupData, + { + fill: getFillColorByLandCoverType(landCoverType), //'var(--custom-light-blue-70)', + key: landCoverType, + values: lineChartData4SelectedLandCoverType, + dashPattern: '9 3', // use dash pattern to provide user a hint that the feature of interest is just a reference + } as LineGroupData, + ]; + }, [ + bandValuesFromSelectedLocation, + bandValuesFromSelectedLandCoverType, + landCoverType, + ]); + + return chartData; +}; diff --git a/src/spectral-sampling-tool/components/SamplingResults/useChartData.tsx b/src/spectral-sampling-tool/components/SamplingResults/useChartData.tsx index 5aeffe7c..853e8e32 100644 --- a/src/spectral-sampling-tool/components/SamplingResults/useChartData.tsx +++ b/src/spectral-sampling-tool/components/SamplingResults/useChartData.tsx @@ -13,7 +13,7 @@ * limitations under the License. */ -import { formatLandsatBandValuesAsLineChartDataItems } from '@landsat-explorer/components/SpectralTool/helper'; +import { formatBandValuesAsLineChartDataItems } from '@shared/components/SpectralProfileTool/helpers'; import { selectIdOfItem2Highlight, selectSelectedSpectralSamplingPointData, @@ -52,7 +52,7 @@ export const useChartData = () => { const output: LineGroupData[] = samplingPointsData .filter((d) => d.location && d.bandValues) .map((d, index) => { - const values = formatLandsatBandValuesAsLineChartDataItems( + const values = formatBandValuesAsLineChartDataItems( d.bandValues ); @@ -77,9 +77,7 @@ export const useChartData = () => { output.push({ fill: 'var(--custom-light-blue-90)', key: 'average', - values: formatLandsatBandValuesAsLineChartDataItems( - averageBandValues - ), + values: formatBandValuesAsLineChartDataItems(averageBandValues), dashPattern: '9 3', // use dash pattern to provide user a hint that the feature of interest is just a reference }); } From 38ff58730757eced4ec9d823ca2037c89ef3a254 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 5 Sep 2024 12:23:15 -0700 Subject: [PATCH 030/306] refactor(shared): add useFetchSpectralProfileToolData custom hook --- .../SpectralTool/SpectralToolContainer.tsx | 55 ++++++--------- .../useUpdateSpectralProfileToolData.tsx | 69 +++++++++++++++++++ .../store/SpectralProfileTool/thunks.ts | 19 ++++- 3 files changed, 105 insertions(+), 38 deletions(-) create mode 100644 src/shared/components/SpectralProfileTool/useUpdateSpectralProfileToolData.tsx diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index d9d2f856..4e7ea9b5 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -14,20 +14,14 @@ */ import { AnalysisToolHeader } from '@shared/components/AnalysisToolHeader'; -import { - selectActiveAnalysisTool, - selectQueryParams4SceneInSelectedMode, -} from '@shared/store/ImageryScene/selectors'; +import { selectActiveAnalysisTool } from '@shared/store/ImageryScene/selectors'; import { selectData4SpectralProfileTool, selectError4SpectralProfileTool, selectIsLoadingData4SpectralProfileTool, - selectQueryLocation4SpectralProfileTool, } from '@shared/store/SpectralProfileTool/selectors'; -import { updateSpectralProfileData } from '@shared/store/SpectralProfileTool/thunks'; import classNames from 'classnames'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useDispatch } from 'react-redux'; import { useSelector } from 'react-redux'; import { SpectralProfileChart, @@ -35,7 +29,6 @@ import { LandCoverType, useGenerateSpectralProfileChartData, } from '@shared/components/SpectralProfileTool'; -import { debounce } from '@shared/utils/snippets/debounce'; import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; import { getFillColorByLandCoverType, @@ -43,17 +36,14 @@ import { } from '@shared/components/SpectralProfileTool/helpers'; import { ListOfLandCoverTypes } from '@shared/components/SpectralProfileTool/config'; import { LandsatSpectralProfileData } from './config'; +import { Point } from '@arcgis/core/geometry'; +import { getLandsatPixelValues } from '@shared/services/landsat-level-2/getLandsatPixelValues'; +import { useFetchSpectralProfileToolData } from '@shared/components/SpectralProfileTool/useUpdateSpectralProfileToolData'; +import { FetchPixelValuesFuncParams } from '@shared/store/SpectralProfileTool/thunks'; export const SpectralToolContainer = () => { - const dispatch = useDispatch(); - const tool = useSelector(selectActiveAnalysisTool); - const { objectIdOfSelectedScene } = - useSelector(selectQueryParams4SceneInSelectedMode) || {}; - - const queryLocation = useSelector(selectQueryLocation4SpectralProfileTool); - const isLoading = useSelector(selectIsLoadingData4SpectralProfileTool); const spectralProfileData = useSelector(selectData4SpectralProfileTool); @@ -99,29 +89,24 @@ export const SpectralToolContainer = () => { return true; }, [isLoading, error4SpectralProfileTool, spectralProfileData]); - const updateSpectralProfileDataDebounced = useCallback( - debounce(async () => { - dispatch(updateSpectralProfileData()); - }, 200), + const fetchLandsatPixelValuesFunc = useCallback( + async ({ + point, + objectIds, + abortController, + }: FetchPixelValuesFuncParams) => { + const res: number[] = await getLandsatPixelValues({ + point, + objectIds, + abortController, + }); + + return res; + }, [] ); - // triggers when user selects a new query location - useEffect(() => { - (async () => { - if (tool !== 'spectral') { - return; - } - - // When the user selects a new acquisition date from the calendar, - // the currently selected imagery scene is deselected first, - // followed by the selection of a new scene. These two actions occur sequentially, - // potentially causing `updateSpectralProfileData` to be called twice in rapid succession, - // resulting in unnecessary network requests being triggered. To mitigate this issue, - // we need to debounce the `updateSpectralProfileData` function with a 200 ms delay. - updateSpectralProfileDataDebounced(); - })(); - }, [queryLocation, objectIdOfSelectedScene]); + useFetchSpectralProfileToolData(fetchLandsatPixelValuesFunc); useEffect(() => { if (!spectralProfileData || !spectralProfileData.length) { diff --git a/src/shared/components/SpectralProfileTool/useUpdateSpectralProfileToolData.tsx b/src/shared/components/SpectralProfileTool/useUpdateSpectralProfileToolData.tsx new file mode 100644 index 00000000..b5fcf1eb --- /dev/null +++ b/src/shared/components/SpectralProfileTool/useUpdateSpectralProfileToolData.tsx @@ -0,0 +1,69 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + selectActiveAnalysisTool, + selectQueryParams4SceneInSelectedMode, +} from '@shared/store/ImageryScene/selectors'; +import { selectQueryLocation4SpectralProfileTool } from '@shared/store/SpectralProfileTool/selectors'; +import { + FetchPixelValuesFunc, + updateSpectralProfileData, +} from '@shared/store/SpectralProfileTool/thunks'; +import { debounce } from '@shared/utils/snippets/debounce'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; + +/** + * This custom hook triggers `updateSpectralProfileData` thunk function to get spectral profile data for the user-selected location + * when query location or selected scene are changed. + * @param {FetchPixelValuesFunc} fetchPixelBandValuesFunc - async function that retrieves the band values from the pixel that intersects with the user-selected location. This function will be invoked by the `updateSpectralProfileData` thunk function. + */ +export const useFetchSpectralProfileToolData = ( + fetchPixelBandValuesFunc: FetchPixelValuesFunc +) => { + const dispatch = useDispatch(); + + const { objectIdOfSelectedScene } = + useSelector(selectQueryParams4SceneInSelectedMode) || {}; + + const queryLocation = useSelector(selectQueryLocation4SpectralProfileTool); + + const tool = useSelector(selectActiveAnalysisTool); + + const updateSpectralProfileDataDebounced = useCallback( + debounce(async () => { + dispatch(updateSpectralProfileData(fetchPixelBandValuesFunc)); + }, 200), + [fetchPixelBandValuesFunc] + ); + + // triggers when user selects a new query location + useEffect(() => { + (async () => { + if (tool !== 'spectral') { + return; + } + + // When the user selects a new acquisition date from the calendar, + // the currently selected imagery scene is deselected first, + // followed by the selection of a new scene. These two actions occur sequentially, + // potentially causing `updateSpectralProfileData` to be called twice in rapid succession, + // resulting in unnecessary network requests being triggered. To mitigate this issue, + // we need to debounce the `updateSpectralProfileData` function with a 200 ms delay. + updateSpectralProfileDataDebounced(); + })(); + }, [queryLocation, objectIdOfSelectedScene]); +}; diff --git a/src/shared/store/SpectralProfileTool/thunks.ts b/src/shared/store/SpectralProfileTool/thunks.ts index 375ca725..3d098436 100644 --- a/src/shared/store/SpectralProfileTool/thunks.ts +++ b/src/shared/store/SpectralProfileTool/thunks.ts @@ -26,7 +26,19 @@ import { selectActiveAnalysisTool, selectQueryParams4MainScene, } from '../ImageryScene/selectors'; -import { getLandsatPixelValues } from '@shared/services/landsat-level-2/getLandsatPixelValues'; + +export type FetchPixelValuesFuncParams = { + point: Point; + objectIds: number[]; + abortController: AbortController; +}; + +/** + * Type definition for an async function that retrieves the pixel band values from user-selected location. + */ +export type FetchPixelValuesFunc = ( + params: FetchPixelValuesFuncParams +) => Promise; let abortController: AbortController = null; @@ -43,7 +55,8 @@ export const updateQueryLocation4SpectralProfileTool = }; export const updateSpectralProfileData = - () => async (dispatch: StoreDispatch, getState: StoreGetState) => { + (fetchPixelValuesFunc: FetchPixelValuesFunc) => + async (dispatch: StoreDispatch, getState: StoreGetState) => { const rootState = getState(); const queryLocation = @@ -69,7 +82,7 @@ export const updateSpectralProfileData = dispatch(errorChanged(null)); try { - const bandValues = await getLandsatPixelValues({ + const bandValues = await fetchPixelValuesFunc({ point: queryLocation, objectIds: [objectIdOfSelectedScene], abortController, From b030158fe3ba91f9934c8226a8c908c6cabee4c4 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 5 Sep 2024 12:30:51 -0700 Subject: [PATCH 031/306] refactor(shared): add SpectralProfileToolMessage component --- .../SpectralTool/SpectralToolContainer.tsx | 36 +----------- .../SpectralProfileToolMessage.tsx | 58 +++++++++++++++++++ .../components/SpectralProfileTool/index.ts | 1 + 3 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 src/shared/components/SpectralProfileTool/SpectralProfileToolMessage.tsx diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index 4e7ea9b5..c453c5d3 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -28,6 +28,7 @@ import { SpectralProfileChartLegend, LandCoverType, useGenerateSpectralProfileChartData, + SpectralProfileToolMessage, } from '@shared/components/SpectralProfileTool'; import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; import { @@ -36,7 +37,6 @@ import { } from '@shared/components/SpectralProfileTool/helpers'; import { ListOfLandCoverTypes } from '@shared/components/SpectralProfileTool/config'; import { LandsatSpectralProfileData } from './config'; -import { Point } from '@arcgis/core/geometry'; import { getLandsatPixelValues } from '@shared/services/landsat-level-2/getLandsatPixelValues'; import { useFetchSpectralProfileToolData } from '@shared/components/SpectralProfileTool/useUpdateSpectralProfileToolData'; import { FetchPixelValuesFuncParams } from '@shared/store/SpectralProfileTool/thunks'; @@ -61,22 +61,6 @@ export const SpectralToolContainer = () => { selectedLandCoverType ); - const spectralProfileToolMessage = useMemo(() => { - if (isLoading) { - return 'fetching spectral profile data'; - } - - if (error4SpectralProfileTool) { - return error4SpectralProfileTool; - } - - if (!spectralProfileData.length) { - return 'Select a scene and click on the map to identify the spectral profile for the point of interest.'; - } - - return ''; - }, [isLoading, error4SpectralProfileTool, spectralProfileData]); - const shouldShowChart = useMemo(() => { if (isLoading || error4SpectralProfileTool) { return false; @@ -126,14 +110,7 @@ export const SpectralToolContainer = () => { } return ( -
+
{ )} - {spectralProfileToolMessage && ( -
- {isLoading && } -

- {spectralProfileToolMessage} -

-
- )} +
); }; diff --git a/src/shared/components/SpectralProfileTool/SpectralProfileToolMessage.tsx b/src/shared/components/SpectralProfileTool/SpectralProfileToolMessage.tsx new file mode 100644 index 00000000..f13cbb5d --- /dev/null +++ b/src/shared/components/SpectralProfileTool/SpectralProfileToolMessage.tsx @@ -0,0 +1,58 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + selectData4SpectralProfileTool, + selectError4SpectralProfileTool, + selectIsLoadingData4SpectralProfileTool, +} from '@shared/store/SpectralProfileTool/selectors'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; + +export const SpectralProfileToolMessage = () => { + const isLoading = useSelector(selectIsLoadingData4SpectralProfileTool); + + const spectralProfileData = useSelector(selectData4SpectralProfileTool); + + const error4SpectralProfileTool = useSelector( + selectError4SpectralProfileTool + ); + + const spectralProfileToolMessage = useMemo(() => { + if (isLoading) { + return 'fetching spectral profile data'; + } + + if (error4SpectralProfileTool) { + return error4SpectralProfileTool; + } + + if (!spectralProfileData.length) { + return 'Select a scene and click on the map to identify the spectral profile for the point of interest.'; + } + + return ''; + }, [isLoading, error4SpectralProfileTool, spectralProfileData]); + + if (!spectralProfileToolMessage) { + return null; + } + + return ( +
+ {isLoading && } +

{spectralProfileToolMessage}

+
+ ); +}; diff --git a/src/shared/components/SpectralProfileTool/index.ts b/src/shared/components/SpectralProfileTool/index.ts index cff3d9e8..bc8555a6 100644 --- a/src/shared/components/SpectralProfileTool/index.ts +++ b/src/shared/components/SpectralProfileTool/index.ts @@ -1,5 +1,6 @@ export { SpectralProfileChart } from './SpectralProfileChart'; export { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; +export { SpectralProfileToolMessage } from './SpectralProfileToolMessage'; export { useGenerateSpectralProfileChartData } from './useGenerateSpectralProfileChartData'; export type { LandCoverType, From 07653dad87e72913997989574dd6a31d03619e0d Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 5 Sep 2024 12:57:10 -0700 Subject: [PATCH 032/306] refactor(shared): add SpectralProfileToolContainer component --- .../components/Layout/Layout.tsx | 4 +- .../SpectralTool/SpectralToolContainer.tsx | 98 +----------- .../components/SpectralTool/index.ts | 2 +- .../SpectralProfileToolContainer.tsx | 143 ++++++++++++++++++ .../components/SpectralProfileTool/index.ts | 4 +- .../useGenerateSpectralProfileChartData.tsx | 18 ++- 6 files changed, 165 insertions(+), 104 deletions(-) create mode 100644 src/shared/components/SpectralProfileTool/SpectralProfileToolContainer.tsx diff --git a/src/landsat-explorer/components/Layout/Layout.tsx b/src/landsat-explorer/components/Layout/Layout.tsx index c036d53d..c83376e3 100644 --- a/src/landsat-explorer/components/Layout/Layout.tsx +++ b/src/landsat-explorer/components/Layout/Layout.tsx @@ -35,7 +35,7 @@ import { SwipeLayerSelector } from '@shared/components/SwipeLayerSelector'; import { useSaveAppState2HashParams } from '@shared/hooks/useSaveAppState2HashParams'; import { IS_MOBILE_DEVICE } from '@shared/constants/UI'; // import { DynamicModeInfo } from '@shared/components/DynamicModeInfo'; -import { SpectralTool } from '../SpectralTool'; +import { LandsatSpectralProfileTool } from '../SpectralTool'; import { ChangeCompareLayerSelector } from '@shared/components/ChangeCompareLayerSelector'; import { ChangeCompareTool } from '../ChangeCompareTool'; import { appConfig } from '@shared/config'; @@ -124,7 +124,7 @@ const Layout = () => {
- +
)} diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index c453c5d3..364177be 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -23,56 +23,16 @@ import { import classNames from 'classnames'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; -import { - SpectralProfileChart, - SpectralProfileChartLegend, - LandCoverType, - useGenerateSpectralProfileChartData, - SpectralProfileToolMessage, -} from '@shared/components/SpectralProfileTool'; +import { SpectralProfileTool } from '@shared/components/SpectralProfileTool'; import { LANDSAT_BAND_NAMES } from '@shared/services/landsat-level-2/config'; -import { - getFillColorByLandCoverType, - findMostSimilarLandCoverType, -} from '@shared/components/SpectralProfileTool/helpers'; -import { ListOfLandCoverTypes } from '@shared/components/SpectralProfileTool/config'; import { LandsatSpectralProfileData } from './config'; import { getLandsatPixelValues } from '@shared/services/landsat-level-2/getLandsatPixelValues'; import { useFetchSpectralProfileToolData } from '@shared/components/SpectralProfileTool/useUpdateSpectralProfileToolData'; import { FetchPixelValuesFuncParams } from '@shared/store/SpectralProfileTool/thunks'; -export const SpectralToolContainer = () => { +export const LandsatSpectralProfileTool = () => { const tool = useSelector(selectActiveAnalysisTool); - const isLoading = useSelector(selectIsLoadingData4SpectralProfileTool); - - const spectralProfileData = useSelector(selectData4SpectralProfileTool); - - const error4SpectralProfileTool = useSelector( - selectError4SpectralProfileTool - ); - - const [selectedLandCoverType, setSelectedLandCoverType] = - useState(); - - const chartData = useGenerateSpectralProfileChartData( - spectralProfileData, - LandsatSpectralProfileData[selectedLandCoverType], - selectedLandCoverType - ); - - const shouldShowChart = useMemo(() => { - if (isLoading || error4SpectralProfileTool) { - return false; - } - - if (!spectralProfileData || !spectralProfileData.length) { - return false; - } - - return true; - }, [isLoading, error4SpectralProfileTool, spectralProfileData]); - const fetchLandsatPixelValuesFunc = useCallback( async ({ point, @@ -92,60 +52,14 @@ export const SpectralToolContainer = () => { useFetchSpectralProfileToolData(fetchLandsatPixelValuesFunc); - useEffect(() => { - if (!spectralProfileData || !spectralProfileData.length) { - return; - } - - const mostSimilarLandCoverType = findMostSimilarLandCoverType( - spectralProfileData, - LandsatSpectralProfileData - ); - - setSelectedLandCoverType(mostSimilarLandCoverType); - }, [spectralProfileData]); - if (tool !== 'spectral') { return null; } return ( -
- { - return { - value: landCoverType, - }; - } - )} - selectedValue={selectedLandCoverType} - tooltipText={`The spectral reflectance of different materials on the Earth's surface is variable. Spectral profiles can be used to identify different land cover types.`} - dropdownMenuSelectedItemOnChange={(val) => { - setSelectedLandCoverType(val as LandCoverType); - }} - /> - - {shouldShowChart && ( - <> -
- -
- - - - )} - - -
+ ); }; diff --git a/src/landsat-explorer/components/SpectralTool/index.ts b/src/landsat-explorer/components/SpectralTool/index.ts index 0e6fb20e..d939e863 100644 --- a/src/landsat-explorer/components/SpectralTool/index.ts +++ b/src/landsat-explorer/components/SpectralTool/index.ts @@ -13,4 +13,4 @@ * limitations under the License. */ -export { SpectralToolContainer as SpectralTool } from './SpectralToolContainer'; +export { LandsatSpectralProfileTool } from './SpectralToolContainer'; diff --git a/src/shared/components/SpectralProfileTool/SpectralProfileToolContainer.tsx b/src/shared/components/SpectralProfileTool/SpectralProfileToolContainer.tsx new file mode 100644 index 00000000..f0a97f5e --- /dev/null +++ b/src/shared/components/SpectralProfileTool/SpectralProfileToolContainer.tsx @@ -0,0 +1,143 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AnalysisToolHeader } from '@shared/components/AnalysisToolHeader'; +import { selectActiveAnalysisTool } from '@shared/store/ImageryScene/selectors'; +import { + selectData4SpectralProfileTool, + selectError4SpectralProfileTool, + selectIsLoadingData4SpectralProfileTool, +} from '@shared/store/SpectralProfileTool/selectors'; +import classNames from 'classnames'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { LandCoverType } from '@shared/components/SpectralProfileTool'; +import { + getFillColorByLandCoverType, + findMostSimilarLandCoverType, +} from '@shared/components/SpectralProfileTool/helpers'; +import { + ListOfLandCoverTypes, + SpectralProfileDataByLandCoverType, +} from '@shared/components/SpectralProfileTool/config'; +import { useGenerateSpectralProfileChartData } from './useGenerateSpectralProfileChartData'; +import { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; +import { SpectralProfileToolMessage } from './SpectralProfileToolMessage'; +import { SpectralProfileChart } from './SpectralProfileChart'; + +type Props = { + /** + * A lookup table containing spectral profile data for various land cover types. + */ + spectralProfileDataByLandCoverTypes: SpectralProfileDataByLandCoverType; + /** + * A list of descriptive names for each spectral band. These names will be + * used as labels for the bottom axis of the chart. + */ + bandNames: string[]; +}; + +export const SpectralProfileToolContainer: FC = ({ + spectralProfileDataByLandCoverTypes, + bandNames, +}) => { + const tool = useSelector(selectActiveAnalysisTool); + + const isLoading = useSelector(selectIsLoadingData4SpectralProfileTool); + + const spectralProfileData = useSelector(selectData4SpectralProfileTool); + + const error4SpectralProfileTool = useSelector( + selectError4SpectralProfileTool + ); + + const [selectedLandCoverType, setSelectedLandCoverType] = + useState(); + + const chartData = useGenerateSpectralProfileChartData( + spectralProfileData, + spectralProfileDataByLandCoverTypes, + selectedLandCoverType + ); + + const shouldShowChart = useMemo(() => { + if (isLoading || error4SpectralProfileTool) { + return false; + } + + if (!spectralProfileData || !spectralProfileData.length) { + return false; + } + + return true; + }, [isLoading, error4SpectralProfileTool, spectralProfileData]); + + useEffect(() => { + if (!spectralProfileData || !spectralProfileData.length) { + return; + } + + const mostSimilarLandCoverType = findMostSimilarLandCoverType( + spectralProfileData, + spectralProfileDataByLandCoverTypes + ); + + setSelectedLandCoverType(mostSimilarLandCoverType); + }, [spectralProfileData]); + + if (tool !== 'spectral') { + return null; + } + + return ( +
+ { + return { + value: landCoverType, + }; + } + )} + selectedValue={selectedLandCoverType} + tooltipText={`The spectral reflectance of different materials on the Earth's surface is variable. Spectral profiles can be used to identify different land cover types.`} + dropdownMenuSelectedItemOnChange={(val) => { + setSelectedLandCoverType(val as LandCoverType); + }} + /> + + {shouldShowChart && ( + <> +
+ +
+ + + + )} + + +
+ ); +}; diff --git a/src/shared/components/SpectralProfileTool/index.ts b/src/shared/components/SpectralProfileTool/index.ts index bc8555a6..db8c8ef0 100644 --- a/src/shared/components/SpectralProfileTool/index.ts +++ b/src/shared/components/SpectralProfileTool/index.ts @@ -1,7 +1,5 @@ +export { SpectralProfileToolContainer as SpectralProfileTool } from './SpectralProfileToolContainer'; export { SpectralProfileChart } from './SpectralProfileChart'; -export { SpectralProfileChartLegend } from './SpectralProfileChartLegend'; -export { SpectralProfileToolMessage } from './SpectralProfileToolMessage'; -export { useGenerateSpectralProfileChartData } from './useGenerateSpectralProfileChartData'; export type { LandCoverType, SpectralProfileDataByLandCoverType, diff --git a/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx b/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx index c7a2e57c..57a68325 100644 --- a/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx +++ b/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx @@ -14,7 +14,10 @@ */ import React, { FC, useMemo } from 'react'; import { LineGroupData } from '@vannizhang/react-d3-charts/dist/MultipleLinesChart/types'; -import { LandCoverType } from '@shared/components/SpectralProfileTool'; +import { + LandCoverType, + SpectralProfileDataByLandCoverType, +} from '@shared/components/SpectralProfileTool'; import { formatBandValuesAsLineChartDataItems, getFillColorByLandCoverType, @@ -30,27 +33,30 @@ import { * taking the minimum of the available bands from both sources. * * @param {number[]} bandValuesFromSelectedLocation - Array of band values from the user-selected location. - * @param {number[]} bandValuesFromSelectedLandCoverType - Array of band values from the matched or user-selected land cover type. + * @param {spectralProfileDataByLandCoverTypes} spectralProfileDataByLandCoverTypes - Lookup table that contains spectral profile data for different land cover types. * @param {LandCoverType} landCoverType - The name of the matched or user-selected land cover type to be displayed in the Spectral Profile Chart. * * @returns {LineGroupData[]} An array of LineGroupData objects for populating the Spectral Profile Chart. */ export const useGenerateSpectralProfileChartData = ( bandValuesFromSelectedLocation: number[], - bandValuesFromSelectedLandCoverType: number[], + spectralProfileDataByLandCoverTypes: SpectralProfileDataByLandCoverType, landCoverType: LandCoverType ) => { const chartData: LineGroupData[] = useMemo(() => { if ( !bandValuesFromSelectedLocation || - !bandValuesFromSelectedLandCoverType || + !spectralProfileDataByLandCoverTypes || !bandValuesFromSelectedLocation?.length || - !bandValuesFromSelectedLandCoverType?.length || + !spectralProfileDataByLandCoverTypes[landCoverType] || !landCoverType ) { return []; } + const bandValuesFromSelectedLandCoverType = + spectralProfileDataByLandCoverTypes[landCoverType]; + // The band values from the selected location may contain more data than necessary for display. // To ensure only the required number of band values are used, calculate the appropriate length // by taking the minimum of the available band values from both the selected location and the selected land cover type. @@ -86,7 +92,7 @@ export const useGenerateSpectralProfileChartData = ( ]; }, [ bandValuesFromSelectedLocation, - bandValuesFromSelectedLandCoverType, + spectralProfileDataByLandCoverTypes, landCoverType, ]); From 7c83b6bc0c17f1857cd9e09d22c782191c97f952 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 5 Sep 2024 13:35:29 -0700 Subject: [PATCH 033/306] feat(sentinel2explorer): add Spectral Profile Tool --- .../SpectralTool/SpectralToolContainer.tsx | 7 -- .../components/Layout/Layout.tsx | 5 +- .../Sentinel2SpectralProfileTool.tsx | 59 +++++++++++++ .../components/SpectralProfileTool/config.ts | 85 +++++++++++++++++++ .../components/SpectralProfileTool/index.ts | 1 + src/shared/services/helpers/getPixelValues.ts | 2 +- .../landsat-level-2/getLandsatPixelValues.ts | 14 ++- src/shared/services/sentinel-2/config.ts | 2 + .../sentinel-2/getSentinel2PixelValues.tsx | 52 ++++++++++++ 9 files changed, 209 insertions(+), 18 deletions(-) create mode 100644 src/sentinel-2-explorer/components/SpectralProfileTool/Sentinel2SpectralProfileTool.tsx create mode 100644 src/sentinel-2-explorer/components/SpectralProfileTool/config.ts create mode 100644 src/sentinel-2-explorer/components/SpectralProfileTool/index.ts create mode 100644 src/shared/services/sentinel-2/getSentinel2PixelValues.tsx diff --git a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx index 364177be..820f8123 100644 --- a/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx +++ b/src/landsat-explorer/components/SpectralTool/SpectralToolContainer.tsx @@ -13,14 +13,7 @@ * limitations under the License. */ -import { AnalysisToolHeader } from '@shared/components/AnalysisToolHeader'; import { selectActiveAnalysisTool } from '@shared/store/ImageryScene/selectors'; -import { - selectData4SpectralProfileTool, - selectError4SpectralProfileTool, - selectIsLoadingData4SpectralProfileTool, -} from '@shared/store/SpectralProfileTool/selectors'; -import classNames from 'classnames'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { SpectralProfileTool } from '@shared/components/SpectralProfileTool'; diff --git a/src/sentinel-2-explorer/components/Layout/Layout.tsx b/src/sentinel-2-explorer/components/Layout/Layout.tsx index 4624cea5..9e5baec3 100644 --- a/src/sentinel-2-explorer/components/Layout/Layout.tsx +++ b/src/sentinel-2-explorer/components/Layout/Layout.tsx @@ -49,6 +49,7 @@ import { SceneInfo } from '../SceneInfo'; import { Sentinel2AnalyzeToolSelector } from '../AnalyzeToolSelector/AnalyzeToolSelector'; import { Sentinel2MaskTool } from '../MaskTool'; import { Sentinel2ChangeCompareTool } from '../ChangeCompareTool'; +import { Sentinel2SpectralProfileTool } from '../SpectralProfileTool'; const Layout = () => { const mode = useSelector(selectAppMode); @@ -121,8 +122,8 @@ const Layout = () => { {mode === 'analysis' && (
- {/* - */} + {/* */} +
)} diff --git a/src/sentinel-2-explorer/components/SpectralProfileTool/Sentinel2SpectralProfileTool.tsx b/src/sentinel-2-explorer/components/SpectralProfileTool/Sentinel2SpectralProfileTool.tsx new file mode 100644 index 00000000..adf4b281 --- /dev/null +++ b/src/sentinel-2-explorer/components/SpectralProfileTool/Sentinel2SpectralProfileTool.tsx @@ -0,0 +1,59 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { selectActiveAnalysisTool } from '@shared/store/ImageryScene/selectors'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { SpectralProfileTool } from '@shared/components/SpectralProfileTool'; +import { Sentinel2SpectralProfileData } from './config'; +import { getLandsatPixelValues } from '@shared/services/landsat-level-2/getLandsatPixelValues'; +import { useFetchSpectralProfileToolData } from '@shared/components/SpectralProfileTool/useUpdateSpectralProfileToolData'; +import { FetchPixelValuesFuncParams } from '@shared/store/SpectralProfileTool/thunks'; +import { SENTINEL2_BAND_NAMES } from '@shared/services/sentinel-2/config'; +import { getSentinel2PixelValues } from '@shared/services/sentinel-2/getSentinel2PixelValues'; + +export const Sentinel2SpectralProfileTool = () => { + const tool = useSelector(selectActiveAnalysisTool); + + const fetchSentinel2PixelValuesFunc = useCallback( + async ({ + point, + objectIds, + abortController, + }: FetchPixelValuesFuncParams) => { + const res: number[] = await getSentinel2PixelValues({ + point, + objectIds, + abortController, + }); + + return res; + }, + [] + ); + + useFetchSpectralProfileToolData(fetchSentinel2PixelValuesFunc); + + if (tool !== 'spectral') { + return null; + } + + return ( + + ); +}; diff --git a/src/sentinel-2-explorer/components/SpectralProfileTool/config.ts b/src/sentinel-2-explorer/components/SpectralProfileTool/config.ts new file mode 100644 index 00000000..5683eaf9 --- /dev/null +++ b/src/sentinel-2-explorer/components/SpectralProfileTool/config.ts @@ -0,0 +1,85 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SpectralProfileDataByLandCoverType } from '@shared/components/SpectralProfileTool'; + +/** + * The typical spectral profiles data from the config file used by the legacy Sentinel-2 Explorer app, + * and the data prepared using `/scripts/landsat-spectral-signatures`. + * + * @see https://github.com/Esri/Imagery-Apps/blob/master/Sentinel%20Explorer/configs/Identify/config_Identify.json + */ +export const Sentinel2SpectralProfileData: SpectralProfileDataByLandCoverType = + { + Cloud: [ + 0.7598, 0.8401, 0.8105, 0.8851, 0.8812, 0.8878, 0.8947, 0.9541, + 0.883, 0.5035, 0.0157, 0.4387, 0.2543, + ], + 'Snow and Ice': [ + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.997, 0.9507, 0.9352, 0.6584, 0.0485, + 0.0355, 0.0351, + ], + Sand: [ + 0.2121, 0.2203, 0.2703, 0.3643, 0.3759, 0.3932, 0.426, 0.4066, + 0.4432, 0.141, 0.0022, 0.5477, 0.4689, + ], + 'Dry Vegetation': [ + 0.1347, 0.119, 0.1189, 0.1552, 0.1659, 0.1802, 0.205, 0.2094, + 0.2354, 0.0549, 0.0018, 0.3401, 0.2355, + ], + 'Paved Surface': [ + 0.2416, 0.2381, 0.242, 0.2711, 0.2814, 0.2928, 0.3016, 0.2696, + 0.3068, 0.1042, 0.0011, 0.3076, 0.2281, + ], + 'Healthy Vegetation': [ + 0.1307, 0.0981, 0.0875, 0.0506, 0.0901, 0.2563, 0.3342, 0.3195, + 0.3801, 0.035, 0.0009, 0.1821, 0.0631, + ], + 'Bare Soil': [ + 0.1524, 0.1449, 0.1483, 0.1521, 0.1522, 0.1566, 0.1601, 0.1458, + 0.1577, 0.056, 0.0005, 0.1291, 0.1062, + ], + Trees: [ + 0.115, 0.0852, 0.064, 0.0406, 0.0763, 0.2183, 0.273, 0.2763, 0.3115, + 0.0974, 0.0011, 0.082, 0.0315, + ], + 'Turbid Water': [ + 0.1231, 0.0959, 0.0776, 0.0612, 0.0509, 0.0301, 0.0285, 0.0223, + 0.019, 0.0059, 0.0006, 0.0044, 0.0037, + ], + 'Clear Water': [ + 0.1231, 0.0959, 0.0776, 0.0612, 0.0509, 0.0301, 0.0285, 0.0223, + 0.019, 0.0059, 0.0006, 0.0044, 0.0037, + ], + }; + +// /** +// * The typical spectral profiles data of Sentinel-2 imagery +// * using the Spectral Sampling Tool +// * +// * @see https://github.com/Esri/Imagery-Apps/blob/master/Sentinel%20Explorer/configs/Identify/config_Identify.json +// */ +// export const Sentinel2SpectralProfileData: SpectralProfileDataByLandCoverType = { +// Cloud: [], +// 'Clear Water': [], +// 'Turbid Water': [], +// 'Snow and Ice': [], +// Sand: [], +// 'Bare Soil': [], +// 'Paved Surface': [], +// Trees: [], +// 'Healthy Vegetation': [], +// 'Dry Vegetation': [], +// }; diff --git a/src/sentinel-2-explorer/components/SpectralProfileTool/index.ts b/src/sentinel-2-explorer/components/SpectralProfileTool/index.ts new file mode 100644 index 00000000..1e229c08 --- /dev/null +++ b/src/sentinel-2-explorer/components/SpectralProfileTool/index.ts @@ -0,0 +1 @@ +export { Sentinel2SpectralProfileTool } from './Sentinel2SpectralProfileTool'; diff --git a/src/shared/services/helpers/getPixelValues.ts b/src/shared/services/helpers/getPixelValues.ts index 3168aaa6..f7f7645b 100644 --- a/src/shared/services/helpers/getPixelValues.ts +++ b/src/shared/services/helpers/getPixelValues.ts @@ -21,7 +21,7 @@ import { canBeConvertedToNumber } from '@shared/utils/snippets/canBeConvertedToN /** * Parameters for the Get Pixel Values */ -type GetPixelValuesParams = { +export type GetPixelValuesParams = { /** * URL of the imagery service */ diff --git a/src/shared/services/landsat-level-2/getLandsatPixelValues.ts b/src/shared/services/landsat-level-2/getLandsatPixelValues.ts index 9946ab2a..c2c85dfc 100644 --- a/src/shared/services/landsat-level-2/getLandsatPixelValues.ts +++ b/src/shared/services/landsat-level-2/getLandsatPixelValues.ts @@ -15,14 +15,12 @@ import { Point } from '@arcgis/core/geometry'; import { LANDSAT_LEVEL_2_SERVICE_URL } from './config'; -import { getPixelValues } from '../helpers/getPixelValues'; - -type GetPixelValuesParams = { - point: Point; - objectIds: number[]; - abortController: AbortController; -}; +import { + getPixelValues, + GetPixelValuesParams, +} from '../helpers/getPixelValues'; +type Params = Omit; /** * Run identify task to get values of the pixel that intersects with the input point from the scene with input object id. * @param param0 @@ -32,7 +30,7 @@ export const getLandsatPixelValues = async ({ point, objectIds, abortController, -}: GetPixelValuesParams): Promise => { +}: Params): Promise => { // const res = await identify({ // serviceURL: LANDSAT_LEVEL_2_SERVICE_URL, // point, diff --git a/src/shared/services/sentinel-2/config.ts b/src/shared/services/sentinel-2/config.ts index 89249246..3ad2145a 100644 --- a/src/shared/services/sentinel-2/config.ts +++ b/src/shared/services/sentinel-2/config.ts @@ -157,3 +157,5 @@ export const SENTINEL2_RASTER_FUNCTION_INFOS: { export const SENTINEL2_SERVICE_SORT_FIELD = 'best'; export const SENTINEL2_SERVICE_SORT_VALUE = '0'; + +export const SENTINEL2_BAND_NAMES: string[] = []; diff --git a/src/shared/services/sentinel-2/getSentinel2PixelValues.tsx b/src/shared/services/sentinel-2/getSentinel2PixelValues.tsx new file mode 100644 index 00000000..5425c628 --- /dev/null +++ b/src/shared/services/sentinel-2/getSentinel2PixelValues.tsx @@ -0,0 +1,52 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + getPixelValues, + GetPixelValuesParams, +} from '../helpers/getPixelValues'; +import { SENTINEL_2_SERVICE_URL } from './config'; + +type Params = Omit; + +/** + * Run identify task to get values of the pixel that intersects with the input point from the scene with input object id. + * @param param0 + * @returns + */ +export const getSentinel2PixelValues = async ({ + point, + objectIds, + abortController, +}: Params): Promise => { + const res = await getPixelValues({ + serviceURL: SENTINEL_2_SERVICE_URL, + point, + objectIds, + abortController, + }); + // console.log(res) + + if (!res.length) { + throw new Error( + 'Failed to fetch pixel values. Please select a location inside of the selected sentinel-2 scene.' + ); + } + + if (!res[0]?.values) { + throw new Error('Identify task does not return band values'); + } + + return res[0].values; +}; From b6d52c9a568abe0563aa1e26b7bea744875a0c57 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Mon, 9 Sep 2024 09:51:23 -0700 Subject: [PATCH 034/306] refactor(shared): add useDataFromSelectedImageryScene custom hook --- CHANGELOG.md | 1 + .../SceneInfo/SceneInfoContainer.tsx | 14 ++- .../SceneInfo/useDataFromSelectedScene.tsx | 88 ------------------- .../useDataFromSelectedScene.tsx | 67 ++++++++++++++ 4 files changed, 79 insertions(+), 91 deletions(-) delete mode 100644 src/sentinel-2-explorer/components/SceneInfo/useDataFromSelectedScene.tsx create mode 100644 src/shared/components/SceneInfoTable/useDataFromSelectedScene.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 58256d88..6c831d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed - add `useMaskLayerVisibility` custom hook - add `getChangeCompareLayerRasterFunction` helper function +- add `useDataFromSelectedImageryScene` custom hook ### Fixed diff --git a/src/sentinel-2-explorer/components/SceneInfo/SceneInfoContainer.tsx b/src/sentinel-2-explorer/components/SceneInfo/SceneInfoContainer.tsx index b9bf3540..14a6b35c 100644 --- a/src/sentinel-2-explorer/components/SceneInfo/SceneInfoContainer.tsx +++ b/src/sentinel-2-explorer/components/SceneInfo/SceneInfoContainer.tsx @@ -13,21 +13,29 @@ * limitations under the License. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { SceneInfoTable, SceneInfoTableData, } from '@shared/components/SceneInfoTable'; -import { useDataFromSelectedLandsatScene } from './useDataFromSelectedScene'; import { DATE_FORMAT } from '@shared/constants/UI'; import { useSelector } from 'react-redux'; import { selectAppMode } from '@shared/store/ImageryScene/selectors'; import { formatInUTCTimeZone } from '@shared/utils/date-time/formatInUTCTimeZone'; +import { Sentinel2Scene } from '@typing/imagery-service'; +import { getSentinel2SceneByObjectId } from '@shared/services/sentinel-2/getSentinel2Scenes'; +import { useDataFromSelectedImageryScene } from '@shared/components/SceneInfoTable/useDataFromSelectedScene'; export const SceneInfoContainer = () => { const mode = useSelector(selectAppMode); - const data = useDataFromSelectedLandsatScene(); + const fetchSceneByObjectId = useCallback(async (objectId: number) => { + const res = await getSentinel2SceneByObjectId(objectId); + return res; + }, []); + + const data = + useDataFromSelectedImageryScene(fetchSceneByObjectId); const tableData: SceneInfoTableData[] = useMemo(() => { if (!data) { diff --git a/src/sentinel-2-explorer/components/SceneInfo/useDataFromSelectedScene.tsx b/src/sentinel-2-explorer/components/SceneInfo/useDataFromSelectedScene.tsx deleted file mode 100644 index e8554b52..00000000 --- a/src/sentinel-2-explorer/components/SceneInfo/useDataFromSelectedScene.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useEffect, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { - selectAppMode, - selectQueryParams4SceneInSelectedMode, -} from '@shared/store/ImageryScene/selectors'; -import { Sentinel2Scene } from '@typing/imagery-service'; -import { - selectAnimationStatus, - selectIsAnimationPlaying, -} from '@shared/store/UI/selectors'; -import { selectAvailableScenesByObjectId } from '@shared/store/Sentinel2/selectors'; -import { getSentinel2SceneByObjectId } from '@shared/services/sentinel-2/getSentinel2Scenes'; - -// /** -// * Save/cache Landsat scene data using the object ID as the key. -// * Why is it necessary to do this? The reason is that the `availableScenesByObjectId` does not get updated during animation playback. -// * As a result, it may not contain data for the Landsat scene associated with the animation frame. However, we still want to populate -// * the scene information for each animation frame. Therefore, it is a good idea to retrieve the cached data from this map. -// */ -// const landsatSceneByObjectId: Map = new Map(); - -/** - * This custom hook returns the data for the selected Landsat Scene. - * @returns - */ -export const useDataFromSelectedLandsatScene = () => { - const { objectIdOfSelectedScene } = - useSelector(selectQueryParams4SceneInSelectedMode) || {}; - - const availableScenesByObjectId = useSelector( - selectAvailableScenesByObjectId - ); - - const mode = useSelector(selectAppMode); - - const animationPlaying = useSelector(selectIsAnimationPlaying); - - const [sentinel2Scene, setSentinel2Scene] = useState(); - - useEffect(() => { - (async () => { - if ( - !objectIdOfSelectedScene || - animationPlaying || - mode === 'analysis' - ) { - // return null; - setSentinel2Scene(null); - return; - } - - try { - const data = - availableScenesByObjectId[objectIdOfSelectedScene] || - (await getSentinel2SceneByObjectId( - objectIdOfSelectedScene - )); - - setSentinel2Scene(data); - } catch (err) { - console.error(err); - } - })(); - }, [ - objectIdOfSelectedScene, - availableScenesByObjectId, - mode, - animationPlaying, - ]); - - return sentinel2Scene; -}; diff --git a/src/shared/components/SceneInfoTable/useDataFromSelectedScene.tsx b/src/shared/components/SceneInfoTable/useDataFromSelectedScene.tsx new file mode 100644 index 00000000..1e22af7a --- /dev/null +++ b/src/shared/components/SceneInfoTable/useDataFromSelectedScene.tsx @@ -0,0 +1,67 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + selectAppMode, + selectQueryParams4SceneInSelectedMode, +} from '@shared/store/ImageryScene/selectors'; +import { selectIsAnimationPlaying } from '@shared/store/UI/selectors'; + +/** + * This custom hook returns the data for the selected scene of a generic type. + * @template T - The type of the scene data. + * @param fetchSceneByObjectId - An async function to fetch the scene by object ID. + * @returns {T | undefined} + */ +export const useDataFromSelectedImageryScene = ( + fetchSceneByObjectId: (objectId: number) => Promise +) => { + const { objectIdOfSelectedScene } = + useSelector(selectQueryParams4SceneInSelectedMode) || {}; + + const mode = useSelector(selectAppMode); + + const animationPlaying = useSelector(selectIsAnimationPlaying); + + const [selectedScene, setSelectedScene] = useState(); + + useEffect(() => { + (async () => { + if ( + !objectIdOfSelectedScene || + animationPlaying || + mode === 'analysis' + ) { + // return null; + setSelectedScene(null); + return; + } + + try { + const data = await fetchSceneByObjectId( + objectIdOfSelectedScene + ); + + setSelectedScene(data as T); + } catch (err) { + console.error(err); + } + })(); + }, [objectIdOfSelectedScene, mode, animationPlaying, fetchSceneByObjectId]); + + return selectedScene; +}; From 6fa81fe37ad8a5d97fe0bb8e728ef8dc23227f32 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Mon, 9 Sep 2024 09:54:43 -0700 Subject: [PATCH 035/306] refactor(sentinel1explorer): use useDataFromSelectedImageryScene custom hook --- .../SceneInfo/SceneInfoContainer.tsx | 14 +++- .../useDataFromSelectedSentinel1Scene.tsx | 64 ------------------- 2 files changed, 11 insertions(+), 67 deletions(-) delete mode 100644 src/sentinel-1-explorer/components/SceneInfo/useDataFromSelectedSentinel1Scene.tsx diff --git a/src/sentinel-1-explorer/components/SceneInfo/SceneInfoContainer.tsx b/src/sentinel-1-explorer/components/SceneInfo/SceneInfoContainer.tsx index 169bd063..0f54fa4f 100644 --- a/src/sentinel-1-explorer/components/SceneInfo/SceneInfoContainer.tsx +++ b/src/sentinel-1-explorer/components/SceneInfo/SceneInfoContainer.tsx @@ -13,21 +13,29 @@ * limitations under the License. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { SceneInfoTable, SceneInfoTableData, } from '@shared/components/SceneInfoTable'; -import { useDataFromSelectedSentinel1Scene } from './useDataFromSelectedSentinel1Scene'; import { DATE_FORMAT } from '@shared/constants/UI'; import { useSelector } from 'react-redux'; import { selectAppMode } from '@shared/store/ImageryScene/selectors'; import { formatInUTCTimeZone } from '@shared/utils/date-time/formatInUTCTimeZone'; +import { useDataFromSelectedImageryScene } from '@shared/components/SceneInfoTable/useDataFromSelectedScene'; +import { Sentinel1Scene } from '@typing/imagery-service'; +import { getSentinel1SceneByObjectId } from '@shared/services/sentinel-1/getSentinel1Scenes'; export const SceneInfoContainer = () => { const mode = useSelector(selectAppMode); - const data = useDataFromSelectedSentinel1Scene(); + const fetchSceneByObjectId = useCallback(async (objectId: number) => { + const res = await getSentinel1SceneByObjectId(objectId); + return res; + }, []); + + const data = + useDataFromSelectedImageryScene(fetchSceneByObjectId); const tableData: SceneInfoTableData[] = useMemo(() => { if (!data) { diff --git a/src/sentinel-1-explorer/components/SceneInfo/useDataFromSelectedSentinel1Scene.tsx b/src/sentinel-1-explorer/components/SceneInfo/useDataFromSelectedSentinel1Scene.tsx deleted file mode 100644 index 497b0245..00000000 --- a/src/sentinel-1-explorer/components/SceneInfo/useDataFromSelectedSentinel1Scene.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useEffect, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { - selectAppMode, - selectQueryParams4SceneInSelectedMode, -} from '@shared/store/ImageryScene/selectors'; -import { Sentinel1Scene } from '@typing/imagery-service'; -import { selectIsAnimationPlaying } from '@shared/store/UI/selectors'; -import { getSentinel1SceneByObjectId } from '@shared/services/sentinel-1/getSentinel1Scenes'; - -/** - * This custom hook returns the data for the selected Sentinel-1 Scene. - * @returns - */ -export const useDataFromSelectedSentinel1Scene = () => { - const { objectIdOfSelectedScene } = - useSelector(selectQueryParams4SceneInSelectedMode) || {}; - - const mode = useSelector(selectAppMode); - - const animationPlaying = useSelector(selectIsAnimationPlaying); - - const [sentinel1Scene, setSentinel1Scene] = useState(); - - useEffect(() => { - (async () => { - if ( - !objectIdOfSelectedScene || - animationPlaying || - mode === 'analysis' - ) { - // return null; - setSentinel1Scene(null); - return; - } - - try { - const data = await getSentinel1SceneByObjectId( - objectIdOfSelectedScene - ); - setSentinel1Scene(data); - } catch (err) { - console.error(err); - } - })(); - }, [objectIdOfSelectedScene, mode, animationPlaying]); - - return sentinel1Scene; -}; From cf52edd08a6ca51d64fbd6e15a17ddadafb04b1a Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Mon, 9 Sep 2024 10:14:22 -0700 Subject: [PATCH 036/306] refactor(landsatexplorer): use useDataFromSelectedImageryScene custom hook --- .../SceneInfo/SceneInfoContainer.tsx | 14 ++- .../useDataFromSelectedLandsatScene.tsx | 86 ------------------- 2 files changed, 11 insertions(+), 89 deletions(-) delete mode 100644 src/landsat-explorer/components/SceneInfo/useDataFromSelectedLandsatScene.tsx diff --git a/src/landsat-explorer/components/SceneInfo/SceneInfoContainer.tsx b/src/landsat-explorer/components/SceneInfo/SceneInfoContainer.tsx index 46d226b8..58969d16 100644 --- a/src/landsat-explorer/components/SceneInfo/SceneInfoContainer.tsx +++ b/src/landsat-explorer/components/SceneInfo/SceneInfoContainer.tsx @@ -13,21 +13,29 @@ * limitations under the License. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { SceneInfoTable, SceneInfoTableData, } from '@shared/components/SceneInfoTable'; -import { useDataFromSelectedLandsatScene } from './useDataFromSelectedLandsatScene'; import { DATE_FORMAT } from '@shared/constants/UI'; import { useSelector } from 'react-redux'; import { selectAppMode } from '@shared/store/ImageryScene/selectors'; import { formatInUTCTimeZone } from '@shared/utils/date-time/formatInUTCTimeZone'; +import { useDataFromSelectedImageryScene } from '@shared/components/SceneInfoTable/useDataFromSelectedScene'; +import { LandsatScene } from '@typing/imagery-service'; +import { getLandsatSceneByObjectId } from '@shared/services/landsat-level-2/getLandsatScenes'; export const SceneInfoContainer = () => { const mode = useSelector(selectAppMode); - const data = useDataFromSelectedLandsatScene(); + const fetchSceneByObjectId = useCallback(async (objectId: number) => { + const res = await getLandsatSceneByObjectId(objectId); + return res; + }, []); + + const data = + useDataFromSelectedImageryScene(fetchSceneByObjectId); const tableData: SceneInfoTableData[] = useMemo(() => { if (!data) { diff --git a/src/landsat-explorer/components/SceneInfo/useDataFromSelectedLandsatScene.tsx b/src/landsat-explorer/components/SceneInfo/useDataFromSelectedLandsatScene.tsx deleted file mode 100644 index 110555ab..00000000 --- a/src/landsat-explorer/components/SceneInfo/useDataFromSelectedLandsatScene.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useEffect, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { - selectAppMode, - selectQueryParams4SceneInSelectedMode, -} from '@shared/store/ImageryScene/selectors'; -import { selectAvailableScenesByObjectId } from '@shared/store/Landsat/selectors'; -import { LandsatScene } from '@typing/imagery-service'; -import { - selectAnimationStatus, - selectIsAnimationPlaying, -} from '@shared/store/UI/selectors'; -import { getLandsatSceneByObjectId } from '@shared/services/landsat-level-2/getLandsatScenes'; - -// /** -// * Save/cache Landsat scene data using the object ID as the key. -// * Why is it necessary to do this? The reason is that the `availableScenesByObjectId` does not get updated during animation playback. -// * As a result, it may not contain data for the Landsat scene associated with the animation frame. However, we still want to populate -// * the scene information for each animation frame. Therefore, it is a good idea to retrieve the cached data from this map. -// */ -// const landsatSceneByObjectId: Map = new Map(); - -/** - * This custom hook returns the data for the selected Landsat Scene. - * @returns - */ -export const useDataFromSelectedLandsatScene = () => { - const { objectIdOfSelectedScene } = - useSelector(selectQueryParams4SceneInSelectedMode) || {}; - - const availableScenesByObjectId = useSelector( - selectAvailableScenesByObjectId - ); - - const mode = useSelector(selectAppMode); - - const animationPlaying = useSelector(selectIsAnimationPlaying); - - const [landsatScene, setLandsatScene] = useState(); - - useEffect(() => { - (async () => { - if ( - !objectIdOfSelectedScene || - animationPlaying || - mode === 'analysis' - ) { - // return null; - setLandsatScene(null); - return; - } - - try { - const data = - availableScenesByObjectId[objectIdOfSelectedScene] || - (await getLandsatSceneByObjectId(objectIdOfSelectedScene)); - - setLandsatScene(data); - } catch (err) { - console.error(err); - } - })(); - }, [ - objectIdOfSelectedScene, - availableScenesByObjectId, - mode, - animationPlaying, - ]); - - return landsatScene; -}; From 3459bc8d9e59fa1374bd267d6882a2966f7aeefa Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Mon, 9 Sep 2024 10:38:11 -0700 Subject: [PATCH 037/306] feat(shared): add auto-swipe for swipe model - feat(shared): update layout of Swipe Layer Selector container - feat(shared): add AutoSwipeControls - feat(shared): add useAutoSwipe custom hook - feat(shared): update AutoSwipeControls to not show pausing button - refactor(shared): update Slider component to get min and max values from steps - feat(shared): add speed control slider to AutoSwipe - feat(shared): update AUTO_SWIPE_SPEEDS values --- .../AnimationControl/AnimationControl.tsx | 8 +- src/shared/components/Slider/Slider.tsx | 15 +++- .../SwipeLayerSelector/AutoSwipeControls.tsx | 80 +++++++++++++++++ .../SwipeLayerSelectorContainer.tsx | 40 ++++++--- .../components/SwipeWidget/SwipeWidget.tsx | 3 + .../components/SwipeWidget/useAutoSwipe.tsx | 86 +++++++++++++++++++ src/shared/store/Map/reducer.ts | 32 +++++++ src/shared/store/Map/selectors.ts | 10 +++ tsconfig.json | 1 + 9 files changed, 256 insertions(+), 19 deletions(-) create mode 100644 src/shared/components/SwipeLayerSelector/AutoSwipeControls.tsx create mode 100644 src/shared/components/SwipeWidget/useAutoSwipe.tsx diff --git a/src/shared/components/AnimationControl/AnimationControl.tsx b/src/shared/components/AnimationControl/AnimationControl.tsx index 7b83495b..b52028a6 100644 --- a/src/shared/components/AnimationControl/AnimationControl.tsx +++ b/src/shared/components/AnimationControl/AnimationControl.tsx @@ -20,7 +20,7 @@ import { AnimationSpeedControl } from './AnimationSpeedControl'; const ICON_SIZE = 22; -const StartPlayButton = ( +export const StartPlayButton = ( ); -const ContinuePlayButton = ( +export const ContinuePlayButton = ( ); -const PauseButton = ( +export const PauseButton = ( ); -const CloseButton = ( +export const CloseButton = ( = ({ const sliderRef = useRef(); const init = async () => { + steps = steps || [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]; + + const min = steps[0]; + const max = steps[steps.length - 1]; + sliderRef.current = new SliderWidget({ container: containerRef.current, - min: 0, - max: 1, - steps: steps || [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1], + min, + max, + steps: steps, values: [value], snapOnClickEnabled: false, visibleElements: { diff --git a/src/shared/components/SwipeLayerSelector/AutoSwipeControls.tsx b/src/shared/components/SwipeLayerSelector/AutoSwipeControls.tsx new file mode 100644 index 00000000..e3d90add --- /dev/null +++ b/src/shared/components/SwipeLayerSelector/AutoSwipeControls.tsx @@ -0,0 +1,80 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { + selectAutoSwipeStatus, + selectAutoSwipeSpeed, +} from '@shared/store/Map/selectors'; +import { + StartPlayButton, + CloseButton, +} from '../AnimationControl/AnimationControl'; +import { useDispatch } from 'react-redux'; +import { + AUTO_SWIPE_SPEEDS, + autoSwipeSpeedChanged, + AutoSwipeStatus, + autoSwipeStatusChanged, +} from '@shared/store/Map/reducer'; +import { Slider } from '../Slider'; + +export const AutoSwipeControls = () => { + const dispatch = useDispatch(); + + const status = useSelector(selectAutoSwipeStatus); + + const speed = useSelector(selectAutoSwipeSpeed); + + const statusOnChange = (status: AutoSwipeStatus) => { + dispatch(autoSwipeStatusChanged(status)); + }; + + /** + * use index of items in auto swipe speeds arrar + * as the steps of the slider + */ + const sliderSteps = useMemo(() => { + const indice = []; + + for (let i = 0; i < AUTO_SWIPE_SPEEDS.length; i++) { + indice.push(i); + } + + return indice; + }, [AUTO_SWIPE_SPEEDS]); + + return ( +
+
+ { + // console.log(val) + + // get actual speed by index + const speed = AUTO_SWIPE_SPEEDS[index]; + + dispatch(autoSwipeSpeedChanged(speed)); + }} + /> +
+ +
+ {!status && ( +
+ {StartPlayButton} +
+ )} + {status && ( +
+ {CloseButton} +
+ )} +
+
+ ); +}; diff --git a/src/shared/components/SwipeLayerSelector/SwipeLayerSelectorContainer.tsx b/src/shared/components/SwipeLayerSelector/SwipeLayerSelectorContainer.tsx index 049c17db..e0657b4d 100644 --- a/src/shared/components/SwipeLayerSelector/SwipeLayerSelectorContainer.tsx +++ b/src/shared/components/SwipeLayerSelector/SwipeLayerSelectorContainer.tsx @@ -25,6 +25,7 @@ import { useDispatch } from 'react-redux'; import { SwipeLayerSelector } from './SwipeLayerSelector'; import { isSecondarySceneActiveToggled } from '@shared/store/ImageryScene/reducer'; import { swapMainAndSecondaryScenes } from '@shared/store/ImageryScene/thunks'; +import { AutoSwipeControls } from './AutoSwipeControls'; export const SwipeLayerSelectorContainer = () => { const dispatch = useDispatch(); @@ -42,17 +43,32 @@ export const SwipeLayerSelectorContainer = () => { } return ( - { - const isSecondarySceneActive = value === 'right'; - dispatch(isSecondarySceneActiveToggled(isSecondarySceneActive)); - }} - swapButtonOnClick={() => { - dispatch(swapMainAndSecondaryScenes()); - }} - /> + <> +
+ { + const isSecondarySceneActive = value === 'right'; + dispatch( + isSecondarySceneActiveToggled( + isSecondarySceneActive + ) + ); + }} + swapButtonOnClick={() => { + dispatch(swapMainAndSecondaryScenes()); + }} + /> +
+ + + ); }; diff --git a/src/shared/components/SwipeWidget/SwipeWidget.tsx b/src/shared/components/SwipeWidget/SwipeWidget.tsx index 7e086fc0..089706bc 100644 --- a/src/shared/components/SwipeWidget/SwipeWidget.tsx +++ b/src/shared/components/SwipeWidget/SwipeWidget.tsx @@ -20,6 +20,7 @@ import Swipe from '@arcgis/core/widgets/Swipe'; import MapView from '@arcgis/core/views/MapView'; import { watch } from '@arcgis/core/core/reactiveUtils'; import ImageryLayer from '@arcgis/core/layers/ImageryLayer'; +import { useAutoSwipe } from './useAutoSwipe'; type Props = { /** @@ -55,6 +56,8 @@ const SwipeWidget: FC = ({ }: Props) => { const swipeWidgetRef = useRef(); + useAutoSwipe(swipeWidgetRef.current); + const init = async () => { // this swipe widget layers should be added at index of one so that the // hillsahde/terrain layer can be added on top of it with blend mode applied diff --git a/src/shared/components/SwipeWidget/useAutoSwipe.tsx b/src/shared/components/SwipeWidget/useAutoSwipe.tsx new file mode 100644 index 00000000..860b8fde --- /dev/null +++ b/src/shared/components/SwipeWidget/useAutoSwipe.tsx @@ -0,0 +1,86 @@ +import Swipe from '@arcgis/core/widgets/Swipe'; +import React, { useEffect, useRef } from 'react'; +import { useSelector } from 'react-redux'; +import { + selectAutoSwipeStatus, + selectAutoSwipeSpeed, +} from '@shared/store/Map/selectors'; +import { AutoSwipeStatus } from '@shared/store/Map/reducer'; + +/** + * Custom React hook to manage the auto-swipe functionality for the Swipe widget. + * This hook automates the swipe action based on + * the status and speed settings from the Redux store. + * + * @param {Swipe} swipeWidget - The ArcGIS Swipe widget instance to control. + */ +export const useAutoSwipe = (swipeWidget: Swipe) => { + const status = useSelector(selectAutoSwipeStatus); + + const autoSwipeSpeed = useSelector(selectAutoSwipeSpeed); + + /** + * Reference to track the current position of the swipe handle (0 to 100) + */ + const positionRef = useRef(50); + + /** + * Reference to track the current status of auto-swipe (kept in sync with the Redux store) + */ + const statusRef = useRef(); + + /** + * Boolean ref to track if the swipe is moving towards the left (false indicates right) + */ + const movingToLeft = useRef(true); + + const speedRef = useRef(autoSwipeSpeed); + + /** + * Updates the position of the swipe handle based on the auto-swipe status and speed. + * Called recursively using requestAnimationFrame to create a smooth animation loop. + */ + const autoUpdateSwipePosition = () => { + if (statusRef.current !== 'playing') { + return; + } + + // Update the swipe position by moving the handle either left or right by 0.5% + if (movingToLeft.current) { + positionRef.current = Math.min( + positionRef.current + speedRef.current, + 100 + ); + } else { + positionRef.current = Math.max( + positionRef.current - speedRef.current, + 0 + ); + } + + // Reverse direction when the swipe handle reaches either end (0% or 100%) + if (positionRef.current === 100 || positionRef.current === 0) { + movingToLeft.current = !movingToLeft.current; + } + + swipeWidget.position = positionRef.current; + + requestAnimationFrame(autoUpdateSwipePosition); + }; + + useEffect(() => { + if (!swipeWidget) { + return; + } + + statusRef.current = status; + + if (statusRef.current === 'playing') { + requestAnimationFrame(autoUpdateSwipePosition); + } + }, [status, swipeWidget]); + + useEffect(() => { + speedRef.current = autoSwipeSpeed; + }, [autoSwipeSpeed]); +}; diff --git a/src/shared/store/Map/reducer.ts b/src/shared/store/Map/reducer.ts index 11dbfaad..2fd806a0 100644 --- a/src/shared/store/Map/reducer.ts +++ b/src/shared/store/Map/reducer.ts @@ -23,6 +23,8 @@ import { MAP_CENTER, MAP_ZOOM } from '../../constants/map'; import Point from '@arcgis/core/geometry/Point'; import { Extent } from '@arcgis/core/geometry'; +export type AutoSwipeStatus = 'playing' | 'pausing'; + // import { RootState, StoreDispatch, StoreGetState } from '../configureStore'; export type MapState = { @@ -67,6 +69,14 @@ export type MapState = { * handler position of swipe widget ranged from 0 - 100 */ swipeWidgetHanlderPosition: number; + /** + * Status of the auto-swipe feature for the Swipe Widget. + */ + autoSwipeStatus: AutoSwipeStatus; + /** + * The speed of the auto-swipe feature, specified in percent of position change per update. + */ + autoSwipeSpeed: number; /** * anchor location of the map popup windown */ @@ -85,6 +95,15 @@ export type MapState = { countOfVisiblePixels: number; }; +/** + * Array of numbers representing the increment/decrement of the swipe widget position + * per auto-swipe update. Each value defines the possible percentage of total movement + * that the swipe widget can make during each update cycle. + * + * The values represent movement speed, with 1 being the slowest and 5 the fastest. + */ +export const AUTO_SWIPE_SPEEDS = [0.25, 0.5, 1, 2.5, 5]; + export const initialMapState: MapState = { // webmapId: WEB_MAP_ID, // Topographic center: MAP_CENTER, @@ -96,6 +115,8 @@ export const initialMapState: MapState = { showTerrain: true, showBasemap: true, swipeWidgetHanlderPosition: 50, + autoSwipeStatus: null, + autoSwipeSpeed: AUTO_SWIPE_SPEEDS[2], popupAnchorLocation: null, isUpadting: false, totalVisibleAreaInSqKm: null, @@ -154,6 +175,15 @@ const slice = createSlice({ countOfVisiblePixelsChanged: (state, action: PayloadAction) => { state.countOfVisiblePixels = action.payload; }, + autoSwipeStatusChanged: ( + state, + action: PayloadAction + ) => { + state.autoSwipeStatus = action.payload; + }, + autoSwipeSpeedChanged: (state, action: PayloadAction) => { + state.autoSwipeSpeed = action.payload; + }, }, }); @@ -174,6 +204,8 @@ export const { isUpdatingChanged, totalVisibleAreaInSqKmChanged, countOfVisiblePixelsChanged, + autoSwipeStatusChanged, + autoSwipeSpeedChanged, } = slice.actions; export default reducer; diff --git a/src/shared/store/Map/selectors.ts b/src/shared/store/Map/selectors.ts index 7b666ba3..9d56e5e3 100644 --- a/src/shared/store/Map/selectors.ts +++ b/src/shared/store/Map/selectors.ts @@ -85,3 +85,13 @@ export const selectCountOfVisiblePixels = createSelector( (state: RootState) => state.Map.countOfVisiblePixels, (countOfVisiblePixels) => countOfVisiblePixels ); + +export const selectAutoSwipeStatus = createSelector( + (state: RootState) => state.Map.autoSwipeStatus, + (autoSwipeStatus) => autoSwipeStatus +); + +export const selectAutoSwipeSpeed = createSelector( + (state: RootState) => state.Map.autoSwipeSpeed, + (autoSwipeSpeed) => autoSwipeSpeed +); diff --git a/tsconfig.json b/tsconfig.json index 795eff26..a572e80f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "esModuleInterop": true , // "suppressImplicitAnyIndexErrors": true, "downlevelIteration": true, + "isolatedModules": true, "typeRoots": [ "./src/types", "./node_modules/@types" From 669d35a360e9b1246a8ebf2f08b6d3546a82f4cf Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 25 Sep 2024 10:47:13 -0700 Subject: [PATCH 038/306] fix(landcoverexplorer): fix getLandCoverChangeInAcres to ignore item without landcoverClassificationData from output --- .../computeHistograms.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/shared/services/sentinel-2-10m-landcover/computeHistograms.ts b/src/shared/services/sentinel-2-10m-landcover/computeHistograms.ts index 1e6e6afa..8e7ce5bc 100644 --- a/src/shared/services/sentinel-2-10m-landcover/computeHistograms.ts +++ b/src/shared/services/sentinel-2-10m-landcover/computeHistograms.ts @@ -321,7 +321,19 @@ export const getLandCoverChangeInAcres = async ({ const output: LandCoverChangeInAcres[] = []; - for (let i = 0; i < countsFromEarlierYear.length; i++) { + const len = Math.min( + countsFromEarlierYear.length, + countsFromLaterYear.length + ); + + for (let i = 0; i < len; i++) { + const landcoverClassificationData = + getLandCoverClassificationByPixelValue(i); + + if (!landcoverClassificationData) { + continue; + } + const countEarlierYear = countsFromEarlierYear[i]; const countLaterYear = countsFromLaterYear[i]; @@ -352,8 +364,7 @@ export const getLandCoverChangeInAcres = async ({ (laterYearAreaInAcres / totalAreaLaterYear) * 100; output.push({ - landcoverClassificationData: - getLandCoverClassificationByPixelValue(i), + landcoverClassificationData, earlierYearAreaInAcres, earlierYearAreaInPercentage, laterYearAreaInAcres, From ac9f202717b208ed364eae16b1aafdf51135407d Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 9 Oct 2024 16:45:49 -0700 Subject: [PATCH 039/306] refactor: updateHashParams should use update the URL using replaceState --- src/shared/utils/url-hash-params/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/shared/utils/url-hash-params/index.ts b/src/shared/utils/url-hash-params/index.ts index 0d4ddd33..9db7dc25 100644 --- a/src/shared/utils/url-hash-params/index.ts +++ b/src/shared/utils/url-hash-params/index.ts @@ -93,7 +93,17 @@ export const updateHashParams = (key: UrlHashParamKey, value: string) => { hashParams.set(key, value); } - window.location.hash = hashParams.toString(); + // window.location.hash = hashParams.toString(); + + // Get the current URL without the hash + const baseUrl = window.location.href.split('#')[0]; + + const newHash = hashParams.toString(); + + const newUrl = `${baseUrl}#${newHash}`; + + // Update the URL using replaceState + window.history.replaceState(null, '', newUrl); }; export const getHashParamValueByKey = (key: UrlHashParamKey): string => { From 2bbdec054f976e87509dfa3e6a59c5c4c8ad4409 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 15 Oct 2024 17:23:47 -0700 Subject: [PATCH 040/306] chore: add sandbox folder --- sandbox/rasterAnalysis/.env.template | 3 + sandbox/rasterAnalysis/config.js | 21 ++ .../createHostedImageryService.js | 74 +++++ sandbox/rasterAnalysis/generateRaster.js | 253 ++++++++++++++++++ sandbox/rasterAnalysis/generateToken.js | 46 ++++ 5 files changed, 397 insertions(+) create mode 100644 sandbox/rasterAnalysis/.env.template create mode 100644 sandbox/rasterAnalysis/config.js create mode 100644 sandbox/rasterAnalysis/createHostedImageryService.js create mode 100644 sandbox/rasterAnalysis/generateRaster.js create mode 100644 sandbox/rasterAnalysis/generateToken.js diff --git a/sandbox/rasterAnalysis/.env.template b/sandbox/rasterAnalysis/.env.template new file mode 100644 index 00000000..eaa9a00b --- /dev/null +++ b/sandbox/rasterAnalysis/.env.template @@ -0,0 +1,3 @@ +# Credential of ArcGIS Online Account with Pro Plus license to use Raster Analysis Tool +ACCOUNT_USERNAME=your_username +ACCOUNT_PASSWORD=your_password \ No newline at end of file diff --git a/sandbox/rasterAnalysis/config.js b/sandbox/rasterAnalysis/config.js new file mode 100644 index 00000000..1635fe09 --- /dev/null +++ b/sandbox/rasterAnalysis/config.js @@ -0,0 +1,21 @@ +const argv = new Set(process.argv) + +const IS_PROD = argv.has('-prod') +const TIER = IS_PROD ? 'production' : 'development' +const ARCGIS_ONLINE_PORTAL = TIER === 'development' ? 'https://devext.arcgis.com' : 'https://www.arcgis.com'; +const ARCGIS_ONLINE_PORTAL_REST_ROOT = ARCGIS_ONLINE_PORTAL + '/sharing/rest'; +const RASTER_ANALYSIS_ROOT = TIER === 'development' + ? 'https://rasteranalysisdev.arcgis.com/arcgis/rest/services/RasterAnalysisTools/GPServer' + : 'https://rasteranalysis.arcgis.com/arcgis/rest/services/RasterAnalysisTools/GPServer' + +const USERNAME = process.env['ACCOUNT_USERNAME'] +const PASSWORD = process.env['ACCOUNT_PASSWORD'] + +module.exports = { + ARCGIS_ONLINE_PORTAL, + ARCGIS_ONLINE_PORTAL_REST_ROOT, + USERNAME, + PASSWORD, + IS_PROD, + RASTER_ANALYSIS_ROOT +} \ No newline at end of file diff --git a/sandbox/rasterAnalysis/createHostedImageryService.js b/sandbox/rasterAnalysis/createHostedImageryService.js new file mode 100644 index 00000000..64f2dfc9 --- /dev/null +++ b/sandbox/rasterAnalysis/createHostedImageryService.js @@ -0,0 +1,74 @@ +const { ARCGIS_ONLINE_PORTAL_REST_ROOT, USERNAME } = require("./config"); + +/** + * Create a new hosted Imagery Service on ArcGIS Online. + * + * This function sends a request to the ArcGIS Online API to create an image service with the specified name and properties. + * It constructs the request parameters and posts them to the appropriate API endpoint. + * + * @param {string} name - The name of the imagery service to be created. + * @param {string} token - The authentication token for the ArcGIS Online API. + * + * @returns {Promise} A promise that resolves with the API response object containing details of the created service. + * + * @throws {Error} Throws an error if the API response contains an error. + * + * Example response from the API: + * ``` + * { + * "encodedServiceURL": "https://service.arcgis.com/Gkz.../arcgis/rest/services/Name_of_the_service/ImageServer", + * "itemId": "7eeab1...", + * "name": "Name_of_the_service", + * "serviceItemId": "7eeab1...", + * "serviceurl": "https://service.arcgis.com/Gkz.../arcgis/rest/services/Name_of_the_service/ImageServer", + * "size": -1, + * "success": true, + * "type": "Image Service", + * "typeKeywords": [ + * "Dynamic Imagery Layer" + * ], + * "isView": false + * } + * ``` + */ +const createHostedImageryService = async(name, token)=>{ + const params = new URLSearchParams({ + createParameters: JSON.stringify({ + name, + "description":"", + "capabilities":"Image", + "properties":{ + "isCached":true, + "path":"@", + "description":"", + "copyright":"" + } + }), + outputType: 'imageService', + f: 'json', + token + }) + + const requestURL = `${ARCGIS_ONLINE_PORTAL_REST_ROOT}/content/users/${USERNAME}/createService` + + const res = await fetch(requestURL, { + method: 'POST', + body: params + }) + + const data = await res.json() + + if(data.error){ + throw data.error + } + + if(!data?.success){ + throw data + } + + return data +} + +module.exports = { + createHostedImageryService +} \ No newline at end of file diff --git a/sandbox/rasterAnalysis/generateRaster.js b/sandbox/rasterAnalysis/generateRaster.js new file mode 100644 index 00000000..e73b93c0 --- /dev/null +++ b/sandbox/rasterAnalysis/generateRaster.js @@ -0,0 +1,253 @@ +require('dotenv').config() + +const { RASTER_ANALYSIS_ROOT } = require('./config') +const { createHostedImageryService } = require('./createHostedImageryService') +const { generateToken } = require('./generateToken') + +/** + * Submit new Generate Raster GP Job. + * @param {Object} param0 - An object containing the following properties: + * - token (string): ArcGIS authentication token. + * - createServiceResponse (Object): Response object from a previous service creation step. (Optional) + * @returns {Object} - Response object containing job information or throws an error if unsuccessful. + * + * example of response: + * ``` + * { + * "jobId":"11cba2e5eb994caab277f3df887044c6", + * "jobStatus":"esriJobNew" + * } + * ``` + */ +const submitNewJob = async({ + token, + createServiceResponse +})=>{ + const requestURL = RASTER_ANALYSIS_ROOT + "/GenerateRaster/submitJob"; + + const params = new URLSearchParams({ + f: "json", + token, + outputType: 'dynamicLayer', + rasterFunction: JSON.stringify({ + "rasterFunction": "Clip", + "rasterFunctionArguments": { + "ClippingType": "1", + "ClippingGeometry": { + "xmin": -12583429.576139491, + "ymin": 3900032.88035888, + "xmax": -12545688.793423735, + "ymax": 3910963.375403645, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857, + "xyTolerance": 0.001, + "zTolerance": 0.001, + "mTolerance": 0.001, + "falseX": -20037700, + "falseY": -30241100, + "xyUnits": 10000, + "falseZ": -100000, + "zUnits": 10000, + "falseM": -100000, + "mUnits": 10000 + } + }, + "clippingMethod": "byExtent", + "valueLayer": { + "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, + "name":"LandsatC2L2", + "renderingRule":{ + "rasterFunction":"Natural Color with DRA" + }, + "mosaicRule":{ + "mosaicMethod":"esriMosaicAttribute", + "sortField":"best", + "sortValue":"0", + "ascending":true, + "mosaicOperation":"MT_FIRST" + } + } + } + }), + functionArguments: JSON.stringify({ + "raster": { + "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, + "name": "LandsatC2L2", + "renderingRule": { + "rasterFunction": "Natural Color with DRA" + }, + "mosaicRule": { + "mosaicMethod": "esriMosaicAttribute", + "sortField": "best", + "sortValue": "0", + "ascending": true, + "mosaicOperation": "MT_FIRST" + } + } + }), + OutputName: JSON.stringify({ + "serviceProperties": { + "name": createServiceResponse.name, + "capabilities": "Image", + "serviceUrl": createServiceResponse.serviceurl + }, + "itemProperties": { + "itemId": createServiceResponse.itemId + } + }) + }) + + const res = await fetch(requestURL, { + method: 'POST', + body: params + }) + + const data = await res.json() + + if(data.error){ + throw data.error + } + + if(!data?.success){ + throw data + } + + return data; +} + +/** + * + * @param {*} jobId + * @param {*} token + * @returns + * + * example of response: + * ```js + * { + "jobId": "11cba2e5eb994caab277f3df887044c6", + "jobStatus": "esriJobSucceeded", + "inputs": { + "rasterFunction": { + "paramUrl": "inputs/rasterFunction" + }, + "functionArguments": { + "paramUrl": "inputs/functionArguments" + }, + "OutputName": { + "paramUrl": "inputs/OutputName" + }, + "context": { + "paramUrl": "inputs/context" + }, + "outputType": { + "paramUrl": "inputs/outputType" + } + }, + "results": { + "outputRaster": { + "paramUrl": "results/outputRaster" + } + }, + "messages": [ + { + "type": "esriJobMessageTypeWarning", + "description": "{\"cost\": 1}" + }, + { + "type": "esriJobMessageTypeInformative", + "description": "Raster Analysis Privilege Check: OK" + }, + { + "type": "esriJobMessageTypeInformative", + "description": "Output item id is: 7eeab16829b44ebe8ed707c38fda37f0" + }, + { + "type": "esriJobMessageTypeInformative", + "description": "Output image service url is: https://iservicesdev.arcgis.com/LkFyxb9zDq7vAOAm/arcgis/rest/services/Data_extracted_from_LandsatC2L2_202410151433/ImageServer" + }, + { + "type": "esriJobMessageTypeInformative", + "description": "Output cloud raster name is: /cloudStores/LkFyxb9zDq7vAOAm/7eeab16829b44ebe8ed707c38fda37f0/Data_extracted_from_LandsatC2L2_202410151433.crf" + }, + { + "type": "esriJobMessageTypeInformative", + "description": "{\"rasterFunction\":\"Clip\",\"rasterFunctionArguments\":{\"ClippingType\":\"1\",\"ClippingGeometry\":{\"xmin\":-12583429.576139491,\"ymin\":3900032.88035888,\"xmax\":-12545688.793423735,\"ymax\":3910963.375403645,\"spatialReference\":{\"wkid\":102100,\"latestWkid\":3857,\"xyTolerance\":0.001,\"zTolerance\":0.001,\"mTolerance\":0.001,\"falseX\":-20037700,\"falseY\":-30241100,\"xyUnits\":10000,\"falseZ\":-100000,\"zUnits\":10000,\"falseM\":-100000,\"mUnits\":10000}},\"clippingMethod\":\"byExtent\",\"valueLayer\":\"{\\\"url\\\":\\\"https://iservicesdev1-eu1.arcgis.com/03e6LFX6hxm1ywlK/arcgis/rest/services/LivingAtlas_Landsat_OLI_TM_ETM/ImageServer?token=9RK8DY0qvaqYcPFrrqGyoc_Hw2w_BiuUfWSbVAwTisP5_rcoZesuFjwM_Y5mRo0ow_UCCbyhr3VWNLAkFpt5nzI9bPiHwNLMPXPn-qiJHI-3m-KbrzYtpMR_iEAHRn91mjvRrRajjv-EGAQm7dFR0QFpPaP85n9we1PpGcGhCBOht33KoRjQw3kjxruAO5LkTc_i_vLJvUb0dD7MMfHLLo5ja5-Tzuvru43LME6EO1HSRngkywAlngUor66va9byjY8C4BMwCRXOHpZIdmWgiII2OGOaIW8lGjR9ZPhw6f4lLmmM84O8849dDcKxU_OB\\\",\\\"name\\\":\\\"LandsatC2L2\\\",\\\"renderingRule\\\":{\\\"rasterFunction\\\":\\\"Natural Color with DRA\\\"},\\\"mosaicRule\\\":{\\\"mosaicMethod\\\":\\\"esriMosaicAttribute\\\",\\\"sortField\\\":\\\"best\\\",\\\"sortValue\\\":\\\"0\\\",\\\"ascending\\\":true,\\\"mosaicOperation\\\":\\\"MT_FIRST\\\"}}\"}}" + }, + { + "type": "esriJobMessageTypeInformative", + "description": "/cloudStores/LkFyxb9zDq7vAOAm/7eeab16829b44ebe8ed707c38fda37f0/Data_extracted_from_LandsatC2L2_202410151433.crf" + }, + { + "type": "esriJobMessageTypeInformative", + "description": "raster '{\"url\": \"https://iservicesdev1-eu1.arcgis.com/03e6LFX6hxm1ywlK/arcgis/rest/services/LivingAtlas_Landsat_OLI_TM_ETM/ImageServer?token=9RK8DY0qvaqYcPFrrqGyoc_Hw2w_BiuUfWSbVAwTisP5_rcoZesuFjwM_Y5mRo0ow_UCCbyhr3VWNLAkFpt5nzI9bPiHwNLMPXPn-qiJHI-3m-KbrzYtpMR_iEAHRn91mjvRrRajjv-EGAQm7dFR0QFpPaP85n9we1PpGcGhCBOht33KoRjQw3kjxruAO5LkTc_i_vLJvUb0dD7MMfHLLo5ja5-Tzuvru43LME6EO1HSRngkywAlngUor66va9byjY8C4BMwCRXOHpZIdmWgiII2OGOaIW8lGjR9ZPhw6f4lLmmM84O8849dDcKxU_OB\", \"name\": \"LandsatC2L2\", \"renderingRule\": {\"rasterFunction\": \"Natural Color with DRA\"}, \"mosaicRule\": {\"mosaicMethod\": \"esriMosaicAttribute\", \"sortField\": \"best\", \"sortValue\": \"0\", \"ascending\": true, \"mosaicOperation\": \"MT_FIRST\"}}'" + }, + { + "type": "esriJobMessageTypeInformative", + "description": "Updating service with data store URI." + }, + { + "type": "esriJobMessageTypeInformative", + "description": "Getting image service info..." + }, + { + "type": "esriJobMessageTypeInformative", + "description": "Updating service: https://iservicesdev.arcgis.com/LkFyxb9zDq7vAOAm/arcgis/rest/admin/services/Data_extracted_from_LandsatC2L2_202410151433/ImageServer/edit" + }, + { + "type": "esriJobMessageTypeInformative", + "description": "Update item service: https://iservicesdev.arcgis.com/LkFyxb9zDq7vAOAm/arcgis/rest/admin/services/Data_extracted_from_LandsatC2L2_202410151433/ImageServer successfully." + }, + { + "type": "esriJobMessageTypeInformative", + "description": "Portal item refreshed." + }, + { + "type": "esriJobMessageTypeInformative", + "description": "Succeeded at Tuesday, October 15, 2024 9:36:17 PM (Elapsed Time: 58.13 seconds)" + } + ] + } + ``` + */ +const checkJobStatus = async(jobId, token)=>{ + + const requestURL = RASTER_ANALYSIS_ROOT + `/GenerateRaster/jobs/${jobId}?f=json&token=${token}`; + // console.log(requestURL) + + const res = await fetch(requestURL) + + const data = await res.json() + + if(data.error){ + throw data.error + } + + return data; +} + +const start = async()=>{ + + try { + const token = await generateToken() + // console.log(token) + + const serviceName = 'Landsat_Level2_' + (Math.random() * 1000).toFixed(0); + const createServiceResponse = await createHostedImageryService(serviceName, token) + // console.log(createServiceResponse) + + const generateRasterNewJobRes = await submitNewJob({ + createServiceResponse, + token + }) + console.log(generateRasterNewJob) + + const checkJobStatusRes = await checkJobStatus(generateRasterNewJobRes.jobId, token); + console.log(checkJobStatusRes) + + } catch(err){ + console.log(err); + } +} + +start(); \ No newline at end of file diff --git a/sandbox/rasterAnalysis/generateToken.js b/sandbox/rasterAnalysis/generateToken.js new file mode 100644 index 00000000..fd4b1b0c --- /dev/null +++ b/sandbox/rasterAnalysis/generateToken.js @@ -0,0 +1,46 @@ +const { ARCGIS_ONLINE_PORTAL_REST_ROOT, USERNAME, PASSWORD } = require("./config"); + +/** + * Generate a access token for ArcGIS Online + * @returns Promise token string that expires in 15 minutes + */ +const generateToken = async()=>{ + + if(!USERNAME || !PASSWORD){ + throw new Error('ArcGIS Online credential is required to generate token. Please make sure they are in the .env file') + } + + const params = new URLSearchParams({ + 'username': USERNAME, + 'password': PASSWORD, + // Tried to use requestip but had issue using the generated token to export image from the imagery server like Landsat Level-2 + // Figured out this workaround to use the referrer as client and pass an arbitary referrer URL... + 'client': 'referer', + 'referer': 'https://www.arcgis.com', + 'expiration': '15', + 'f': 'json', + }) + + const res = await fetch( + `${ARCGIS_ONLINE_PORTAL_REST_ROOT}/generateToken`, + { + method: 'POST', + body: params + } + ); + + const data = await res.json() + // console.log(data) + + if(data?.error){ + throw data.error; + } + + // console.log(res, data) + return data?.token + +} + +module.exports = { + generateToken +} \ No newline at end of file From feae25176f2ed558702021f76cb405ac5363f3fe Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 16 Oct 2024 10:50:11 -0700 Subject: [PATCH 041/306] chore: update generateRaster.js in sandbox --- sandbox/rasterAnalysis/generateRaster.js | 71 ++++++++++++++---------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/sandbox/rasterAnalysis/generateRaster.js b/sandbox/rasterAnalysis/generateRaster.js index e73b93c0..95461da2 100644 --- a/sandbox/rasterAnalysis/generateRaster.js +++ b/sandbox/rasterAnalysis/generateRaster.js @@ -34,32 +34,32 @@ const submitNewJob = async({ "rasterFunctionArguments": { "ClippingType": "1", "ClippingGeometry": { - "xmin": -12583429.576139491, - "ymin": 3900032.88035888, - "xmax": -12545688.793423735, - "ymax": 3910963.375403645, - "spatialReference": { - "wkid": 102100, - "latestWkid": 3857, - "xyTolerance": 0.001, - "zTolerance": 0.001, - "mTolerance": 0.001, - "falseX": -20037700, - "falseY": -30241100, - "xyUnits": 10000, - "falseZ": -100000, - "zUnits": 10000, - "falseM": -100000, - "mUnits": 10000 + "xmin":-13049612.365756018, + "ymin":3855701.538507286, + "xmax":-13030741.974398142, + "ymax":3859867.356548822, + "spatialReference":{ + "wkid":102100, + "latestWkid":3857, + "xyTolerance":0.001, + "zTolerance":0.001, + "mTolerance":0.001, + "falseX":-20037700, + "falseY":-30241100, + "xyUnits":10000, + "falseZ":-100000, + "zUnits":10000, + "falseM":-100000, + "mUnits":10000 } }, "clippingMethod": "byExtent", "valueLayer": { "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, "name":"LandsatC2L2", - "renderingRule":{ - "rasterFunction":"Natural Color with DRA" - }, + // "renderingRule":{ + // "rasterFunction":"Natural Color with DRA" + // }, "mosaicRule":{ "mosaicMethod":"esriMosaicAttribute", "sortField":"best", @@ -74,9 +74,9 @@ const submitNewJob = async({ "raster": { "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, "name": "LandsatC2L2", - "renderingRule": { - "rasterFunction": "Natural Color with DRA" - }, + // "renderingRule": { + // "rasterFunction": "Natural Color with DRA" + // }, "mosaicRule": { "mosaicMethod": "esriMosaicAttribute", "sortField": "best", @@ -95,6 +95,21 @@ const submitNewJob = async({ "itemProperties": { "itemId": createServiceResponse.itemId } + }), + context: JSON.stringify({ + "extent":{ + "xmin":-13049612.365756018, + "ymin":3855701.538507286, + "xmax":-13030741.974398142, + "ymax":3859867.356548822, + "spatialReference":{ + "wkid":102100, + "latestWkid":3857, + "xyTolerance":0.001, + "zTolerance":0.001, + "mTolerance":0.001,"falseX":-20037700,"falseY":-30241100,"xyUnits":10000,"falseZ":-100000,"zUnits":10000,"falseM":-100000,"mUnits":10000 + } + } }) }) @@ -109,10 +124,6 @@ const submitNewJob = async({ throw data.error } - if(!data?.success){ - throw data - } - return data; } @@ -213,7 +224,7 @@ const submitNewJob = async({ const checkJobStatus = async(jobId, token)=>{ const requestURL = RASTER_ANALYSIS_ROOT + `/GenerateRaster/jobs/${jobId}?f=json&token=${token}`; - // console.log(requestURL) + console.log('requestURL for checkJobStatus', requestURL) const res = await fetch(requestURL) @@ -240,10 +251,10 @@ const start = async()=>{ createServiceResponse, token }) - console.log(generateRasterNewJob) + console.log('generateRasterNewJobRes', generateRasterNewJobRes) const checkJobStatusRes = await checkJobStatus(generateRasterNewJobRes.jobId, token); - console.log(checkJobStatusRes) + console.log('checkJobStatusRes', checkJobStatusRes) } catch(err){ console.log(err); From ada6a32df8098e507dcb460a194b8fc9768cdc7b Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 16 Oct 2024 13:27:47 -0700 Subject: [PATCH 042/306] chore: update generateRaster.js in sandbox --- sandbox/rasterAnalysis/generateRaster.js | 300 +++++++++++++---------- 1 file changed, 169 insertions(+), 131 deletions(-) diff --git a/sandbox/rasterAnalysis/generateRaster.js b/sandbox/rasterAnalysis/generateRaster.js index 95461da2..417f410b 100644 --- a/sandbox/rasterAnalysis/generateRaster.js +++ b/sandbox/rasterAnalysis/generateRaster.js @@ -1,8 +1,79 @@ require('dotenv').config() -const { RASTER_ANALYSIS_ROOT } = require('./config') -const { createHostedImageryService } = require('./createHostedImageryService') -const { generateToken } = require('./generateToken') +const { + RASTER_ANALYSIS_ROOT +} = require('./config') +const { + createHostedImageryService +} = require('./createHostedImageryService') +const { + generateToken +} = require('./generateToken') + +const OBJECTID = 9445689; + +const CLIPPING_GEOM = { + "rings": [ + [ + [ + -13126451.3033000007, + 4058763.9095999971 + ], + [ + -12908191.6368, + 4007893.42949999869 + ], + [ + -12963104.7622, + 3779150.445600003 + ], + [ + -13177316.9085000008, + 3829010.012000002 + ], + [ + -13126451.3033000007, + 4058763.9095999971 + ] + ] + ], + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857, + "xyTolerance": 0.001, + "zTolerance": 0.001, + "mTolerance": 0.001, + "falseX": -20037700, + "falseY": -30241100, + "xyUnits": 10000, + "falseZ": -100000, + "zUnits": 10000, + "falseM": -100000, + "mUnits": 10000 + } +} + +const EXTENT_OF_CLIPPING_GEOM = { + "xmin": -13177316.908500001, + "ymin": 3779150.445600003, + "xmax": -12908191.6368, + "ymax": 4058763.9095999971, + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857, + "xyTolerance": 0.001, + "zTolerance": 0.001, + "mTolerance": 0.001, + "falseX": -20037700, + "falseY": -30241100, + "xyUnits": 10000, + "falseZ": -100000, + "zUnits": 10000, + "falseM": -100000, + "mUnits": 10000 + } +} + /** * Submit new Generate Raster GP Job. @@ -19,112 +90,79 @@ const { generateToken } = require('./generateToken') * } * ``` */ -const submitNewJob = async({ - token, - createServiceResponse -})=>{ - const requestURL = RASTER_ANALYSIS_ROOT + "/GenerateRaster/submitJob"; +const submitNewJob = async ({ + token, + createServiceResponse +}) => { + const requestURL = RASTER_ANALYSIS_ROOT + "/GenerateRaster/submitJob"; - const params = new URLSearchParams({ - f: "json", - token, - outputType: 'dynamicLayer', - rasterFunction: JSON.stringify({ - "rasterFunction": "Clip", - "rasterFunctionArguments": { - "ClippingType": "1", - "ClippingGeometry": { - "xmin":-13049612.365756018, - "ymin":3855701.538507286, - "xmax":-13030741.974398142, - "ymax":3859867.356548822, - "spatialReference":{ - "wkid":102100, - "latestWkid":3857, - "xyTolerance":0.001, - "zTolerance":0.001, - "mTolerance":0.001, - "falseX":-20037700, - "falseY":-30241100, - "xyUnits":10000, - "falseZ":-100000, - "zUnits":10000, - "falseM":-100000, - "mUnits":10000 - } - }, - "clippingMethod": "byExtent", - "valueLayer": { - "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, - "name":"LandsatC2L2", - // "renderingRule":{ - // "rasterFunction":"Natural Color with DRA" - // }, - "mosaicRule":{ - "mosaicMethod":"esriMosaicAttribute", - "sortField":"best", - "sortValue":"0", - "ascending":true, - "mosaicOperation":"MT_FIRST" - } - } - } - }), - functionArguments: JSON.stringify({ - "raster": { - "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, - "name": "LandsatC2L2", - // "renderingRule": { - // "rasterFunction": "Natural Color with DRA" - // }, - "mosaicRule": { - "mosaicMethod": "esriMosaicAttribute", - "sortField": "best", - "sortValue": "0", - "ascending": true, - "mosaicOperation": "MT_FIRST" - } - } - }), - OutputName: JSON.stringify({ - "serviceProperties": { - "name": createServiceResponse.name, - "capabilities": "Image", - "serviceUrl": createServiceResponse.serviceurl - }, - "itemProperties": { - "itemId": createServiceResponse.itemId - } - }), - context: JSON.stringify({ - "extent":{ - "xmin":-13049612.365756018, - "ymin":3855701.538507286, - "xmax":-13030741.974398142, - "ymax":3859867.356548822, - "spatialReference":{ - "wkid":102100, - "latestWkid":3857, - "xyTolerance":0.001, - "zTolerance":0.001, - "mTolerance":0.001,"falseX":-20037700,"falseY":-30241100,"xyUnits":10000,"falseZ":-100000,"zUnits":10000,"falseM":-100000,"mUnits":10000 - } - } - }) - }) + const params = new URLSearchParams({ + f: "json", + token, + outputType: 'dynamicLayer', + rasterFunction: JSON.stringify({ + "rasterFunction": "Clip", + "rasterFunctionArguments": { + "ClippingType": "1", + "ClippingGeometry": CLIPPING_GEOM, + // "clippingMethod": "byExtent", + "valueLayer": { + "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, + "name": "LandsatC2L2", + // "renderingRule":{ + // "rasterFunction":"Natural Color with DRA" + // }, + "mosaicRule": { + "ascending": false, + "lockRasterIds": [OBJECTID], + "mosaicMethod": "esriMosaicLockRaster", + "where": `objectid in (${OBJECTID})` + } + } + } + }), + functionArguments: JSON.stringify({ + "raster": { + "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, + "name": "LandsatC2L2", + // "renderingRule": { + // "rasterFunction": "Natural Color with DRA" + // }, + "mosaicRule": { + "ascending": false, + "lockRasterIds": [OBJECTID], + "mosaicMethod": "esriMosaicLockRaster", + "where": `objectid in (${OBJECTID})` + } + } + }), + OutputName: JSON.stringify({ + "serviceProperties": { + "name": createServiceResponse.name, + "capabilities": "Image", + "serviceUrl": createServiceResponse.serviceurl + }, + "itemProperties": { + "itemId": createServiceResponse.itemId + } + }), + context: JSON.stringify({ + "geometry": CLIPPING_GEOM + }) + }) - const res = await fetch(requestURL, { - method: 'POST', - body: params - }) + const res = await fetch(requestURL, { + method: 'POST', + body: params + }) - const data = await res.json() + const data = await res.json() - if(data.error){ - throw data.error - } + if (data.error) { + throw data.error + } - return data; + return data; } /** @@ -221,44 +259,44 @@ const submitNewJob = async({ } ``` */ -const checkJobStatus = async(jobId, token)=>{ +const checkJobStatus = async (jobId, token) => { - const requestURL = RASTER_ANALYSIS_ROOT + `/GenerateRaster/jobs/${jobId}?f=json&token=${token}`; - console.log('requestURL for checkJobStatus', requestURL) + const requestURL = RASTER_ANALYSIS_ROOT + `/GenerateRaster/jobs/${jobId}?f=json&token=${token}`; + console.log('requestURL for checkJobStatus', requestURL) - const res = await fetch(requestURL) - - const data = await res.json() + const res = await fetch(requestURL) - if(data.error){ - throw data.error - } + const data = await res.json() - return data; + if (data.error) { + throw data.error + } + + return data; } -const start = async()=>{ +const start = async () => { - try { - const token = await generateToken() - // console.log(token) + try { + const token = await generateToken() + // console.log(token) - const serviceName = 'Landsat_Level2_' + (Math.random() * 1000).toFixed(0); - const createServiceResponse = await createHostedImageryService(serviceName, token) - // console.log(createServiceResponse) + const serviceName = 'Landsat_Level2_' + (Math.random() * 1000).toFixed(0); + const createServiceResponse = await createHostedImageryService(serviceName, token) + // console.log(createServiceResponse) - const generateRasterNewJobRes = await submitNewJob({ - createServiceResponse, - token - }) - console.log('generateRasterNewJobRes', generateRasterNewJobRes) + const generateRasterNewJobRes = await submitNewJob({ + createServiceResponse, + token + }) + console.log('generateRasterNewJobRes', generateRasterNewJobRes) - const checkJobStatusRes = await checkJobStatus(generateRasterNewJobRes.jobId, token); - console.log('checkJobStatusRes', checkJobStatusRes) + const checkJobStatusRes = await checkJobStatus(generateRasterNewJobRes.jobId, token); + console.log('checkJobStatusRes', checkJobStatusRes) - } catch(err){ - console.log(err); - } + } catch (err) { + console.log(err); + } } start(); \ No newline at end of file From 95f8a21b2103f04a5ba92402eb051eff6af6b0dc Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 17 Oct 2024 09:10:56 -0700 Subject: [PATCH 043/306] chore: generate raster GPJob should include extent param --- sandbox/rasterAnalysis/generateRaster.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sandbox/rasterAnalysis/generateRaster.js b/sandbox/rasterAnalysis/generateRaster.js index 417f410b..d27e7870 100644 --- a/sandbox/rasterAnalysis/generateRaster.js +++ b/sandbox/rasterAnalysis/generateRaster.js @@ -105,7 +105,7 @@ const submitNewJob = async ({ "rasterFunctionArguments": { "ClippingType": "1", "ClippingGeometry": CLIPPING_GEOM, - // "clippingMethod": "byExtent", + "extent": EXTENT_OF_CLIPPING_GEOM, "valueLayer": { "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, "name": "LandsatC2L2", From 37dee8b1909fea9453a2b444a31dc3b2e0fe6e01 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 17 Oct 2024 09:39:18 -0700 Subject: [PATCH 044/306] feat(shared): add tooltip to Spectral Profile Tool --- .../SpectralProfileChart.tsx | 1 + .../components/SpectralProfileTool/helpers.ts | 55 ++++++++++++++----- .../useGenerateSpectralProfileChartData.tsx | 18 +++--- .../SamplingResults/useChartData.tsx | 10 ++-- 4 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/shared/components/SpectralProfileTool/SpectralProfileChart.tsx b/src/shared/components/SpectralProfileTool/SpectralProfileChart.tsx index 491f1391..3eca5c92 100644 --- a/src/shared/components/SpectralProfileTool/SpectralProfileChart.tsx +++ b/src/shared/components/SpectralProfileTool/SpectralProfileChart.tsx @@ -58,6 +58,7 @@ export const SpectralProfileChart: FC = ({ = { Cloud: '#888888', 'Clear Water': '#0079F2', @@ -93,28 +110,40 @@ export const findMostSimilarLandCoverType = ( }; /** - * Converts an array of band values to an array of LineChartDataItem objects. + * Converts an array of band values into an array of LineChartDataItem objects. * - * This function processes an array of numeric band values, selecting up to a specified maximum number of bands. - * It normalizes the values to ensure they fall within the range of 0 to 1, and then maps each band value to - * a LineChartDataItem object with `x` and `y` coordinates, where `x` represents the band index and `y` the normalized value. + * This function processes an array of numeric band values, optionally limiting the number + * of values based on the specified length. It normalizes each value to a range between 0 and 1, + * and maps each value to a LineChartDataItem object, where the `x` property represents the band's index + * and the `y` property represents the normalized value. Optionally includes a title in the tooltip text. * - * @param {number[]} bandValues - An array of numeric values representing imagery band values. - * @param {number} length - The number of band values to include in the output array. - * @returns {LineChartDataItem[]} An array of LineChartDataItem objects with normalized x and y values. + * @param {FormatBandValuesAsLineChartDataItemsParams} params - The input parameters, including band values and optional title and length. + * @returns {LineChartDataItem[]} An array of LineChartDataItem objects with `x`, `y` coordinates and tooltips. */ -export const formatBandValuesAsLineChartDataItems = ( - bandValues: number[], - length?: number -) => { +export const formatBandValuesAsLineChartDataItems = ({ + bandValues, + title, + length, +}: FormatBandValuesAsLineChartDataItemsParams) => { if (!bandValues || !bandValues.length) { return []; } - return bandValues.slice(0, length).map((val, index) => { + if (length !== undefined) { + bandValues = bandValues.slice(0, length); + } + + return bandValues.map((val, index) => { + const normalizedY = normalizeBandValue(val, 0, 1); + + const tooltipText = title + ? `${title}: ${normalizedY.toFixed(3)}` + : normalizedY.toFixed(3); + return { x: index, - y: normalizeBandValue(val, 0, 1), + y: normalizedY, + tooltip: tooltipText, } as LineChartDataItem; }); }; diff --git a/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx b/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx index 57a68325..a29adaa7 100644 --- a/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx +++ b/src/shared/components/SpectralProfileTool/useGenerateSpectralProfileChartData.tsx @@ -66,16 +66,18 @@ export const useGenerateSpectralProfileChartData = ( ); const lineChartData4SelectedLocation = - formatBandValuesAsLineChartDataItems( - bandValuesFromSelectedLocation, - length - ); + formatBandValuesAsLineChartDataItems({ + bandValues: bandValuesFromSelectedLocation, + title: 'Selected Location', + length, + }); const lineChartData4SelectedLandCoverType = - formatBandValuesAsLineChartDataItems( - bandValuesFromSelectedLandCoverType, - length - ); + formatBandValuesAsLineChartDataItems({ + bandValues: bandValuesFromSelectedLandCoverType, + title: landCoverType, + length, + }); return [ { diff --git a/src/spectral-sampling-tool/components/SamplingResults/useChartData.tsx b/src/spectral-sampling-tool/components/SamplingResults/useChartData.tsx index 853e8e32..6dce6797 100644 --- a/src/spectral-sampling-tool/components/SamplingResults/useChartData.tsx +++ b/src/spectral-sampling-tool/components/SamplingResults/useChartData.tsx @@ -52,9 +52,9 @@ export const useChartData = () => { const output: LineGroupData[] = samplingPointsData .filter((d) => d.location && d.bandValues) .map((d, index) => { - const values = formatBandValuesAsLineChartDataItems( - d.bandValues - ); + const values = formatBandValuesAsLineChartDataItems({ + bandValues: d.bandValues, + }); return { fill: @@ -77,7 +77,9 @@ export const useChartData = () => { output.push({ fill: 'var(--custom-light-blue-90)', key: 'average', - values: formatBandValuesAsLineChartDataItems(averageBandValues), + values: formatBandValuesAsLineChartDataItems({ + bandValues: averageBandValues, + }), dashPattern: '9 3', // use dash pattern to provide user a hint that the feature of interest is just a reference }); } From 2e842f85d04e35df67ae19a59e239497cdaa526f Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 18 Oct 2024 12:35:44 -0700 Subject: [PATCH 045/306] chore: generateRaster should use correct syntax --- sandbox/rasterAnalysis/generateRaster.js | 138 ++++++++++++++--------- sandbox/rasterAnalysis/generateToken.js | 2 +- 2 files changed, 86 insertions(+), 54 deletions(-) diff --git a/sandbox/rasterAnalysis/generateRaster.js b/sandbox/rasterAnalysis/generateRaster.js index d27e7870..1c150fc3 100644 --- a/sandbox/rasterAnalysis/generateRaster.js +++ b/sandbox/rasterAnalysis/generateRaster.js @@ -39,17 +39,7 @@ const CLIPPING_GEOM = { ], "spatialReference": { "wkid": 102100, - "latestWkid": 3857, - "xyTolerance": 0.001, - "zTolerance": 0.001, - "mTolerance": 0.001, - "falseX": -20037700, - "falseY": -30241100, - "xyUnits": 10000, - "falseZ": -100000, - "zUnits": 10000, - "falseM": -100000, - "mUnits": 10000 + "latestWkid": 3857 } } @@ -60,17 +50,7 @@ const EXTENT_OF_CLIPPING_GEOM = { "ymax": 4058763.9095999971, "spatialReference": { "wkid": 102100, - "latestWkid": 3857, - "xyTolerance": 0.001, - "zTolerance": 0.001, - "mTolerance": 0.001, - "falseX": -20037700, - "falseY": -30241100, - "xyUnits": 10000, - "falseZ": -100000, - "zUnits": 10000, - "falseM": -100000, - "mUnits": 10000 + "latestWkid": 3857 } } @@ -95,46 +75,98 @@ const submitNewJob = async ({ createServiceResponse }) => { const requestURL = RASTER_ANALYSIS_ROOT + "/GenerateRaster/submitJob"; + console.log(requestURL) const params = new URLSearchParams({ f: "json", token, outputType: 'dynamicLayer', + // rasterFunction: JSON.stringify({ + // "rasterFunction": "Clip", + // "rasterFunctionArguments": { + // "ClippingType": "1", + // "ClippingGeometry": CLIPPING_GEOM, + // "extent": EXTENT_OF_CLIPPING_GEOM, + // "valueLayer": { + // "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, + // "name": "LandsatC2L2", + // // "renderingRule":{ + // // "rasterFunction":"Natural Color with DRA" + // // }, + // "mosaicRule": { + // "ascending": false, + // "lockRasterIds": [OBJECTID], + // "mosaicMethod": "esriMosaicLockRaster", + // "where": `objectid in (${OBJECTID})` + // } + // } + // } + // }), rasterFunction: JSON.stringify({ - "rasterFunction": "Clip", - "rasterFunctionArguments": { - "ClippingType": "1", - "ClippingGeometry": CLIPPING_GEOM, - "extent": EXTENT_OF_CLIPPING_GEOM, - "valueLayer": { - "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, - "name": "LandsatC2L2", - // "renderingRule":{ - // "rasterFunction":"Natural Color with DRA" - // }, + "name": "Clip", + "description": "Sets the extent of a raster using coordinates or another dataset.", + "function": { + "type": "ClipFunction", + "pixelType": "UNKNOWN", + "name": "Clip", + "description": "Sets the extent of a raster using coordinates or another dataset." + }, + "arguments": { + "Raster": { + "name": "Raster", + "isPublic": false, + "isDataset": true, + "value": { + "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, + "name": "LandsatC2L2", "mosaicRule": { "ascending": false, "lockRasterIds": [OBJECTID], "mosaicMethod": "esriMosaicLockRaster", "where": `objectid in (${OBJECTID})` } - } - } - }), - functionArguments: JSON.stringify({ - "raster": { - "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, - "name": "LandsatC2L2", - // "renderingRule": { - // "rasterFunction": "Natural Color with DRA" - // }, - "mosaicRule": { - "ascending": false, - "lockRasterIds": [OBJECTID], - "mosaicMethod": "esriMosaicLockRaster", - "where": `objectid in (${OBJECTID})` - } - } + }, + "type": "RasterFunctionVariable" + }, + "ClippingType": { + "name": "ClippingType", + "isPublic": false, + "isDataset": false, + "value": 1, + "type": "RasterFunctionVariable" + }, + "ClippingRaster": { + "name": "ClippingRaster", + "isPublic": false, + "isDataset": true, + "type": "RasterFunctionVariable" + }, + "ClippingGeometry": { + "name": "ClippingGeometry", + "isPublic": false, + "isDataset": false, + "value": CLIPPING_GEOM, + "type": "RasterFunctionVariable" + }, + // "Extent": { + // "name": "Extent", + // "isPublic": false, + // "isDataset": false, + // "value": EXTENT_OF_CLIPPING_GEOM, + // "type": "RasterFunctionVariable" + // }, + "UseInputFeatureGeometry": { + "name": "UseInputFeatureGeometry", + "isPublic": false, + "isDataset": false, + "type": "RasterFunctionVariable" + }, + "type": "ClipFunctionArguments" + }, + "functionType": 0, + "thumbnail": "", + "thumbnailEx": "", + "help": "" }), OutputName: JSON.stringify({ "serviceProperties": { @@ -146,9 +178,9 @@ const submitNewJob = async ({ "itemId": createServiceResponse.itemId } }), - context: JSON.stringify({ - "geometry": CLIPPING_GEOM - }) + // context: JSON.stringify({ + // "geometry": CLIPPING_GEOM + // }) }) const res = await fetch(requestURL, { diff --git a/sandbox/rasterAnalysis/generateToken.js b/sandbox/rasterAnalysis/generateToken.js index fd4b1b0c..9e444171 100644 --- a/sandbox/rasterAnalysis/generateToken.js +++ b/sandbox/rasterAnalysis/generateToken.js @@ -17,7 +17,7 @@ const generateToken = async()=>{ // Figured out this workaround to use the referrer as client and pass an arbitary referrer URL... 'client': 'referer', 'referer': 'https://www.arcgis.com', - 'expiration': '15', + 'expiration': '60', 'f': 'json', }) From 7d3164096ebaf4486ee740d84cdbd35fa45348e6 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 18 Oct 2024 12:36:52 -0700 Subject: [PATCH 046/306] chore: rename generateRaster to clipRaster --- sandbox/rasterAnalysis/{generateRaster.js => clipRaster.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sandbox/rasterAnalysis/{generateRaster.js => clipRaster.js} (100%) diff --git a/sandbox/rasterAnalysis/generateRaster.js b/sandbox/rasterAnalysis/clipRaster.js similarity index 100% rename from sandbox/rasterAnalysis/generateRaster.js rename to sandbox/rasterAnalysis/clipRaster.js From 16afd0e11cabd3858fddfbac454d9850c43719d2 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 18 Oct 2024 14:33:50 -0700 Subject: [PATCH 047/306] chore: add water mask example to sandbox --- sandbox/waterMask.html | 68 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 sandbox/waterMask.html diff --git a/sandbox/waterMask.html b/sandbox/waterMask.html new file mode 100644 index 00000000..f5a319e7 --- /dev/null +++ b/sandbox/waterMask.html @@ -0,0 +1,68 @@ + + + + + Intro to ImageryLayer | Sample | ArcGIS Maps SDK for JavaScript 4.30 + + + + + + + + + + +
+ + \ No newline at end of file From 0656def05a7f5067e361565355252fc675e63a02 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Sat, 19 Oct 2024 05:51:37 -0700 Subject: [PATCH 048/306] chore: save bandArithmetic and remapPixelValues to sandbox --- sandbox/rasterAnalysis/bandArithmetic.js | 149 ++++++++++++++ sandbox/rasterAnalysis/clipRaster.js | 4 +- sandbox/rasterAnalysis/remapPixelValues.js | 214 +++++++++++++++++++++ 3 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 sandbox/rasterAnalysis/bandArithmetic.js create mode 100644 sandbox/rasterAnalysis/remapPixelValues.js diff --git a/sandbox/rasterAnalysis/bandArithmetic.js b/sandbox/rasterAnalysis/bandArithmetic.js new file mode 100644 index 00000000..b211660a --- /dev/null +++ b/sandbox/rasterAnalysis/bandArithmetic.js @@ -0,0 +1,149 @@ +require('dotenv').config() + +const { + RASTER_ANALYSIS_ROOT +} = require('./config') +const { + createHostedImageryService +} = require('./createHostedImageryService') +const { + generateToken +} = require('./generateToken') + +/** + * Submit new Generate Raster GP Job. + * @param {Object} param0 - An object containing the following properties: + * - token (string): ArcGIS authentication token. + * - createServiceResponse (Object): Response object from a previous service creation step. (Optional) + * @returns {Object} - Response object containing job information or throws an error if unsuccessful. + * + * example of response: + * ``` + * { + * "jobId":"11cba2e5eb994caab277f3df887044c6", + * "jobStatus":"esriJobNew" + * } + * ``` + */ +const submitNewJob = async ({ + token, + createServiceResponse +}) => { + const requestURL = RASTER_ANALYSIS_ROOT + "/GenerateRaster/submitJob"; + console.log(requestURL) + + const params = new URLSearchParams({ + f: "json", + token, + outputType: 'dynamicLayer', + rasterFunction: JSON.stringify({ + "name":"Band Arithmetic", + "description":"Calculates indexes using predefined formulas or a user-defined expression.", + "function":{ + "type":"BandArithmeticFunction", + "pixelType":"UNKNOWN", + "name":"Band Arithmetic", + "description":"Calculates indexes using predefined formulas or a user-defined expression." + }, + "arguments":{ + "Raster":{ + "name":"Raster", + "isPublic":false, + "isDataset":true, + "value":{ + "url":`https://iservicesdev.arcgis.com/LkFyxb9zDq7vAOAm/arcgis/rest/services/Landsat_Level2_456/ImageServer?token=${token}`, + "name":"Landsat_Level2_762" + }, + "type":"RasterFunctionVariable" + }, + "Method":{ + "name":"Method", + "isPublic":false, + "isDataset":false, + "value":0, + "type":"RasterFunctionVariable" + }, + "BandIndexes":{ + "name":"BandIndexes", + "isPublic":false, + "isDataset":false, + "value":"(B3-B6)/(B3+B6)", + "type":"RasterFunctionVariable" + }, + "type":"BandArithmeticFunctionArguments" + }, + "functionType":0, + "thumbnail":"", + "thumbnailEx":"", + "help":"" + }), + OutputName: JSON.stringify({ + "serviceProperties": { + "name": createServiceResponse.name, + "capabilities": "Image", + "serviceUrl": createServiceResponse.serviceurl + }, + "itemProperties": { + "itemId": createServiceResponse.itemId + } + }), + // context: JSON.stringify({ + // "geometry": CLIPPING_GEOM + // }) + }) + + const res = await fetch(requestURL, { + method: 'POST', + body: params + }) + + const data = await res.json() + + if (data.error) { + throw data.error + } + + return data; +} + +const checkJobStatus = async (jobId, token) => { + + const requestURL = RASTER_ANALYSIS_ROOT + `/GenerateRaster/jobs/${jobId}?f=json&token=${token}`; + console.log('requestURL for checkJobStatus', requestURL) + + const res = await fetch(requestURL) + + const data = await res.json() + + if (data.error) { + throw data.error + } + + return data; +} + +const start = async () => { + + try { + const token = await generateToken() + // console.log(token) + + const serviceName = 'Landsat_Level2_BandArithmetic_' + (Math.random() * 1000).toFixed(0); + const createServiceResponse = await createHostedImageryService(serviceName, token) + // console.log(createServiceResponse) + + const bandArithmeticNewJobRes = await submitNewJob({ + createServiceResponse, + token + }) + console.log('bandArithmeticNewJobRes', bandArithmeticNewJobRes) + + const checkJobStatusRes = await checkJobStatus(bandArithmeticNewJobRes.jobId, token); + console.log('checkJobStatusRes', checkJobStatusRes) + + } catch (err) { + console.log(err); + } +} + +start(); \ No newline at end of file diff --git a/sandbox/rasterAnalysis/clipRaster.js b/sandbox/rasterAnalysis/clipRaster.js index 1c150fc3..fdef5b73 100644 --- a/sandbox/rasterAnalysis/clipRaster.js +++ b/sandbox/rasterAnalysis/clipRaster.js @@ -317,11 +317,11 @@ const start = async () => { const createServiceResponse = await createHostedImageryService(serviceName, token) // console.log(createServiceResponse) - const generateRasterNewJobRes = await submitNewJob({ + const clipRasterNewJobRes = await submitNewJob({ createServiceResponse, token }) - console.log('generateRasterNewJobRes', generateRasterNewJobRes) + console.log('clipRasterNewJobRes', clipRasterNewJobRes) const checkJobStatusRes = await checkJobStatus(generateRasterNewJobRes.jobId, token); console.log('checkJobStatusRes', checkJobStatusRes) diff --git a/sandbox/rasterAnalysis/remapPixelValues.js b/sandbox/rasterAnalysis/remapPixelValues.js new file mode 100644 index 00000000..902dafce --- /dev/null +++ b/sandbox/rasterAnalysis/remapPixelValues.js @@ -0,0 +1,214 @@ +require('dotenv').config() + +const { + RASTER_ANALYSIS_ROOT +} = require('./config') +const { + createHostedImageryService +} = require('./createHostedImageryService') +const { + generateToken +} = require('./generateToken') + +/** + * Submit new Generate Raster GP Job. + * @param {Object} param0 - An object containing the following properties: + * - token (string): ArcGIS authentication token. + * - createServiceResponse (Object): Response object from a previous service creation step. (Optional) + * @returns {Object} - Response object containing job information or throws an error if unsuccessful. + * + * example of response: + * ``` + * { + * "jobId":"11cba2e5eb994caab277f3df887044c6", + * "jobStatus":"esriJobNew" + * } + * ``` + */ +const submitNewJob = async ({ + token, + createServiceResponse +}) => { + const requestURL = RASTER_ANALYSIS_ROOT + "/GenerateRaster/submitJob"; + console.log(requestURL) + + const params = new URLSearchParams({ + f: "json", + token, + outputType: 'dynamicLayer', + rasterFunction: JSON.stringify({ + "name":"Remap", + "description":"Changes pixel values by assigning new values to ranges of pixel values or using an external table.", + "function":{ + "type":"RemapFunction", + "pixelType":"UNKNOWN", + "name":"Remap", + "description":"Changes pixel values by assigning new values to ranges of pixel values or using an external table." + }, + "arguments":{ + "Raster":{ + "name":"Raster", + "isPublic":false, + "isDataset":true, + "value":{ + "url":"https://iservicesdev.arcgis.com/LkFyxb9zDq7vAOAm/arcgis/rest/services/Landsat_Level2_BandArithmetic_966/ImageServer?token=9RK8DY0qvaqYcPFrrqGyoc_Hw2w_BiuUfWSbVAwTisP5_rcoZesuFjwM_Y5mRo0ow_UCCbyhr3VWNLAkFpt5n2NArsT6paTQXYXukY1LUWxI958EoRgakDYGEmRp1ggPY_bf_dE0dxa5lya6Hdx2_AfAZBq5zV6BWbwAY-8rlob0upRmIZ7-cpTzEZj0ZzC3LGXrrnf1a0XqGaXAM-fuS0eVzePb5Y7aTbSSWi-rxWWnmrS0CjukX50joiZlUisdukQe3MRXnay6AzR7qcsWMi9gaZI-D4jPbPdjpKdlo_InYIdRFhF4KJx4Zua9_pYDmwU0TlyX47LVbIHdruNYHddh5ECDIhu7cL7pSIMSSA8.", + "name":"Landsat_Level2_BandArithmetic_966" + }, + "type":"RasterFunctionVariable" + }, + "UseTable":{ + "name":"UseTable", + "isPublic":false, + "isDataset":false, + "value":false, + "type":"RasterFunctionVariable" + }, + "InputRanges":{ + "name":"InputRanges", + "isPublic":false, + "isDataset":false, + "value":[ + 0, + 1 + ], + "type":"RasterFunctionVariable" + }, + "OutputValues":{ + "name":"OutputValues", + "isPublic":false, + "isDataset":false, + "value":[ + 1 + ], + "type":"RasterFunctionVariable" + }, + "NoDataRanges":{ + "name":"NoDataRanges", + "isPublic":false, + "isDataset":false, + "value":[ + + ], + "type":"RasterFunctionVariable" + }, + "Table":{ + "name":"Table", + "isPublic":false, + "isDataset":false, + "type":"RasterFunctionVariable" + }, + "InputField":{ + "name":"InputField", + "isPublic":false, + "isDataset":false, + "type":"RasterFunctionVariable" + }, + "OutputField":{ + "name":"OutputField", + "isPublic":false, + "isDataset":false, + "type":"RasterFunctionVariable" + }, + "InputMaxField":{ + "name":"InputMaxField", + "isPublic":false, + "isDataset":false, + "type":"RasterFunctionVariable" + }, + "RemapTableType":{ + "name":"RemapTableType", + "isPublic":false, + "isDataset":false, + "value":1, + "type":"RasterFunctionVariable" + }, + "AllowUnmatched":{ + "name":"AllowUnmatched", + "isPublic":false, + "isDataset":false, + "value":false, + "type":"RasterFunctionVariable" + }, + "ReplacementValue":{ + "name":"ReplacementValue", + "isPublic":false, + "isDataset":false, + "type":"RasterFunctionVariable" + }, + "type":"RemapFunctionArguments" + }, + "functionType":0, + "thumbnail":"", + "thumbnailEx":"", + "help":"" + }), + OutputName: JSON.stringify({ + "serviceProperties": { + "name": createServiceResponse.name, + "capabilities": "Image", + "serviceUrl": createServiceResponse.serviceurl + }, + "itemProperties": { + "itemId": createServiceResponse.itemId + } + }), + // context: JSON.stringify({ + // "geometry": CLIPPING_GEOM + // }) + }) + + const res = await fetch(requestURL, { + method: 'POST', + body: params + }) + + const data = await res.json() + + if (data.error) { + throw data.error + } + + return data; +} + +const checkJobStatus = async (jobId, token) => { + + const requestURL = RASTER_ANALYSIS_ROOT + `/GenerateRaster/jobs/${jobId}?f=json&token=${token}`; + console.log('requestURL for checkJobStatus', requestURL) + + const res = await fetch(requestURL) + + const data = await res.json() + + if (data.error) { + throw data.error + } + + return data; +} + +const start = async () => { + + try { + const token = await generateToken() + // console.log(token) + + const serviceName = 'Landsat_Level2_Remap_' + (Math.random() * 1000).toFixed(0); + const createServiceResponse = await createHostedImageryService(serviceName, token) + // console.log(createServiceResponse) + + const remapNewJobRes = await submitNewJob({ + createServiceResponse, + token + }) + console.log('remapNewJobRes', remapNewJobRes) + + const checkJobStatusRes = await checkJobStatus(remapNewJobRes.jobId, token); + console.log('checkJobStatusRes', checkJobStatusRes) + + } catch (err) { + console.log(err); + } +} + +start(); \ No newline at end of file From d3ab3aeb80e94f81fb944cae18ce992f3f1424b2 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 29 Oct 2024 17:00:06 -0700 Subject: [PATCH 049/306] chore: add saveMaskResult.js to sandbox --- sandbox/rasterAnalysis/saveMaskResult.js | 335 +++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 sandbox/rasterAnalysis/saveMaskResult.js diff --git a/sandbox/rasterAnalysis/saveMaskResult.js b/sandbox/rasterAnalysis/saveMaskResult.js new file mode 100644 index 00000000..31b775d1 --- /dev/null +++ b/sandbox/rasterAnalysis/saveMaskResult.js @@ -0,0 +1,335 @@ +require('dotenv').config() + +const { + RASTER_ANALYSIS_ROOT +} = require('./config') +const { + createHostedImageryService +} = require('./createHostedImageryService') +const { + generateToken +} = require('./generateToken') + + +const OBJECTID = 9445689; + +const CLIPPING_GEOM = { + "rings": [ + [ + [ + -13126451.3033000007, + 4058763.9095999971 + ], + [ + -12908191.6368, + 4007893.42949999869 + ], + [ + -12963104.7622, + 3779150.445600003 + ], + [ + -13177316.9085000008, + 3829010.012000002 + ], + [ + -13126451.3033000007, + 4058763.9095999971 + ] + ] + ], + "spatialReference": { + "wkid": 102100, + "latestWkid": 3857 + } +} + +/** + * Submit new Generate Raster GP Job. + * @param {Object} param0 - An object containing the following properties: + * - token (string): ArcGIS authentication token. + * - createServiceResponse (Object): Response object from a previous service creation step. (Optional) + * @returns {Object} - Response object containing job information or throws an error if unsuccessful. + * + * example of response: + * ``` + * { + * "jobId":"11cba2e5eb994caab277f3df887044c6", + * "jobStatus":"esriJobNew" + * } + * ``` + */ +const submitNewJob = async ({ + token, + createServiceResponse +}) => { + const requestURL = RASTER_ANALYSIS_ROOT + "/GenerateRaster/submitJob"; + console.log(requestURL) + + const params = new URLSearchParams({ + f: "json", + token, + outputType: 'dynamicLayer', + rasterFunction: JSON.stringify({ + "name": "Remap", + "description": "Changes pixel values by assigning new values to ranges of pixel values or using an external table.", + "function": { + "type": "RemapFunction", + "pixelType": "UNKNOWN", + "name": "Remap", + "description": "Changes pixel values by assigning new values to ranges of pixel values or using an external table." + }, + "arguments": { + "Raster": { + "name": "Band Arithmetic", + "description": "Calculates indexes using predefined formulas or a user-defined expression.", + "function": { + "type": "BandArithmeticFunction", + "pixelType": "UNKNOWN", + "name": "Band Arithmetic", + "description": "Calculates indexes using predefined formulas or a user-defined expression." + }, + "arguments": { + "Raster": { + "name": "Clip", + "description": "Sets the extent of a raster using coordinates or another dataset.", + "function": { + "type": "ClipFunction", + "pixelType": "UNKNOWN", + "name": "Clip", + "description": "Sets the extent of a raster using coordinates or another dataset." + }, + "arguments": { + "Raster": { + "name": "Raster", + "isPublic": false, + "isDataset": true, + "value": { + "url": `https://landsatdev.imagery1.arcgis.com/arcgis/rest/services/LandsatC2L2/ImageServer?token=${token}`, + "name": "LandsatC2L2", + "mosaicRule": { + "ascending": false, + "lockRasterIds": [OBJECTID], + "mosaicMethod": "esriMosaicLockRaster", + "where": `objectid in (${OBJECTID})` + } + }, + "type": "RasterFunctionVariable" + }, + "ClippingType": { + "name": "ClippingType", + "isPublic": false, + "isDataset": false, + "value": 1, + "type": "RasterFunctionVariable" + }, + "ClippingRaster": { + "name": "ClippingRaster", + "isPublic": false, + "isDataset": true, + "type": "RasterFunctionVariable" + }, + "ClippingGeometry": { + "name": "ClippingGeometry", + "isPublic": false, + "isDataset": false, + "value": CLIPPING_GEOM, + "type": "RasterFunctionVariable" + }, + // "Extent": { + // "name": "Extent", + // "isPublic": false, + // "isDataset": false, + // "value": EXTENT_OF_CLIPPING_GEOM, + // "type": "RasterFunctionVariable" + // }, + "UseInputFeatureGeometry": { + "name": "UseInputFeatureGeometry", + "isPublic": false, + "isDataset": false, + "type": "RasterFunctionVariable" + }, + "type": "ClipFunctionArguments" + }, + "functionType": 0, + "thumbnail": "", + "thumbnailEx": "", + "help": "" + }, + "Method": { + "name": "Method", + "isPublic": false, + "isDataset": false, + "value": 0, + "type": "RasterFunctionVariable" + }, + "BandIndexes": { + "name": "BandIndexes", + "isPublic": false, + "isDataset": false, + "value": "(B3-B6)/(B3+B6)", + "type": "RasterFunctionVariable" + }, + "type": "BandArithmeticFunctionArguments" + }, + "functionType": 0, + "thumbnail": "", + "thumbnailEx": "", + "help": "" + }, + "UseTable": { + "name": "UseTable", + "isPublic": false, + "isDataset": false, + "value": false, + "type": "RasterFunctionVariable" + }, + "InputRanges": { + "name": "InputRanges", + "isPublic": false, + "isDataset": false, + "value": [ + 0, + 1 + ], + "type": "RasterFunctionVariable" + }, + "OutputValues": { + "name": "OutputValues", + "isPublic": false, + "isDataset": false, + "value": [ + 1 + ], + "type": "RasterFunctionVariable" + }, + "NoDataRanges": { + "name": "NoDataRanges", + "isPublic": false, + "isDataset": false, + "value": [ + + ], + "type": "RasterFunctionVariable" + }, + "Table": { + "name": "Table", + "isPublic": false, + "isDataset": false, + "type": "RasterFunctionVariable" + }, + "InputField": { + "name": "InputField", + "isPublic": false, + "isDataset": false, + "type": "RasterFunctionVariable" + }, + "OutputField": { + "name": "OutputField", + "isPublic": false, + "isDataset": false, + "type": "RasterFunctionVariable" + }, + "InputMaxField": { + "name": "InputMaxField", + "isPublic": false, + "isDataset": false, + "type": "RasterFunctionVariable" + }, + "RemapTableType": { + "name": "RemapTableType", + "isPublic": false, + "isDataset": false, + "value": 1, + "type": "RasterFunctionVariable" + }, + "AllowUnmatched": { + "name": "AllowUnmatched", + "isPublic": false, + "isDataset": false, + "value": false, + "type": "RasterFunctionVariable" + }, + "ReplacementValue": { + "name": "ReplacementValue", + "isPublic": false, + "isDataset": false, + "type": "RasterFunctionVariable" + }, + "type": "RemapFunctionArguments" + }, + "functionType": 0, + "thumbnail": "", + "thumbnailEx": "", + "help": "" + }), + OutputName: JSON.stringify({ + "serviceProperties": { + "name": createServiceResponse.name, + "capabilities": "Image", + "serviceUrl": createServiceResponse.serviceurl + }, + "itemProperties": { + "itemId": createServiceResponse.itemId + } + }), + // context: JSON.stringify({ + // "geometry": CLIPPING_GEOM + // }) + }) + + const res = await fetch(requestURL, { + method: 'POST', + body: params + }) + + const data = await res.json() + + if (data.error) { + throw data.error + } + + return data; +} + +const checkJobStatus = async (jobId, token) => { + + const requestURL = RASTER_ANALYSIS_ROOT + `/GenerateRaster/jobs/${jobId}?f=json&token=${token}`; + console.log('requestURL for checkJobStatus', requestURL) + + const res = await fetch(requestURL) + + const data = await res.json() + + if (data.error) { + throw data.error + } + + return data; +} + +const start = async () => { + + try { + const token = await generateToken() + // console.log(token) + + const serviceName = 'Landsat_Level2_Mask_Tool_Result_' + (Math.random() * 1000).toFixed(0); + const createServiceResponse = await createHostedImageryService(serviceName, token) + // console.log(createServiceResponse) + + const res = await submitNewJob({ + createServiceResponse, + token + }) + console.log('Mask_Tool_Result_Job_Response', res) + + const checkJobStatusRes = await checkJobStatus(res.jobId, token); + console.log('checkJobStatusRes', checkJobStatusRes) + + } catch (err) { + console.log(err); + } +} + +start(); \ No newline at end of file From 28c3d441ab3a4fb5ebcfd3a7c248c567ea9cb400 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 31 Oct 2024 04:42:45 -0700 Subject: [PATCH 050/306] chore: fix package vulnerabilities --- package-lock.json | 583 +++++++++++++++++++++++++++++----------------- 1 file changed, 372 insertions(+), 211 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7b36c31..19d67233 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15018,26 +15018,6 @@ "integrity": "sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==", "dev": true }, - "node_modules/@types/eslint": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz", - "integrity": "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "0.0.51", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", @@ -17254,10 +17234,11 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, + "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -17267,7 +17248,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -17282,6 +17263,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -17291,6 +17273,7 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -17475,6 +17458,7 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -17506,13 +17490,20 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -18159,6 +18150,7 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -18173,10 +18165,11 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -19600,6 +19593,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -19701,6 +19712,7 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -20057,10 +20069,11 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -20190,6 +20203,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -20982,6 +21018,7 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -21050,37 +21087,38 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -21383,13 +21421,14 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -21405,6 +21444,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -21414,6 +21454,7 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -21795,6 +21836,7 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -21976,14 +22018,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -22268,12 +22316,26 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -22571,6 +22633,7 @@ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -22587,6 +22650,7 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -22596,6 +22660,7 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -22647,10 +22712,11 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -22723,6 +22789,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -26312,10 +26379,11 @@ "dev": true }, "node_modules/markdown-to-jsx": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.2.0.tgz", - "integrity": "sha512-3l4/Bigjm4bEqjCR6Xr+d4DtM1X6vvtGsMGSjJYyep8RjjIvcWtrXBS8Wbfe1/P+atKNMccpsraESIaWVplzVg==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.5.0.tgz", + "integrity": "sha512-RrBNcMHiFPcz/iqIj0n3wclzHXjwS7mzjBNWecKKVhNTIxQepIix6Il/wZCn2Cg5Y1ow2Qi84+eJrryFRWBEWw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10" }, @@ -26358,6 +26426,7 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -26384,10 +26453,14 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -26414,12 +26487,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -27141,10 +27215,14 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -27726,10 +27804,11 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true, + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -29523,12 +29602,13 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -29596,6 +29676,7 @@ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -30540,10 +30621,11 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -30568,6 +30650,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -30576,13 +30659,25 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/send/node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -30591,13 +30686,15 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/send/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -30667,15 +30764,16 @@ "dev": true }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, + "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -30687,6 +30785,24 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -30696,7 +30812,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/shallow-clone": { "version": "3.0.1", @@ -30742,14 +30859,19 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -31963,6 +32085,7 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6" } @@ -32156,6 +32279,7 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -32345,6 +32469,7 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -32677,13 +32802,12 @@ } }, "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -32692,7 +32816,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -43506,26 +43630,6 @@ "integrity": "sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==", "dev": true }, - "@types/eslint": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz", - "integrity": "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==", - "dev": true, - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "dev": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "@types/estree": { "version": "0.0.51", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", @@ -45256,9 +45360,9 @@ } }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "requires": { "bytes": "3.1.2", @@ -45269,7 +45373,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -45438,13 +45542,16 @@ } }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "callsites": { @@ -45941,9 +46048,9 @@ } }, "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true }, "cookie-signature": { @@ -46915,6 +47022,17 @@ "clone": "^1.0.2" } }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -47259,9 +47377,9 @@ "dev": true }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true }, "end-of-stream": { @@ -47366,6 +47484,21 @@ "unbox-primitive": "^1.0.2" } }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, "es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -47983,37 +48116,37 @@ } }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -48258,13 +48391,13 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -48690,14 +48823,16 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-nonce": { @@ -48896,14 +49031,20 @@ "dev": true }, "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "requires": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" } }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true + }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -49158,9 +49299,9 @@ } }, "http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "requires": { "@types/http-proxy": "^1.17.8", @@ -51787,9 +51928,9 @@ "dev": true }, "markdown-to-jsx": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.2.0.tgz", - "integrity": "sha512-3l4/Bigjm4bEqjCR6Xr+d4DtM1X6vvtGsMGSjJYyep8RjjIvcWtrXBS8Wbfe1/P+atKNMccpsraESIaWVplzVg==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.5.0.tgz", + "integrity": "sha512-RrBNcMHiFPcz/iqIj0n3wclzHXjwS7mzjBNWecKKVhNTIxQepIix6Il/wZCn2Cg5Y1ow2Qi84+eJrryFRWBEWw==", "dev": true, "requires": {} }, @@ -51839,9 +51980,9 @@ } }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true }, "merge-stream": { @@ -51863,12 +52004,12 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, @@ -52376,9 +52517,9 @@ "dev": true }, "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true }, "object-is": { @@ -52808,9 +52949,9 @@ } }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true }, "path-type": { @@ -53872,12 +54013,12 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "querystringify": { @@ -54602,9 +54743,9 @@ "dev": true }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "requires": { "debug": "2.6.9", @@ -54645,6 +54786,12 @@ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -54719,15 +54866,15 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-blocking": { @@ -54736,6 +54883,20 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -54778,14 +54939,15 @@ "dev": true }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { @@ -56198,12 +56360,11 @@ "dev": true }, "webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "requires": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -56212,7 +56373,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", From 9c061ff9fdf18deb34c90bf38739c31c7fba13f7 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 12 Nov 2024 09:26:20 -0800 Subject: [PATCH 051/306] feat(shared): add SavePanel and SavePanelContainer components with UI state management --- src/shared/components/SavePanel/SavePanel.tsx | 5 +++++ .../components/SavePanel/SavePanelContainer.tsx | 14 ++++++++++++++ src/shared/store/UI/reducer.ts | 9 +++++++++ src/shared/store/UI/selectors.ts | 5 +++++ 4 files changed, 33 insertions(+) create mode 100644 src/shared/components/SavePanel/SavePanel.tsx create mode 100644 src/shared/components/SavePanel/SavePanelContainer.tsx diff --git a/src/shared/components/SavePanel/SavePanel.tsx b/src/shared/components/SavePanel/SavePanel.tsx new file mode 100644 index 00000000..bf1acbf2 --- /dev/null +++ b/src/shared/components/SavePanel/SavePanel.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const SavePanel = () => { + return
SavePanel
; +}; diff --git a/src/shared/components/SavePanel/SavePanelContainer.tsx b/src/shared/components/SavePanel/SavePanelContainer.tsx new file mode 100644 index 00000000..6721802c --- /dev/null +++ b/src/shared/components/SavePanel/SavePanelContainer.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { SavePanel } from './SavePanel'; +import { useSelector } from 'react-redux'; +import { selectShowSavePanel } from '@shared/store/UI/selectors'; + +export const SavePanelContainer = () => { + const shouldShowSavePanel = useSelector(selectShowSavePanel); + + if (!shouldShowSavePanel) { + return null; + } + + return ; +}; diff --git a/src/shared/store/UI/reducer.ts b/src/shared/store/UI/reducer.ts index 5ba7b0f2..f16485d1 100644 --- a/src/shared/store/UI/reducer.ts +++ b/src/shared/store/UI/reducer.ts @@ -88,6 +88,10 @@ export type UIState = { * if true, show Documentation Panel */ showDocPanel?: boolean; + /** + * if true, show Save Panel + */ + showSavePanel?: boolean; }; export const initialUIState: UIState = { @@ -103,6 +107,7 @@ export const initialUIState: UIState = { showDownloadPanel: false, showSaveWebMapPanel: false, showDocPanel: false, + showSavePanel: false, }; const slice = createSlice({ @@ -161,6 +166,9 @@ const slice = createSlice({ showDocPanelToggled: (state) => { state.showDocPanel = !state.showDocPanel; }, + showSavePanelToggled: (state) => { + state.showSavePanel = !state.showSavePanel; + }, }, }); @@ -180,6 +188,7 @@ export const { showDownloadPanelToggled, showSaveWebMapPanelToggled, showDocPanelToggled, + showSavePanelToggled, } = slice.actions; export default reducer; diff --git a/src/shared/store/UI/selectors.ts b/src/shared/store/UI/selectors.ts index 34eacaf7..a6a1f527 100644 --- a/src/shared/store/UI/selectors.ts +++ b/src/shared/store/UI/selectors.ts @@ -80,3 +80,8 @@ export const selectShouldShowDocPanel = createSelector( (state: RootState) => state.UI.showDocPanel, (showDocPanel) => showDocPanel ); + +export const selectShowSavePanel = createSelector( + (state: RootState) => state.UI.showSavePanel, + (showSavePanel) => showSavePanel +); From aebada70d582574ef32248f43c3db098e714e9c2 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 12 Nov 2024 10:00:04 -0800 Subject: [PATCH 052/306] feat(shared): integrate SavePanel and OpenSavePanelButton components into Layout and Map --- .../components/Layout/Layout.tsx | 2 + src/landsat-explorer/components/Map/Map.tsx | 2 + .../OpenSavePanelButton.tsx | 77 +++++++++++++++++++ .../OpenSavePanelButtonContainer.tsx | 60 +++++++++++++++ .../components/OpenSavePanelButton/index.ts | 1 + src/shared/components/SavePanel/SavePanel.tsx | 5 -- .../SavePanel/SavePanelContainer.tsx | 26 ++++++- src/shared/components/SavePanel/index.ts | 1 + 8 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 src/shared/components/OpenSavePanelButton/OpenSavePanelButton.tsx create mode 100644 src/shared/components/OpenSavePanelButton/OpenSavePanelButtonContainer.tsx create mode 100644 src/shared/components/OpenSavePanelButton/index.ts delete mode 100644 src/shared/components/SavePanel/SavePanel.tsx create mode 100644 src/shared/components/SavePanel/index.ts diff --git a/src/landsat-explorer/components/Layout/Layout.tsx b/src/landsat-explorer/components/Layout/Layout.tsx index c83376e3..895b699f 100644 --- a/src/landsat-explorer/components/Layout/Layout.tsx +++ b/src/landsat-explorer/components/Layout/Layout.tsx @@ -47,6 +47,7 @@ import { AnalyzeToolSelector4Landsat } from '../AnalyzeToolSelector/AnalyzeToolS import { useShouldShowSecondaryControls } from '@shared/hooks/useShouldShowSecondaryControls'; import { CloudFilter } from '@shared/components/CloudFilter'; import { LandsatDynamicModeInfo } from '../LandsatDynamicModeInfo/LandsatDynamicModeInfo'; +import { SavePanel } from '@shared/components/SavePanel'; const Layout = () => { const mode = useSelector(selectAppMode); @@ -136,6 +137,7 @@ const Layout = () => { + ); }; diff --git a/src/landsat-explorer/components/Map/Map.tsx b/src/landsat-explorer/components/Map/Map.tsx index c5ba314a..0cb984ed 100644 --- a/src/landsat-explorer/components/Map/Map.tsx +++ b/src/landsat-explorer/components/Map/Map.tsx @@ -37,6 +37,7 @@ import { updateQueryLocation4TrendTool } from '@shared/store/TrendTool/thunks'; import { updateQueryLocation4SpectralProfileTool } from '@shared/store/SpectralProfileTool/thunks'; import { SwipeWidget4ImageryLayers } from '@shared/components/SwipeWidget/SwipeWidget4ImageryLayers'; import { ZoomToExtent } from '@shared/components/ZoomToExtent'; +import { OpenSavePanelButton } from '@shared/components/OpenSavePanelButton'; const Map = () => { const dispatch = useDispatch(); @@ -78,6 +79,7 @@ const Map = () => { + diff --git a/src/shared/components/OpenSavePanelButton/OpenSavePanelButton.tsx b/src/shared/components/OpenSavePanelButton/OpenSavePanelButton.tsx new file mode 100644 index 00000000..9ceb2c26 --- /dev/null +++ b/src/shared/components/OpenSavePanelButton/OpenSavePanelButton.tsx @@ -0,0 +1,77 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import classNames from 'classnames'; +import React, { FC } from 'react'; +import { MapActionButton } from '../MapActionButton/MapActionButton'; + +type Props = { + /** + * if true, this button should be disabled + */ + disabled?: boolean; + /** + * if true, hide the button + */ + hidden?: boolean; + /** + * if true, show loading indicator + */ + showLoadingIndicator?: boolean; + /** + * tooltip text for the button + */ + tooltip?: string; + /** + * emit when user click on + * @returns void + */ + onClick: () => void; +}; + +export const OpenSavePanelButton: FC = ({ + disabled, + hidden, + showLoadingIndicator, + tooltip, + onClick, +}) => { + return ( + + <> + {/* save icon (https://esri.github.io/calcite-ui-icons/#save) */} +
+ + + + +
+ +
+ ); +}; diff --git a/src/shared/components/OpenSavePanelButton/OpenSavePanelButtonContainer.tsx b/src/shared/components/OpenSavePanelButton/OpenSavePanelButtonContainer.tsx new file mode 100644 index 00000000..34e4c69f --- /dev/null +++ b/src/shared/components/OpenSavePanelButton/OpenSavePanelButtonContainer.tsx @@ -0,0 +1,60 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, useMemo, useState } from 'react'; +import { + selectAnimationStatus, + selectIsAnimationPlaying, +} from '@shared/store/UI/selectors'; +import MapView from '@arcgis/core/views/MapView'; +import { useSelector, useDispatch } from 'react-redux'; +import { selectAppMode } from '@shared/store/ImageryScene/selectors'; +// import { +// getExtentOfLandsatSceneByObjectId, +// // getLandsatFeatureByObjectId, +// } from '@shared/services/landsat-level-2/getLandsatScenes'; +import { getExtentByObjectId } from '@shared/services/helpers/getExtentById'; +import { OpenSavePanelButton } from './OpenSavePanelButton'; +import { showSavePanelToggled } from '@shared/store/UI/reducer'; + +type Props = { + mapView?: MapView; +}; + +export const OpenSavePanelButtonContainer: FC = ({ mapView }) => { + // const animationStatus = useSelector(selectAnimationStatus); + + const dispatch = useDispatch(); + + const isAnimationPlaying = useSelector(selectIsAnimationPlaying); + + const mode = useSelector(selectAppMode); + + const disabled = useMemo(() => { + return false; + }, []); + + return ( +