├── .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 | 
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 |
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 |
40 | ) : (
41 |
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 |
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 |
--------------------------------------------------------------------------------