├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── ci.yaml │ └── codeql-analysis.yml ├── .gitignore ├── .prettierignore ├── .storybook ├── main.ts └── preview.tsx ├── CHANGES.md ├── Dockerfile ├── LICENSE ├── README.md ├── chromatic.config.json ├── docs ├── assets │ ├── favicon.ico │ ├── images │ │ ├── about_viewer.png │ │ ├── add_place.png │ │ ├── add_statistics.png │ │ ├── add_timeseries.png │ │ ├── analysis_import_places.png │ │ ├── analysis_infobox.png │ │ ├── analysis_places.png │ │ ├── analysis_places_dark.png │ │ ├── analysis_places_light.png │ │ ├── analysis_player_dark.png │ │ ├── analysis_player_light.png │ │ ├── analysis_statistics.png │ │ ├── analysis_timeseries.png │ │ ├── analysis_timeseries_export.png │ │ ├── analysis_timeseries_graphs.png │ │ ├── analysis_uservariables.png │ │ ├── color_mapping.png │ │ ├── color_valuerange.png │ │ ├── colormap_custom.png │ │ ├── colormap_legend.png │ │ ├── colormap_menu.png │ │ ├── colormap_valuerange.png │ │ ├── datamanagement_dataset.png │ │ ├── datamanagement_meta.png │ │ ├── datamanagement_variables.png │ │ ├── datamanagement_visibility.png │ │ ├── datamanagement_visibility_added.png │ │ ├── export_data.png │ │ ├── export_data_button.png │ │ ├── features_label_dark.png │ │ ├── features_label_light.png │ │ ├── import_places.png │ │ ├── infobox_box.png │ │ ├── infobox_button.png │ │ ├── layerpanel.png │ │ ├── locate_dataset.png │ │ ├── locate_place.png │ │ ├── permalink_button.png │ │ ├── pin_variable.png │ │ ├── player_autostep_pause.png │ │ ├── player_autostep_start.png │ │ ├── player_first_last.png │ │ ├── player_next_prev.png │ │ ├── remove_place.png │ │ ├── rename_place.png │ │ ├── select_dataset.png │ │ ├── select_place.png │ │ ├── select_place_group.png │ │ ├── select_place_map.png │ │ ├── select_timestep_calender.png │ │ ├── select_timestep_slider.png │ │ ├── select_variable.png │ │ ├── settings_on_selection.png │ │ ├── settings_overlay.png │ │ ├── settings_player.png │ │ ├── settings_server.png │ │ ├── settings_timeseries.png │ │ ├── settings_usermaps.png │ │ ├── sidebar_button.png │ │ ├── sidebar_highlighted.png │ │ ├── sidebar_initial_timeseries.png │ │ ├── sidebar_meta_json.png │ │ ├── sidebar_meta_tabular.png │ │ ├── sidebar_meta_textual.png │ │ ├── sidebar_metadata.png │ │ ├── sidebar_navigate_timeseries.png │ │ ├── sidebar_statistics_overview.png │ │ ├── sidebar_statistics_points.png │ │ ├── sidebar_statistics_polygons.png │ │ ├── splitmode.png │ │ ├── style_place.png │ │ ├── user_variables.png │ │ ├── user_variables_add.png │ │ ├── user_variables_management.png │ │ └── xcube-viewer.png │ ├── logo192.png │ ├── logo512.png │ └── videos │ │ ├── Player_hh.gif │ │ ├── analysis_compare-mode.gif │ │ └── share_link.gif ├── build_viewer.md ├── concepts.md ├── css │ └── custom.css ├── features.md ├── index.md ├── javascripts │ └── mathjax.js └── user_guide │ ├── analyse.md │ ├── colormaps.md │ ├── getting_started.md │ └── settings.md ├── eslint.config.mjs ├── index.html ├── mkdocs.yml ├── package-lock.json ├── package.json ├── public ├── docs │ ├── add-layer-wms.de.md │ ├── add-layer-wms.en.md │ ├── add-layer-wms.se.md │ ├── add-layer-xyz.de.md │ ├── add-layer-xyz.en.md │ ├── add-layer-xyz.se.md │ ├── color-mappings.de.md │ ├── color-mappings.en.md │ ├── color-mappings.se.md │ ├── dev-reference.en.md │ ├── imprint.en.md │ ├── privacy-note.de.md │ ├── privacy-note.en.md │ ├── privacy-note.se.md │ ├── user-variables.de.md │ ├── user-variables.en.md │ └── user-variables.se.md ├── images │ ├── favicon.ico │ ├── logo.png │ ├── logo192.png │ ├── logo512.png │ └── textures │ │ ├── cm_gray.png │ │ └── cm_viridis.png ├── manifest.json └── robots.txt ├── resources ├── demo.csv ├── logo-inv.pdn ├── logo.pdn └── test │ ├── HH_WMS_Gewaesserunterhaltung.xml │ └── ogcsample.xml ├── src ├── actions │ ├── controlActions.tsx │ ├── dataActions.tsx │ ├── mapActions.tsx │ ├── messageLogActions.ts │ ├── otherActions.tsx │ └── userAuthActions.ts ├── api │ ├── callApi.ts │ ├── errors.ts │ ├── getColorBars.ts │ ├── getDatasetPlaceGroup.ts │ ├── getDatasets.ts │ ├── getExpressionCapabilities.ts │ ├── getPointValue.ts │ ├── getServerInfo.ts │ ├── getStatistics.ts │ ├── getTimeSeries.ts │ ├── getViewerState.ts │ ├── hasViewerStateApi.ts │ ├── index.ts │ ├── putViewerState.ts │ ├── updateResources.ts │ └── validateExpression.ts ├── components │ ├── AuthWrapper.tsx │ ├── ColorBarLegend │ │ ├── ColorBarCanvas.tsx │ │ ├── ColorBarColorEditor.tsx │ │ ├── ColorBarGroupComponent.tsx │ │ ├── ColorBarGroupHeader.tsx │ │ ├── ColorBarItem.tsx │ │ ├── ColorBarLabels.tsx │ │ ├── ColorBarLegend.tsx │ │ ├── ColorBarLegendCategorical.tsx │ │ ├── ColorBarLegendScalable.tsx │ │ ├── ColorBarRangeEditor.tsx │ │ ├── ColorBarRangeSlider.tsx │ │ ├── ColorBarSelect.tsx │ │ ├── ColorBarStyleEditor.tsx │ │ ├── ColorMapTypeEditor.tsx │ │ ├── UserColorBarEditor.tsx │ │ ├── UserColorBarGroup.tsx │ │ ├── UserColorBarItem.tsx │ │ ├── constants.ts │ │ ├── index.tsx │ │ ├── scaling.test.ts │ │ ├── scaling.ts │ │ └── style.ts │ ├── ControlBar.tsx │ ├── ControlBarActions.tsx │ ├── ControlBarItem.tsx │ ├── DatasetSelect.tsx │ ├── DevRefPage.tsx │ ├── DoneCancel.tsx │ ├── EditableSelect.tsx │ ├── ErrorBoundary.css │ ├── ErrorBoundary.test.tsx │ ├── ErrorBoundary.tsx │ ├── ExportDialog.tsx │ ├── FileUpload.tsx │ ├── HelpButton.tsx │ ├── HoverVisibleBox.stories.tsx │ ├── HoverVisibleBox.tsx │ ├── ImprintPage.tsx │ ├── InfoPanel │ │ ├── DatasetInfoCard.tsx │ │ ├── InfoPanel.tsx │ │ ├── PlaceInfoCard.tsx │ │ ├── VariableInfoCard.tsx │ │ ├── common │ │ │ ├── CodeContent.tsx │ │ │ ├── HtmlContent.tsx │ │ │ ├── InfoCard.tsx │ │ │ ├── InfoCardActions.tsx │ │ │ ├── InfoCardContent.tsx │ │ │ ├── InfoCardHeader.tsx │ │ │ ├── JsonCodeContent.tsx │ │ │ ├── KeyValueContent.tsx │ │ │ ├── PythonCodeContent.tsx │ │ │ ├── styles.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ └── index.tsx │ ├── LayerControlPanel │ │ ├── LayerControlPanel.tsx │ │ ├── LayerMenu.tsx │ │ ├── LayerMenuItem.tsx │ │ └── index.tsx │ ├── LegalAgreementDialog.tsx │ ├── LoadingDialog.tsx │ ├── MapControlActions.tsx │ ├── MapInteractionsBar.tsx │ ├── MapPointInfoBox │ │ ├── MapPointInfo.ts │ │ ├── MapPointInfoBox.tsx │ │ ├── MapPointInfoContent.tsx │ │ ├── index.tsx │ │ └── useMapPointInfo.ts │ ├── MapSplitter.tsx │ ├── Markdown.tsx │ ├── MarkdownPage.tsx │ ├── MarkdownPopover.tsx │ ├── MessageLog.tsx │ ├── PlaceGroupsSelect.tsx │ ├── PlaceSelect.tsx │ ├── PlaceStyleEditor │ │ ├── PlaceStyleEditor.tsx │ │ └── index.tsx │ ├── RadioSetting.tsx │ ├── ScrollbarStyles.tsx │ ├── SelectableMenuItem.tsx │ ├── ServerDialog.tsx │ ├── SettingsDialog.tsx │ ├── SettingsPanel.tsx │ ├── SettingsSubPanel.tsx │ ├── SidePanel │ │ ├── SidePanel.stories.tsx │ │ ├── SidePanel.tsx │ │ ├── SidePanelContent.tsx │ │ ├── SidePanelHeader.tsx │ │ ├── Sidebar.stories.tsx │ │ ├── Sidebar.tsx │ │ ├── genText.ts │ │ ├── index.tsx │ │ ├── panelModel.test.ts │ │ ├── panelModel.ts │ │ └── styles.ts │ ├── SnapshotButton.tsx │ ├── SplitPane.stories.tsx │ ├── SplitPane.tsx │ ├── StatisticsPanel │ │ ├── HistogramChart.tsx │ │ ├── StatisticsDataRow.tsx │ │ ├── StatisticsFirstRow.tsx │ │ ├── StatisticsPanel.tsx │ │ ├── StatisticsRow.tsx │ │ ├── StatisticsTable.tsx │ │ └── index.tsx │ ├── TimePlayer.tsx │ ├── TimeSelect.tsx │ ├── TimeSeriesPanel │ │ ├── CustomDot.tsx │ │ ├── CustomLegend.tsx │ │ ├── CustomTooltip.tsx │ │ ├── NoTimeSeriesChart.tsx │ │ ├── TimeRangeSlider.tsx │ │ ├── TimeSeriesAddButton.tsx │ │ ├── TimeSeriesChart.tsx │ │ ├── TimeSeriesChartHeader.tsx │ │ ├── TimeSeriesLine.tsx │ │ ├── TimeSeriesPanel.tsx │ │ ├── ValueRangeEditor.tsx │ │ ├── index.tsx │ │ └── util.ts │ ├── TimeSlider.tsx │ ├── ToggleSetting.tsx │ ├── ToolButton.stories.tsx │ ├── ToolButton.tsx │ ├── UserControl.tsx │ ├── UserLayersDialog │ │ ├── UserLayerEditorWms.tsx │ │ ├── UserLayerEditorXyz.tsx │ │ ├── UserLayersDialog.tsx │ │ ├── UserLayersPanel.tsx │ │ └── index.tsx │ ├── UserPlacesDialog.tsx │ ├── UserProfile.tsx │ ├── UserVariablesDialog │ │ ├── ExprEditor.tsx │ │ ├── ExprPartChip.tsx │ │ ├── ExprPartFilterMenu.tsx │ │ ├── HeaderBar.tsx │ │ ├── UserVariableEditor.tsx │ │ ├── UserVariablesDialog.tsx │ │ ├── UserVariablesTable.tsx │ │ ├── index.tsx │ │ └── utils.ts │ ├── UserVectorLayer.tsx │ ├── VariableSelect.tsx │ ├── Viewer │ │ ├── MapButton.tsx │ │ ├── MapButtonGroup.tsx │ │ ├── Viewer.tsx │ │ └── index.tsx │ ├── VolumePanel │ │ ├── VolumeCanvas.css │ │ ├── VolumeCanvas.tsx │ │ ├── VolumePanel.tsx │ │ └── index.tsx │ ├── common-styles.ts │ ├── ol │ │ ├── Map.css │ │ ├── Map.tsx │ │ ├── MapComponent.tsx │ │ ├── View.tsx │ │ ├── control │ │ │ ├── Control.tsx │ │ │ └── ScaleLine.tsx │ │ ├── interaction │ │ │ ├── Draw.tsx │ │ │ └── Select.tsx │ │ ├── layer │ │ │ ├── Layers.tsx │ │ │ ├── Tile.tsx │ │ │ ├── Vector.tsx │ │ │ └── common.ts │ │ ├── style.ts │ │ └── util.ts │ └── user-place │ │ ├── CsvOptionsEditor.tsx │ │ ├── GeoJsonOptionsEditor.tsx │ │ ├── OptionsTextField.tsx │ │ └── WktOptionsEditor.tsx ├── config.ts ├── connected │ ├── App.tsx │ ├── AppBar.tsx │ ├── AppPane.tsx │ ├── ColorBarLegend.tsx │ ├── ColorBarLegend2.tsx │ ├── ControlBar.tsx │ ├── ControlBarActions.tsx │ ├── DatasetSelect.tsx │ ├── ExportDialog.tsx │ ├── InfoPanel.tsx │ ├── LayerControlPanel.tsx │ ├── LegalAgreementDialog.tsx │ ├── LoadingDialog.tsx │ ├── MapControlActions.tsx │ ├── MapInteractionsBar.tsx │ ├── MapPointInfoBox.tsx │ ├── MapSplitter.tsx │ ├── MessageLog.tsx │ ├── PlaceGroupsSelect.tsx │ ├── PlaceSelect.tsx │ ├── ServerDialog.tsx │ ├── SettingsDialog.tsx │ ├── SidePanel.tsx │ ├── StatisticsPanel.tsx │ ├── TimePlayer.tsx │ ├── TimeSelect.tsx │ ├── TimeSeriesPanel.tsx │ ├── TimeSlider.tsx │ ├── UserControl.tsx │ ├── UserLayersDialog.tsx │ ├── UserPlacesDialog.tsx │ ├── UserVariablesDialog.tsx │ ├── VariableSelect.tsx │ ├── Viewer.tsx │ ├── VolumePanel.tsx │ └── Workspace.tsx ├── ext │ ├── actions.ts │ ├── components │ │ └── ContributedPanel.tsx │ ├── config.ts │ ├── plugin.ts │ └── store.ts ├── hooks │ ├── useFetchText.ts │ ├── useMouseDrag.ts │ ├── usePromise.ts │ ├── useResizeObserver.ts │ └── useUndo.ts ├── i18n.ts ├── index.css ├── index.tsx ├── model │ ├── apiServer.ts │ ├── bg.png │ ├── colorBar.test.ts │ ├── colorBar.ts │ ├── dataset.ts │ ├── encode.ts │ ├── layerDefinition.ts │ ├── layerState.ts │ ├── place.ts │ ├── proj.ts │ ├── statistics.ts │ ├── timeSeries.ts │ ├── user-place │ │ ├── common.ts │ │ ├── csv.ts │ │ ├── geojson.ts │ │ └── wkt.ts │ ├── userColorBar.test.ts │ ├── userColorBar.ts │ ├── userVariable.ts │ └── variable.ts ├── reducers │ ├── appReducer.ts │ ├── controlReducer.ts │ ├── dataReducer.ts │ ├── messageLogReducer.ts │ └── userAuthReducer.ts ├── resources │ ├── config.json │ ├── config.schema.json │ ├── lang.json │ ├── maps.json │ ├── python-bw.png │ ├── python.png │ └── spectral-indexes.txt ├── selectors │ ├── controlSelectors.test.tsx │ ├── controlSelectors.tsx │ └── dataSelectors.tsx ├── setupTests.ts ├── states │ ├── appState.ts │ ├── controlState.ts │ ├── dataState.ts │ ├── messageLogState.ts │ ├── persistedState.ts │ ├── userAuthState.ts │ └── userSettings.ts ├── theme.ts ├── util │ ├── assert.test.ts │ ├── assert.ts │ ├── auth.ts │ ├── baseurl.ts │ ├── branding.ts │ ├── color.test.ts │ ├── color.ts │ ├── csv.test.ts │ ├── csv.ts │ ├── export.ts │ ├── find.test.ts │ ├── find.ts │ ├── history.ts │ ├── id.ts │ ├── identifier.test.ts │ ├── identifier.ts │ ├── json.ts │ ├── label.test.ts │ ├── label.ts │ ├── lang.test.ts │ ├── lang.ts │ ├── maps.test.ts │ ├── maps.ts │ ├── path.test.ts │ ├── path.ts │ ├── qparam.test.ts │ ├── qparam.ts │ ├── storage.ts │ ├── styles.test.ts │ ├── styles.ts │ ├── throttle.ts │ ├── time.ts │ ├── types.test.ts │ ├── types.ts │ ├── wms.test.ts │ └── wms.ts ├── version.ts ├── vite-env.d.ts └── volume │ ├── ColorBarTextures.ts │ ├── NRRDLoader.js │ ├── OrbitControls.js │ ├── Volume.js │ ├── VolumeScene.ts │ ├── VolumeShader.js │ ├── VolumeSlice.js │ └── webgl-utils.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | Dockerfile.prod 4 | node_modules 5 | dist 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # DO NOT CHANGE THIS FILE, IF AT ALL, CHANGE .prettierrc.json 4 | 5 | # Top-most EditorConfig file 6 | root = true 7 | 8 | # The following are the prettier settings we use (all defaults) 9 | # See https://prettier.io/docs/en/configuration#editorconfig 10 | 11 | [*] 12 | charset = utf-8 13 | insert_final_newline = true 14 | end_of_line = lf 15 | indent_style = space 16 | indent_size = 2 17 | max_line_length = 80 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, 2 | # cache/restore them, build the source code and run tests across different 3 | # versions of node. 4 | # For more information see: 5 | # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 6 | 7 | name: CI 8 | 9 | on: 10 | push: 11 | branches: [ "main" ] 12 | pull_request: 13 | branches: [ "main" ] 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | node-version: [18.x, 20.x] 23 | # See supported Node.js release schedule at 24 | # https://nodejs.org/en/about/releases/ 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Use Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | cache: 'npm' 33 | - run: npm ci 34 | - run: npm run lint 35 | - run: npm run build 36 | - run: npm run test 37 | # TODO: address in subsequent PR 38 | #- run: npm run coverage 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Artefacts 4 | node_modules 5 | dist 6 | dist-ssr 7 | *.local 8 | coverage 9 | site 10 | 11 | # Logs 12 | logs 13 | *.log 14 | 15 | # Dotenv 16 | .env.* 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | 29 | *storybook.log 30 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | dist 3 | coverage 4 | docs/api 5 | public 6 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/components/**/*.stories.@(ts|tsx)"], 5 | addons: [ 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-interactions", 8 | "@storybook/addon-links", 9 | "@storybook/addon-onboarding", 10 | "storybook-dark-mode", 11 | ], 12 | framework: { 13 | name: "@storybook/react-vite", 14 | options: {}, 15 | }, 16 | }; 17 | 18 | // noinspection JSUnusedGlobalSymbols 19 | export default config; 20 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import type { Preview } from "@storybook/react"; 3 | import { CssBaseline, ThemeProvider } from "@mui/material"; 4 | 5 | import { lightTheme, darkTheme } from "../src/theme"; 6 | 7 | import "@fontsource/roboto/300.css"; 8 | import "@fontsource/roboto/400.css"; 9 | import "@fontsource/roboto/500.css"; 10 | import "@fontsource/roboto/700.css"; 11 | import "@fontsource/material-icons"; 12 | 13 | // noinspection JSUnusedGlobalSymbols 14 | const preview: Preview = { 15 | parameters: { 16 | controls: { 17 | matchers: { 18 | color: /(background|color)$/i, 19 | date: /Date$/i, 20 | }, 21 | }, 22 | backgrounds: { 23 | default: "light", 24 | values: [ 25 | { name: "light", value: lightTheme.palette.background.default }, 26 | { name: "dark", value: darkTheme.palette.background.default }, 27 | ], 28 | }, 29 | }, 30 | decorators: [ 31 | (Story, context) => { 32 | // Get the currently selected background in Storybook 33 | const background = context.globals.backgrounds?.value; 34 | 35 | // Determine the theme based on the Storybook background 36 | const theme = useMemo(() => { 37 | if (background === darkTheme.palette.background.default) { 38 | return darkTheme; 39 | } 40 | return lightTheme; 41 | }, [background]); 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | ); 49 | }, 50 | ], 51 | }; 52 | 53 | // noinspection JSUnusedGlobalSymbols 54 | export default preview; 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine as build 2 | WORKDIR /usr/src/app 3 | COPY . ./ 4 | RUN npx browserslist@latest --update-db 5 | RUN npm install 6 | RUN npm build 7 | 8 | FROM nginx:stable-alpine 9 | COPY --from=build /usr/src/app/build /usr/share/nginx/html 10 | EXPOSE 80 11 | CMD ["nginx", "-g", "daemon off;"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2025 by Brockmann Consult GmbH and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /chromatic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "onlyChanged": true, 3 | "projectId": "Project:67c04900fc4ab71d88d4de37", 4 | "zip": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/images/about_viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/about_viewer.png -------------------------------------------------------------------------------- /docs/assets/images/add_place.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/add_place.png -------------------------------------------------------------------------------- /docs/assets/images/add_statistics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/add_statistics.png -------------------------------------------------------------------------------- /docs/assets/images/add_timeseries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/add_timeseries.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_import_places.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_import_places.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_infobox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_infobox.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_places.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_places.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_places_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_places_dark.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_places_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_places_light.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_player_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_player_dark.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_player_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_player_light.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_statistics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_statistics.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_timeseries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_timeseries.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_timeseries_export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_timeseries_export.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_timeseries_graphs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_timeseries_graphs.png -------------------------------------------------------------------------------- /docs/assets/images/analysis_uservariables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/analysis_uservariables.png -------------------------------------------------------------------------------- /docs/assets/images/color_mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/color_mapping.png -------------------------------------------------------------------------------- /docs/assets/images/color_valuerange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/color_valuerange.png -------------------------------------------------------------------------------- /docs/assets/images/colormap_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/colormap_custom.png -------------------------------------------------------------------------------- /docs/assets/images/colormap_legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/colormap_legend.png -------------------------------------------------------------------------------- /docs/assets/images/colormap_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/colormap_menu.png -------------------------------------------------------------------------------- /docs/assets/images/colormap_valuerange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/colormap_valuerange.png -------------------------------------------------------------------------------- /docs/assets/images/datamanagement_dataset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/datamanagement_dataset.png -------------------------------------------------------------------------------- /docs/assets/images/datamanagement_meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/datamanagement_meta.png -------------------------------------------------------------------------------- /docs/assets/images/datamanagement_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/datamanagement_variables.png -------------------------------------------------------------------------------- /docs/assets/images/datamanagement_visibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/datamanagement_visibility.png -------------------------------------------------------------------------------- /docs/assets/images/datamanagement_visibility_added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/datamanagement_visibility_added.png -------------------------------------------------------------------------------- /docs/assets/images/export_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/export_data.png -------------------------------------------------------------------------------- /docs/assets/images/export_data_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/export_data_button.png -------------------------------------------------------------------------------- /docs/assets/images/features_label_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/features_label_dark.png -------------------------------------------------------------------------------- /docs/assets/images/features_label_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/features_label_light.png -------------------------------------------------------------------------------- /docs/assets/images/import_places.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/import_places.png -------------------------------------------------------------------------------- /docs/assets/images/infobox_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/infobox_box.png -------------------------------------------------------------------------------- /docs/assets/images/infobox_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/infobox_button.png -------------------------------------------------------------------------------- /docs/assets/images/layerpanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/layerpanel.png -------------------------------------------------------------------------------- /docs/assets/images/locate_dataset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/locate_dataset.png -------------------------------------------------------------------------------- /docs/assets/images/locate_place.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/locate_place.png -------------------------------------------------------------------------------- /docs/assets/images/permalink_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/permalink_button.png -------------------------------------------------------------------------------- /docs/assets/images/pin_variable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/pin_variable.png -------------------------------------------------------------------------------- /docs/assets/images/player_autostep_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/player_autostep_pause.png -------------------------------------------------------------------------------- /docs/assets/images/player_autostep_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/player_autostep_start.png -------------------------------------------------------------------------------- /docs/assets/images/player_first_last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/player_first_last.png -------------------------------------------------------------------------------- /docs/assets/images/player_next_prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/player_next_prev.png -------------------------------------------------------------------------------- /docs/assets/images/remove_place.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/remove_place.png -------------------------------------------------------------------------------- /docs/assets/images/rename_place.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/rename_place.png -------------------------------------------------------------------------------- /docs/assets/images/select_dataset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/select_dataset.png -------------------------------------------------------------------------------- /docs/assets/images/select_place.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/select_place.png -------------------------------------------------------------------------------- /docs/assets/images/select_place_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/select_place_group.png -------------------------------------------------------------------------------- /docs/assets/images/select_place_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/select_place_map.png -------------------------------------------------------------------------------- /docs/assets/images/select_timestep_calender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/select_timestep_calender.png -------------------------------------------------------------------------------- /docs/assets/images/select_timestep_slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/select_timestep_slider.png -------------------------------------------------------------------------------- /docs/assets/images/select_variable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/select_variable.png -------------------------------------------------------------------------------- /docs/assets/images/settings_on_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/settings_on_selection.png -------------------------------------------------------------------------------- /docs/assets/images/settings_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/settings_overlay.png -------------------------------------------------------------------------------- /docs/assets/images/settings_player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/settings_player.png -------------------------------------------------------------------------------- /docs/assets/images/settings_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/settings_server.png -------------------------------------------------------------------------------- /docs/assets/images/settings_timeseries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/settings_timeseries.png -------------------------------------------------------------------------------- /docs/assets/images/settings_usermaps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/settings_usermaps.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_button.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_highlighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_highlighted.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_initial_timeseries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_initial_timeseries.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_meta_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_meta_json.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_meta_tabular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_meta_tabular.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_meta_textual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_meta_textual.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_metadata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_metadata.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_navigate_timeseries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_navigate_timeseries.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_statistics_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_statistics_overview.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_statistics_points.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_statistics_points.png -------------------------------------------------------------------------------- /docs/assets/images/sidebar_statistics_polygons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/sidebar_statistics_polygons.png -------------------------------------------------------------------------------- /docs/assets/images/splitmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/splitmode.png -------------------------------------------------------------------------------- /docs/assets/images/style_place.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/style_place.png -------------------------------------------------------------------------------- /docs/assets/images/user_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/user_variables.png -------------------------------------------------------------------------------- /docs/assets/images/user_variables_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/user_variables_add.png -------------------------------------------------------------------------------- /docs/assets/images/user_variables_management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/user_variables_management.png -------------------------------------------------------------------------------- /docs/assets/images/xcube-viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/images/xcube-viewer.png -------------------------------------------------------------------------------- /docs/assets/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/logo192.png -------------------------------------------------------------------------------- /docs/assets/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/logo512.png -------------------------------------------------------------------------------- /docs/assets/videos/Player_hh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/videos/Player_hh.gif -------------------------------------------------------------------------------- /docs/assets/videos/analysis_compare-mode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/videos/analysis_compare-mode.gif -------------------------------------------------------------------------------- /docs/assets/videos/share_link.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/docs/assets/videos/share_link.gif -------------------------------------------------------------------------------- /docs/build_viewer.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | --- 5 | 6 | # Getting Started 7 | 8 | ## Demo 9 | 10 | To test the viewer app, you can use the [xcube viewer demo](https://bc-viewer.brockmann-consult.de). This is our Brockmann Consult Demo xcube viewer. Via the [viewer's settings](user_guide/settings.md) it is possible to change the xcube server url which is used for displaying data. Here is another demo server that you may add for testing: 11 | 12 | - Euro Data Cube Server (`https://edc-api.brockmann-consult.de/api`) has integrated amongst others a data cube with global essential climate variables (ECVs) variables from the ESA Earth System Data Lab Project. To access the Euro Data Cube viewer directly please visit [https://edc-viewer.brockmann-consult.de](https://edc-viewer.brockmann-consult.de) . 13 | 14 | ## Build and Deploy 15 | 16 | You can also build and deploy your own viewer instance. In the latter case, visit the [xcube-viewer](https://github.com/xcube-dev/xcube-viewer) repository on GitHub and follow the instructions provides in the related [README](https://github.com/xcube-dev/xcube-viewer/blob/main/README.md) file. 17 | -------------------------------------------------------------------------------- /docs/css/custom.css: -------------------------------------------------------------------------------- 1 | /* Hide the dark mode image by default */ 2 | .dark-image { 3 | display: none; 4 | } 5 | 6 | /* Show the dark mode image and hide the light mode image when dark mode is active */ 7 | [data-md-color-scheme="slate"] .light-image { 8 | display: none; 9 | } 10 | 11 | [data-md-color-scheme="slate"] .dark-image { 12 | display: block; 13 | } 14 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | --- 5 | 6 | # xcube Viewer Documentation 7 | 8 | Welcome to the **xcube Viewer** documentation page. The xcube Viewer is a single-page web application that provides tools to visualise and analyse multitemporal spatial datasets. The data is provided via the [xcube Server](https://xcube.readthedocs.io/en/latest/webapi.html). 9 | 10 | ![Start Image](assets/images/about_viewer.png) 11 | -------------------------------------------------------------------------------- /docs/javascripts/mathjax.js: -------------------------------------------------------------------------------- 1 | window.MathJax = { 2 | tex: { 3 | inlineMath: [["\\(", "\\)"]], 4 | displayMath: [["\\[", "\\]"]], 5 | processEscapes: true, 6 | processEnvironments: true, 7 | }, 8 | options: { 9 | ignoreHtmlClass: ".*|", 10 | processHtmlClass: "arithmatex", 11 | }, 12 | }; 13 | 14 | document$.subscribe(() => { 15 | MathJax.startup.output.clearCache(); 16 | MathJax.typesetClear(); 17 | MathJax.texReset(); 18 | MathJax.typesetPromise(); 19 | }); 20 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupConfigRules } from "@eslint/compat"; 2 | import reactRefresh from "eslint-plugin-react-refresh"; 3 | import globals from "globals"; 4 | import tsParser from "@typescript-eslint/parser"; 5 | import path from "node:path"; 6 | import { fileURLToPath } from "node:url"; 7 | import js from "@eslint/js"; 8 | import { FlatCompat } from "@eslint/eslintrc"; 9 | 10 | const compat = new FlatCompat({ 11 | baseDirectory: path.dirname(fileURLToPath(import.meta.url)), 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all, 14 | }); 15 | 16 | export default [ 17 | { 18 | ignores: ["**/dist", "**/site", "src/volume/**", "docs/**"], 19 | }, 20 | ...fixupConfigRules( 21 | compat.extends( 22 | "eslint:recommended", 23 | "plugin:@typescript-eslint/recommended", 24 | "plugin:react-hooks/recommended", 25 | "plugin:storybook/recommended", 26 | ), 27 | ), 28 | { 29 | plugins: { 30 | "react-refresh": reactRefresh, 31 | }, 32 | 33 | languageOptions: { 34 | globals: { 35 | ...globals.browser, 36 | }, 37 | 38 | parser: tsParser, 39 | }, 40 | 41 | rules: { 42 | "no-unused-vars": "off", 43 | 44 | "@typescript-eslint/no-unused-vars": [ 45 | "error", 46 | { 47 | argsIgnorePattern: "^_", 48 | varsIgnorePattern: "^_", 49 | caughtErrorsIgnorePattern: "^_", 50 | }, 51 | ], 52 | 53 | "react-refresh/only-export-components": [ 54 | "warn", 55 | { 56 | allowConstantExport: true, 57 | }, 58 | ], 59 | }, 60 | }, 61 | ]; 62 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | xcube Viewer 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/docs/add-layer-wms.de.md: -------------------------------------------------------------------------------- 1 | *WMS* steht für [Web Map Service](https://de.wikipedia.org/wiki/Web_Map_Service). 2 | Ein WMS kann eine oder mehrere Kartenlayer beeinhalten. 3 | 4 | **WMS URL**: Die URL eines WMS, z.B. 5 | `https://geodienste.hamburg.de/HH_WMS_Gewaesserunterhaltung`. 6 | 7 | **WMS Layer**: Sobald eine gültige WMS-URL eingegeben wurde, kann hier eine der 8 | verfügbaren Ebenen auswählt werden. 9 | -------------------------------------------------------------------------------- /public/docs/add-layer-wms.en.md: -------------------------------------------------------------------------------- 1 | *WMS* stands for [Web Map Service](https://en.wikipedia.org/wiki/Web_Map_Service). 2 | A WMS may offer one or more map layers. 3 | 4 | **WMS URL**: The URL of a WMS, for example 5 | `https://geodienste.hamburg.de/HH_WMS_Gewaesserunterhaltung`. 6 | 7 | **WMS Layer**: Once you have entered a valid WMS URL, you can select one of 8 | its offered layers here. 9 | -------------------------------------------------------------------------------- /public/docs/add-layer-wms.se.md: -------------------------------------------------------------------------------- 1 | *WMS* står för [Web Map Service](https://en.wikipedia.org/wiki/Web_Map_Service). 2 | En WMS kan erbjuda ett eller flera kartlager. 3 | 4 | **WMS URL**: URL för ett WMS, till exempel 5 | `https://geodienste.hamburg.de/HH_WMS_Gewaesserunterhaltung`. 6 | 7 | **WMS Layer**: När du har angett en giltig WMS-URL kan du välja ett av de 8 | dess erbjudna lager här.. -------------------------------------------------------------------------------- /public/docs/add-layer-xyz.de.md: -------------------------------------------------------------------------------- 1 | Der Name XYZ bezieht sich auf die URLs, die von Diensten verwendet werden, die 2 | [Tiled Web Maps](https://en.wikipedia.org/wiki/Tiled_web_map) bereitstellen, 3 | oft auch als [OpenStreetMap (OSM)](https://en.wikipedia.org/wiki/OpenStreetMap) 4 | Standard oder _Slippy Maps_ bezeichnet. Die URLs werden auch häufig von Kartenservern 5 | verwendet, die den [Tile Map Service (TMS)](https://en.wikipedia.org/wiki/Tile_Map_Service) 6 | Standard implementieren. Die URLs für eine solche Karte enthalten die x- und y-Koordinaten 7 | einer Bildkachel und einen optionalen Zoomlevel z. Zum Beispiel, 8 | `https://a.tile.osm.org/{z}/{x}/{y}.png`. 9 | 10 | **XYZ Layer URL**: Die URL des Layers. Diese muss die folgenden Muster enthalten 11 | `{x}`, `{y}`, und optional `{z}`. `{-y}` kann verwendet werden, um 12 | eine gespiegelte y-Achse anzugeben. 13 | 14 | **Layer Titel**: Der beschreibende Titel für den Layer. 15 | 16 | **Layer Attribution**: Optionale Attributionsinformationen für den Layer. -------------------------------------------------------------------------------- /public/docs/add-layer-xyz.en.md: -------------------------------------------------------------------------------- 1 | The name _XYZ_ refers to the URLs used by services that provide 2 | [Tiled Web Maps](https://en.wikipedia.org/wiki/Tiled_web_map) often also 3 | referred to as [OpenStreetMap (OSM)](https://en.wikipedia.org/wiki/OpenStreetMap) 4 | standard or _Slippy Maps_. The URLs are also commonly used by map server that 5 | implement the [Tile Map Service (TMS)](https://en.wikipedia.org/wiki/Tile_Map_Service) 6 | standard. The URLs for such a map contain an image tile's x- and y-coordinates 7 | and an optional zoom level z. For example, 8 | `https://a.tile.osm.org/{z}/{x}/{y}.png`. 9 | 10 | **XYZ Layer URL**: The URL of the layer. It must contain the patterns 11 | `{x}`, `{y}`, and optionally `{z}`. Note that `{-y}` may be used to 12 | indicate a flipped y-axis. 13 | 14 | **Layer Title**: The descriptive title for the layer. 15 | 16 | **Layer Attribution**: Optional attribution information for the layer. 17 | -------------------------------------------------------------------------------- /public/docs/add-layer-xyz.se.md: -------------------------------------------------------------------------------- 1 | Namnet _XYZ_ hänvisar till de webbadresser som används av tjänster som tillhandahåller 2 | [Tiled Web Maps](https://en.wikipedia.org/wiki/Tiled_web_map) ofta också 3 | refereras till som [OpenStreetMap (OSM)](https://en.wikipedia.org/wiki/OpenStreetMap) 4 | standard eller _Slippy Maps_. URL:erna används också ofta av kartservrar som 5 | implementerar [Tile Map Service (TMS)](https://en.wikipedia.org/wiki/Tile_Map_Service) 6 | standard. URL-adresserna för en sådan karta innehåller en bildkakels x- och y-koordinater 7 | och en valfri zoomnivå z. Till exempel, 8 | `https://a.tile.osm.org/{z}/{x}/{y}.png`. 9 | 10 | **XYZ lager URL**: URL-adressen till lagern. Den måste innehålla mönstren 11 | `{x}`, `{y}` och eventuellt `{z}`. Observera att `{-y}` kan användas för att 12 | för att ange en vänd y-axel. 13 | 14 | **Lagertitel**: Den beskrivande titeln för lager. 15 | 16 | **Lagerattribution**: Optionell attributionsinformation för lagret. -------------------------------------------------------------------------------- /public/docs/color-mappings.de.md: -------------------------------------------------------------------------------- 1 | Eine benutzerdefinierte Farbzuordnung ordnet Datenwerte oder Bereiche von 2 | Datenwerten Farbwerten zu. Die Zeilen im Textfeld haben die allgemeine 3 | Syntax ``: ``, wobei `` sein kann: 4 | 5 | * eine Liste von RGB-Werten, mit Werten im Bereich von 0 bis 255, zum Beispiel 6 | `255,165,0` für die Farbe Orange; 7 | * ein hexadezimaler RGB-Wert, z.B. `#FFA500`; 8 | * oder ein gültiger [HTML-Farbname](https://www.w3schools.com/colors/colors_names.asp) 9 | wie `Orange`, `BlanchedAlmond` oder `MediumSeaGreen`. 10 | 11 | Der Farbwert kann durch einen Deckungswert (Alpha-Wert) im Bereich von 0 bis 1 12 | ergänzt werden, zum Beispiel `110,220,230,0.5` oder `#FFA500,0.8` oder `Blue,0`. 13 | Hexadezimale Werte können auch mit einem Alpha-Wert geschrieben werden, wie `#FFA500CD`. 14 | 15 | Die Interpretation des `` hängt vom ausgewählten Farbzuordnungstyp ab 16 | 17 | * **Kontinuierlich:** Kontinuierliche Farbzuordnung, bei der jeder 18 | `` eine Stützstelle eines Farbverlaufs darstellt. 19 | * **Schrittweise:** Schrittweise Farbzuordnung, bei der die Werte 20 | Bereichsgrenzen darstellen, die einer einzelnen Farbe zugeordnet werden. 21 | Eine `` wird dem ersten `` eines Grenzbereiches zugeordnet. 22 | Der letzte Farbwert wird ignoriert. 23 | * **Kategorisch:** Werte stellen eindeutige Kategorien oder Indizes dar, 24 | die einer Farbe zugeordnet sind. Der Inhalt des Datensatzes sowie der 25 | `` muss dem Typ Integer entsprechen. Wenn eine Kategorie keinen 26 | `` in der Farbzuordnung hat, wird diese transparent dargestellt. 27 | Geeignet für kategorische Datensätze. 28 | -------------------------------------------------------------------------------- /public/docs/color-mappings.en.md: -------------------------------------------------------------------------------- 1 | A user-defined color mapping associates data values or ranges of data values 2 | with color values. The lines in the text box have the general syntax 3 | `: `, where `` can be 4 | 5 | * a list of RGB values, with values in the range 0 to 255, for example, 6 | `255,165,0` for the color Orange; 7 | * a hexadecimal RGB value, e.g., `#FFA500`; 8 | * or a valid [HTML color name](https://www.w3schools.com/colors/colors_names.asp) 9 | such as `Orange`, `BlanchedAlmond` or `MediumSeaGreen`. 10 | 11 | The color value may be suffixed by a opaqueness (alpha) value in the range 12 | 0 to 1, for example `110,220,230,0.5` or `#FFA500,0.8` or `Blue,0`. 13 | Hexadecimal values can also be written including an alpha value, 14 | such as `#FFA500CD`. 15 | 16 | The interpretation of the `` depends on the selected color mapping 17 | type: 18 | 19 | * **Continuous:** Continuous color assignment, where each `` 20 | represents a support point of a color gradient. 21 | * **Stepwise:** Stepwise color mapping where values within the range of two 22 | subsequent ``s are mapped to the same color. A `` gets associated with the 23 | first `` of each boundary range, while the last color gets ignored. 24 | * **Categorical:** Values represent unique categories or indexes that are 25 | mapped to a color. The data and the `` must be of type integer. 26 | If a category does not have a `` in the color mapping, it will be 27 | displayed as transparent. Suitable for categorical datasets. 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/docs/color-mappings.se.md: -------------------------------------------------------------------------------- 1 | En användardefinierad färgkarta associerar datavärden eller intervall 2 | av datavärden med färgvärden. Raderna i textrutan har den allmänna 3 | syntaxen ``: ``, där `` kan vara 4 | 5 | * en lista med RGB-värden, med värden i intervallet 0 till 255, 6 | till exempel, `255,165,0` för färgen Orange; 7 | * ett hexadecimalt RGB-värde, t.ex. `#FFA500`; 8 | * eller ett giltigt [HTML-färgnamn](https://www.w3schools.com/colors/colors_names.asp) 9 | som `Orange`, `BlanchedAlmond` eller `MediumSeaGreen`. 10 | 11 | Färgvärdet kan kompletteras med ett opacitetsvärde (alpha-värde) i 12 | intervallet 0 till 1, till exempel `110,220,230,0.5` eller `#FFA500,0.8` 13 | eller `Blue,0`. Hexadecimala värden kan också skrivas med ett alpha-värde, 14 | såsom `#FFA500CD`. 15 | 16 | Tolkningen av `` beror på den valda färgkartläggningstypen: 17 | 18 | * **Kontinuerlig:** Kontinuerlig färgtilldelning där varje `` 19 | representerar en punkt i en färggradient. 20 | * **Stegvis:** Stegvis färgmappning där värden är gränser för intervall. 21 | En `` associeras med det första `` i gränsintervall, medan 22 | den sista färgen ignoreras. 23 | * **Kategorisk:** Värden representerar unika kategorier eller index som 24 | är mappade till en färg. Innehållet i datasetet samt `` måste 25 | vara av typ Integer. Om en kategori inte har något `` i 26 | färgkartan, kommer den att visas som genomskinlig. Lämplig för 27 | kategoriska dataset. 28 | -------------------------------------------------------------------------------- /public/docs/dev-reference.en.md: -------------------------------------------------------------------------------- 1 | ## Server-side UI Contributions 2 | 3 | Starting with xcube Server 1.8 and xcube Viewer 1.4 it is possible to enhance 4 | the viewer UI by _server-side contributions_ programmed in Python. 5 | For this to work, service providers can now configure xcube Server to load 6 | one or more Python modules that provide UI-contributions of type 7 | `xcube.webapi.viewer.contrib.Panel`. 8 | Users can create `Panel` objects and use the two decorators 9 | `layout()` and `callback()` to implement the UI and the interaction 10 | behaviour, respectively. The new functionality is provided by the 11 | [Chartlets](https://bcdev.github.io/chartlets/) Python library. 12 | 13 | A working example can be found in the 14 | [xcube repository](https://github.com/xcube-dev/xcube/tree/5ebf4c76fdccebdd3b65f4e04218e112410f561b/examples/serve/panels-demo). 15 | 16 | ## Contributions 17 | 18 | The following contributions are in use by this instance of xcube Viewer: 19 | 20 | ${extensions} 21 | 22 | ## Available State Properties 23 | 24 | xcube Viewer exposes some of its application state properties to Python 25 | extension components, e.g., `panel = Panel(...)`. The current values of state 26 | properties can be accessed via `Input` and `State` channels you define for your 27 | extension component decorators, i.e., `@panel.layout(...)` and/or `@panel.callback(...)`. 28 | 29 | - To trigger a callback call when a state property changes use the input syntax 30 | `Input("@app", "")`. 31 | - To just read a property from the state use `State("@app", "")`. This 32 | will not trigger a call to your callback. 33 | 34 | The following state properties of xcube Viewer's are made available 35 | to extensions: 36 | 37 | 38 | ${derivedState} 39 | -------------------------------------------------------------------------------- /public/docs/privacy-note.de.md: -------------------------------------------------------------------------------- 1 | Bevor Sie fortfahren, sollten Sie Folgendes über diese Anwendung wissen: 2 | 3 | * Diese Anwendung bezieht ihre Daten von einen Anwendungsserver der Brockmann Consult GmbH. 4 | * Es kommen freie Kartendienste von Drittanbietern zum Einsatz. 5 | * Es werden keine Anwenderdaten gesammelt oder geteilt. 6 | * Anwendungseinstellungen werden im lokalen Browser-Speicher abgelegt. ([HTML5 local storage](https://de.wikipedia.org/wiki/Web_Storage)) 7 | * Eventuelle Anmeldeinformationen werden als funktionale "Cookies" abgelegt. 8 | 9 | Sie können Ihre Zustimmung in den Systemeinstellungen jederzeit widerrufen. 10 | 11 | -------------------------------------------------------------------------------- /public/docs/privacy-note.en.md: -------------------------------------------------------------------------------- 1 | Before you continue, you should know the following about this application: 2 | 3 | * This application obtains its data from an application server of Brockmann Consult GmbH. 4 | * Free third-party map services are used. 5 | * No user data is collected or shared. 6 | * Application settings are stored in the browser's local memory. ([HTML5 local storage](https://en.wikipedia.org/wiki/Web_storage)) 7 | * Any login information is stored as functional "cookies". 8 | 9 | You can revoke your consent in the system settings at any time. 10 | -------------------------------------------------------------------------------- /public/docs/privacy-note.se.md: -------------------------------------------------------------------------------- 1 | Innan du fortsätter bör du veta följande om den här applikationen: 2 | 3 | * Den här applikationen får sina data från en applikationsserver hos Brockmann Consult GmbH. 4 | * Gratis karttjänster från tredje part används. 5 | * Inga användaruppgifter samlas in eller delas. 6 | * Programinställningar lagras i webbläsarens lokala minne. ([HTML5 local storage](https://en.wikipedia.org/wiki/Web_storage)) 7 | * All inloggningsinformation lagras som funktionella "cookies". 8 | 9 | Du kan när som helst återkalla ditt samtycke i systeminställningarna. 10 | 11 | -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/public/images/logo.png -------------------------------------------------------------------------------- /public/images/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/public/images/logo192.png -------------------------------------------------------------------------------- /public/images/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/public/images/logo512.png -------------------------------------------------------------------------------- /public/images/textures/cm_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/public/images/textures/cm_gray.png -------------------------------------------------------------------------------- /public/images/textures/cm_viridis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/public/images/textures/cm_viridis.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "xcube Viewer", 3 | "name": "xcube Viewer", 4 | "icons": [ 5 | { 6 | "src": "images/favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "images/logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "images/logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": "index.html?launcher=1", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /resources/demo.csv: -------------------------------------------------------------------------------- 1 | time; longitude; latitude; cruise; CHL; TSM 2 | 2017-01-16 10:29:10; 1.27; 50.70; Cruise 1; 5.1; 13.8 3 | 2017-01-17 10:11:42; 1.31; 50.84; Cruise 1; 5.5; 21.2 4 | 2017-01-20 10:12:17; 1.44; 50.92; Cruise 1; 6.6; 23.5 5 | 2017-01-22 11:02:19; 1.61; 51.05; Cruise 2; 6.7; 20.2 6 | 2017-01-25 10:11:36; 1.84; 51.13; Cruise 2; 6.9; 26.4 7 | 2017-01-26 10:11:36; 2.13; 51.15; Cruise 3; 6.5; 32.1 8 | 2017-01-28 10:10:53; 2.43; 51.23; Cruise 3; 7.1; 39.9 9 | 2017-01-29 10:11:10; 2.44; 51.25; Cruise 3; 7.3; 45.2 10 | -------------------------------------------------------------------------------- /resources/logo-inv.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/resources/logo-inv.pdn -------------------------------------------------------------------------------- /resources/logo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/resources/logo.pdn -------------------------------------------------------------------------------- /src/actions/messageLogActions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { MessageType } from "@/states/messageLogState"; 8 | 9 | //////////////////////////////////////////////////////////////////////////////// 10 | 11 | export const POST_MESSAGE = "POST_MESSAGE"; 12 | 13 | export interface PostMessage { 14 | type: typeof POST_MESSAGE; 15 | messageType: MessageType; 16 | messageText: string; 17 | } 18 | 19 | export function postMessage( 20 | messageType: MessageType, 21 | messageText: string | Error, 22 | ): PostMessage { 23 | return { 24 | type: POST_MESSAGE, 25 | messageType, 26 | messageText: 27 | typeof messageText === "string" ? messageText : messageText.message, 28 | }; 29 | } 30 | 31 | //////////////////////////////////////////////////////////////////////////////// 32 | 33 | export const HIDE_MESSAGE = "HIDE_MESSAGE"; 34 | 35 | export interface HideMessage { 36 | type: typeof HIDE_MESSAGE; 37 | messageId: number; 38 | } 39 | 40 | export function hideMessage(messageId: number): HideMessage { 41 | return { type: HIDE_MESSAGE, messageId }; 42 | } 43 | 44 | //////////////////////////////////////////////////////////////////////////////// 45 | 46 | export type MessageLogAction = PostMessage | HideMessage; 47 | -------------------------------------------------------------------------------- /src/actions/otherActions.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { Dispatch } from "redux"; 8 | import { default as OlView } from "ol/View"; 9 | 10 | import { PersistedMapState, PersistedState } from "@/states/persistedState"; 11 | import { MAP_OBJECTS } from "@/states/controlState"; 12 | import { default as OlMap } from "ol/Map"; 13 | 14 | //////////////////////////////////////////////////////////////////////////////// 15 | 16 | export const APPLY_PERSISTED_STATE = "APPLY_PERSISTED_STATE"; 17 | 18 | export interface ApplyPersistedState { 19 | type: typeof APPLY_PERSISTED_STATE; 20 | persistedState: PersistedState; 21 | } 22 | 23 | export function applyPersistentState(persistedState: PersistedState) { 24 | return (dispatch: Dispatch) => { 25 | console.debug("Restoring persisted state:", persistedState); 26 | dispatch(_applyPersistentState(persistedState)); 27 | const { mapState } = persistedState.state; 28 | if (mapState) { 29 | restoreMapView(mapState); 30 | } 31 | }; 32 | } 33 | 34 | function _applyPersistentState( 35 | persistedState: PersistedState, 36 | ): ApplyPersistedState { 37 | return { type: APPLY_PERSISTED_STATE, persistedState }; 38 | } 39 | 40 | function restoreMapView(mapState: PersistedMapState) { 41 | if (MAP_OBJECTS["map"]) { 42 | console.debug("Restoring map:", mapState); 43 | const map = MAP_OBJECTS["map"] as OlMap; 44 | map.setView(new OlView(mapState.view)); 45 | } 46 | } 47 | 48 | //////////////////////////////////////////////////////////////////////////////// 49 | 50 | export type OtherAction = ApplyPersistedState; 51 | -------------------------------------------------------------------------------- /src/actions/userAuthActions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | //////////////////////////////////////////////////////////////////////////////// 8 | 9 | import { Action, Dispatch } from "redux"; 10 | 11 | import { AppState } from "@/states/appState"; 12 | import { updateDatasets } from "./dataActions"; 13 | 14 | export const UPDATE_ACCESS_TOKEN = "UPDATE_ACCESS_TOKEN"; 15 | 16 | export interface UpdateAccessToken { 17 | type: typeof UPDATE_ACCESS_TOKEN; 18 | accessToken: string | null; 19 | } 20 | 21 | export function updateAccessToken(accessToken: string | null) { 22 | return (dispatch: Dispatch, getState: () => AppState) => { 23 | const prevAccessToken = getState().userAuthState.accessToken; 24 | if (prevAccessToken !== accessToken) { 25 | dispatch(_updateAccessToken(accessToken)); 26 | if (accessToken === null || prevAccessToken === null) { 27 | dispatch(updateDatasets() as unknown as Action); 28 | } 29 | } 30 | }; 31 | } 32 | 33 | function _updateAccessToken(accessToken: string | null): UpdateAccessToken { 34 | return { type: UPDATE_ACCESS_TOKEN, accessToken }; 35 | } 36 | 37 | //////////////////////////////////////////////////////////////////////////////// 38 | 39 | export type UserAuthAction = UpdateAccessToken; 40 | -------------------------------------------------------------------------------- /src/api/errors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export class HTTPError extends Error { 8 | readonly statusCode: number; 9 | 10 | constructor(statusCode: number, statusMessage: string) { 11 | super(statusMessage); 12 | this.statusCode = statusCode; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/api/getDatasetPlaceGroup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { PlaceGroup } from "@/model/place"; 8 | import { callJsonApi, makeRequestInit } from "./callApi"; 9 | 10 | export function getDatasetPlaceGroup( 11 | apiServerUrl: string, 12 | datasetId: string, 13 | placeGroupId: string, 14 | accessToken: string | null, 15 | ): Promise { 16 | const init = makeRequestInit(accessToken); 17 | const dsId = encodeURIComponent(datasetId); 18 | const pgId = encodeURIComponent(placeGroupId); 19 | return callJsonApi( 20 | `${apiServerUrl}/datasets/${dsId}/places/${pgId}`, 21 | init, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/api/getExpressionCapabilities.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { ExpressionCapabilities } from "@/model/userVariable"; 8 | import { callJsonApi } from "./callApi"; 9 | 10 | export function getExpressionCapabilities( 11 | apiServerUrl: string, 12 | ): Promise { 13 | return callJsonApi(`${apiServerUrl}/expressions/capabilities`); 14 | } 15 | -------------------------------------------------------------------------------- /src/api/getPointValue.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { Dataset } from "@/model/dataset"; 8 | import { Variable } from "@/model/variable"; 9 | import { 10 | callJsonApi, 11 | makeRequestInit, 12 | makeRequestUrl, 13 | QueryComponent, 14 | } from "@/api/callApi"; 15 | import { encodeDatasetId, encodeVariableName } from "@/model/encode"; 16 | 17 | interface Value { 18 | value?: number; 19 | } 20 | 21 | interface Result { 22 | result?: Value; 23 | } 24 | 25 | export function getPointValue( 26 | apiServerUrl: string, 27 | dataset: Dataset, 28 | variable: Variable, 29 | lon: number, 30 | lat: number, 31 | time: string | null, 32 | accessToken: string | null, 33 | ): Promise { 34 | const query: QueryComponent[] = [ 35 | ["lon", lon.toString()], 36 | ["lat", lat.toString()], 37 | ]; 38 | if (time) { 39 | query.push(["time", time]); 40 | } 41 | const url = makeRequestUrl( 42 | `${apiServerUrl}/statistics/${encodeDatasetId(dataset)}/${encodeVariableName(variable)}`, 43 | query, 44 | ); 45 | return callJsonApi(url, makeRequestInit(accessToken), (result: Result) => 46 | result.result ? result.result : {}, 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/api/getServerInfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { ApiServerInfo } from "@/model/apiServer"; 8 | import { callJsonApi } from "./callApi"; 9 | 10 | export function getServerInfo(apiServerUrl: string): Promise { 11 | return callJsonApi(`${apiServerUrl}/`); 12 | } 13 | -------------------------------------------------------------------------------- /src/api/getStatistics.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { Dataset } from "@/model/dataset"; 8 | import { Variable } from "@/model/variable"; 9 | import { PlaceInfo } from "@/model/place"; 10 | import { 11 | Statistics, 12 | StatisticsRecord, 13 | StatisticsSource, 14 | } from "@/model/statistics"; 15 | import { 16 | callJsonApi, 17 | makeRequestInit, 18 | makeRequestUrl, 19 | QueryComponent, 20 | } from "./callApi"; 21 | import { encodeDatasetId, encodeVariableName } from "@/model/encode"; 22 | 23 | interface StatisticsResult { 24 | result: Statistics; 25 | } 26 | 27 | export function getStatistics( 28 | apiServerUrl: string, 29 | dataset: Dataset, 30 | variable: Variable, 31 | placeInfo: PlaceInfo, 32 | timeLabel: string | null, 33 | accessToken: string | null, 34 | ): Promise { 35 | const query: QueryComponent[] = 36 | timeLabel !== null ? [["time", timeLabel]] : []; 37 | const url = makeRequestUrl( 38 | `${apiServerUrl}/statistics/${encodeDatasetId(dataset)}/${encodeVariableName(variable)}`, 39 | query, 40 | ); 41 | 42 | const init = { 43 | ...makeRequestInit(accessToken), 44 | method: "post", 45 | body: JSON.stringify(placeInfo.place.geometry), 46 | }; 47 | 48 | const source: StatisticsSource = { 49 | dataset, 50 | variable, 51 | placeInfo, 52 | time: timeLabel, 53 | }; 54 | 55 | return callJsonApi(url, init, (r: StatisticsResult) => ({ 56 | source, 57 | statistics: r.result, 58 | })); 59 | } 60 | -------------------------------------------------------------------------------- /src/api/getViewerState.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { PersistedState } from "@/states/persistedState"; 8 | import { callJsonApi, makeRequestInit, makeRequestUrl } from "./callApi"; 9 | 10 | export function getViewerState( 11 | apiServerUrl: string, 12 | accessToken: string | null, 13 | stateKey: string, 14 | ): Promise { 15 | const url = makeRequestUrl(`${apiServerUrl}/viewer/state`, [ 16 | ["key", stateKey], 17 | ]); 18 | return callJsonApi(url, makeRequestInit(accessToken)) 19 | .then((state) => { 20 | return state; 21 | }) 22 | .catch((error) => { 23 | return `${error}`; 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/api/hasViewerStateApi.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { makeRequestUrl } from "./callApi"; 8 | 9 | export function hasViewerStateApi(apiServerUrl: string): Promise { 10 | const url = makeRequestUrl(`${apiServerUrl}/viewer/state`, [ 11 | ["key", "sentinel"], 12 | ]); 13 | try { 14 | return fetch(url) 15 | .then((response) => { 16 | // status 501 = "Not Implemented" 17 | return response.status !== 501; 18 | }) 19 | .catch(() => { 20 | return false; 21 | }); 22 | } catch (_) { 23 | return Promise.resolve(false); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export { getColorBars } from "./getColorBars"; 8 | export { getDatasets } from "./getDatasets"; 9 | export { getDatasetPlaceGroup } from "./getDatasetPlaceGroup"; 10 | export { getExpressionCapabilities } from "./getExpressionCapabilities"; 11 | export { getServerInfo } from "./getServerInfo"; 12 | export { getTimeSeriesForGeometry } from "./getTimeSeries"; 13 | export { getStatistics } from "./getStatistics"; 14 | export { getPointValue } from "./getPointValue"; 15 | export { updateResources } from "./updateResources"; 16 | export { getViewerState } from "./getViewerState"; 17 | export { putViewerState } from "./putViewerState"; 18 | export { HTTPError } from "./errors"; 19 | -------------------------------------------------------------------------------- /src/api/putViewerState.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { PersistedState } from "@/states/persistedState"; 8 | import { callJsonApi, makeRequestInit, makeRequestUrl } from "./callApi"; 9 | 10 | export function putViewerState( 11 | apiServerUrl: string, 12 | accessToken: string | null, 13 | state: PersistedState, 14 | ): Promise { 15 | const url = makeRequestUrl(`${apiServerUrl}/viewer/state`, []); 16 | const init = { 17 | ...makeRequestInit(accessToken), 18 | method: "PUT", 19 | body: JSON.stringify(state), 20 | }; 21 | try { 22 | return callJsonApi<{ key: string }>(url, init) 23 | .then((result) => { 24 | return result.key; 25 | }) 26 | .catch((error) => { 27 | console.error(error); 28 | return undefined; 29 | }); 30 | } catch (error) { 31 | console.error(error); 32 | return Promise.resolve(undefined); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/api/updateResources.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { callJsonApi, makeRequestInit, makeRequestUrl } from "./callApi"; 8 | 9 | export function updateResources( 10 | apiServerUrl: string, 11 | accessToken: string | null, 12 | ): Promise { 13 | const url = makeRequestUrl(`${apiServerUrl}/maintenance/update`, []); 14 | const init = makeRequestInit(accessToken); 15 | try { 16 | return callJsonApi(url, init) 17 | .then(() => { 18 | return true; 19 | }) 20 | .catch((error) => { 21 | console.error(error); 22 | return false; 23 | }); 24 | } catch (error) { 25 | console.error(error); 26 | return Promise.resolve(false); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/api/validateExpression.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { callApi } from "./callApi"; 8 | import { encodeDatasetId } from "@/model/encode"; 9 | import i18n from "@/i18n"; 10 | 11 | export async function validateExpression( 12 | apiServerUrl: string, 13 | datasetId: string, 14 | expression: string, 15 | ): Promise { 16 | if (expression!.trim() === "") { 17 | return i18n.get("Must not be empty"); 18 | } 19 | const url = `${apiServerUrl}/expressions/validate/${encodeDatasetId(datasetId)}/${encodeURIComponent(expression)}`; 20 | try { 21 | await callApi(url); 22 | return null; 23 | } catch (e) { 24 | const message = (e as Error).message; 25 | if (message) { 26 | const i1 = message.indexOf("("); 27 | const i2 = message.lastIndexOf(")"); 28 | return message.slice(i1 >= 0 ? i1 + 1 : 0, i2 >= 0 ? i2 : message.length); 29 | } 30 | return i18n.get("Invalid expression"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/ColorBarLegend/ColorBarGroupComponent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { ColorBarGroup } from "@/model/colorBar"; 8 | import ColorBarGroupHeader from "./ColorBarGroupHeader"; 9 | import ColorBarItem from "./ColorBarItem"; 10 | 11 | interface ColorBarGroupComponentProps { 12 | colorBarGroup: ColorBarGroup; 13 | selectedColorBarName: string | null; 14 | onSelectColorBar: (colorBarName: string) => void; 15 | images: Record; 16 | } 17 | 18 | export default function ColorBarGroupComponent({ 19 | colorBarGroup, 20 | selectedColorBarName, 21 | onSelectColorBar, 22 | images, 23 | }: ColorBarGroupComponentProps) { 24 | return ( 25 | <> 26 | 30 | {colorBarGroup.names.map((name) => ( 31 | onSelectColorBar(name)} 37 | /> 38 | ))} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ColorBarLegend/ColorBarGroupHeader.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { Theme } from "@mui/system"; 8 | import Box from "@mui/material/Box"; 9 | import Tooltip from "@mui/material/Tooltip"; 10 | 11 | import { COLOR_BAR_ITEM_GAP } from "./constants"; 12 | import { makeStyles } from "@/util/styles"; 13 | 14 | const styles = makeStyles({ 15 | colorBarGroupTitle: (theme: Theme) => ({ 16 | marginTop: theme.spacing(2 * COLOR_BAR_ITEM_GAP), 17 | fontSize: "small", 18 | color: theme.palette.text.secondary, 19 | }), 20 | }); 21 | 22 | interface ColorBarGroupHeaderProps { 23 | title: string; 24 | description: string; 25 | } 26 | 27 | export default function ColorBarGroupHeader({ 28 | title, 29 | description, 30 | }: ColorBarGroupHeaderProps) { 31 | return ( 32 | 33 | {title} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/ColorBarLegend/ColorBarLabels.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import React, { useMemo } from "react"; 8 | import Box from "@mui/material/Box"; 9 | import Typography from "@mui/material/Typography"; 10 | 11 | import { getLabelsForRange } from "@/util/label"; 12 | import { makeStyles } from "@/util/styles"; 13 | 14 | const styles = makeStyles({ 15 | container: { 16 | width: "100%", 17 | display: "flex", 18 | flexWrap: "nowrap", 19 | justifyContent: "space-between", 20 | cursor: "pointer", 21 | }, 22 | label: { 23 | fontSize: "0.7rem", 24 | fontWeight: "normal", 25 | }, 26 | }); 27 | 28 | interface ColorBarLabelsProps { 29 | minValue: number; 30 | maxValue: number; 31 | numTicks: number; 32 | logScaled?: boolean; 33 | onClick: (event: React.MouseEvent) => void; 34 | } 35 | 36 | export default function ColorBarLabels({ 37 | minValue, 38 | maxValue, 39 | numTicks, 40 | logScaled, 41 | onClick, 42 | }: ColorBarLabelsProps) { 43 | const labels = useMemo( 44 | () => getLabelsForRange(minValue, maxValue, numTicks, logScaled), 45 | [minValue, maxValue, numTicks, logScaled], 46 | ); 47 | return ( 48 | 49 | {labels.map((label, i) => ( 50 | 51 | {label} 52 | 53 | ))} 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/ColorBarLegend/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export const COLOR_BAR_BOX_MARGIN = 1; 8 | export const COLOR_BAR_ITEM_GAP = 0.2; 9 | export const COLOR_BAR_ITEM_WIDTH = 240; 10 | export const COLOR_BAR_ITEM_HEIGHT = 20; 11 | -------------------------------------------------------------------------------- /src/components/ColorBarLegend/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import ColorBarLegend from "./ColorBarLegend"; 8 | 9 | export default ColorBarLegend; 10 | -------------------------------------------------------------------------------- /src/components/ColorBarLegend/scaling.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { expect, it, describe } from "vitest"; 8 | import Scaling from "./scaling"; 9 | 10 | describe("Assert that Scaling", () => { 11 | it("works in the log case", () => { 12 | const scaling = new Scaling(true); 13 | 14 | expect(scaling.scale(0.01)).toEqual(-2); 15 | expect(scaling.scaleInv(2)).toEqual(100); 16 | 17 | expect(scaling.scale([0.001, 0.01, 0.1, 1, 10])).toEqual([ 18 | -3, -2, -1, 0, 1, 19 | ]); 20 | expect(scaling.scaleInv([-3, -2, -1, 0, 1])).toEqual([ 21 | 0.001, 0.01, 0.1, 1, 10, 22 | ]); 23 | }); 24 | 25 | it("works in the identity case", () => { 26 | const scaling = new Scaling(false); 27 | 28 | expect(scaling.scale(0.01)).toEqual(0.01); 29 | expect(scaling.scaleInv(100)).toEqual(100); 30 | 31 | expect(scaling.scale([0.1, 0.2, 0.3])).toEqual([0.1, 0.2, 0.3]); 32 | expect(scaling.scaleInv([0.1, 0.2, 0.3])).toEqual([0.1, 0.2, 0.3]); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/ColorBarLegend/scaling.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | type Value = number; 8 | type ValueRange = [number, number]; 9 | type Values = number[]; 10 | type Fn = (x: Value) => Value; 11 | 12 | const ident = (x: Value) => x; 13 | const pow10 = (x: Value) => Math.pow(10, x); 14 | const log10 = Math.log10; 15 | 16 | const applyFn = (x: Value | ValueRange | Values, fn: Fn) => 17 | typeof x === "number" ? fn(x) : x.map(fn); 18 | 19 | // noinspection JSUnusedGlobalSymbols 20 | /** 21 | * A class representing a scaling operation. 22 | * @class 23 | */ 24 | export default class Scaling { 25 | private readonly _fn: Fn; 26 | private readonly _invFn: Fn; 27 | 28 | constructor(isLog: boolean) { 29 | if (isLog) { 30 | this._fn = log10; 31 | this._invFn = pow10; 32 | } else { 33 | this._fn = ident; 34 | this._invFn = ident; 35 | } 36 | } 37 | 38 | scale(x: Value): Value; 39 | scale(x: ValueRange): ValueRange; 40 | scale(x: Values): Values; 41 | scale(x: Value | ValueRange | Values) { 42 | return applyFn(x, this._fn); 43 | } 44 | 45 | scaleInv(x: Value): Value; 46 | scaleInv(x: ValueRange): ValueRange; 47 | scaleInv(x: Values): Values; 48 | scaleInv(x: Value | ValueRange | Values) { 49 | return applyFn(x, this._invFn); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/ColorBarLegend/style.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import type { Theme } from "@mui/system"; 8 | 9 | export const legendThemeDark = { 10 | borderColor: "#3B3B3B", 11 | }; 12 | 13 | export const legendThemeLight = { 14 | borderColor: "#E5E5E5", 15 | }; 16 | 17 | export function getBorderColor(theme: Theme): string { 18 | const legendTheme = 19 | theme.palette.mode === "dark" ? legendThemeDark : legendThemeLight; 20 | return legendTheme.borderColor; 21 | } 22 | 23 | export function getBorderStyle(theme: Theme): string { 24 | return `1px solid ${getBorderColor(theme)}`; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ControlBar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { PropsWithChildren } from "react"; 8 | import { styled, Theme } from "@mui/system"; 9 | 10 | const ControlBarForm = styled("form")(({ theme }: { theme: Theme }) => ({ 11 | display: "flex", 12 | flexWrap: "wrap", 13 | paddingTop: theme.spacing(1), 14 | paddingLeft: theme.spacing(0.5), 15 | paddingRight: theme.spacing(0), 16 | paddingBottom: theme.spacing(0.25), 17 | flexGrow: 0, 18 | })); 19 | 20 | type ControlBarProps = object; 21 | 22 | export default function ControlBar({ 23 | children, 24 | }: PropsWithChildren) { 25 | return {children}; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ControlBarItem.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import * as React from "react"; 8 | import { Theme, styled, SxProps } from "@mui/system"; 9 | import FormControl from "@mui/material/FormControl"; 10 | import Box from "@mui/material/Box"; 11 | 12 | import { WithLocale } from "@/util/lang"; 13 | 14 | const StyledForm = styled(FormControl)(({ theme }: { theme: Theme }) => ({ 15 | marginRight: theme.spacing(1), 16 | marginLeft: theme.spacing(2), 17 | })); 18 | 19 | interface ControlBarItemProps extends WithLocale { 20 | label: React.ReactNode; 21 | control: React.ReactNode; 22 | actions?: React.ReactNode | null; 23 | sx?: SxProps; 24 | } 25 | 26 | export default function ControlBarItem({ 27 | label, 28 | control, 29 | actions, 30 | sx, 31 | }: ControlBarItemProps) { 32 | return ( 33 | 34 | 35 | {label} 36 | {control} 37 | {actions} 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | .errorBoundary-header { 8 | } 9 | 10 | .errorBoundary-details { 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { expect, it, describe } from "vitest"; 8 | import { render } from "@testing-library/react"; 9 | import ErrorBoundary from "./ErrorBoundary"; 10 | 11 | function MyWidget(props: { name: string | null }) { 12 | if (!props.name) { 13 | throw new Error("Oh no!"); 14 | } 15 | return
{`Hi ${props.name}!`}
; 16 | } 17 | 18 | describe("ErrorBoundary", () => { 19 | it("renders the child if child does not throw", () => { 20 | const { getByText } = render( 21 | 22 | 23 | , 24 | ); 25 | const element = getByText(/Hi Bibo!/i); 26 | expect(element).toBeInTheDocument(); 27 | }); 28 | 29 | it("renders the correct text when a child throws", () => { 30 | const { getByText } = render( 31 | 32 | 33 | , 34 | ); 35 | const element1 = getByText(/Something went wrong/i); 36 | expect(element1).toBeInTheDocument(); 37 | const element2 = getByText(/Oh no/i); 38 | expect(element2).toBeInTheDocument(); 39 | }); 40 | 41 | it("throws when no children given", () => { 42 | expect(() => { 43 | render(); 44 | }).toThrow(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/HelpButton.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { useRef, useState } from "react"; 8 | import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; 9 | import IconButton from "@mui/material/IconButton"; 10 | 11 | import useFetchText from "@/hooks/useFetchText"; 12 | import MarkdownPopover from "@/components/MarkdownPopover"; 13 | 14 | interface HelpButtonProps { 15 | size?: "small" | "medium" | "large"; 16 | helpUrl?: string; 17 | } 18 | 19 | export default function HelpButton({ size, helpUrl }: HelpButtonProps) { 20 | const [helpAnchorEl, setHelpAnchorEl] = useState( 21 | null, 22 | ); 23 | const helpButtonRef = useRef(null); 24 | const helpText = useFetchText(helpUrl); 25 | 26 | const handleHelpOpen = () => { 27 | setHelpAnchorEl(helpButtonRef.current); 28 | }; 29 | 30 | const handleHelpClose = () => { 31 | setHelpAnchorEl(null); 32 | }; 33 | 34 | return ( 35 | <> 36 | 37 | 38 | 39 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/HoverVisibleBox.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import type { ReactNode } from "react"; 8 | import Box, { type BoxProps } from "@mui/material/Box"; 9 | 10 | export interface HoverVisibleBoxProps extends BoxProps { 11 | children: ReactNode; 12 | initialOpacity?: number; 13 | } 14 | 15 | const HoverVisibleBox = ({ 16 | children, 17 | initialOpacity, 18 | sx, 19 | ...props 20 | }: HoverVisibleBoxProps) => { 21 | return ( 22 | *": { 27 | opacity: 1, 28 | visibility: "visible", 29 | }, 30 | "& > *": { 31 | opacity: initialOpacity || 0, 32 | visibility: !initialOpacity ? "hidden" : undefined, 33 | transition: "opacity 0.5s ease, visibility 0.5s ease", 34 | }, 35 | }} 36 | > 37 | {children} 38 | 39 | ); 40 | }; 41 | 42 | export default HoverVisibleBox; 43 | -------------------------------------------------------------------------------- /src/components/ImprintPage.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import i18n from "@/i18n"; 8 | import { type WithLocale } from "@/util/lang"; 9 | import MarkdownPage from "@/components/MarkdownPage"; 10 | import useFetchText from "@/hooks/useFetchText"; 11 | 12 | interface ImprintPageProps extends WithLocale { 13 | open: boolean; 14 | onClose: () => void; 15 | } 16 | 17 | const ImprintPage = ({ open, onClose }: ImprintPageProps) => { 18 | const text = useFetchText(i18n.get("docs/imprint.en.md")); 19 | return ( 20 | 26 | ); 27 | }; 28 | 29 | export default ImprintPage; 30 | -------------------------------------------------------------------------------- /src/components/InfoPanel/common/CodeContent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import React from "react"; 8 | import CodeMirror from "@uiw/react-codemirror"; 9 | import { type Extension } from "@codemirror/state"; 10 | 11 | import { useTheme } from "@mui/material"; 12 | import InfoCardContent from "./InfoCardContent"; 13 | 14 | export interface CodeContentBaseProps { 15 | code: string; 16 | } 17 | 18 | export interface CodeContentProps extends CodeContentBaseProps { 19 | extension: Extension; 20 | } 21 | 22 | const CodeContent: React.FC = ({ code, extension }) => { 23 | const themeMode = useTheme(); 24 | return ( 25 | 26 | 33 | 34 | ); 35 | }; 36 | 37 | export default CodeContent; 38 | -------------------------------------------------------------------------------- /src/components/InfoPanel/common/HtmlContent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { useEffect, useRef } from "react"; 8 | import InfoCardContent from "@/components/InfoPanel/common/InfoCardContent"; 9 | import Box from "@mui/material/Box"; 10 | 11 | import { commonSx } from "./styles"; 12 | 13 | interface HtmlContentProps { 14 | innerHTML: string; 15 | } 16 | 17 | const HtmlContent = ({ innerHTML }: HtmlContentProps) => { 18 | const divRef = useRef(null); 19 | useEffect(() => { 20 | if (divRef.current && innerHTML) { 21 | divRef.current.innerHTML = innerHTML; 22 | } 23 | }, [innerHTML]); 24 | 25 | useEffect(() => { 26 | const svgTextElements = document.querySelectorAll( 27 | ".svg-container svg text", 28 | ); 29 | svgTextElements.forEach((el) => { 30 | const svgTextElement = el as SVGTextElement; 31 | svgTextElement.setAttribute("font-size", "11px"); 32 | // The following didn't work: 33 | // svgTextElement.setAttribute("font-weight", "400"); 34 | // svgTextElement.setAttribute("fill", "grey"); 35 | }); 36 | }, []); 37 | 38 | return ( 39 | innerHTML && ( 40 | 41 | 42 | 43 | ) 44 | ); 45 | }; 46 | 47 | export default HtmlContent; 48 | -------------------------------------------------------------------------------- /src/components/InfoPanel/common/InfoCardContent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import React from "react"; 8 | import MuiCardContent from "@mui/material/CardContent"; 9 | 10 | import { commonSx } from "./styles"; 11 | 12 | interface InfoCardContentProps { 13 | children: React.ReactNode; 14 | } 15 | 16 | const InfoCardContent: React.FC = ({ children }) => { 17 | return {children}; 18 | }; 19 | 20 | export default InfoCardContent; 21 | -------------------------------------------------------------------------------- /src/components/InfoPanel/common/InfoCardHeader.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import React from "react"; 8 | import Box from "@mui/material/Box"; 9 | import CardHeader from "@mui/material/CardHeader"; 10 | import Tooltip from "@mui/material/Tooltip"; 11 | 12 | import { commonSx } from "./styles"; 13 | 14 | interface InfoCardHeaderProps { 15 | title: React.ReactNode; 16 | subheader?: React.ReactNode; 17 | icon: React.ReactElement; 18 | tooltipText: string; 19 | } 20 | 21 | const InfoCardHeader: React.FC = ({ 22 | title, 23 | subheader, 24 | icon, 25 | tooltipText, 26 | }) => { 27 | return ( 28 | 31 | {icon} 32 | {title} 33 | 34 | } 35 | subheader={subheader} 36 | sx={commonSx.cardHeader} 37 | /> 38 | ); 39 | }; 40 | 41 | export default InfoCardHeader; 42 | -------------------------------------------------------------------------------- /src/components/InfoPanel/common/JsonCodeContent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import React from "react"; 8 | import { json } from "@codemirror/lang-json"; 9 | 10 | import type { CodeContentBaseProps } from "./CodeContent"; 11 | import CodeContent from "./CodeContent"; 12 | 13 | const JsonCodeContent: React.FC = ({ code }) => { 14 | return ; 15 | }; 16 | 17 | export default JsonCodeContent; 18 | -------------------------------------------------------------------------------- /src/components/InfoPanel/common/PythonCodeContent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import React from "react"; 8 | import { python } from "@codemirror/lang-python"; 9 | 10 | import type { CodeContentBaseProps } from "./CodeContent"; 11 | import CodeContent from "./CodeContent"; 12 | 13 | const PythonCodeContent: React.FC = ({ code }) => { 14 | return ; 15 | }; 16 | 17 | export default PythonCodeContent; 18 | -------------------------------------------------------------------------------- /src/components/InfoPanel/common/styles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { makeStyles } from "@/util/styles"; 8 | 9 | export const commonSx = makeStyles({ 10 | accordion: { 11 | border: "none", 12 | background: "none", 13 | }, 14 | accordionSummary: { 15 | padding: "0 4px", 16 | }, 17 | accordionDetails: { 18 | padding: "0 4px", 19 | display: "flex", 20 | flexDirection: "column", 21 | gap: 1, 22 | }, 23 | cardHeader: { 24 | padding: 0, 25 | }, 26 | cardTitle: { 27 | display: "flex", 28 | gap: 1, 29 | fontSize: "1rem", 30 | }, 31 | cardContent: { 32 | padding: "4px 0", 33 | }, 34 | table: { borderRadius: 0 }, 35 | media: { 36 | maxHeight: 200, 37 | }, 38 | code: { 39 | fontFamily: "Monospace", 40 | }, 41 | toggleButton: {}, 42 | htmlContent: (theme) => ({ 43 | background: theme.palette.mode === "dark" ? "#383838" : "#e0e0e0", 44 | padding: 1, 45 | fontFamily: "Roboto", 46 | fontSize: "0.75rem", 47 | }), 48 | }); 49 | -------------------------------------------------------------------------------- /src/components/InfoPanel/common/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export type ViewMode = "text" | "list" | "code" | "python"; 8 | -------------------------------------------------------------------------------- /src/components/InfoPanel/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import InfoPanel from "./InfoPanel"; 8 | 9 | export default InfoPanel; 10 | -------------------------------------------------------------------------------- /src/components/LayerControlPanel/LayerMenu.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import type { ReactNode } from "react"; 8 | import Divider from "@mui/material/Divider"; 9 | import MenuList from "@mui/material/MenuList"; 10 | 11 | import type { WithLocale } from "@/util/lang"; 12 | import type { LayerState } from "@/model/layerState"; 13 | import LayerMenuItem from "./LayerMenuItem"; 14 | 15 | interface LayerMenuProps extends WithLocale { 16 | layerStates: LayerState[]; 17 | setLayerVisibility: (layerId: string, visible: boolean) => void; 18 | disableI18n?: boolean; 19 | extraItems?: ReactNode; 20 | } 21 | 22 | export default function LayerMenu({ 23 | layerStates, 24 | setLayerVisibility, 25 | disableI18n, 26 | extraItems, 27 | }: LayerMenuProps) { 28 | return ( 29 | 30 | {layerStates.map((layerState) => ( 31 | 37 | ))} 38 | {layerStates.length && extraItems && } 39 | {extraItems} 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/LayerControlPanel/LayerMenuItem.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import PushPinIcon from "@mui/icons-material/PushPin"; 8 | 9 | import i18n from "@/i18n"; 10 | import SelectableMenuItem from "@/components/SelectableMenuItem"; 11 | import { LayerState } from "@/model/layerState"; 12 | 13 | interface LayerMenuItemProps { 14 | layerState: LayerState; 15 | setLayerVisibility: (layerId: string, visible: boolean) => void; 16 | disableI18n?: boolean; 17 | } 18 | 19 | export default function LayerMenuItem({ 20 | layerState, 21 | setLayerVisibility, 22 | disableI18n, 23 | }: LayerMenuItemProps) { 24 | if (layerState.disabled) { 25 | return null; 26 | } 27 | return ( 28 | <> 29 | 35 | } 36 | onClick={() => setLayerVisibility(layerState.id, !layerState.visible)} 37 | dense 38 | /> 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/LayerControlPanel/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import LayerControlPanel from "./LayerControlPanel"; 8 | 9 | export default LayerControlPanel; 10 | -------------------------------------------------------------------------------- /src/components/LoadingDialog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import CircularProgress from "@mui/material/CircularProgress"; 8 | import Dialog from "@mui/material/Dialog"; 9 | import DialogTitle from "@mui/material/DialogTitle"; 10 | import { Theme, styled } from "@mui/system"; 11 | import Typography from "@mui/material/Typography"; 12 | 13 | import i18n from "@/i18n"; 14 | import { WithLocale } from "@/util/lang"; 15 | 16 | const StyledProgress = styled(CircularProgress)( 17 | ({ theme }: { theme: Theme }) => ({ 18 | margin: theme.spacing(2), 19 | }), 20 | ); 21 | const StyledMessage = styled(Typography)(({ theme }: { theme: Theme }) => ({ 22 | margin: theme.spacing(1), 23 | })); 24 | 25 | const StyledContainerDiv = styled("div")(({ theme }: { theme: Theme }) => ({ 26 | margin: theme.spacing(1), 27 | textAlign: "center", 28 | display: "flex", 29 | alignItems: "center", 30 | flexDirection: "column", 31 | })); 32 | 33 | interface LoadingDialogProps extends WithLocale { 34 | messages: string[]; 35 | } 36 | 37 | export default function LoadingDialog({ messages }: LoadingDialogProps) { 38 | if (messages.length === 0) { 39 | return null; 40 | } 41 | return ( 42 | 43 | {i18n.get("Please wait...")} 44 | 45 | 46 | {messages.map((message, i) => ( 47 | {message} 48 | ))} 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/MapPointInfoBox/MapPointInfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { Variable } from "@/model/variable"; 8 | import { Dataset } from "@/model/dataset"; 9 | 10 | export interface Location { 11 | pixelX: number; 12 | pixelY: number; 13 | lon: number; 14 | lat: number; 15 | } 16 | 17 | export interface Payload { 18 | dataset: Dataset; 19 | variable: Variable; 20 | result: { 21 | value?: number; 22 | fetching?: boolean; 23 | error?: unknown; 24 | }; 25 | } 26 | 27 | export default interface MapPointInfo { 28 | location: Location; 29 | payload: Payload; 30 | payload2?: Payload; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/MapPointInfoBox/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import MapPointInfoBox from "./MapPointInfoBox"; 8 | 9 | export default MapPointInfoBox; 10 | -------------------------------------------------------------------------------- /src/components/MarkdownPopover.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import Popover from "@mui/material/Popover"; 8 | import Paper from "@mui/material/Paper"; 9 | 10 | import Markdown from "@/components/Markdown"; 11 | 12 | interface MarkdownPopoverProps { 13 | anchorEl: HTMLElement | null; 14 | open: boolean; 15 | onClose?: () => void; 16 | markdownText?: string; 17 | } 18 | 19 | export default function MarkdownPopover({ 20 | anchorEl, 21 | markdownText, 22 | open, 23 | onClose, 24 | }: MarkdownPopoverProps) { 25 | if (!markdownText) { 26 | return null; 27 | } 28 | return ( 29 | 30 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/PlaceStyleEditor/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import PlaceStyleEditor from "./PlaceStyleEditor"; 8 | 9 | export default PlaceStyleEditor; 10 | -------------------------------------------------------------------------------- /src/components/RadioSetting.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import React from "react"; 8 | import Radio from "@mui/material/Radio"; 9 | import RadioGroup from "@mui/material/RadioGroup"; 10 | import { FormControlLabel } from "@mui/material"; 11 | 12 | import { ControlState } from "@/states/controlState"; 13 | 14 | interface RadioSettingProps { 15 | propertyName: keyof ControlState; 16 | settings: ControlState; 17 | updateSettings: (settings: ControlState) => void; 18 | options: Array<[string, string | number]>; 19 | disabled?: boolean; 20 | } 21 | 22 | const RadioSetting: React.FC = ({ 23 | propertyName, 24 | settings, 25 | updateSettings, 26 | options, 27 | disabled, 28 | }) => { 29 | const handleChange = ( 30 | _event: React.ChangeEvent, 31 | value: string, 32 | ) => { 33 | updateSettings({ ...settings, [propertyName]: value }); 34 | }; 35 | return ( 36 | 37 | {options.map(([label, value]) => ( 38 | } 41 | value={value} 42 | label={label} 43 | disabled={disabled} 44 | /> 45 | ))} 46 | 47 | ); 48 | }; 49 | 50 | export default RadioSetting; 51 | -------------------------------------------------------------------------------- /src/components/ScrollbarStyles.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import React from "react"; 8 | import { useTheme, GlobalStyles } from "@mui/material"; 9 | 10 | const scrollbarTheme = { 11 | size: "0.5rem", 12 | borderRadius: 0, 13 | }; 14 | 15 | const stylesDark = { 16 | trackColor: "#222", 17 | thumbColor: "#666", 18 | thumbColorHover: "#444", 19 | }; 20 | 21 | const stylesLight = { 22 | trackColor: "#eee", 23 | thumbColor: "#ccc", 24 | thumbColorHover: "#aaa", 25 | }; 26 | 27 | const ScrollbarStyles: React.FC = () => { 28 | const theme = useTheme(); 29 | const scrollbarStyles = 30 | theme.palette.mode === "dark" ? stylesDark : stylesLight; 31 | return ( 32 | 54 | ); 55 | }; 56 | 57 | export default ScrollbarStyles; 58 | -------------------------------------------------------------------------------- /src/components/SelectableMenuItem.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { ReactNode } from "react"; 8 | import Check from "@mui/icons-material/Check"; 9 | import MenuItem from "@mui/material/MenuItem"; 10 | import ListItemText from "@mui/material/ListItemText"; 11 | import ListItemIcon from "@mui/material/ListItemIcon"; 12 | 13 | interface SelectableMenuItemProps { 14 | title: string; 15 | subtitle?: string; 16 | disabled?: boolean; 17 | dense?: boolean; 18 | selected: boolean; 19 | secondaryIcon?: ReactNode; 20 | onClick: () => void; 21 | } 22 | 23 | export default function SelectableMenuItem({ 24 | title, 25 | subtitle, 26 | disabled, 27 | dense, 28 | selected, 29 | secondaryIcon, 30 | onClick, 31 | }: SelectableMenuItemProps) { 32 | return selected ? ( 33 | 34 | 35 | 36 | 37 | 38 | {secondaryIcon} 39 | 40 | ) : ( 41 | 42 | 43 | {secondaryIcon} 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/SettingsSubPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import React from "react"; 8 | import ListItemText from "@mui/material/ListItemText"; 9 | import ListItem from "@mui/material/ListItem"; 10 | import ListItemSecondaryAction from "@mui/material/ListItemSecondaryAction"; 11 | import { ListItemButton } from "@mui/material"; 12 | 13 | interface SettingsSubPanelProps { 14 | label: string; 15 | value?: string | number; 16 | onClick?: (event: React.MouseEvent) => void; 17 | children?: React.ReactNode; 18 | } 19 | 20 | const SettingsSubPanel: React.FC = ({ 21 | label, 22 | value, 23 | onClick, 24 | children, 25 | }) => { 26 | let listItemStyle; 27 | if (!value) { 28 | listItemStyle = { marginBottom: 10 }; 29 | } 30 | 31 | const listItemText = ; 32 | 33 | let listItemSecondaryAction; 34 | if (children) { 35 | listItemSecondaryAction = ( 36 | {children} 37 | ); 38 | } 39 | 40 | if (onClick) { 41 | return ( 42 | 43 | {listItemText} 44 | {listItemSecondaryAction} 45 | 46 | ); 47 | } 48 | 49 | return ( 50 | 51 | {listItemText} 52 | {listItemSecondaryAction} 53 | 54 | ); 55 | }; 56 | 57 | export default SettingsSubPanel; 58 | -------------------------------------------------------------------------------- /src/components/SidePanel/SidePanel.stories.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import type { Meta, StoryObj } from "@storybook/react"; 8 | import { fn } from "@storybook/test"; 9 | 10 | import { ModelData } from "./Sidebar.stories"; 11 | import SidePanel from "./SidePanel"; 12 | 13 | export const ActionsData = { 14 | setSelectedPanelId: fn(), 15 | }; 16 | 17 | const meta = { 18 | title: "SidePanel", 19 | component: SidePanel, 20 | parameters: { 21 | // Optional parameter to center the component in the Canvas. 22 | // More info: https://storybook.js.org/docs/configure/story-layout 23 | layout: "centered", 24 | }, 25 | // This component will have an automatically generated Autodocs entry: 26 | // https://storybook.js.org/docs/writing-docs/autodocs 27 | tags: ["autodocs"], 28 | //👇 Our exports that end in "Data" are not stories. 29 | excludeStories: /.*Data$/, 30 | args: { 31 | ...ActionsData, 32 | ...ModelData, 33 | }, 34 | } satisfies Meta; 35 | 36 | // noinspection JSUnusedGlobalSymbols 37 | export default meta; 38 | 39 | type Story = StoryObj; 40 | 41 | // noinspection JSUnusedGlobalSymbols 42 | export const NoneSelected: Story = { 43 | args: { 44 | selectedPanelId: null, 45 | width: 350, 46 | }, 47 | }; 48 | 49 | // noinspection JSUnusedGlobalSymbols 50 | export const OneSelected: Story = { 51 | args: { 52 | selectedPanelId: "statistics", 53 | width: 350, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/SidePanel/SidePanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { useMemo } from "react"; 8 | import Box from "@mui/material/Box"; 9 | 10 | import type { PanelModel } from "./panelModel"; 11 | import styles from "./styles"; 12 | import Sidebar from "./Sidebar"; 13 | import SidePanelHeader from "./SidePanelHeader"; 14 | import SidePanelContent from "./SidePanelContent"; 15 | 16 | export interface SidePanelProps { 17 | width?: number | string; 18 | height?: number | string; 19 | panels?: PanelModel[]; 20 | selectedPanelId?: string | null; 21 | setSelectedPanelId: (panelId: string | null) => void; 22 | } 23 | 24 | function SidePanel({ 25 | width, 26 | height, 27 | panels, 28 | selectedPanelId, 29 | setSelectedPanelId, 30 | }: SidePanelProps) { 31 | const selectedPanel = useMemo(() => { 32 | return panels && panels.find((p) => p.id === selectedPanelId); 33 | }, [panels, selectedPanelId]); 34 | return ( 35 | 40 | {selectedPanelId && ( 41 | 42 | 43 | 44 | 45 | )} 46 | 51 | 52 | ); 53 | } 54 | 55 | export default SidePanel; 56 | -------------------------------------------------------------------------------- /src/components/SidePanel/SidePanelContent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import Box from "@mui/material/Box"; 8 | 9 | import type { PanelModel } from "./panelModel"; 10 | import styles from "./styles"; 11 | 12 | export interface SidePanelContentProps { 13 | selectedPanel?: PanelModel; 14 | } 15 | 16 | function SidePanelContent({ selectedPanel }: SidePanelContentProps) { 17 | return {selectedPanel?.content}; 18 | } 19 | 20 | export default SidePanelContent; 21 | -------------------------------------------------------------------------------- /src/components/SidePanel/SidePanelHeader.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import Box from "@mui/material/Box"; 8 | import Typography from "@mui/material/Typography"; 9 | 10 | import type { PanelModel } from "./panelModel"; 11 | import styles from "./styles"; 12 | 13 | export interface SidePanelHeaderProps { 14 | selectedPanel?: PanelModel; 15 | } 16 | 17 | function SidePanelHeader({ selectedPanel }: SidePanelHeaderProps) { 18 | return ( 19 | 20 | 25 | {selectedPanel?.title} 26 | 27 | 28 | ); 29 | } 30 | 31 | export default SidePanelHeader; 32 | -------------------------------------------------------------------------------- /src/components/SidePanel/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { useMemo } from "react"; 8 | import Box from "@mui/material/Box"; 9 | 10 | import ToolButton from "@/components/ToolButton"; 11 | import { type PanelModel, getEffectivePanelModels } from "./panelModel"; 12 | import styles from "./styles"; 13 | 14 | export interface SidebarProps { 15 | hidden?: boolean; 16 | panels?: PanelModel[]; 17 | selectedPanelId?: string | null; 18 | setSelectedPanelId: (panelId: string | null) => void; 19 | } 20 | 21 | function Sidebar({ 22 | hidden, 23 | panels, 24 | selectedPanelId, 25 | setSelectedPanelId, 26 | }: SidebarProps) { 27 | const effectivePanels = useMemo(() => { 28 | return getEffectivePanelModels(panels || []); 29 | }, [panels]); 30 | 31 | if (hidden) { 32 | return null; 33 | } 34 | return ( 35 | 36 | {effectivePanels.map((p) => ( 37 | 50 | setSelectedPanelId(p.id !== selectedPanelId ? p.id : null) 51 | } 52 | /> 53 | ))} 54 | 55 | ); 56 | } 57 | 58 | export default Sidebar; 59 | -------------------------------------------------------------------------------- /src/components/SidePanel/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export type { PanelModel } from "./panelModel"; 8 | 9 | import SidePanel from "./SidePanel"; 10 | export default SidePanel; 11 | -------------------------------------------------------------------------------- /src/components/StatisticsPanel/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import StatisticsPanel from "./StatisticsPanel"; 8 | export default StatisticsPanel; 9 | -------------------------------------------------------------------------------- /src/components/TimeSeriesPanel/NoTimeSeriesChart.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import Box from "@mui/material/Box"; 8 | import Button from "@mui/material/Button"; 9 | import Typography from "@mui/material/Typography"; 10 | 11 | import i18n from "@/i18n"; 12 | import { makeStyles } from "@/util/styles"; 13 | import { WithLocale } from "@/util/lang"; 14 | 15 | const styles = makeStyles({ 16 | container: { 17 | display: "flex", 18 | flexDirection: "column", 19 | alignItems: "center", 20 | paddingTop: 6, 21 | gap: 2, 22 | }, 23 | }); 24 | 25 | interface NoTimeSeriesChartProps extends WithLocale { 26 | canAddTimeSeries: boolean; 27 | addTimeSeries: () => void; 28 | } 29 | 30 | export default function NoTimeSeriesChart({ 31 | canAddTimeSeries, 32 | addTimeSeries, 33 | }: NoTimeSeriesChartProps) { 34 | return ( 35 | 36 | 39 | 40 | {i18n.get( 41 | "No time-series have been obtained yet. Select a variable and a place first.", 42 | )} 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/TimeSeriesPanel/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import TimeSeriesPanel from "./TimeSeriesPanel"; 8 | export default TimeSeriesPanel; 9 | -------------------------------------------------------------------------------- /src/components/TimeSeriesPanel/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { isNumber } from "@/util/types"; 8 | import { utcTimeToIsoDateString } from "@/util/time"; 9 | 10 | export const formatTimeTick = (value: number | string) => { 11 | if (!isNumber(value) || !Number.isFinite(value)) { 12 | return ""; 13 | } 14 | return utcTimeToIsoDateString(value); 15 | }; 16 | 17 | export const formatValueTick = (value: number) => { 18 | return value.toPrecision(3); 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/ToggleSetting.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import React from "react"; 8 | import Switch from "@mui/material/Switch"; 9 | 10 | import { ControlState } from "@/states/controlState"; 11 | 12 | interface ToggleSettingProps { 13 | propertyName: keyof ControlState; 14 | settings: ControlState; 15 | updateSettings: (settings: ControlState) => void; 16 | disabled?: boolean; 17 | } 18 | 19 | const ToggleSetting: React.FC = ({ 20 | propertyName, 21 | settings, 22 | updateSettings, 23 | disabled, 24 | }) => { 25 | return ( 26 | 29 | updateSettings({ ...settings, [propertyName]: !settings[propertyName] }) 30 | } 31 | disabled={disabled} 32 | /> 33 | ); 34 | }; 35 | 36 | export default ToggleSetting; 37 | -------------------------------------------------------------------------------- /src/components/UserLayersDialog/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import UserLayersDialog from "./UserLayersDialog"; 8 | export default UserLayersDialog; 9 | -------------------------------------------------------------------------------- /src/components/UserVariablesDialog/ExprPartChip.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import Box from "@mui/material/Box"; 8 | import Chip from "@mui/material/Chip"; 9 | import { makeStyles } from "@/util/styles"; 10 | import { ExprPartType } from "@/components/UserVariablesDialog/utils"; 11 | 12 | const styles = makeStyles({ 13 | expressionPart: { padding: 0.2 }, 14 | expressionPartChip: { fontFamily: "monospace" }, 15 | }); 16 | 17 | interface ExprPartChipProps { 18 | part: string; 19 | partType: ExprPartType; 20 | onPartClicked: (part: string) => void; 21 | } 22 | 23 | export default function ExprPartChip({ 24 | part, 25 | partType, 26 | onPartClicked, 27 | }: ExprPartChipProps) { 28 | return ( 29 | 30 | onPartClicked(part)} 43 | /> 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/UserVariablesDialog/ExprPartFilterMenu.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import Menu from "@mui/material/Menu"; 8 | 9 | import i18n from "@/i18n"; 10 | import { WithLocale } from "@/util/lang"; 11 | import SelectableMenuItem from "@/components/SelectableMenuItem"; 12 | import { exprPartKeys, exprPartLabels, type ExprPartType } from "./utils"; 13 | 14 | interface ExprPartFilterMenuProps extends WithLocale { 15 | anchorEl: HTMLElement | null; 16 | exprPartTypes: Record; 17 | setExprPartTypes: (exprPartTypes: Record) => void; 18 | onClose: () => void; 19 | } 20 | 21 | export default function ExprPartFilterMenu({ 22 | anchorEl, 23 | exprPartTypes, 24 | setExprPartTypes, 25 | onClose, 26 | }: ExprPartFilterMenuProps) { 27 | const handleExprPartTypeSelected = (key: ExprPartType) => { 28 | setExprPartTypes({ ...exprPartTypes, [key]: !exprPartTypes[key] }); 29 | }; 30 | return ( 31 | 32 | {exprPartKeys.map((key) => ( 33 | handleExprPartTypeSelected(key)} 38 | dense 39 | /> 40 | ))} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/UserVariablesDialog/HeaderBar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { ReactNode } from "react"; 8 | import { alpha } from "@mui/system"; 9 | import Toolbar from "@mui/material/Toolbar"; 10 | import Typography from "@mui/material/Typography"; 11 | import CalculateIcon from "@mui/icons-material/Calculate"; 12 | 13 | interface HeaderBarProps { 14 | selected: boolean; 15 | title: ReactNode; 16 | actions: ReactNode; 17 | } 18 | 19 | export default function HeaderBar({ 20 | selected, 21 | title, 22 | actions, 23 | }: HeaderBarProps) { 24 | return ( 25 | 31 | alpha( 32 | theme.palette.primary.main, 33 | theme.palette.action.activatedOpacity, 34 | ), 35 | }), 36 | }} 37 | > 38 | 39 | {title} 40 | {actions} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/UserVariablesDialog/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import UserVariablesDialog from "./UserVariablesDialog"; 8 | export default UserVariablesDialog; 9 | -------------------------------------------------------------------------------- /src/components/Viewer/MapButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { CSSProperties, PropsWithChildren } from "react"; 8 | import { SxProps } from "@mui/material"; 9 | import Box from "@mui/material/Box"; 10 | 11 | const CONTAINER_STYLE: CSSProperties = { 12 | position: "absolute", 13 | display: "flex", 14 | flexDirection: "column", 15 | zIndex: 1000, 16 | }; 17 | 18 | interface MapButtonGroupProps { 19 | style?: CSSProperties; 20 | sx?: SxProps; 21 | } 22 | 23 | export default function MapButtonGroup({ 24 | style, 25 | sx, 26 | children, 27 | }: PropsWithChildren) { 28 | return ( 29 | 34 | {children} 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Viewer/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import Viewer from "./Viewer"; 8 | 9 | export default Viewer; 10 | -------------------------------------------------------------------------------- /src/components/VolumePanel/VolumeCanvas.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | @keyframes hint { 8 | 0%, 9 | 100% { 10 | opacity: 20%; 11 | } 12 | 10% { 13 | opacity: 100%; 14 | } 15 | 90% { 16 | opacity: 100%; 17 | } 18 | } 19 | 20 | .hint_wrap { 21 | animation: hint 4s linear none; 22 | opacity: 20%; 23 | transition: all 0.3s ease-in-out; 24 | 25 | color: orange; 26 | position: absolute; 27 | bottom: 8px; 28 | right: 16px; 29 | z-index: 10; 30 | } 31 | 32 | .hint_wrap:hover { 33 | opacity: 100%; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/VolumePanel/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import VolumePanel from "./VolumePanel"; 8 | 9 | export default VolumePanel; 10 | -------------------------------------------------------------------------------- /src/components/common-styles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { makeStyles } from "@/util/styles"; 8 | 9 | export const commonStyles = makeStyles({ 10 | toggleButton: { padding: 0.5 }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/ol/Map.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | .map { 8 | height: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ol/control/ScaleLine.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { default as OlMap } from "ol/Map"; 8 | import { default as OlScaleLineControl } from "ol/control/ScaleLine"; 9 | import { Options as OlScaleLineControlOptions } from "ol/control/ScaleLine"; 10 | 11 | import { MapComponent, MapComponentProps } from "../MapComponent"; 12 | 13 | interface ScaleLineProps extends MapComponentProps, OlScaleLineControlOptions { 14 | bar?: boolean; 15 | steps?: number; 16 | text?: boolean; 17 | } 18 | 19 | export class ScaleLine extends MapComponent< 20 | OlScaleLineControl, 21 | ScaleLineProps 22 | > { 23 | addMapObject(map: OlMap): OlScaleLineControl { 24 | const control = new OlScaleLineControl(this.getOptions()); 25 | map.addControl(control); 26 | return control; 27 | } 28 | 29 | updateMapObject( 30 | _map: OlMap, 31 | control: OlScaleLineControl, 32 | _prevProps: Readonly, 33 | ): OlScaleLineControl { 34 | control.setProperties(this.getOptions()); 35 | return control; 36 | } 37 | 38 | removeMapObject(map: OlMap, control: OlScaleLineControl): void { 39 | map.removeControl(control); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ol/layer/Layers.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import * as React from "react"; 8 | import { MapElement } from "../Map"; 9 | 10 | interface LayersProps { 11 | children: MapElement[]; 12 | } 13 | 14 | export function Layers(props: LayersProps) { 15 | return {props.children}; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/ol/layer/Vector.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { default as OlMap } from "ol/Map"; 8 | import { default as OlVectorLayer } from "ol/layer/Vector"; 9 | import { default as OlVectorSource } from "ol/source/Vector"; 10 | import { Options as OlVectorLayerOptions } from "ol/layer/BaseVector"; 11 | 12 | import { MapComponent, MapComponentProps } from "../MapComponent"; 13 | import { processLayerProperties } from "./common"; 14 | 15 | interface VectorProps 16 | extends MapComponentProps, 17 | OlVectorLayerOptions {} 18 | 19 | export class Vector extends MapComponent< 20 | OlVectorLayer, 21 | VectorProps 22 | > { 23 | addMapObject(map: OlMap): OlVectorLayer { 24 | const layer = new OlVectorLayer(this.props); 25 | layer.set("id", this.props.id); 26 | map.getLayers().push(layer); 27 | return layer; 28 | } 29 | 30 | updateMapObject( 31 | _map: OlMap, 32 | layer: OlVectorLayer, 33 | prevProps: Readonly, 34 | ): OlVectorLayer { 35 | // TODO: Code duplication in ./Tile.tsx 36 | if (this.props.source !== prevProps.source && this.props.source) { 37 | layer.setSource(this.props.source); 38 | } 39 | processLayerProperties(layer, prevProps, this.props); 40 | return layer; 41 | } 42 | 43 | removeMapObject(map: OlMap, layer: OlVectorLayer): void { 44 | map.getLayers().remove(layer); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/ol/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { default as OlMap } from "ol/Map"; 8 | import { default as OlLayer } from "ol/layer/Layer"; 9 | 10 | export function findMapLayer(map: OlMap, layerId: string): OlLayer | null { 11 | const layerGroup = map.getLayers(); 12 | for (let i = 0; i < layerGroup.getLength(); i++) { 13 | const layer = layerGroup.item(i); 14 | if (layer.get("id") === layerId) { 15 | return layer as OlLayer; 16 | } 17 | } 18 | return null; 19 | } 20 | -------------------------------------------------------------------------------- /src/connected/ControlBarActions.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { type AppState } from "@/states/appState"; 10 | import _ControlBarActions from "@/components/ControlBarActions"; 11 | import { setSidePanelOpen } from "@/actions/controlActions"; 12 | 13 | const mapStateToProps = (state: AppState) => { 14 | return { 15 | locale: state.controlState.locale, 16 | visible: !!( 17 | state.controlState.selectedDatasetId || state.controlState.selectedPlaceId 18 | ), 19 | sidePanelOpen: state.controlState.sidePanelOpen, 20 | }; 21 | }; 22 | 23 | // noinspection JSUnusedGlobalSymbols 24 | const mapDispatchToProps = { 25 | setSidePanelOpen, 26 | }; 27 | 28 | const ControlBarActions = connect( 29 | mapStateToProps, 30 | mapDispatchToProps, 31 | )(_ControlBarActions); 32 | export default ControlBarActions; 33 | -------------------------------------------------------------------------------- /src/connected/DatasetSelect.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import _DatasetSelect from "@/components/DatasetSelect"; 10 | import { AppState } from "@/states/appState"; 11 | import { 12 | selectDataset, 13 | toggleDatasetRgbLayer, 14 | locateSelectedDatasetInMap, 15 | } from "@/actions/controlActions"; 16 | import { selectedDatasetSelector } from "@/selectors/controlSelectors"; 17 | 18 | const mapStateToProps = (state: AppState) => { 19 | return { 20 | locale: state.controlState.locale, 21 | selectedDataset: selectedDatasetSelector(state), 22 | datasets: state.dataState.datasets, 23 | layerVisibilities: state.controlState.layerVisibilities, 24 | }; 25 | }; 26 | 27 | const mapDispatchToProps = { 28 | selectDataset, 29 | toggleDatasetRgbLayer, 30 | locateSelectedDataset: locateSelectedDatasetInMap, 31 | }; 32 | 33 | const DatasetSelect = connect( 34 | mapStateToProps, 35 | mapDispatchToProps, 36 | )(_DatasetSelect); 37 | export default DatasetSelect; 38 | -------------------------------------------------------------------------------- /src/connected/ExportDialog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _ExportDialog from "@/components/ExportDialog"; 11 | import { closeDialog, updateSettings } from "@/actions/controlActions"; 12 | import { exportData } from "@/actions/dataActions"; 13 | 14 | const mapStateToProps = (state: AppState) => { 15 | return { 16 | locale: state.controlState.locale, 17 | open: Boolean(state.controlState.dialogOpen["export"]), 18 | settings: state.controlState, 19 | }; 20 | }; 21 | 22 | const mapDispatchToProps = { 23 | closeDialog, 24 | updateSettings, 25 | downloadTimeSeries: exportData, 26 | }; 27 | 28 | const ExportDialog = connect( 29 | mapStateToProps, 30 | mapDispatchToProps, 31 | )(_ExportDialog); 32 | export default ExportDialog; 33 | -------------------------------------------------------------------------------- /src/connected/InfoPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { 10 | infoCardElementViewModesSelector, 11 | selectedDatasetSelector, 12 | selectedPlaceInfoSelector, 13 | selectedServerSelector, 14 | selectedTimeSelector, 15 | selectedVariableSelector, 16 | visibleInfoCardElementsSelector, 17 | } from "@/selectors/controlSelectors"; 18 | import { AppState } from "@/states/appState"; 19 | import { 20 | setVisibleInfoCardElements, 21 | updateInfoCardElementViewMode, 22 | } from "@/actions/controlActions"; 23 | import _InfoPanel from "@/components/InfoPanel"; 24 | import { Config } from "@/config"; 25 | 26 | const mapStateToProps = (state: AppState) => { 27 | return { 28 | locale: state.controlState.locale, 29 | visibleInfoCardElements: visibleInfoCardElementsSelector(state), 30 | infoCardElementViewModes: infoCardElementViewModesSelector(state), 31 | selectedDataset: selectedDatasetSelector(state), 32 | selectedVariable: selectedVariableSelector(state), 33 | selectedPlaceInfo: selectedPlaceInfoSelector(state), 34 | selectedTime: selectedTimeSelector(state), 35 | serverConfig: selectedServerSelector(state), 36 | allowViewModePython: !!Config.instance.branding.allowViewModePython, 37 | }; 38 | }; 39 | 40 | const mapDispatchToProps = { 41 | setVisibleInfoCardElements, 42 | updateInfoCardElementViewMode, 43 | }; 44 | 45 | const InfoPanel = connect(mapStateToProps, mapDispatchToProps)(_InfoPanel); 46 | export default InfoPanel; 47 | -------------------------------------------------------------------------------- /src/connected/LayerControlPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _LayerControlPanel from "@/components/LayerControlPanel"; 11 | import { 12 | openDialog, 13 | setLayerGroupStates, 14 | setLayerMenuOpen, 15 | setLayerVisibilities, 16 | } from "@/actions/controlActions"; 17 | import { layerStatesSelector } from "@/selectors/controlSelectors"; 18 | 19 | const mapStateToProps = (state: AppState) => { 20 | return { 21 | locale: state.controlState.locale, 22 | layerMenuOpen: state.controlState.layerMenuOpen, 23 | layerStates: layerStatesSelector(state), 24 | layerGroupStates: state.controlState.layerGroupStates, 25 | }; 26 | }; 27 | 28 | // noinspection JSUnusedGlobalSymbols 29 | const mapDispatchToProps = { 30 | openDialog, 31 | setLayerMenuOpen, 32 | setLayerVisibilities, 33 | setLayerGroupStates, 34 | }; 35 | 36 | const LayerControlPanel = connect( 37 | mapStateToProps, 38 | mapDispatchToProps, 39 | )(_LayerControlPanel); 40 | export default LayerControlPanel; 41 | -------------------------------------------------------------------------------- /src/connected/LegalAgreementDialog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _LegalAgreementDialog from "@/components/LegalAgreementDialog"; 11 | import { updateSettings } from "@/actions/controlActions"; 12 | import { syncWithServer } from "@/actions/dataActions"; 13 | 14 | const mapStateToProps = (state: AppState) => { 15 | return { 16 | open: !state.controlState.privacyNoticeAccepted, 17 | settings: state.controlState, 18 | }; 19 | }; 20 | 21 | const mapDispatchToProps = { 22 | updateSettings, 23 | syncWithServer, 24 | }; 25 | 26 | const LegalAgreementDialog = connect( 27 | mapStateToProps, 28 | mapDispatchToProps, 29 | )(_LegalAgreementDialog); 30 | export default LegalAgreementDialog; 31 | -------------------------------------------------------------------------------- /src/connected/LoadingDialog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _LoadingDialog from "@/components/LoadingDialog"; 11 | import { activityMessagesSelector } from "@/selectors/controlSelectors"; 12 | 13 | const mapStateToProps = (state: AppState) => { 14 | return { 15 | locale: state.controlState.locale, 16 | messages: activityMessagesSelector(state), 17 | }; 18 | }; 19 | 20 | const mapDispatchToProps = {}; 21 | 22 | const LoadingDialog = connect( 23 | mapStateToProps, 24 | mapDispatchToProps, 25 | )(_LoadingDialog); 26 | export default LoadingDialog; 27 | -------------------------------------------------------------------------------- /src/connected/MapControlActions.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _MapControlActions from "@/components/MapControlActions"; 11 | import { 12 | setLayerMenuOpen, 13 | setMapPointInfoBoxEnabled, 14 | setVariableCompareMode, 15 | } from "@/actions/controlActions"; 16 | 17 | const mapStateToProps = (state: AppState) => { 18 | return { 19 | layerMenuOpen: state.controlState.layerMenuOpen, 20 | variableCompareMode: state.controlState.variableCompareMode, 21 | mapPointInfoBoxEnabled: state.controlState.mapPointInfoBoxEnabled, 22 | }; 23 | }; 24 | 25 | const mapDispatchToProps = { 26 | setLayerMenuOpen, 27 | setVariableCompareMode, 28 | setMapPointInfoBoxEnabled, 29 | }; 30 | 31 | const MapControlActions = connect( 32 | mapStateToProps, 33 | mapDispatchToProps, 34 | )(_MapControlActions); 35 | export default MapControlActions; 36 | -------------------------------------------------------------------------------- /src/connected/MapInteractionsBar.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import _MapInteractionsBar from "@/components/MapInteractionsBar"; 10 | import { AppState } from "@/states/appState"; 11 | import { setMapInteraction } from "@/actions/controlActions"; 12 | 13 | const mapStateToProps = (state: AppState) => { 14 | return { 15 | mapInteraction: state.controlState.mapInteraction, 16 | }; 17 | }; 18 | 19 | const mapDispatchToProps = { 20 | setMapInteraction, 21 | }; 22 | 23 | const MapInteractionsBar = connect( 24 | mapStateToProps, 25 | mapDispatchToProps, 26 | )(_MapInteractionsBar); 27 | export default MapInteractionsBar; 28 | -------------------------------------------------------------------------------- /src/connected/MapPointInfoBox.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _MapPointInfoBox from "@/components/MapPointInfoBox"; 11 | import { 12 | selectedDataset2Selector, 13 | selectedDatasetSelector, 14 | selectedDatasetTimeLabelSelector, 15 | selectedServerSelector, 16 | selectedVariable2Selector, 17 | selectedVariableSelector, 18 | } from "@/selectors/controlSelectors"; 19 | 20 | const mapStateToProps = (state: AppState) => { 21 | return { 22 | enabled: state.controlState.mapPointInfoBoxEnabled, 23 | serverUrl: selectedServerSelector(state).url, 24 | dataset1: selectedDatasetSelector(state), 25 | variable1: selectedVariableSelector(state), 26 | dataset2: selectedDataset2Selector(state), 27 | variable2: selectedVariable2Selector(state), 28 | time: selectedDatasetTimeLabelSelector(state), 29 | }; 30 | }; 31 | 32 | const mapDispatchToProps = {}; 33 | 34 | const MapPointInfoBox = connect( 35 | mapStateToProps, 36 | mapDispatchToProps, 37 | )(_MapPointInfoBox); 38 | export default MapPointInfoBox; 39 | -------------------------------------------------------------------------------- /src/connected/MapSplitter.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _MapSplitter from "@/components/MapSplitter"; 11 | import { updateVariableSplitPos } from "@/actions/controlActions"; 12 | 13 | const mapStateToProps = (state: AppState) => { 14 | return { 15 | hidden: !state.controlState.variableCompareMode, 16 | position: state.controlState.variableSplitPos, 17 | }; 18 | }; 19 | 20 | const mapDispatchToProps = { 21 | updatePosition: updateVariableSplitPos, 22 | }; 23 | 24 | const MapSplitter = connect(mapStateToProps, mapDispatchToProps)(_MapSplitter); 25 | export default MapSplitter; 26 | -------------------------------------------------------------------------------- /src/connected/MessageLog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _MessageLog from "@/components/MessageLog"; 11 | import { hideMessage } from "@/actions/messageLogActions"; 12 | 13 | const mapStateToProps = (state: AppState) => { 14 | const newEntries = state.messageLogState.newEntries; 15 | return { 16 | locale: state.controlState.locale, 17 | message: newEntries.length > 0 ? newEntries[0] : null, 18 | }; 19 | }; 20 | 21 | const mapDispatchToProps = { 22 | hideMessage, 23 | }; 24 | 25 | const MessageLog = connect(mapStateToProps, mapDispatchToProps)(_MessageLog); 26 | export default MessageLog; 27 | -------------------------------------------------------------------------------- /src/connected/PlaceGroupsSelect.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import _PlaceGroupsSelect from "@/components/PlaceGroupsSelect"; 10 | import { AppState } from "@/states/appState"; 11 | import { 12 | renameUserPlaceGroup, 13 | removeUserPlaceGroup, 14 | } from "@/actions/dataActions"; 15 | import { selectPlaceGroups } from "@/actions/controlActions"; 16 | import { 17 | selectedDatasetAndUserPlaceGroupsSelector, 18 | selectedPlaceGroupsTitleSelector, 19 | } from "@/selectors/controlSelectors"; 20 | 21 | const mapStateToProps = (state: AppState) => { 22 | return { 23 | locale: state.controlState.locale, 24 | 25 | selectedPlaceGroupIds: state.controlState.selectedPlaceGroupIds, 26 | placeGroups: selectedDatasetAndUserPlaceGroupsSelector(state), 27 | selectedPlaceGroupsTitle: selectedPlaceGroupsTitleSelector(state), 28 | }; 29 | }; 30 | 31 | const mapDispatchToProps = { 32 | selectPlaceGroups, 33 | renameUserPlaceGroup, 34 | removeUserPlaceGroup, 35 | }; 36 | 37 | const PlaceGroupsSelect = connect( 38 | mapStateToProps, 39 | mapDispatchToProps, 40 | )(_PlaceGroupsSelect); 41 | export default PlaceGroupsSelect; 42 | -------------------------------------------------------------------------------- /src/connected/PlaceSelect.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import _PlaceSelect from "@/components/PlaceSelect"; 10 | import { AppState } from "@/states/appState"; 11 | import { 12 | renameUserPlace, 13 | removeUserPlace, 14 | restyleUserPlace, 15 | } from "@/actions/dataActions"; 16 | import { 17 | selectPlace, 18 | locateSelectedPlaceInMap, 19 | openDialog, 20 | } from "@/actions/controlActions"; 21 | import { 22 | selectedPlaceGroupPlacesSelector, 23 | selectedPlaceGroupPlaceLabelsSelector, 24 | selectedPlaceInfoSelector, 25 | } from "@/selectors/controlSelectors"; 26 | 27 | const mapStateToProps = (state: AppState) => { 28 | return { 29 | locale: state.controlState.locale, 30 | datasets: state.dataState.datasets, 31 | selectedPlaceGroupIds: state.controlState.selectedPlaceGroupIds, 32 | selectedPlaceId: state.controlState.selectedPlaceId, 33 | selectedPlaceInfo: selectedPlaceInfoSelector(state), 34 | places: selectedPlaceGroupPlacesSelector(state), 35 | placeLabels: selectedPlaceGroupPlaceLabelsSelector(state), 36 | }; 37 | }; 38 | 39 | const mapDispatchToProps = { 40 | selectPlace, 41 | renameUserPlace, 42 | restyleUserPlace, 43 | removeUserPlace, 44 | locateSelectedPlace: locateSelectedPlaceInMap, 45 | openDialog, 46 | }; 47 | 48 | const PlaceSelect = connect(mapStateToProps, mapDispatchToProps)(_PlaceSelect); 49 | export default PlaceSelect; 50 | -------------------------------------------------------------------------------- /src/connected/ServerDialog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _ServerDialog from "@/components/ServerDialog"; 11 | import { closeDialog } from "@/actions/controlActions"; 12 | import { configureServers } from "@/actions/dataActions"; 13 | import { selectedServerSelector } from "@/selectors/controlSelectors"; 14 | import { userServersSelector } from "@/selectors/dataSelectors"; 15 | 16 | const mapStateToProps = (state: AppState) => { 17 | return { 18 | open: !!state.controlState.dialogOpen["server"], 19 | servers: userServersSelector(state), 20 | selectedServer: selectedServerSelector(state), 21 | }; 22 | }; 23 | 24 | const mapDispatchToProps = { 25 | closeDialog, 26 | configureServers, 27 | }; 28 | 29 | const ServerDialog = connect( 30 | mapStateToProps, 31 | mapDispatchToProps, 32 | )(_ServerDialog); 33 | export default ServerDialog; 34 | -------------------------------------------------------------------------------- /src/connected/SettingsDialog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { 10 | changeLocale, 11 | closeDialog, 12 | openDialog, 13 | updateSettings, 14 | } from "@/actions/controlActions"; 15 | import _SettingsDialog from "@/components/SettingsDialog"; 16 | import { 17 | selectedServerSelector, 18 | userBaseMapsSelector, 19 | userOverlaysSelector, 20 | } from "@/selectors/controlSelectors"; 21 | import { AppState } from "@/states/appState"; 22 | import version from "@/version"; 23 | 24 | const mapStateToProps = (state: AppState) => { 25 | return { 26 | locale: state.controlState.locale, 27 | open: state.controlState.dialogOpen["settings"], 28 | settings: state.controlState, 29 | userBaseMapLayers: userBaseMapsSelector(state), 30 | userOverlayLayers: userOverlaysSelector(state), 31 | selectedServer: selectedServerSelector(state), 32 | viewerVersion: version, 33 | serverInfo: state.dataState.serverInfo, 34 | }; 35 | }; 36 | 37 | // noinspection JSUnusedGlobalSymbols 38 | const mapDispatchToProps = { 39 | closeDialog, 40 | updateSettings, 41 | changeLocale, 42 | openDialog, 43 | }; 44 | 45 | const SettingsDialog = connect( 46 | mapStateToProps, 47 | mapDispatchToProps, 48 | )(_SettingsDialog); 49 | export default SettingsDialog; 50 | -------------------------------------------------------------------------------- /src/connected/StatisticsPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import { 11 | canAddStatisticsSelector, 12 | resolvedStatisticsRecordsSelector, 13 | selectedDatasetSelector, 14 | selectedDatasetTimeLabelSelector, 15 | selectedPlaceInfoSelector, 16 | selectedVariableSelector, 17 | } from "@/selectors/controlSelectors"; 18 | import _StatisticsPanel from "@/components/StatisticsPanel"; 19 | import { addStatistics, removeStatistics } from "@/actions/dataActions"; 20 | import { postMessage } from "@/actions/messageLogActions"; 21 | import { statisticsLoadingSelector } from "@/selectors/dataSelectors"; 22 | 23 | const mapStateToProps = (state: AppState) => { 24 | return { 25 | selectedDataset: selectedDatasetSelector(state), 26 | selectedVariable: selectedVariableSelector(state), 27 | selectedTime: selectedDatasetTimeLabelSelector(state), 28 | selectedPlaceInfo: selectedPlaceInfoSelector(state), 29 | statisticsLoading: statisticsLoadingSelector(state), 30 | statisticsRecords: resolvedStatisticsRecordsSelector(state), 31 | canAddStatistics: canAddStatisticsSelector(state), 32 | }; 33 | }; 34 | 35 | const mapDispatchToProps = { 36 | addStatistics, 37 | removeStatistics, 38 | postMessage, 39 | }; 40 | 41 | const StatisticsPanel = connect( 42 | mapStateToProps, 43 | mapDispatchToProps, 44 | )(_StatisticsPanel); 45 | export default StatisticsPanel; 46 | -------------------------------------------------------------------------------- /src/connected/TimePlayer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _TimePlayer from "@/components/TimePlayer"; 11 | import { 12 | selectTime, 13 | incSelectedTime, 14 | updateTimeAnimation, 15 | } from "@/actions/controlActions"; 16 | 17 | const mapStateToProps = (state: AppState) => { 18 | return { 19 | locale: state.controlState.locale, 20 | 21 | selectedTime: state.controlState.selectedTime, 22 | selectedTimeRange: state.controlState.selectedTimeRange, 23 | timeAnimationActive: state.controlState.timeAnimationActive, 24 | timeAnimationInterval: state.controlState.timeAnimationInterval, 25 | }; 26 | }; 27 | 28 | const mapDispatchToProps = { 29 | selectTime, 30 | incSelectedTime, 31 | updateTimeAnimation, 32 | }; 33 | 34 | const TimePlayer = connect(mapStateToProps, mapDispatchToProps)(_TimePlayer); 35 | export default TimePlayer; 36 | -------------------------------------------------------------------------------- /src/connected/TimeSelect.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _TimeSelect from "@/components/TimeSelect"; 11 | import { selectTime } from "@/actions/controlActions"; 12 | import { selectedDatasetTimeDimensionSelector } from "@/selectors/controlSelectors"; 13 | 14 | const mapStateToProps = (state: AppState) => { 15 | return { 16 | locale: state.controlState.locale, 17 | 18 | hasTimeDimension: !!selectedDatasetTimeDimensionSelector(state), 19 | selectedTime: state.controlState.selectedTime, 20 | selectedTimeRange: state.controlState.selectedTimeRange, 21 | }; 22 | }; 23 | 24 | const mapDispatchToProps = { 25 | selectTime, 26 | }; 27 | 28 | const TimeSelect = connect(mapStateToProps, mapDispatchToProps)(_TimeSelect); 29 | export default TimeSelect; 30 | -------------------------------------------------------------------------------- /src/connected/TimeSlider.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _TimeSlider from "@/components/TimeSlider"; 11 | import { selectTime, selectTimeRange } from "@/actions/controlActions"; 12 | import { selectedDatasetTimeDimensionSelector } from "@/selectors/controlSelectors"; 13 | 14 | const mapStateToProps = (state: AppState) => { 15 | return { 16 | locale: state.controlState.locale, 17 | 18 | hasTimeDimension: !!selectedDatasetTimeDimensionSelector(state), 19 | selectedTime: state.controlState.selectedTime, 20 | selectedTimeRange: state.controlState.selectedTimeRange, 21 | }; 22 | }; 23 | 24 | const mapDispatchToProps = { 25 | selectTime, 26 | selectTimeRange, 27 | }; 28 | 29 | const TimeSlider = connect(mapStateToProps, mapDispatchToProps)(_TimeSlider); 30 | export default TimeSlider; 31 | -------------------------------------------------------------------------------- /src/connected/UserControl.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _UserControl from "@/components/UserControl"; 11 | import { updateAccessToken } from "@/actions/userAuthActions"; 12 | 13 | // noinspection JSUnusedLocalSymbols 14 | const mapStateToProps = (_state: AppState) => { 15 | return {}; 16 | }; 17 | 18 | const mapDispatchToProps = { 19 | updateAccessToken, 20 | }; 21 | 22 | const UserControl = connect(mapStateToProps, mapDispatchToProps)(_UserControl); 23 | export default UserControl; 24 | -------------------------------------------------------------------------------- /src/connected/UserLayersDialog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { 10 | closeDialog, 11 | updateSettings, 12 | setLayerVisibilities, 13 | } from "@/actions/controlActions"; 14 | import { AppState } from "@/states/appState"; 15 | import _UserLayersDialog from "@/components/UserLayersDialog"; 16 | import { layerVisibilitiesSelector } from "@/selectors/controlSelectors"; 17 | 18 | interface OwnProps { 19 | dialogId: "userOverlays" | "userBaseMaps"; 20 | } 21 | 22 | const mapStateToProps = (state: AppState, ownProps: OwnProps) => { 23 | return { 24 | open: state.controlState.dialogOpen[ownProps.dialogId], 25 | settings: state.controlState, 26 | dialogId: ownProps.dialogId, 27 | layerVisibilities: layerVisibilitiesSelector(state), 28 | }; 29 | }; 30 | 31 | // noinspection JSUnusedGlobalSymbols 32 | const mapDispatchToProps = { 33 | closeDialog, 34 | updateSettings, 35 | setLayerVisibilities, 36 | }; 37 | 38 | const UserLayersDialog = connect( 39 | mapStateToProps, 40 | mapDispatchToProps, 41 | )(_UserLayersDialog); 42 | export default UserLayersDialog; 43 | -------------------------------------------------------------------------------- /src/connected/UserPlacesDialog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import _UserPlacesDialog from "@/components/UserPlacesDialog"; 11 | import { 12 | closeDialog, 13 | updateSettings, 14 | setMapInteraction, 15 | } from "@/actions/controlActions"; 16 | import { importUserPlacesFromText } from "@/actions/dataActions"; 17 | 18 | const mapStateToProps = (state: AppState) => { 19 | return { 20 | open: state.controlState.dialogOpen["addUserPlacesFromText"], 21 | userPlacesFormatName: state.controlState.userPlacesFormatName, 22 | userPlacesFormatOptions: state.controlState.userPlacesFormatOptions, 23 | nextMapInteraction: state.controlState.lastMapInteraction, 24 | }; 25 | }; 26 | 27 | const mapDispatchToProps = { 28 | closeDialog, 29 | updateSettings, 30 | setMapInteraction, 31 | addUserPlacesFromText: importUserPlacesFromText, 32 | }; 33 | 34 | const UserPlacesDialog = connect( 35 | mapStateToProps, 36 | mapDispatchToProps, 37 | )(_UserPlacesDialog); 38 | export default UserPlacesDialog; 39 | -------------------------------------------------------------------------------- /src/connected/UserVariablesDialog.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { AppState } from "@/states/appState"; 10 | import { updateDatasetUserVariables } from "@/actions/dataActions"; 11 | import { closeDialog, selectVariable } from "@/actions/controlActions"; 12 | import { USER_VARIABLES_DIALOG_ID } from "@/components/UserVariablesDialog/utils"; 13 | import { 14 | selectedDatasetSelector, 15 | selectedServerSelector, 16 | selectedUserVariablesSelector, 17 | selectedVariableNameSelector, 18 | } from "@/selectors/controlSelectors"; 19 | import { expressionCapabilitiesSelector } from "@/selectors/dataSelectors"; 20 | import _UserVariablesDialog from "@/components/UserVariablesDialog"; 21 | 22 | const mapStateToProps = (state: AppState) => { 23 | return { 24 | open: state.controlState.dialogOpen[USER_VARIABLES_DIALOG_ID], 25 | selectedDataset: selectedDatasetSelector(state), 26 | selectedVariableName: selectedVariableNameSelector(state), 27 | userVariables: selectedUserVariablesSelector(state), 28 | expressionCapabilities: expressionCapabilitiesSelector(state), 29 | serverUrl: selectedServerSelector(state).url, 30 | themeMode: state.controlState.themeMode, 31 | }; 32 | }; 33 | 34 | const mapDispatchToProps = { 35 | closeDialog, 36 | selectVariable, 37 | updateDatasetUserVariables, 38 | }; 39 | 40 | const UserVariablesDialog = connect( 41 | mapStateToProps, 42 | mapDispatchToProps, 43 | )(_UserVariablesDialog); 44 | export default UserVariablesDialog; 45 | -------------------------------------------------------------------------------- /src/connected/VariableSelect.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import _VariableSelect from "@/components/VariableSelect"; 10 | import { AppState } from "@/states/appState"; 11 | import { addStatistics, addTimeSeries } from "@/actions/dataActions"; 12 | import { 13 | openDialog, 14 | selectVariable, 15 | selectVariable2, 16 | } from "@/actions/controlActions"; 17 | import { 18 | canAddTimeSeriesSelector, 19 | userVariablesAllowedSelector, 20 | selectedVariablesSelector, 21 | canAddStatisticsSelector, 22 | } from "@/selectors/controlSelectors"; 23 | 24 | const mapStateToProps = (state: AppState) => { 25 | return { 26 | locale: state.controlState.locale, 27 | selectedDatasetId: state.controlState.selectedDatasetId, 28 | selectedVariableName: state.controlState.selectedVariableName, 29 | selectedDataset2Id: state.controlState.selectedDataset2Id, 30 | selectedVariable2Name: state.controlState.selectedVariable2Name, 31 | userVariablesAllowed: userVariablesAllowedSelector(state), 32 | canAddTimeSeries: canAddTimeSeriesSelector(state), 33 | canAddStatistics: canAddStatisticsSelector(state), 34 | variables: selectedVariablesSelector(state), 35 | }; 36 | }; 37 | 38 | const mapDispatchToProps = { 39 | openDialog, 40 | selectVariable, 41 | selectVariable2, 42 | addTimeSeries, 43 | addStatistics, 44 | }; 45 | 46 | const VariableSelect = connect( 47 | mapStateToProps, 48 | mapDispatchToProps, 49 | )(_VariableSelect); 50 | export default VariableSelect; 51 | -------------------------------------------------------------------------------- /src/connected/VolumePanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { connect } from "react-redux"; 8 | 9 | import { 10 | selectedDatasetSelector, 11 | selectedPlaceInfoSelector, 12 | selectedServerSelector, 13 | selectedVariableColorBarSelector, 14 | selectedVariableSelector, 15 | selectedVolumeIdSelector, 16 | } from "@/selectors/controlSelectors"; 17 | import { AppState } from "@/states/appState"; 18 | import { 19 | setVolumeRenderMode, 20 | updateVolumeState, 21 | } from "@/actions/controlActions"; 22 | import { updateVariableVolume } from "@/actions/dataActions"; 23 | import _VolumePanel from "@/components/VolumePanel/VolumePanel"; 24 | 25 | const mapStateToProps = (state: AppState) => { 26 | return { 27 | locale: state.controlState.locale, 28 | selectedDataset: selectedDatasetSelector(state), 29 | selectedVariable: selectedVariableSelector(state), 30 | selectedPlaceInfo: selectedPlaceInfoSelector(state), 31 | variableColorBar: selectedVariableColorBarSelector(state), 32 | volumeRenderMode: state.controlState.volumeRenderMode, 33 | volumeId: selectedVolumeIdSelector(state), 34 | volumeStates: state.controlState.volumeStates, 35 | serverUrl: selectedServerSelector(state).url, 36 | }; 37 | }; 38 | 39 | const mapDispatchToProps = { 40 | setVolumeRenderMode, 41 | updateVolumeState, 42 | updateVariableVolume, 43 | }; 44 | 45 | const VolumePanel = connect(mapStateToProps, mapDispatchToProps)(_VolumePanel); 46 | export default VolumePanel; 47 | -------------------------------------------------------------------------------- /src/ext/actions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { Dispatch, Store } from "redux"; 8 | import { initializeContributions } from "chartlets"; 9 | import mui from "chartlets/plugins/mui"; 10 | import vega from "chartlets/plugins/vega"; 11 | 12 | import { AppState } from "@/states/appState"; 13 | import { selectedServerSelector } from "@/selectors/controlSelectors"; 14 | import { newDerivedStore } from "./store"; 15 | import { loggingEnabled } from "./config"; 16 | import xc_viewer from "./plugin"; 17 | 18 | export function initializeExtensions(store: Store) { 19 | return (_dispatch: Dispatch, getState: () => AppState) => { 20 | const apiServer = selectedServerSelector(getState()); 21 | initializeContributions({ 22 | plugins: [mui(), vega(), xc_viewer()], 23 | hostStore: newDerivedStore(store), 24 | logging: { enabled: loggingEnabled }, 25 | api: { 26 | serverUrl: apiServer.url, 27 | endpointName: "viewer/ext", 28 | }, 29 | }); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/ext/components/ContributedPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { FC } from "react"; 8 | import CircularProgress from "@mui/material/CircularProgress"; 9 | import Typography from "@mui/material/Typography"; 10 | 11 | import { 12 | type ContributionState, 13 | Component, 14 | handleComponentChange, 15 | } from "chartlets"; 16 | import type { PanelModel } from "@/components/SidePanel"; 17 | 18 | interface ContributedPanelProps { 19 | contribution: ContributionState; 20 | panelIndex: number; 21 | } 22 | 23 | const ContributedPanel: FC = ({ 24 | contribution, 25 | panelIndex, 26 | }) => { 27 | const componentStateResult = contribution.componentResult; 28 | if (componentStateResult.status === "pending") { 29 | return ; 30 | } else if (componentStateResult.error) { 31 | return ( 32 |
33 | 34 | {componentStateResult.error.message} 35 | 36 |
37 | ); 38 | } else if (contribution.component) { 39 | return ( 40 | { 44 | handleComponentChange("panels", panelIndex, event); 45 | }} 46 | /> 47 | ); 48 | } 49 | return null; 50 | }; 51 | 52 | export default ContributedPanel; 53 | -------------------------------------------------------------------------------- /src/ext/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | // export const loggingEnabled = import.meta.env.DEV; 8 | export const loggingEnabled = false; 9 | -------------------------------------------------------------------------------- /src/ext/plugin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { ComponentType } from "react"; 8 | import type { Plugin, ComponentProps } from "chartlets"; 9 | 10 | import Markdown from "@/components/Markdown"; 11 | 12 | export default function xc_viewer(): Plugin { 13 | return { 14 | // TODO: the following type cast is not acceptable, but there is 15 | // no reason why component props must implement ComponentProps 16 | // from chartlets. This need to be fixed in chartlets! 17 | components: [["Markdown", Markdown as ComponentType]], 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/useFetchText.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { useEffect, useState } from "react"; 8 | 9 | export default function useFetchText(markdownUrl: string | null | undefined) { 10 | const [markdownText, setMarkdownText] = useState(); 11 | 12 | useEffect(() => { 13 | if (!markdownUrl) { 14 | setMarkdownText(undefined); 15 | } else { 16 | fetch(markdownUrl) 17 | .then((response) => response.text()) 18 | .then((text) => setMarkdownText(text)) 19 | .catch((error) => { 20 | console.error(error); 21 | }); 22 | } 23 | }, [markdownUrl]); 24 | 25 | return markdownText; 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/usePromise.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { useEffect, useState } from "react"; 8 | 9 | export interface PromiseState { 10 | pending?: boolean; 11 | error?: unknown; 12 | value?: T; 13 | } 14 | 15 | export default function usePromise(promise: Promise): PromiseState { 16 | const [state, setState] = useState>({}); 17 | useEffect(() => { 18 | setState({ pending: true }); 19 | promise 20 | .then((value) => { 21 | setState({ value }); 22 | }) 23 | .catch((error) => { 24 | setState({ error }); 25 | }); 26 | }, [promise]); 27 | return state; 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/useResizeObserver.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { type MutableRefObject, useEffect, useRef } from "react"; 8 | 9 | export type Size = { width: number; height: number }; 10 | 11 | export default function useResizeObserver( 12 | callback: (size: Size) => void, 13 | ref?: MutableRefObject, 14 | ): void { 15 | const sizeRef = useRef(); 16 | 17 | useEffect(() => { 18 | if (ref && !ref.current) { 19 | return; 20 | } 21 | 22 | const observer = new ResizeObserver((entries) => { 23 | for (const entry of entries) { 24 | if (entry.contentRect) { 25 | const width = entry.contentRect.width; 26 | const height = entry.contentRect.height; 27 | const prevSize = sizeRef.current; 28 | if ( 29 | !prevSize || 30 | prevSize.width !== width || 31 | prevSize.height !== height 32 | ) { 33 | const size = { 34 | width: width, 35 | height: height, 36 | }; 37 | sizeRef.current = size; 38 | if (prevSize) { 39 | callback(size); 40 | } 41 | } 42 | } 43 | } 44 | }); 45 | 46 | observer.observe(ref ? ref.current! : document.documentElement); 47 | 48 | return () => observer.disconnect(); 49 | }, [callback, ref]); 50 | } 51 | -------------------------------------------------------------------------------- /src/hooks/useUndo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { useEffect, useRef } from "react"; 8 | 9 | type Undo = () => void; 10 | type UndoSetter = (undo: Undo | undefined) => void; 11 | 12 | export default function useUndo(): [Undo, UndoSetter] { 13 | const undoRef = useRef(); 14 | const undo = useRef(() => { 15 | if (undoRef.current) { 16 | undoRef.current(); 17 | undoRef.current = undefined; 18 | } 19 | }); 20 | const setUndo = useRef((undo: Undo | undefined) => { 21 | undoRef.current = undo; 22 | }); 23 | useEffect(() => undo.current, []); 24 | return [undo.current, setUndo.current]; 25 | } 26 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { getCurrentLocale, LanguageDictionary } from "@/util/lang"; 8 | import lang from "@/resources/lang.json"; 9 | 10 | const i18n = new LanguageDictionary(lang); 11 | i18n.locale = getCurrentLocale(); 12 | 13 | export default i18n; 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | width: 100vw; 11 | height: 100vh; 12 | overflow: hidden; 13 | font-family: "Roboto", "Segoe UI", "sans-serif"; 14 | } 15 | -------------------------------------------------------------------------------- /src/model/apiServer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export interface ApiServerConfig { 8 | id: string; 9 | name: string; 10 | url: string; 11 | } 12 | 13 | export interface ApiServerInfo { 14 | name: string; 15 | description: string; 16 | version: string; 17 | configTime: string; 18 | serverTime: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/model/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/src/model/bg.png -------------------------------------------------------------------------------- /src/model/encode.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { Dataset } from "@/model/dataset"; 8 | import { Variable } from "@/model/variable"; 9 | import { isUserVariable } from "@/model/userVariable"; 10 | import { isString } from "@/util/types"; 11 | 12 | export function encodeDatasetId(dataset: Dataset | string): string { 13 | return encodeURIComponent(isString(dataset) ? dataset : dataset.id); 14 | } 15 | 16 | export function encodeVariableName(variable: Variable | string): string { 17 | return encodeURIComponent( 18 | isString(variable) 19 | ? variable 20 | : isUserVariable(variable) 21 | ? `${variable.name}=${variable.expression}` 22 | : variable.name, 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/model/layerState.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export interface LayerState { 8 | id: string; 9 | type?: string; 10 | title: string; 11 | subTitle?: string; 12 | disabled?: boolean; 13 | visible?: boolean; 14 | pinned?: boolean; 15 | exclusive?: boolean; 16 | } 17 | -------------------------------------------------------------------------------- /src/model/proj.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export const GEOGRAPHIC_CRS = "EPSG:4326"; 8 | export const WEB_MERCATOR_CRS = "EPSG:3857"; 9 | 10 | export const DEFAULT_MAP_CRS = WEB_MERCATOR_CRS; 11 | -------------------------------------------------------------------------------- /src/model/statistics.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { Dataset } from "./dataset"; 8 | import { Variable } from "./variable"; 9 | import { PlaceInfo } from "./place"; 10 | 11 | export interface StatisticsSource { 12 | dataset: Dataset; 13 | variable: Variable; 14 | time: string | null; 15 | placeInfo: PlaceInfo; 16 | } 17 | 18 | export interface Histogram { 19 | values: number[]; 20 | edges: number[]; 21 | } 22 | 23 | export interface NullStatistics { 24 | count: 0; 25 | } 26 | 27 | export interface AreaStatistics { 28 | count: number; 29 | minimum: number; 30 | maximum: number; 31 | mean: number; 32 | deviation: number; 33 | histogram: Histogram; 34 | } 35 | 36 | export interface PointStatistics extends Omit { 37 | count: 1; 38 | } 39 | 40 | export type Statistics = NullStatistics | PointStatistics | AreaStatistics; 41 | 42 | export interface StatisticsRecord { 43 | source: StatisticsSource; 44 | statistics: Statistics; 45 | } 46 | 47 | export function isNullStatistics(s: Statistics): s is NullStatistics { 48 | return s.count === 0; 49 | } 50 | 51 | export function isPointStatistics(s: Statistics): s is PointStatistics { 52 | return s.count === 1; 53 | } 54 | 55 | export function isAreaStatistics(s: Statistics): s is AreaStatistics { 56 | return s.count > 1; 57 | } 58 | -------------------------------------------------------------------------------- /src/model/user-place/common.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export interface Format { 8 | name: string; 9 | fileExt: string; 10 | checkError: (text: string) => string | null; 11 | } 12 | 13 | const WKT_GEOM_NAMES = [ 14 | "Point", 15 | "LineString", 16 | "Polygon", 17 | "MultiPoint", 18 | "MultiLineString", 19 | "MultiPolygon", 20 | "GeometryCollection", 21 | ].map((k) => k.toLowerCase()); 22 | 23 | export function detectFormatName(text: string): "csv" | "geojson" | "wkt" { 24 | text = text.trim(); 25 | if (text === "") { 26 | return "csv"; 27 | } 28 | 29 | if (text[0] === "{") { 30 | return "geojson"; 31 | } 32 | 33 | const marker = text.substring(0, 20).toLowerCase(); 34 | const geomName = WKT_GEOM_NAMES.find( 35 | (geomName) => 36 | marker.startsWith(geomName) && 37 | (marker.length === geomName.length || 38 | "\n\t (".indexOf(marker[geomName.length]) >= 0), 39 | ); 40 | if (geomName) { 41 | return "wkt"; 42 | } 43 | 44 | return "csv"; 45 | } 46 | 47 | /** 48 | * Helper function that splits a comma-separated string into lower-case 49 | * name tokens. 50 | * 51 | * @param alternatives 52 | */ 53 | export function parseAlternativeNames(alternatives: string): string[] { 54 | return alternatives 55 | .split(",") 56 | .map((s) => s.trim().toLowerCase()) 57 | .filter((name) => name !== ""); 58 | } 59 | -------------------------------------------------------------------------------- /src/model/userVariable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { Variable } from "./variable"; 8 | import { isString } from "@/util/types"; 9 | 10 | export interface ExpressionCapabilities { 11 | namespace: { 12 | constants: string[]; 13 | arrayFunctions: string[]; 14 | otherFunctions: string[]; 15 | arrayOperators: string[]; 16 | otherOperators: string[]; 17 | }; 18 | } 19 | 20 | export interface UserVariable extends Variable { 21 | expression: string; 22 | } 23 | 24 | export function isUserVariable(variable: Variable): variable is UserVariable { 25 | return isString(variable.expression); 26 | } 27 | -------------------------------------------------------------------------------- /src/model/variable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { type VolumeRenderMode } from "@/states/controlState"; 8 | import { type JsonPrimitive } from "@/util/json"; 9 | 10 | export type ColorBarNorm = "lin" | "log"; 11 | 12 | export interface Variable { 13 | id: string; 14 | name: string; 15 | dims: string[]; 16 | shape: number[]; 17 | dtype: string; 18 | units: string; 19 | title: string; 20 | description?: string; 21 | expression?: string; // user-defined variables only 22 | timeChunkSize: number | null; 23 | // The following are new since xcube 0.11 24 | tileLevelMin?: number; 25 | tileLevelMax?: number; 26 | // colorBarName may be prefixed by "_alpha" and/or "_r" (reversed) 27 | colorBarName: string; 28 | colorBarMin: number; 29 | colorBarMax: number; 30 | colorBarNorm?: ColorBarNorm; 31 | opacity?: number; 32 | volumeRenderMode?: VolumeRenderMode; 33 | volumeIsoThreshold?: number; 34 | htmlRepr?: string; 35 | attrs: Record; 36 | } 37 | -------------------------------------------------------------------------------- /src/reducers/appReducer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { AppState } from "@/states/appState"; 8 | import { ChangeLocale, ControlAction } from "@/actions/controlActions"; 9 | import { DataAction } from "@/actions/dataActions"; 10 | import { MessageLogAction } from "@/actions/messageLogActions"; 11 | import { UserAuthAction } from "@/actions/userAuthActions"; 12 | import { controlReducer } from "./controlReducer"; 13 | import { dataReducer } from "./dataReducer"; 14 | import { messageLogReducer } from "./messageLogReducer"; 15 | import { userAuthReducer } from "./userAuthReducer"; 16 | 17 | export function appReducer( 18 | state: AppState | undefined, 19 | action: DataAction & 20 | ControlAction & 21 | MessageLogAction & 22 | UserAuthAction & 23 | ChangeLocale, 24 | ): AppState { 25 | // Not using redux.combineReducers(), because we need to pass app state into controlReducer() 26 | return { 27 | dataState: dataReducer(state && state.dataState, action), 28 | controlState: controlReducer(state && state.controlState, action, state), 29 | messageLogState: messageLogReducer(state && state.messageLogState, action), 30 | userAuthState: userAuthReducer(state && state.userAuthState, action), 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/reducers/userAuthReducer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { UPDATE_ACCESS_TOKEN, UserAuthAction } from "@/actions/userAuthActions"; 8 | import { newUserAuthState, UserAuthState } from "@/states/userAuthState"; 9 | 10 | export function userAuthReducer( 11 | state: UserAuthState | undefined, 12 | action: UserAuthAction, 13 | ): UserAuthState { 14 | if (state === undefined) { 15 | state = newUserAuthState(); 16 | } 17 | switch (action.type) { 18 | case UPDATE_ACCESS_TOKEN: 19 | return { 20 | ...state, 21 | accessToken: action.accessToken, 22 | }; 23 | } 24 | return state!; 25 | } 26 | -------------------------------------------------------------------------------- /src/resources/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "default", 3 | "server": { 4 | "id": "local", 5 | "name": "Local Server", 6 | "url": "http://localhost:8080" 7 | }, 8 | "branding": { 9 | "appBarTitle": "xcube Viewer", 10 | "windowTitle": "xcube Viewer", 11 | "headerBackgroundColor": "#606060", 12 | "themeMode": "system", 13 | "compact": false, 14 | "organisationUrl": "https://xcube.readthedocs.io/", 15 | "logoImage": "images/logo.png", 16 | "logoWidth": 32, 17 | "headerTitleStyle": { 18 | "fontFamily": "Arial", 19 | "fontStyle": "italic", 20 | "fontSize": "1.2rem" 21 | }, 22 | "baseMapUrl": "https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", 23 | "defaultAgg": "mean", 24 | "polygonFillOpacity": 0.2, 25 | "mapProjection": "EPSG:3857", 26 | "allowDownloads": true, 27 | "allowRefresh": true, 28 | "allowSharing": true, 29 | "allowUserVariables": true, 30 | "allowViewModePython": true, 31 | "allow3D": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/resources/python-bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/src/resources/python-bw.png -------------------------------------------------------------------------------- /src/resources/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xcube-dev/xcube-viewer/087b8c6d26b332b49dc148da4c1a67a1cc9b279c/src/resources/python.png -------------------------------------------------------------------------------- /src/resources/spectral-indexes.txt: -------------------------------------------------------------------------------- 1 | # Frequently used Sentinel-2 indexes 2 | moisture_index = where((SCL >= 2) & (SCL <= 7), (B08 - B11) / (B08 + B11), nan) 3 | vegetation_index = where(SCL == 6, (B08 - B04) / (B08 + B04), nan) 4 | chlorophyll_index = where(SCL == 6, (B05 - B04) - 0.52 * (B06 - B04), nan) 5 | -------------------------------------------------------------------------------- /src/selectors/controlSelectors.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { describe, expect, it } from "vitest"; 8 | import { Dataset } from "@/model/dataset"; 9 | import { Variable } from "@/model/variable"; 10 | import { getTileUrl } from "./controlSelectors"; 11 | 12 | describe("Assert that controlSelectors.getTileUrl()", () => { 13 | it("works for RGB", () => { 14 | const dataset = { id: "demo" } as Dataset; 15 | expect(getTileUrl("https://xcube.com/api", dataset, "rgb")).toEqual( 16 | "https://xcube.com/api/tiles/demo/rgb/{z}/{y}/{x}", 17 | ); 18 | }); 19 | 20 | it("works for normal variables", () => { 21 | const dataset = { id: "demo" } as Dataset; 22 | const variable = { name: "conc_chl" } as Variable; 23 | expect(getTileUrl("https://xcube.com/api", dataset, variable)).toEqual( 24 | "https://xcube.com/api/tiles/demo/conc_chl/{z}/{y}/{x}", 25 | ); 26 | }); 27 | 28 | it("works for user variables", () => { 29 | const dataset = { id: "demo" } as Dataset; 30 | const variable = { 31 | name: "ndvi", 32 | expression: "(B08 - B04) / (B08 + B04)", 33 | } as Variable; 34 | expect(getTileUrl("https://xcube.com/api", dataset, variable)).toEqual( 35 | "https://xcube.com/api/tiles/demo/ndvi%3D(B08%20-%20B04)%20%2F%20(B08%20%2B%20B04)/{z}/{y}/{x}", 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 8 | // allows you to do things like: 9 | // expect(element).toHaveTextContent(/react/i) 10 | // learn more: https://github.com/testing-library/jest-dom 11 | 12 | import "@testing-library/jest-dom/vitest"; 13 | -------------------------------------------------------------------------------- /src/states/appState.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { MessageLogState } from "./messageLogState"; 8 | import { DataState } from "./dataState"; 9 | import { ControlState } from "./controlState"; 10 | import { UserAuthState } from "./userAuthState"; 11 | 12 | export interface AppState { 13 | dataState: DataState; 14 | controlState: ControlState; 15 | messageLogState: MessageLogState; 16 | userAuthState: UserAuthState; 17 | } 18 | -------------------------------------------------------------------------------- /src/states/messageLogState.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export type MessageType = "error" | "warning" | "info" | "success"; 8 | 9 | export interface MessageLogEntry { 10 | id: number; 11 | type: MessageType; 12 | text: string; 13 | } 14 | 15 | export interface MessageLogState { 16 | newEntries: MessageLogEntry[]; 17 | oldEntries: MessageLogEntry[]; 18 | } 19 | 20 | export function newMessageLogState() { 21 | return { 22 | newEntries: [], 23 | oldEntries: [], 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/states/userAuthState.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export interface UserAuthState { 8 | accessToken: string | null; 9 | } 10 | 11 | export function newUserAuthState(): UserAuthState { 12 | return { 13 | accessToken: null, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { createTheme, type Theme } from "@mui/material"; 8 | 9 | const baseTheme = { 10 | typography: { 11 | fontSize: 12, 12 | }, 13 | }; 14 | 15 | export const lightTheme: Theme = createTheme({ 16 | ...baseTheme, 17 | palette: { 18 | mode: "light", 19 | primary: { main: "#1976d2" }, 20 | secondary: { main: "#00bc4e" }, 21 | background: { default: "#ffffff" }, 22 | }, 23 | }); 24 | 25 | export const darkTheme: Theme = createTheme({ 26 | ...baseTheme, 27 | palette: { 28 | mode: "dark", 29 | primary: { main: "#39a6f2" }, 30 | secondary: { main: "#20dc6e" }, 31 | background: { default: "#2b2d30" }, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/util/assert.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export class DeveloperError extends Error {} 8 | 9 | export function assertTrue(condition: unknown, message: string) { 10 | if (!condition) { 11 | throw new DeveloperError(`assertion failed: ${message}`); 12 | } 13 | } 14 | 15 | export function assertNotNull(object: unknown, objectName: string) { 16 | if (object === null) { 17 | throw new DeveloperError( 18 | `assertion failed: ${objectName} must not be null`, 19 | ); 20 | } 21 | } 22 | 23 | export function assertDefined(object: unknown, objectName: string) { 24 | if (typeof object === "undefined") { 25 | throw new DeveloperError( 26 | `assertion failed: ${objectName} must not be undefined`, 27 | ); 28 | } 29 | } 30 | 31 | export function assertDefinedAndNotNull(object: unknown, objectName: string) { 32 | assertNotNull(object, objectName); 33 | assertDefined(object, objectName); 34 | } 35 | 36 | export function assertArray(array: unknown, arrayName: string) { 37 | if (!Array.isArray(array)) { 38 | throw new DeveloperError(`assertion failed: ${arrayName} must be an array`); 39 | } 40 | } 41 | 42 | export function assertArrayNotEmpty(array: unknown, arrayName: string) { 43 | if (Array.isArray(array)) { 44 | if (array.length === 0) { 45 | throw new DeveloperError( 46 | `assertion failed: ${arrayName} must be a non-empty array`, 47 | ); 48 | } 49 | } else { 50 | throw new DeveloperError(`assertion failed: ${arrayName} must be an array`); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/util/auth.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { UserManagerSettings } from "oidc-client-ts"; 8 | 9 | export type AuthClientConfig = UserManagerSettings; 10 | -------------------------------------------------------------------------------- /src/util/baseurl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | function _getBaseUrl(): URL { 8 | const url = new URL(window.location.href); 9 | const pathComponents = url.pathname.split("/"); 10 | const numPathComponents = pathComponents.length; 11 | if (numPathComponents > 0) { 12 | const lastComponent = pathComponents[numPathComponents - 1]; 13 | if (lastComponent === "index.html") { 14 | return new URL( 15 | pathComponents.slice(0, numPathComponents - 1).join("/"), 16 | window.location.origin, 17 | ); 18 | } else { 19 | return new URL(url.pathname, window.location.origin); 20 | } 21 | } 22 | return new URL(window.location.origin); 23 | } 24 | 25 | const baseUrl = _getBaseUrl(); 26 | 27 | export default baseUrl; 28 | -------------------------------------------------------------------------------- /src/util/find.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | /** 8 | * Find index into `array` so that `array[index]` is closest to `value`. 9 | * Uses a bi-section algorithm, so it should perform according to O(log2(N)). 10 | * 11 | * @param {number[]} array of monotonically increasing values 12 | * @param {number} value some value to find the index for 13 | * @returns {number} the integer index or -1 if the array is empty 14 | */ 15 | export function findIndexCloseTo(array: number[], value: number): number { 16 | const n = array.length; 17 | if (n === 0) { 18 | return -1; 19 | } 20 | if (n === 1) { 21 | return 0; 22 | } 23 | let i1 = 0, 24 | i3 = n - 1; 25 | if (value <= array[i1]) { 26 | return i1; 27 | } 28 | if (value >= array[i3]) { 29 | return i3; 30 | } 31 | let i2 = Math.floor(n / 2), 32 | otherValue; 33 | for (let i = 0; i < n; i++) { 34 | otherValue = array[i2]; 35 | if (value < otherValue) { 36 | [i2, i3] = [Math.floor((i1 + i2) / 2), i2]; 37 | } else if (value > otherValue) { 38 | [i1, i2] = [i2, Math.floor((i2 + i3) / 2)]; 39 | } else { 40 | return i2; 41 | } 42 | if (i1 === i2 || i2 === i3) { 43 | return Math.abs(array[i1] - value) <= Math.abs(array[i3] - value) 44 | ? i1 45 | : i3; 46 | } 47 | } 48 | return -1; 49 | } 50 | -------------------------------------------------------------------------------- /src/util/history.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { Action, createBrowserHistory, Location } from "history"; 8 | 9 | const history = createBrowserHistory(); 10 | 11 | if (import.meta.env.DEV) { 12 | history.listen((location: Location, action: Action) => { 13 | if (import.meta.env.DEV) { 14 | console.debug(`history ${action}:`, location); 15 | } 16 | }); 17 | } 18 | 19 | export default history; 20 | -------------------------------------------------------------------------------- /src/util/id.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export function newId(prefix?: string): string { 8 | return (prefix || "") + Math.random().toString(16).substring(2); 9 | } 10 | -------------------------------------------------------------------------------- /src/util/identifier.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { expect, it, describe } from "vitest"; 8 | import { isIdentifier, getIdentifiers } from "./identifier"; 9 | 10 | describe("Assert that identifier.isIdentifier()", () => { 11 | it("returns true for identifiers", () => { 12 | expect(isIdentifier("test9")).toEqual(true); 13 | expect(isIdentifier("value")).toEqual(true); 14 | expect(isIdentifier("value_min")).toEqual(true); 15 | }); 16 | 17 | it("returns false for non-identifiers", () => { 18 | expect(isIdentifier("")).toEqual(false); 19 | expect(isIdentifier("2.4")).toEqual(false); 20 | expect(isIdentifier("9test")).toEqual(false); 21 | expect(isIdentifier("+test")).toEqual(false); 22 | expect(isIdentifier("value-min")).toEqual(false); 23 | }); 24 | }); 25 | 26 | describe("Assert that identifier.getIdentifiers()", () => { 27 | it("works", () => { 28 | expect(getIdentifiers("")).toEqual(new Set()); 29 | expect(getIdentifiers("B07")).toEqual(new Set(["B07"])); 30 | expect(getIdentifiers("(B04 - B05) / (max(B06, B07) + B05)")).toEqual( 31 | new Set(["B04", "B05", "B06", "B07", "max"]), 32 | ); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/util/identifier.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | const reIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; 8 | const reVariables = /[a-zA-Z_$][a-zA-Z0-9_$]*/g; 9 | 10 | const emptySet = new Set(); 11 | 12 | export function isIdentifier(value: string): boolean { 13 | return reIdentifier.test(value); 14 | } 15 | 16 | export function getIdentifiers(expression: string): Set { 17 | const matches = expression.match(reVariables); 18 | return matches !== null ? new Set(matches) : emptySet; 19 | } 20 | -------------------------------------------------------------------------------- /src/util/json.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export type JsonPrimitive = null | boolean | number | string; 8 | export type JsonArray = JsonValue[]; 9 | export type JsonObject = { [key: string]: JsonValue }; 10 | export type JsonValue = JsonPrimitive | JsonArray | JsonObject; 11 | -------------------------------------------------------------------------------- /src/util/maps.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { expect, it, describe } from "vitest"; 8 | import { maps } from "./maps"; 9 | 10 | describe("maps", () => { 11 | it("can load maps", () => { 12 | expect(maps).toBeInstanceOf(Array); 13 | expect(maps.length).toBeGreaterThan(0); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/util/maps.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | // Thanks to alexurquhart, maps.json is content of 8 | // https://github.com/alexurquhart/free-tiles/blob/master/tiles.json. 9 | // Check http://alexurquhart.github.io/free-tiles/. 10 | import _maps from "@/resources/maps.json"; 11 | 12 | export const maps = _maps as MapGroup[]; 13 | 14 | export interface MapGroup { 15 | name: string; 16 | link: string; 17 | baseMaps: MapSource[]; 18 | overlays: MapSource[]; 19 | } 20 | 21 | export interface MapSource { 22 | name: string; 23 | endpoint: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/util/path.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { expect, it, describe } from "vitest"; 8 | import { buildPath } from "./path"; 9 | 10 | describe("buildPath", () => { 11 | it("works", () => { 12 | expect(buildPath("")).toEqual(""); 13 | expect(buildPath("p1")).toEqual("p1"); 14 | expect(buildPath("p1", "p2", "")).toEqual("p1/p2"); 15 | expect(buildPath("p1", "", "p2")).toEqual("p1/p2"); 16 | expect(buildPath("p1", "", "", "p2")).toEqual("p1/p2"); 17 | expect(buildPath("", "p1", "p2")).toEqual("/p1/p2"); 18 | }); 19 | it("works with separators", () => { 20 | expect(buildPath("p1/")).toEqual("p1/"); 21 | expect(buildPath("p1/", "p2")).toEqual("p1/p2"); 22 | expect(buildPath("p1/", "/p2")).toEqual("p1/p2"); 23 | expect(buildPath("p1", "/p2")).toEqual("p1/p2"); 24 | expect(buildPath("/p1/")).toEqual("/p1/"); 25 | expect(buildPath("/p1/", "p2")).toEqual("/p1/p2"); 26 | expect(buildPath("/p1/", "/p2")).toEqual("/p1/p2"); 27 | expect(buildPath("/p1", "/p2")).toEqual("/p1/p2"); 28 | expect(buildPath("p1/", "p2/")).toEqual("p1/p2/"); 29 | expect(buildPath("p1/", "/p2/")).toEqual("p1/p2/"); 30 | expect(buildPath("p1", "/p2/")).toEqual("p1/p2/"); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/util/path.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | /** 8 | * Builds a path by concatenating a base path and subsequent path components. 9 | * The function ensures that only single path separators are used between 10 | * path components. 11 | * 12 | * @param base 13 | * @param components 14 | */ 15 | export function buildPath(base: string, ...components: string[]): string { 16 | let path = base; 17 | for (const c of components) { 18 | if (c !== "") { 19 | if (path.endsWith("/")) { 20 | if (c.startsWith("/")) { 21 | path += c.substring(1); 22 | } else { 23 | path += c; 24 | } 25 | } else { 26 | if (c.startsWith("/")) { 27 | path += c; 28 | } else { 29 | path += "/" + c; 30 | } 31 | } 32 | } 33 | } 34 | return path; 35 | } 36 | -------------------------------------------------------------------------------- /src/util/qparam.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | // based on https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript 8 | 9 | export function getQueryParameterByName( 10 | queryStr: string | null, 11 | name: string, 12 | defaultValue: string | null = null, 13 | ): string | null { 14 | queryStr = queryStr || window.location.search; 15 | if (!queryStr) { 16 | return defaultValue; 17 | } 18 | const match = RegExp("[?&]" + name + "=([^&]*)").exec(queryStr); 19 | if (!match) { 20 | return defaultValue; 21 | } 22 | return decodeURIComponent(match[1].replace(/\+/g, " ")); 23 | } 24 | -------------------------------------------------------------------------------- /src/util/styles.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { expect, it, describe } from "vitest"; 8 | import { makeStyles, makeCssStyles } from "./styles"; 9 | 10 | describe("makeStyles", () => { 11 | it("just converts the type and not the value", () => { 12 | const rawStyles = {}; 13 | expect(makeStyles(rawStyles)).toBe(rawStyles); 14 | }); 15 | }); 16 | 17 | describe("makeCssStyles", () => { 18 | it("just converts the type and not the value", () => { 19 | const rawStyles = {}; 20 | expect(makeCssStyles(rawStyles)).toBe(rawStyles); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/util/styles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { SxProps, Theme } from "@mui/system"; 8 | import { CSSProperties } from "react"; 9 | 10 | type RawStyles = Record>; 11 | type Styles = { 12 | [P in keyof S]: SxProps; 13 | }; 14 | 15 | /** 16 | * Convert given styles object into its type-safe version 17 | * where `S` the type of the "raw" styles 18 | * and `T` is the MUI theme type. 19 | * @param rawStyles Object that maps a property key into an `SxProps` value. 20 | */ 21 | export function makeStyles( 22 | rawStyles: RawStyles, 23 | ): Styles { 24 | return rawStyles; 25 | } 26 | 27 | type CssStyles = { 28 | [P in keyof S]: CSSProperties; 29 | }; 30 | 31 | export function makeCssStyles( 32 | rawStyles: CssStyles, 33 | ): CssStyles { 34 | return rawStyles; 35 | } 36 | -------------------------------------------------------------------------------- /src/util/throttle.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { isNumber } from "@/util/types"; 8 | 9 | export function throttle) => ReturnType>( 10 | callback: T, 11 | delay?: number, 12 | ): T { 13 | return isNumber(delay) && delay > 0 14 | ? throttleWithDelay(callback, delay) 15 | : throttleWithRAF(callback); 16 | } 17 | 18 | function throttleWithDelay) => ReturnType>( 19 | callback: T, 20 | delay: number, 21 | ): T { 22 | let lastExecutionTime = 0; 23 | let lastResult: ReturnType; 24 | return ((...args: Parameters) => { 25 | const currentTime = Date.now(); 26 | if (lastExecutionTime === 0 || currentTime - lastExecutionTime >= delay) { 27 | lastResult = callback(...args); 28 | lastExecutionTime = currentTime; 29 | } 30 | return lastResult; 31 | }) as T; 32 | } 33 | 34 | function throttleWithRAF) => ReturnType>( 35 | callback: T, 36 | ): T { 37 | let isThrottled = false; 38 | return ((...args: Parameters) => { 39 | if (!isThrottled) { 40 | isThrottled = true; 41 | requestAnimationFrame(() => { 42 | callback(...args); 43 | isThrottled = false; 44 | }); 45 | } 46 | }) as T; 47 | } 48 | -------------------------------------------------------------------------------- /src/util/time.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | function getTimezoneOffset(date: Date): number { 8 | return date.getTimezoneOffset() * 60000; 9 | } 10 | 11 | export function localToUtcTime(local: Date): number { 12 | return local.getTime() - getTimezoneOffset(local); 13 | } 14 | 15 | export function utcTimeToLocal(utcTime: number): Date { 16 | const dateTime = new Date(utcTime); 17 | return new Date(dateTime.getTime() + getTimezoneOffset(dateTime)); 18 | } 19 | 20 | export function utcTimeToIsoDateString(utcTime: number) { 21 | return new Date(utcTime).toISOString().substring(0, 10); 22 | } 23 | 24 | export function utcTimeToIsoDateTimeString(utcTime: number) { 25 | return isoDateTimeStringToLabel(new Date(utcTime).toISOString()); 26 | } 27 | 28 | export function isoDateTimeStringToLabel(utcDateTimeString: string) { 29 | return utcDateTimeString.substring(0, 19).replace("T", " "); 30 | } 31 | -------------------------------------------------------------------------------- /src/util/types.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import { expect, it, describe } from "vitest"; 8 | import { isNumber, isObject } from "@/util/types"; 9 | 10 | class A {} 11 | 12 | const TEST_VALUES = [ 13 | undefined, 14 | null, 15 | true, 16 | 1.5, 17 | "Hi", 18 | () => {}, 19 | [], 20 | {}, 21 | A, 22 | new A(), 23 | ]; 24 | 25 | describe("Assert", () => { 26 | it("isNumber() works", () => { 27 | expect(TEST_VALUES.map(isNumber)).toEqual([ 28 | false, 29 | false, 30 | false, 31 | true, 32 | false, 33 | false, 34 | false, 35 | false, 36 | false, 37 | false, 38 | ]); 39 | }); 40 | 41 | it("isObject() works", () => { 42 | expect(TEST_VALUES.map(isObject)).toEqual([ 43 | false, 44 | false, 45 | false, 46 | false, 47 | false, 48 | false, 49 | false, 50 | true, 51 | false, 52 | false, 53 | ]); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/util/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export function isNumber(value: unknown): value is number { 8 | return typeof value === "number"; 9 | } 10 | 11 | export function isString(value: unknown): value is string { 12 | return typeof value === "string"; 13 | } 14 | 15 | export function isFunction( 16 | value: unknown, 17 | ): value is (...args: unknown[]) => unknown { 18 | return typeof value === "function"; 19 | } 20 | 21 | export function isObject(value: unknown): value is Record { 22 | return ( 23 | value !== null && typeof value === "object" && value.constructor === Object 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | // Important: use semantic versioning (https://semver.org/) 8 | const version = "1.6.1-dev.0"; 9 | 10 | export default version; 11 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | /// 8 | 9 | interface ImportMetaEnv { 10 | readonly XCV_OAUTH2_AUTHORITY?: string; 11 | readonly XCV_OAUTH2_CLIENT_ID?: string; 12 | readonly XCV_OAUTH2_AUDIENCE?: string; 13 | readonly XCV_APP_SERVER_ID?: string; 14 | readonly XCV_SERVER_NAME?: string; 15 | readonly XCV_SERVER_URL?: string; 16 | // more env variables... 17 | } 18 | 19 | interface ImportMeta { 20 | readonly env: ImportMetaEnv; 21 | } 22 | -------------------------------------------------------------------------------- /src/volume/ColorBarTextures.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | import * as THREE from "three"; 8 | 9 | import { ColorBar, formatColorBarName } from "@/model/colorBar"; 10 | 11 | class ColorBarTextures { 12 | private readonly textures: { [cmName: string]: THREE.Texture }; 13 | 14 | constructor() { 15 | this.textures = {}; 16 | } 17 | 18 | get(colorBar: ColorBar, onLoad?: () => void): THREE.Texture { 19 | const key = formatColorBarName(colorBar); 20 | let texture = this.textures[key]; 21 | if (!texture) { 22 | // const image = new Image(); 23 | // loadColorBarImage(colorBar, image).then(); 24 | // texture = new THREE.Texture(image); 25 | texture = new THREE.TextureLoader().load( 26 | `data:image/png;base64,${colorBar.imageData}`, 27 | onLoad, 28 | ); 29 | this.textures[key] = texture; 30 | } 31 | return texture; 32 | } 33 | } 34 | 35 | export const colorBarTextures = new ColorBarTextures(); 36 | -------------------------------------------------------------------------------- /src/volume/webgl-utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-2025 by xcube team and contributors 3 | * Permissions are hereby granted under the terms of the MIT License: 4 | * https://opensource.org/licenses/MIT. 5 | */ 6 | 7 | export function isWebGL2Available(): boolean { 8 | try { 9 | const canvas = document.createElement("canvas"); 10 | return !!(window.WebGL2RenderingContext && canvas.getContext("webgl2")); 11 | } catch (e) { 12 | return false; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | }, 7 | 8 | "target": "ES2020", 9 | "useDefineForClassFields": true, 10 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 11 | "module": "ESNext", 12 | "skipLibCheck": true, 13 | 14 | /* Bundler mode */ 15 | "moduleResolution": "bundler", 16 | "allowJs": true, 17 | "allowImportingTsExtensions": false, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | 23 | /* Linting */ 24 | "strict": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | "noFallthroughCasesInSwitch": true 28 | }, 29 | "include": ["src"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | --------------------------------------------------------------------------------