├── src
├── filters.js
├── assets
│ └── .gitignore
├── formats
│ ├── tsv.js
│ ├── native.js
│ ├── browserImage.js
│ ├── json.js
│ ├── csv.js
│ ├── format.js
│ └── formatRegistry.js
├── components
│ ├── datatypes
│ │ ├── api.js
│ │ ├── Budget.vue
│ │ ├── Duration.vue
│ │ ├── FileFormatOptionsEditor.vue
│ │ ├── Kernel.vue
│ │ ├── GeoJsonEditor.vue
│ │ └── TemporalPicker.vue
│ ├── wizards
│ │ ├── WizardMixin.js
│ │ ├── tabs
│ │ │ ├── ChooseTime.vue
│ │ │ ├── ChooseProcessParameters.vue
│ │ │ ├── ChooseBoundingBox.vue
│ │ │ ├── ChooseReducer.vue
│ │ │ ├── ChooseFormat.vue
│ │ │ ├── ChooseCollection.vue
│ │ │ ├── ChooseUserDefinedProcess.vue
│ │ │ ├── ChooseProcessingMode.vue
│ │ │ └── ChooseSpectralIndices.vue
│ │ ├── components
│ │ │ ├── WizardStep.vue
│ │ │ └── WizardTab.vue
│ │ └── Download.vue
│ ├── maps
│ │ ├── ControlMixin.js
│ │ ├── AddDataControl.vue
│ │ ├── osmgeocoder.js
│ │ ├── MapMixin.scss
│ │ ├── UserLocationControl.vue
│ │ ├── TextControl.vue
│ │ ├── GeocoderMixin.vue
│ │ ├── ProgressControl.vue
│ │ ├── MapExtentViewer.vue
│ │ ├── projManager.js
│ │ ├── GeoJsonMixin.vue
│ │ ├── geotiff
│ │ │ ├── state.js
│ │ │ └── fix.js
│ │ ├── ExtentMixin.vue
│ │ ├── ChannelControl.vue
│ │ └── GeoJsonMapEditor.vue
│ ├── modals
│ │ ├── DataModal.vue
│ │ ├── UdfRuntimeModal.vue
│ │ ├── FileFormatModal.vue
│ │ ├── JobEstimateModal.vue
│ │ ├── ServiceInfoModal.vue
│ │ ├── DownloadAssetsModal.vue
│ │ ├── ProcessModal.vue
│ │ ├── WebEditorModal.vue
│ │ ├── ProcessParameterModal.vue
│ │ ├── ErrorModal.vue
│ │ ├── ShareModal.vue
│ │ ├── ParameterModal.vue
│ │ ├── JobInfoModal.vue
│ │ ├── ListModal.vue
│ │ ├── ServerInfoModal.vue
│ │ ├── AddMapDataModal.vue
│ │ ├── ExportCodeModal.vue
│ │ └── CollectionModal.vue
│ ├── SyncButton.vue
│ ├── share
│ │ ├── ShareMixin.js
│ │ ├── XShare.vue
│ │ ├── BlueskyShare.vue
│ │ ├── MastodonSocialShare.vue
│ │ ├── CopyUrl.vue
│ │ └── ShareEditor.vue
│ ├── EventBusMixin.js
│ ├── Collection.vue
│ ├── ProcessingParametersMixin.js
│ ├── TermsOfServiceConsent.vue
│ ├── Logo.vue
│ ├── FieldMixin.js
│ ├── viewer
│ │ ├── MetadataViewer.vue
│ │ ├── DataViewer.vue
│ │ ├── ScatterChart.vue
│ │ ├── LogViewer.vue
│ │ └── TableViewer.vue
│ ├── cancellableRequest.js
│ ├── FullscreenButton.vue
│ ├── jsonSchema.js
│ ├── UserWorkspace.vue
│ ├── WorkPanelMixin.js
│ └── Editor.vue
├── store
│ ├── files.js
│ ├── services.js
│ ├── userProcesses.js
│ └── jobs.js
├── registryExtension.js
├── events.md
├── main.js
├── process.js
├── build.js
└── export
│ ├── r.js
│ └── python.js
├── theme.scss
├── .babelrc
├── public
├── logo.png
└── index.html
├── .gitignore
├── Dockerfile
├── .dockerignore
├── .github
└── workflows
│ ├── actions.yml
│ └── docker.yml
├── vue.config.js
├── package.json
├── docs
├── geotiff.md
└── oidc.md
└── config.js
/src/filters.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | }
--------------------------------------------------------------------------------
/theme.scss:
--------------------------------------------------------------------------------
1 | $mainColor: #1665B6;
2 | $linkColor: #1665B6;
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@vue/app"
4 | ]
5 | }
--------------------------------------------------------------------------------
/src/assets/.gitignore:
--------------------------------------------------------------------------------
1 | epsg-names.json
2 | epsg-proj.json
3 | indices.json
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Open-EO/openeo-web-editor/HEAD/public/logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /dist
3 | /package-lock.json
4 | /public/fontawesome/
5 |
6 | /nbproject/
7 | .vscode
8 |
--------------------------------------------------------------------------------
/src/formats/tsv.js:
--------------------------------------------------------------------------------
1 | import CSV from './csv';
2 |
3 | class TSV extends CSV {
4 |
5 | constructor(asset) {
6 | super(asset, ["\t"]);
7 | }
8 |
9 | }
10 |
11 | export default TSV;
--------------------------------------------------------------------------------
/src/formats/native.js:
--------------------------------------------------------------------------------
1 | import { SupportedFormat } from './format';
2 |
3 | class NativeType extends SupportedFormat {
4 |
5 | constructor(asset) {
6 | super(asset, "DataViewer");
7 | }
8 |
9 | }
10 |
11 | export default NativeType;
--------------------------------------------------------------------------------
/src/components/datatypes/api.js:
--------------------------------------------------------------------------------
1 | import Utils from '../../utils';
2 | export const API_TYPES = Utils.resolveJsonRefs(require('@openeo/js-processgraphs/assets/subtype-schemas.json')).definitions;
3 | export const NATIVE_TYPES = [
4 | 'string',
5 | 'integer',
6 | 'number',
7 | 'boolean',
8 | 'array',
9 | 'object'
10 | ];
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22-alpine AS build
2 |
3 | # Copy source code
4 | COPY . /src/openeo-web-editor
5 | WORKDIR /src/openeo-web-editor
6 |
7 | # Build
8 | RUN npm install
9 | RUN npm run build
10 |
11 | # Copy build folder and run with nginx
12 | FROM nginx:1.28.0-alpine
13 | COPY --from=build /src/openeo-web-editor/dist /usr/share/nginx/html
14 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Contains files which should be ignored in the build context.
2 |
3 | # This includes large files and folders which might exist locally
4 | # but are not needed during docker build to speed up build time
5 | dist
6 | node_modules
7 | .git
8 |
9 | # and files which are equally not needed and would break the
10 | # build cache at an early step (COPY . ...) if changed.
11 | Dockerfile
12 | .dockerignore
13 |
--------------------------------------------------------------------------------
/src/components/wizards/WizardMixin.js:
--------------------------------------------------------------------------------
1 | import WizardTab from './components/WizardTab.vue';
2 |
3 | export default {
4 | components: {
5 | WizardTab
6 | },
7 | props: {
8 | parent: {
9 | type: Object,
10 | required: true
11 | },
12 | options: {
13 | type: Object,
14 | default: () => ({})
15 | }
16 | },
17 | created() {
18 | for(let key in this.options) {
19 | this[key] = this.options[key];
20 | }
21 | }
22 | };
--------------------------------------------------------------------------------
/src/store/files.js:
--------------------------------------------------------------------------------
1 | import storeFactory from './storeFactory';
2 |
3 | export default storeFactory({
4 | namespace: 'files',
5 | listFn: 'listFiles',
6 | paginateFn: 'paginateFiles',
7 | createFn: 'uploadFile',
8 | updateFn: 'uploadFile',
9 | deleteFn: 'deleteFile',
10 | readFn: 'downloadFile',
11 | readFnById: 'getFile',
12 | primaryKey: 'path',
13 | customizations: {
14 | getters: {
15 | },
16 | actions: {
17 | },
18 | mutations: {
19 | }
20 | }
21 | });
--------------------------------------------------------------------------------
/src/store/services.js:
--------------------------------------------------------------------------------
1 | import storeFactory from './storeFactory';
2 |
3 | export default storeFactory({
4 | namespace: 'services',
5 | listFn: 'listServices',
6 | paginateFn: 'paginateServices',
7 | createFn: 'createService',
8 | updateFn: 'updateService',
9 | deleteFn: 'deleteService',
10 | readFn: 'describeService',
11 | readFnById: 'getService',
12 | customizations: {
13 | getters: {
14 | },
15 | actions: {
16 | },
17 | mutations: {
18 | }
19 | }
20 | });
--------------------------------------------------------------------------------
/src/components/maps/ControlMixin.js:
--------------------------------------------------------------------------------
1 | import { Control } from 'ol/control.js';
2 |
3 | export default {
4 | props: {
5 | map: {
6 | type: Object
7 | }
8 | },
9 | data() {
10 | return {
11 | control: null
12 | };
13 | },
14 | mounted() {
15 | this.control = new Control({
16 | element: this.$el
17 | });
18 | },
19 | watch: {
20 | map(newMap) {
21 | if (newMap) {
22 | this.map.addControl(this.control);
23 | }
24 | }
25 | },
26 | methods: {
27 | getControl() {
28 | return this.control;
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/components/modals/DataModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/wizards/tabs/ChooseTime.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Please select the days for which you want to download data for.
4 |
$emit('input', v)" />
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/SyncButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
30 |
--------------------------------------------------------------------------------
/src/formats/browserImage.js:
--------------------------------------------------------------------------------
1 | import { SupportedFormat } from './format';
2 |
3 | class BrowserImage extends SupportedFormat {
4 |
5 | constructor(asset) {
6 | super(asset, 'ImageViewer', 'fa-image');
7 | }
8 |
9 | isBinary() {
10 | return true;
11 | }
12 |
13 | async fetchData() {
14 | return new Promise((resolve, reject) => {
15 | let img = new Image();
16 | img.crossOrigin = 'anonymous';
17 | img.onerror = () => reject(new Error('Failed to load the image'));
18 | img.onload = () => resolve(img);
19 | img.fetchPriotity = 'high';
20 | img.decoding = 'sync';
21 | img.src = this.getUrl();
22 | });
23 | }
24 |
25 | }
26 |
27 | export default BrowserImage;
--------------------------------------------------------------------------------
/.github/workflows/actions.yml:
--------------------------------------------------------------------------------
1 | name: Web Editor Deployment
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | deploy:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/setup-node@v4
11 | with:
12 | node-version: 'lts/*'
13 | - uses: actions/checkout@v4
14 | - run: npm install
15 | - run: npm run build
16 | - uses: peaceiris/actions-gh-pages@v4
17 | with:
18 | github_token: ${{ secrets.GITHUB_TOKEN }}
19 | publish_dir: dist
20 | exclude_assets: 'report.html'
21 | user_name: 'openEO CI'
22 | user_email: openeo.ci@uni-muenster.de
23 | cname: editor.openeo.org
--------------------------------------------------------------------------------
/src/components/share/ShareMixin.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | props: {
4 | show: {
5 | type: Boolean,
6 | default: false
7 | },
8 | // A public URL to the resource
9 | url: {
10 | type: String,
11 | required: true
12 | },
13 | // A title for the resource, if available
14 | title: {
15 | type: String,
16 | default: ""
17 | },
18 | // Any extra data that shall be passed for sharing (e.g. the STAC entity for jobs)
19 | extra: {
20 | type: Object,
21 | default: () => ({})
22 | },
23 | // The source, e.g. a Job or Service
24 | context: {
25 | type: Object,
26 | required: true
27 | },
28 | // The type of the source, e.g. `job` or `service`
29 | type: {
30 | type: String,
31 | required: true
32 | }
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/src/store/userProcesses.js:
--------------------------------------------------------------------------------
1 | import storeFactory from './storeFactory';
2 | import Utils from '../utils';
3 |
4 | export default storeFactory({
5 | namespace: 'userProcesses',
6 | listFn: 'listUserProcesses',
7 | paginateFn: null,
8 | createFn: 'setUserProcess',
9 | updateFn: 'replaceUserProcess',
10 | deleteFn: 'deleteUserProcess',
11 | readFn: 'describeUserProcess',
12 | readFnById: 'getUserProcess',
13 | customizations: {
14 | getters: {
15 | },
16 | actions: {
17 | },
18 | mutations: {
19 | data(state, data) {
20 | state.userProcesses = data
21 | .map(p => Object.assign(p, {namespace: 'user'}))
22 | .filter(p => (typeof p.id === 'string'))
23 | .sort(Utils.sortById);
24 | state.missing = data['federation:missing'];
25 | }
26 | }
27 | }
28 | });
--------------------------------------------------------------------------------
/src/components/EventBusMixin.js:
--------------------------------------------------------------------------------
1 | export default {
2 | data() {
3 | return {
4 | eventBusListeners: {}
5 | };
6 | },
7 | beforeDestroy() {
8 | for (var eventName in this.eventBusListeners) {
9 | this.$root.$off(eventName, this.eventBusListeners[eventName]);
10 | }
11 | },
12 | methods: {
13 | hasListener(eventName) {
14 | return !!this.eventBusListeners[eventName];
15 | },
16 | listen(eventName, callback) {
17 | this.unlisten(eventName);
18 | this.$root.$on(eventName, callback);
19 | this.eventBusListeners[eventName] = callback;
20 | },
21 | unlisten(eventName) {
22 | if (this.hasListener(eventName)) {
23 | this.$root.$off(eventName, this.eventBusListeners[eventName]);
24 | delete this.eventBusListeners[eventName];
25 | }
26 | },
27 | broadcast() {
28 | this.$root.$emit(...arguments);
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/components/wizards/components/WizardStep.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{index + 1}}
8 |
9 | {{index + 1}}
10 |
11 |
12 | {{tab.title}}
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/modals/UdfRuntimeModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/maps/AddDataControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
32 |
33 |
--------------------------------------------------------------------------------
/src/components/modals/FileFormatModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/Collection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Worldwide
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/components/maps/osmgeocoder.js:
--------------------------------------------------------------------------------
1 | export default class OSMGeocoder {
2 | constructor(url, geojson = false) {
3 | this.url = url;
4 | this.geojson = geojson;
5 | }
6 |
7 | getParameters(opt) {
8 | return {
9 | url: this.url,
10 | params: {
11 | q: opt.query,
12 | format: 'json',
13 | limit: 10,
14 | 'accept-language': 'en',
15 | polygon_geojson: this.geojson ? 1 : 0,
16 | polygon_threshold: 0.001,
17 | },
18 | };
19 | }
20 |
21 | handleResponse(results) {
22 | if (results.length === 0) {
23 | return [];
24 | }
25 | return results
26 | .filter(result => ["boundary", "geological", "leisure", "natural", "place", "water", "waterway"].includes(result.class))
27 | .map(result => ({
28 | lon: result.lon,
29 | lat: result.lat,
30 | bbox: result.boundingbox,
31 | address: {
32 | name: result.display_name
33 | },
34 | original: {
35 | formatted: result.display_name,
36 | details: result.address,
37 | geojson: result.geojson
38 | }
39 | }));
40 | }
41 | }
--------------------------------------------------------------------------------
/src/registryExtension.js:
--------------------------------------------------------------------------------
1 | import Utils from './utils';
2 | import Process from './process';
3 | import { Formula } from '@openeo/js-client';
4 | import { ProcessGraph } from '@openeo/js-processgraphs';
5 |
6 | export default {
7 | mathProcesses: null,
8 | getMathProcesses() {
9 | if (!this.mathProcesses) {
10 | this.mathProcesses = this.all().filter(Process.isMathProcess);
11 | }
12 | return this.mathProcesses;
13 | },
14 | isMath(process) {
15 | if (process instanceof ProcessGraph) {
16 | process = process.process;
17 | }
18 | if (!Utils.isObject(process) || Utils.size(process.process_graph) === 0) {
19 | return null;
20 | }
21 |
22 | let mathProcessIds = this.getMathProcesses().map(p => p.id)
23 | .concat(Object.values(Formula.operatorMapping))
24 | .concat(Object.keys(Formula.arrayOperatorMapping))
25 | .concat(['array_element']);
26 | let unsupportedFuncs = Object.values(process.process_graph).find(node => !mathProcessIds.includes(node.process_id));
27 | return (typeof unsupportedFuncs === 'undefined');
28 | }
29 | };
--------------------------------------------------------------------------------
/src/components/wizards/tabs/ChooseProcessParameters.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | This process doesn't expose any parameters.
5 | You can skip this step.
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/wizards/tabs/ChooseBoundingBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Please select the area of interest which you want to download data for. You can add or remove a bounding box by clicking into the map.
4 |
5 |
6 |
7 |
8 |
41 |
42 |
--------------------------------------------------------------------------------
/src/formats/json.js:
--------------------------------------------------------------------------------
1 | import Utils from '../utils';
2 | import { SupportedFormat } from './format';
3 |
4 | class JSON_ extends SupportedFormat {
5 |
6 | constructor(asset, component = "DataViewer") {
7 | super(asset, component);
8 |
9 | this.isGeoJson = false;
10 | // this.isCovJson = false;
11 | }
12 |
13 | async parseData(data) {
14 | if (typeof data === 'string') {
15 | try {
16 | data = JSON.parse(data);
17 | }
18 | catch (error) {
19 | console.log(error);
20 | }
21 | }
22 | if (Utils.detectGeoJson(data)) {
23 | this.isGeoJson = true;
24 | this.component = 'MapViewer';
25 | this.icon = 'fa-map';
26 | }
27 | else if (this.isTable(data)) {
28 | this.component = 'TableViewer';
29 | this.icon = 'fa-table';
30 | }
31 | return data;
32 | }
33 |
34 | isTable(data) {
35 | if (!data || typeof data !== 'object' || Utils.size(data) === 0) {
36 | return false;
37 | }
38 | let values = Object.values(data);
39 | let keys = Object.keys(values[0]);
40 | return !values.some(row => !row || typeof row !== 'object' || !Utils.equals(Object.keys(row), keys));
41 | }
42 | }
43 |
44 | export default JSON_;
--------------------------------------------------------------------------------
/src/components/maps/MapMixin.scss:
--------------------------------------------------------------------------------
1 | /* Customize layerswitcher control */
2 | .ol-layerswitcher {
3 | top: 2.75em !important;
4 |
5 | > button {
6 | font-size: 1em;
7 |
8 | &:before,
9 | &:after {
10 | background: transparent;
11 | background-image: none;
12 | box-shadow: none;
13 | position: inherit;
14 | transform: none;
15 | display: inline-block;
16 | width: auto;
17 | height: auto;
18 | }
19 |
20 | &:after {
21 | font-family: "Font Awesome\ 5 Free";
22 | content: "\f5fd";
23 | font-weight: 900;
24 | left: 1px;
25 | top: 1px;
26 | }
27 | }
28 | }
29 |
30 | /* Customize scale line control */
31 | .ol-scale-line {
32 | background-color: rgba(0,60,136,.5);
33 | }
34 | .ol-scale-line-inner {
35 | color: #fff;
36 | border-color: #fff;
37 | }
38 |
39 | .ol-unselectable,
40 | .ol-control {
41 | z-index: 2;
42 |
43 | &.ol-timeline {
44 | z-index: 1;
45 |
46 | .ol-buttons {
47 | width: auto;
48 | font-size: 0.8em;
49 |
50 | > button {
51 | display: inline-block;
52 | }
53 |
54 | .timeline-date-label {
55 | width: 7em;
56 | font-weight: normal;
57 | }
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/events.md:
--------------------------------------------------------------------------------
1 | # Events
2 |
3 | ## Custom Processes
4 |
5 | ### editProcess(object $resource)
6 | Sends the current custom process and inserts it into the currently active editor.
7 |
8 | ## Modals
9 |
10 | ### showModal(component, props, events, id = null)
11 |
12 | ### hideModal(modal)
13 |
14 | ### showListModal(string $title, array $list, array $listActions)
15 | Shows a list in a modal.
16 |
17 | ### showWebEditorInfo()
18 | Shows information about the web editor in a modal.
19 |
20 | ### showCollection(id)
21 | Shows collection information in a modal.
22 |
23 | ### showProcess(process)
24 | Shows process information in a modal.
25 |
26 | ## Viewer & Web Services
27 |
28 | ### viewSyncResult(object $result)
29 | Shows the result of a synchronous job.
30 |
31 | ### viewJobResults(object $jobResult, object $job = null)
32 | Shows data from a job result document.
33 |
34 | ### viewWebService(object $service, function $onClose = null)
35 | Shows a web service on the map.
36 |
37 | ### removeWebService(string $id)
38 | Removes a web service from the map.
39 |
40 | ## UI
41 |
42 | ### windowResized()
43 | The panels or the browser window have been resized.
--------------------------------------------------------------------------------
/src/components/maps/UserLocationControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/modals/JobEstimateModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Batch Job
7 |
8 | ID:
9 | {{ job.id }}
10 |
11 |
12 | Title:
13 | {{ job.title }}
14 |
15 |
16 |
17 |
18 |
19 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/wizards/components/WizardTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/components/modals/ServiceInfoModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
38 |
39 |
--------------------------------------------------------------------------------
/src/components/share/XShare.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Open X
6 |
7 |
8 |
9 |
10 |
39 |
40 |
--------------------------------------------------------------------------------
/src/store/jobs.js:
--------------------------------------------------------------------------------
1 | import storeFactory from './storeFactory';
2 |
3 | export default storeFactory({
4 | namespace: 'jobs',
5 | listFn: 'listJobs',
6 | paginateFn: 'paginateJobs',
7 | createFn: 'createJob',
8 | updateFn: 'updateJob',
9 | deleteFn: 'deleteJob',
10 | readFn: 'describeJob',
11 | readFnById: 'getJob',
12 | customizations: {
13 | getters: {
14 | supportsQueue: (state, getters, rootState, rootGetters) => rootGetters.supports('startJob'),
15 | supportsCancel: (state, getters, rootState, rootGetters) => rootGetters.supports('stopJob')
16 | },
17 | actions: {
18 | async queue(cx, {data}) {
19 | if (cx.getters.supportsQueue) {
20 | let updated = await data.startJob();
21 | cx.commit('upsert', updated);
22 | return updated;
23 | }
24 | else {
25 | throw new Error("Queueing a batch job is not supported by the server.");
26 | }
27 | },
28 | async cancel(cx, {data}) {
29 | if (cx.getters.supportsCancel) {
30 | let updated = await data.stopJob();
31 | cx.commit('upsert', updated);
32 | return updated;
33 | }
34 | else {
35 | throw new Error("Canceling a batch job is not supported by the server.");
36 | }
37 | }
38 | },
39 | mutations: {
40 | }
41 | }
42 | });
--------------------------------------------------------------------------------
/src/components/modals/DownloadAssetsModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/components/share/BlueskyShare.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Open Bluesky
6 |
7 |
8 |
9 |
10 |
39 |
40 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
25 | Sorry, this website requires JavaScript to be enabled!
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/components/datatypes/Budget.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ capabilities.currency() }}
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/share/MastodonSocialShare.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Open Mastodon.social
6 |
7 |
8 |
9 |
10 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/modals/ProcessModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
46 |
47 |
--------------------------------------------------------------------------------
/src/components/wizards/tabs/ChooseReducer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ text }}
4 |
$emit('input', v)" />
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/ProcessingParametersMixin.js:
--------------------------------------------------------------------------------
1 | import Utils from '../utils';
2 |
3 | export default {
4 | computed: {
5 | ...Utils.mapState(['processingParameters']),
6 | ...Utils.mapGetters(['supportsBilling', 'supportsBillingPlans']),
7 | },
8 | methods: {
9 | addProcessingParameters(fields, type, values = {}) {
10 | const key = `create_${type}_parameters`;
11 | for (const field of this.processingParameters[key]) {
12 | const obj = {
13 | advanced: field.optional, // Show required properties not in advanced section
14 | label: field.name // avoid formatting the parameter name
15 | };
16 | if (typeof values[field.name] !== 'undefined') {
17 | obj.value = values[field.name];
18 | }
19 | const schema = Object.assign(obj, field);
20 | fields.push(schema);
21 | }
22 | return fields;
23 | },
24 | normalizeData(data, fields = []) {
25 | data = Object.assign({}, data);
26 | for (const field of fields) {
27 | if (!field) {
28 | continue;
29 | }
30 | const value = data[field.name];
31 | const defaultValue = field.default;
32 | if ((data.optional && Utils.equals(value, defaultValue)) || typeof value === 'undefined') {
33 | delete data[field.name];
34 | }
35 | if (defaultValue === null && typeof value === 'string' && value.trim().length === 0) {
36 | data[field.name] = null;
37 | }
38 | }
39 | return data;
40 | },
41 | }
42 | };
--------------------------------------------------------------------------------
/src/components/modals/WebEditorModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ pkg.description }}
4 | This software is published by the openEO Consortium under the Apache 2.0 license . Please find more information about the openEO project on our homepage and visit the GitHub repository for information about the Web Editor. Feel encouraged to report bugs, feature requests and other issues in the GitHub issue tracker .
5 | Supported API versions:
6 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/TermsOfServiceConsent.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
2 |
3 | module.exports = {
4 | // Path where this instance of the web editor is hosted (string)
5 | // For example, if you host the web editor at the root of your domain (e.g. https://example.com),
6 | // you can leave this as it is (`/`).
7 | // If you'd like to host it in a sub-sirectory, e.g. https://editor.openeo.org/somewhere/else/,
8 | // you need to set this to `/somewhere/else/`.
9 | // You can provide this option via the environment variable CLIENT_URL, too.
10 | publicPath: process.env.CLIENT_URL || '/',
11 | devServer: {
12 | // Port where the development server runs (int)
13 | // This is only needed for `npm start`
14 | port: 80
15 | },
16 | css: {
17 | loaderOptions: {
18 | sass: {
19 | api: 'modern-compiler' // Use modern Sass API
20 | }
21 | }
22 | },
23 | configureWebpack: {
24 | devtool: 'source-map',
25 | externals: {
26 | // Leaflet is part of openeo-vue-components, but not needed here as we use OpenLayers
27 | leaflet: 'L'
28 | },
29 | optimization: {
30 | splitChunks: {
31 | chunks: 'all',
32 | maxSize: 300000,
33 | maxInitialRequests: 8,
34 | maxAsyncRequests: 1
35 | }
36 | },
37 | resolve: {
38 | fallback: {
39 | fs: false
40 | }
41 | }
42 | },
43 | chainWebpack: webpackConfig => {
44 | webpackConfig.plugin('polyfills').use(NodePolyfillPlugin);
45 | }
46 | }
--------------------------------------------------------------------------------
/src/components/maps/TextControl.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ current }}
3 |
4 |
5 |
53 |
54 |
70 |
--------------------------------------------------------------------------------
/src/components/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ $config.appName }} {{ version }}
5 |
6 |
7 |
8 |
27 |
28 |
--------------------------------------------------------------------------------
/src/components/maps/GeocoderMixin.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/wizards/tabs/ChooseFormat.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/modals/ProcessParameterModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | This is a parameter for a user-defined process.
8 | It is a value made available by the parent entity (usually another process or a secondary web service) that is executing this processes for further use.
9 | See below for details about this parameter:
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
45 |
46 |
--------------------------------------------------------------------------------
/src/components/FieldMixin.js:
--------------------------------------------------------------------------------
1 | export default {
2 | methods: {
3 | getTitleField(value = null) {
4 | return {
5 | name: 'title',
6 | label: 'Title',
7 | schema: {type: 'string'},
8 | default: null,
9 | value: value,
10 | optional: true
11 | };
12 | },
13 | getDescriptionField(value = null) {
14 | return {
15 | name: 'description',
16 | label: 'Description',
17 | schema: {type: 'string', subtype: 'commonmark'},
18 | default: null,
19 | value: value,
20 | description: 'CommonMark (Markdown) is allowed.',
21 | optional: true
22 | };
23 | },
24 | getLogLevelField(value = undefined) {
25 | return {
26 | name: 'log_level',
27 | label: 'Log level',
28 | schema: {type: 'string', enum: ['debug', 'info', 'warning', 'error']},
29 | default: 'info',
30 | value: value,
31 | description: 'The minimum severity level for log entries that the back-end stores for the processing request.\n\ndebug (all logs) > info > warning > error (only errors)',
32 | optional: true
33 | };
34 | },
35 | getBillingPlanField(value = undefined) {
36 | return {
37 | name: 'plan',
38 | label: 'Billing plan',
39 | schema: {type: 'string', subtype: 'billing-plan'},
40 | value: value,
41 | optional: true,
42 | advanced: true
43 | };
44 | },
45 | getBudgetField(value = null) {
46 | return {
47 | name: 'budget',
48 | label: 'Budget limit',
49 | schema: {type: 'number', subtype: 'budget', minimum: 0},
50 | default: null,
51 | value: value,
52 | optional: true,
53 | advanced: true
54 | };
55 | }
56 | }
57 | };
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | # Workflow to build and push docker images
2 | # On push to branch, take care of sha-ref tag (e.g. sha-ad132f5)
3 | # On release, take care of latest and release tags (e.g. 1.2.3)
4 |
5 | name: Docker build and push
6 |
7 | on:
8 | push:
9 | branches: [master]
10 | tags: ['*.*.*']
11 | release:
12 | types: [published]
13 |
14 | jobs:
15 | docker:
16 | name: docker build and push
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 | with:
23 | fetch-depth: 0
24 | - name: Create image and tag names
25 | id: meta
26 | uses: docker/metadata-action@v5
27 | with:
28 | images: mundialis/openeo-web-editor
29 | tags: |
30 | type=ref,event=tag
31 | type=sha
32 | flavor: |
33 | latest=auto
34 | - name: Set up QEMU
35 | uses: docker/setup-qemu-action@v3
36 | - name: Set up Docker Buildx
37 | uses: docker/setup-buildx-action@v3
38 | - name: Login to DockerHub
39 | uses: docker/login-action@v3
40 | with:
41 | username: ${{ secrets.DOCKERHUB_USERNAME }}
42 | password: ${{ secrets.DOCKERHUB_TOKEN }}
43 | - name: Build and push
44 | id: docker_build
45 | uses: docker/build-push-action@v6
46 | with:
47 | push: true
48 | tags: ${{ steps.meta.outputs.tags }}
49 | context: .
50 | file: Dockerfile
51 | - name: Image digest
52 | run: echo ${{ steps.docker_build.outputs.digest }}
53 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Snotify from 'vue-snotify';
3 | import 'vue-snotify/styles/simple.css';
4 | import store from './store/index';
5 | import Config from '../config';
6 | import Page from './Page.vue';
7 | import filters from './filters';
8 | import Clipboard from 'v-clipboard';
9 |
10 | Vue.use(Snotify);
11 | Vue.use(Clipboard);
12 |
13 | // Don't show too many repetitive error messages
14 | Vue.prototype.$snotify.singleError = function () {
15 | let message = arguments[0];
16 | if (message !== this.lastMessage) {
17 | this.lastMessage = message;
18 | this.error(...arguments);
19 | setTimeout(() => this.lastMessage = null, 1000);
20 | }
21 | };
22 |
23 | Vue.config.productionTip = false;
24 | Vue.config.errorHandler = function (err, vm, info) {
25 | console.error(err, info);
26 | if (!vm || !vm.$snotify) {
27 | return;
28 | }
29 |
30 | let message;
31 | if (err instanceof Error) {
32 | message = err.message;
33 | }
34 | else if (typeof err === 'string') {
35 | message = err;
36 | }
37 |
38 | if (message) {
39 | vm.$snotify.singleError(message, 'Error', Config.snotifyDefaults);
40 | }
41 | };
42 | Vue.prototype.$config = Config;
43 |
44 | for(var name in filters) {
45 | Vue.filter(name, filters[name]);
46 | }
47 |
48 | const app = new Vue({
49 | store,
50 | render: h => h(Page)
51 | }).$mount('#app');
52 |
53 | window.addEventListener("unhandledrejection", function(event) {
54 | console.warn(event);
55 | if (typeof event.reason === 'String' || event.reason instanceof Error) {
56 | app.$snotify.singleError(event.reason, 'Error', Config.snotifyDefaults);
57 | }
58 | event.preventDefault();
59 | event.stopPropagation();
60 | });
61 |
--------------------------------------------------------------------------------
/src/components/viewer/MetadataViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
54 |
55 |
66 |
--------------------------------------------------------------------------------
/src/process.js:
--------------------------------------------------------------------------------
1 | import Utils from './utils';
2 | import { ProcessSchema, ProcessDataType } from '@openeo/js-commons';
3 |
4 | export default class Process {
5 |
6 | static isMathProcess(p, operatorMapping = {}) {
7 | if (!Utils.isObject(p)) {
8 | return false;
9 | }
10 |
11 | // Skip processes handled by operators, if given
12 | let operatorProcesses = Object.values(operatorMapping);
13 | if (operatorProcesses.includes(p.id)) {
14 | return false;
15 | }
16 |
17 | // Process must return a numerical value
18 | if (!Utils.isObject(p.returns) || !p.returns.schema) {
19 | return false;
20 | }
21 |
22 | let allowedTypes = ['number', 'integer', 'any'];
23 | let returns = new ProcessSchema(p.returns.schema);
24 | if (!allowedTypes.includes(returns.nativeDataType())) {
25 | return false;
26 | }
27 |
28 | // Required Process parameters must accept numerical values
29 | if (Array.isArray(p.parameters)) {
30 | for(var i in p.parameters) {
31 | let param = p.parameters[i];
32 | if (param.optional) {
33 | continue; // Skip optional parameters
34 | }
35 | if (!param.schema) {
36 | return false;
37 | }
38 | let schema = new ProcessSchema(param.schema);
39 | if (!allowedTypes.includes(schema.nativeDataType())) {
40 | return false;
41 | }
42 | }
43 | }
44 |
45 | // ToDo: Parameters with a dash (and other operators) in them are a problem
46 |
47 | return true;
48 | }
49 |
50 | static arrayOf(datatype) {
51 | if (!(datatype instanceof ProcessDataType)) {
52 | datatype = new ProcessDataType(datatype);
53 | }
54 | if (datatype.nativeDataType() === 'array' && Utils.isObject(datatype.schema.items)) {
55 | let subtype = new ProcessDataType(datatype.schema.items);
56 | return subtype.dataType();
57 | }
58 | return undefined;
59 | }
60 | }
--------------------------------------------------------------------------------
/src/components/maps/ProgressControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
58 |
59 |
--------------------------------------------------------------------------------
/src/components/modals/ErrorModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
73 |
74 |
--------------------------------------------------------------------------------
/src/components/maps/MapExtentViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
72 |
73 |
--------------------------------------------------------------------------------
/src/components/share/CopyUrl.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
71 |
72 |
--------------------------------------------------------------------------------
/src/components/share/ShareEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
67 |
68 |
--------------------------------------------------------------------------------
/src/components/viewer/DataViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ content }}
4 |
No data retrieved.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
70 |
71 |
86 |
--------------------------------------------------------------------------------
/src/components/modals/ShareModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
63 |
64 |
85 |
--------------------------------------------------------------------------------
/src/components/maps/projManager.js:
--------------------------------------------------------------------------------
1 | import proj4 from 'proj4';
2 | import { get as getProjection, transformExtent } from 'ol/proj';
3 | import Projection from 'ol/proj/Projection';
4 | import { register } from 'ol/proj/proj4';
5 |
6 | import Utils from '../../utils';
7 |
8 | export default class ProjManager {
9 |
10 | static async get(data) {
11 | if (data instanceof Projection) {
12 | return data;
13 | }
14 |
15 | return await ProjManager._load(data);
16 | }
17 |
18 | static add(code, meta, extent) {
19 | try {
20 | proj4.defs(code, meta);
21 | register(proj4);
22 | let projection = getProjection(code);
23 | if (Array.isArray(extent)) {
24 | extent = transformExtent(extent, 'EPSG:4326', projection);
25 | projection.setExtent(extent);
26 | }
27 | if (meta.includes('+datum=WGS84')) {
28 | projection.basemap = true;
29 | }
30 | return projection;
31 | } catch (error) {
32 | console.error(error);
33 | return null;
34 | }
35 | }
36 |
37 | // Get projection details from STAC (todo: add collection support)
38 | static async addFromStac(stac) {
39 | if (Utils.isObject(stac) && Utils.isObject(stac.properties)) {
40 | if (stac.properties['proj:code']) {
41 | return await ProjManager.get(stac.properties['proj:code']);
42 | }
43 | else if (stac.properties['proj:wkt2']) {
44 | return ProjManager.add(stac.id, stac.properties['proj:wkt2']);
45 | }
46 | }
47 | return null;
48 | }
49 |
50 | static async _load(crs) {
51 | let code, id;
52 | if (typeof crs === 'string' && crs.match(/^EPSG:\d+$/i)) {
53 | code = crs.toUpperCase();
54 | id = crs.substr(5);
55 | }
56 | else if (Number.isInteger(crs)) {
57 | code = `EPSG:${crs}`
58 | id = String(crs);
59 | }
60 | else {
61 | return null;
62 | }
63 |
64 | // Get projection from cache
65 | let projection = getProjection(code);
66 | if (projection) {
67 | return projection;
68 | }
69 |
70 | // Get projection from database
71 | let epsg = await import('../../assets/epsg-proj.json');
72 | if (id in epsg) {
73 | return ProjManager.add(code, epsg[id][0], epsg[id][1]);
74 | }
75 |
76 | // No projection found
77 | return null;
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/src/components/wizards/tabs/ChooseCollection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Please select the collection which you want to download data for.
4 |
5 |
6 |
7 |
8 | {{ item.id }}
9 | {{ item.title }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
64 |
65 |
--------------------------------------------------------------------------------
/src/components/maps/GeoJsonMixin.vue:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/modals/ParameterModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | No editable parameters available.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
85 |
86 |
--------------------------------------------------------------------------------
/src/components/maps/geotiff/state.js:
--------------------------------------------------------------------------------
1 | export default class GeoTiffState {
2 |
3 | constructor(geotiff) {
4 | this.layer = null;
5 | this.colorMap = geotiff.getColorMap();
6 | this.noData = geotiff.getNoData();
7 | this.bands = geotiff.getBands();
8 | this.defaultChannels = this.bands.slice(0, 3);
9 | this.channels = this.bands.slice(0, 3);
10 | this.file = geotiff;
11 | }
12 |
13 | getBandVar(i) {
14 | return ['band', ['var', `${i}band`]];
15 | }
16 |
17 | getFormula(i) {
18 | let min = ['var', `${i}min`];
19 | let max = ['var', `${i}max`];
20 | let x = this.getBandVar(i);
21 | let scale = ['*', ['/', ['-', x, min], ['-', max, min]], 255]; // Linear scaling from min - max to 0 - 255
22 | return ['clamp', scale, 0, 255]; // clamp values in case we get cales < 0 or > 255
23 | }
24 |
25 | getNoDataFormula() {
26 | let band = this.getBandVar('alpha');
27 | // https://github.com/openlayers/openlayers/issues/13588#issuecomment-1125317573
28 | // return ['clamp', band, 0, 1];
29 | // return ['/', band, 255];
30 | return ['case', ['==', band, 0], 0, 1];
31 | }
32 |
33 | setStyle() {
34 | if (!this.layer) {
35 | return;
36 | }
37 |
38 | // Compute variables
39 | let variables = {};
40 | for(let i in this.channels) {
41 | let channel = this.channels[i];
42 | variables[`${i}band`] = channel.id;
43 | variables[`${i}min`] = channel.min;
44 | variables[`${i}max`] = channel.max;
45 | }
46 | variables.alphaband = this.bands.length + 1;
47 |
48 | // Create style
49 | let color = [];
50 | if (this.colorMap) {
51 | color.push('palette');
52 | color.push(['band', 1]);
53 | color.push(this.colorMap);
54 | }
55 | else if (this.channels.length === 0) {
56 | return null;
57 | }
58 | else if (this.channels.length === 1) {
59 | color.push('color');
60 | let formula = this.getFormula(0);
61 | color.push(formula);
62 | color.push(formula);
63 | color.push(formula);
64 | if (this.noData.length > 0) {
65 | color.push(this.getNoDataFormula());
66 | }
67 | }
68 | else {
69 | color.push('color');
70 | color.push(this.getFormula(0));
71 | color.push(this.getFormula(1));
72 | color.push(this.getFormula(2));
73 | if (this.noData.length > 0) {
74 | color.push(this.getNoDataFormula());
75 | }
76 | }
77 |
78 | // Set style
79 | this.layer.setStyle({variables, color});
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/cancellableRequest.js:
--------------------------------------------------------------------------------
1 | import { AbortController } from '@openeo/js-client';
2 | import Utils from '../utils';
3 |
4 | export class CancellableRequestError extends Error {
5 | constructor(message, title = null, cause = null, close = true, isError = true) {
6 | super(message, {cause});
7 | this.title = title;
8 | this.close = close;
9 | this.isError = isError;
10 | }
11 | }
12 |
13 | export function showCancellableRequestError(vm, error) {
14 | if (error instanceof CancellableRequestError) {
15 | if (error.isError) {
16 | Utils.error(vm, error.message, error.title);
17 | }
18 | else {
19 | Utils.ok(vm, error.message, error.title);
20 | }
21 | }
22 | }
23 |
24 | let runIds = {};
25 | export async function cancellableRequest(vm, callback, entity) {
26 | if (!runIds[entity]) {
27 | runIds[entity] = 1;
28 | }
29 | else {
30 | runIds[entity]++;
31 | }
32 |
33 | const abortController = new AbortController();
34 | const snotifyConfig = Object.assign({}, vm.$config.snotifyDefaults, {
35 | timeout: 0,
36 | type: 'async',
37 | buttons: [{
38 | text: 'Cancel',
39 | action: () => {
40 | abortController.abort();
41 | }
42 | }]
43 | });
44 |
45 | let toast;
46 | const toastTitle = `${entity} #${runIds[entity]}`;
47 | try {
48 | const message = `Processing in progress, please wait...`;
49 | // Pass a promise to snotify that never resolves as we manually close the toast
50 | const endlessPromise = () => new Promise(() => {});
51 | toast = vm.$snotify.async(message, toastTitle, endlessPromise, snotifyConfig);
52 |
53 | await callback(abortController);
54 | } catch(error) {
55 | if (Utils.axios().isCancel(error)) {
56 | throw new CancellableRequestError(`Canceled successfully`, toastTitle, error, false, false);
57 | }
58 | else if (typeof error.message === 'string' && Utils.isObject(error.response) && [400,500].includes(error.response.status)) {
59 | vm.broadcast('viewLogs', [{
60 | id: error.id,
61 | code: error.code,
62 | level: 'error',
63 | message: error.message,
64 | links: error.links || []
65 | }]);
66 | Utils.error(vm, `${entity} failed. Please see the logs for details.`, toastTitle);
67 | }
68 | else {
69 | throw new CancellableRequestError(error.message, toastTitle, error, false);
70 | }
71 | } finally {
72 | if (toast) {
73 | vm.$snotify.remove(toast.id, true);
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/src/components/modals/JobInfoModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Results
11 | Below the metadata for the results of the batch job are shown.
12 |
13 |
14 |
15 |
16 |
17 |
18 | -
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
73 |
74 |
--------------------------------------------------------------------------------
/src/components/maps/ExtentMixin.vue:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/modals/ListModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | No data available.
4 |
5 |
6 | {{ Array.isArray(listItems) ? item : key }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
69 |
70 |
106 |
--------------------------------------------------------------------------------
/src/components/maps/geotiff/fix.js:
--------------------------------------------------------------------------------
1 | import { GeoTIFFImage } from 'geotiff';
2 |
3 | // Integrate changes/fixes from https://github.com/geotiffjs/geotiff.js/pull/303 until released/integrated by geotiff.js
4 | GeoTIFFImage.prototype.getSampleByteSize = function(i) {
5 | if (!this.fileDirectory.BitsPerSample || this.fileDirectory.BitsPerSample.length === 0) {
6 | return;
7 | }
8 | if (i >= this.fileDirectory.BitsPerSample.length) {
9 | i = 0;
10 | }
11 | return Math.ceil(this.fileDirectory.BitsPerSample[i] / 8);
12 | };
13 |
14 | GeoTIFFImage.prototype.getReaderForSample = function(sampleIndex) {
15 | const format = this.getSampleFormat(sampleIndex);
16 | const bitsPerSample = this.getBitsPerSample(sampleIndex);
17 | switch (format) {
18 | case 1: // unsigned integer data
19 | if (bitsPerSample <= 8) {
20 | return DataView.prototype.getUint8;
21 | } else if (bitsPerSample <= 16) {
22 | return DataView.prototype.getUint16;
23 | } else if (bitsPerSample <= 32) {
24 | return DataView.prototype.getUint32;
25 | }
26 | break;
27 | case 2: // twos complement signed integer data
28 | if (bitsPerSample <= 8) {
29 | return DataView.prototype.getInt8;
30 | } else if (bitsPerSample <= 16) {
31 | return DataView.prototype.getInt16;
32 | } else if (bitsPerSample <= 32) {
33 | return DataView.prototype.getInt32;
34 | }
35 | break;
36 | case 3:
37 | switch (bitsPerSample) {
38 | case 16:
39 | return function (offset, littleEndian) {
40 | return getFloat16(this, offset, littleEndian);
41 | };
42 | case 32:
43 | return DataView.prototype.getFloat32;
44 | case 64:
45 | return DataView.prototype.getFloat64;
46 | default:
47 | break;
48 | }
49 | break;
50 | default:
51 | break;
52 | }
53 | throw Error('Unsupported data format/bitsPerSample');
54 | };
55 |
56 | GeoTIFFImage.prototype.getSampleFormat = function(sampleIndex = 0) {
57 | if (!this.fileDirectory.SampleFormat || this.fileDirectory.SampleFormat.length === 0) {
58 | return 1;
59 | }
60 | return typeof this.fileDirectory.SampleFormat[sampleIndex] !== 'undefined'
61 | ? this.fileDirectory.SampleFormat[sampleIndex] : this.fileDirectory.SampleFormat[0];
62 | };
63 |
64 | GeoTIFFImage.prototype.getBitsPerSample = function(sampleIndex = 0) {
65 | if (!this.fileDirectory.BitsPerSample || this.fileDirectory.BitsPerSample.length === 0) {
66 | return;
67 | }
68 | return typeof this.fileDirectory.BitsPerSample[sampleIndex] !== 'undefined'
69 | ? this.fileDirectory.BitsPerSample[sampleIndex] : this.fileDirectory.BitsPerSample[0];
70 | };
71 | // End of geotiff.js fixes
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openeo/web-editor",
3 | "version": "0.14.0",
4 | "apiVersions": [
5 | "1.0.0-rc.2",
6 | "1.0.0",
7 | "1.0.1",
8 | "1.1.0",
9 | "1.2.0"
10 | ],
11 | "author": "openEO Consortium",
12 | "contributors": [
13 | {
14 | "name": "Matthias Mohr"
15 | },
16 | {
17 | "name": "Gustav Jv Rensburg"
18 | },
19 | {
20 | "name": "Miha Kadunc"
21 | },
22 | {
23 | "name": "Christoph Friedrich"
24 | },
25 | {
26 | "name": "Sofian Slimani"
27 | }
28 | ],
29 | "description": "An interactive and easy to use web-based editor for the OpenEO API.",
30 | "license": "Apache-2.0",
31 | "homepage": "http://openeo.org",
32 | "bugs": {
33 | "url": "https://github.com/Open-EO/openeo-web-editor/issues"
34 | },
35 | "repository": {
36 | "type": "git",
37 | "url": "https://github.com/Open-EO/openeo-web-editor.git"
38 | },
39 | "funding": {
40 | "type": "github",
41 | "url": "https://github.com/sponsors/m-mohr"
42 | },
43 | "scripts": {
44 | "pre-build": "node src/build.js",
45 | "start": "npm run pre-build && npx vue-cli-service serve",
46 | "build": "npm run pre-build && npx vue-cli-service build --report"
47 | },
48 | "dependencies": {
49 | "@fortawesome/fontawesome-free": "^6.7.2",
50 | "@kirtandesai/ol-geocoder": "^5.0.6",
51 | "@musement/iso-duration": "^1.0.0",
52 | "@openeo/js-client": "^2.9.0",
53 | "@openeo/js-commons": "^1.5.0",
54 | "@openeo/js-processgraphs": "^1.4.1",
55 | "@openeo/vue-components": "^2.21.0",
56 | "@radiantearth/stac-fields": "^1.5.0",
57 | "@radiantearth/stac-migrate": "^2.0.0",
58 | "@tmcw/togeojson": "^5.5.0",
59 | "ajv": "^6.12.6",
60 | "axios": "^1.0.0",
61 | "chart.js": "^3.7.1",
62 | "chartjs-adapter-luxon": "^1.1.0",
63 | "codemirror": "^5.58.2",
64 | "content-type": "^1.0.4",
65 | "core-js": "^3.7.0",
66 | "fontsource-ubuntu": "^4.0.0",
67 | "jsonlint-mod": "^1.7.6",
68 | "luxon": "^2.4.0",
69 | "node-polyfill-webpack-plugin": "^4.0.0",
70 | "ol": "^9.2.0",
71 | "ol-ext": "^4.0.21",
72 | "proj4": "^2.7.5",
73 | "splitpanes": "^2.3.6",
74 | "v-clipboard": "^2.2.3",
75 | "vue": "^2.7.0",
76 | "vue-chartjs": "^4.0.5",
77 | "vue-multiselect": "^2.1.6",
78 | "vue-snotify": "^3.2.1",
79 | "vue-tour": "^2.0.0",
80 | "vue2-datepicker": "^3.9.0",
81 | "vuedraggable": "^2.24.3",
82 | "vuex": "^3.5.1"
83 | },
84 | "devDependencies": {
85 | "@vue/cli-plugin-babel": "~5.0.8",
86 | "@vue/cli-service": "~5.0.8",
87 | "epsg-index": "^1.0.0",
88 | "sass": "^1.80.0",
89 | "sass-loader": "^14.0.0"
90 | },
91 | "browserslist": [
92 | "> 2%",
93 | "not ie > 0"
94 | ]
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/wizards/tabs/ChooseUserDefinedProcess.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Please select the user-defined process to execute:
4 |
5 |
6 |
7 |
8 | {{ item.id }}
9 | {{ item.title }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
Alternatively, provide a URL to a user-defined process:
17 |
18 |
19 |
20 |
21 |
78 |
79 |
--------------------------------------------------------------------------------
/src/formats/csv.js:
--------------------------------------------------------------------------------
1 | import { SupportedFormat } from './format';
2 |
3 | class CSV extends SupportedFormat {
4 |
5 | constructor(asset, delim = [',', ';']) {
6 | super(asset, 'TableViewer', 'fa-table');
7 | this.delim = delim;
8 | }
9 |
10 | async parseData(data) {
11 | if (typeof data === 'string') {
12 | // Parse CSV
13 | let array = this.parseCSV(data.trim());
14 | // Convert values into numbers, if possible
15 | return array.map(row => row.map(col => {
16 | col = col.trim();
17 | if (col.length === 0) {
18 | return NaN;
19 | }
20 | else if (!isNaN(col)) { // https://stackoverflow.com/a/35759874/9709414
21 | return parseFloat(col);
22 | }
23 | else {
24 | return col;
25 | }
26 | }));
27 | }
28 | return data;
29 | }
30 |
31 | // From https://stackoverflow.com/questions/1293147/example-javascript-code-to-parse-csv-data
32 | parseCSV(str) {
33 | var arr = [];
34 | var quote = false; // 'true' means we're inside a quoted field
35 |
36 | // Iterate over each character, keep track of current row and column (of the returned array)
37 | for (var row = 0, col = 0, c = 0; c < str.length; c++) {
38 | var cc = str[c], nc = str[c+1]; // Current character, next character
39 | arr[row] = arr[row] || []; // Create a new row if necessary
40 | arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary
41 |
42 | // If the current character is a quotation mark, and we're inside a
43 | // quoted field, and the next character is also a quotation mark,
44 | // add a quotation mark to the current column and skip the next character
45 | if (cc == '"' && quote && nc == '"') {
46 | arr[row][col] += cc; ++c;
47 | continue;
48 | }
49 |
50 | // If it's just one quotation mark, begin/end quoted field
51 | if (cc == '"') {
52 | quote = !quote;
53 | continue;
54 | }
55 |
56 | // If it's a elimiter and we're not in a quoted field, move on to the next column
57 | if (this.delim.includes(cc) && !quote) {
58 | ++col;
59 | continue;
60 | }
61 |
62 | // If it's a newline (CRLF) and we're not in a quoted field, skip the next character
63 | // and move on to the next row and move to column 0 of that new row
64 | if (cc == '\r' && nc == '\n' && !quote) {
65 | ++row; col = 0; ++c;
66 | continue;
67 | }
68 |
69 | // If it's a newline (LF or CR) and we're not in a quoted field,
70 | // move on to the next row and move to column 0 of that new row
71 | if (cc == '\n' && !quote) {
72 | ++row; col = 0;
73 | continue;
74 | }
75 | if (cc == '\r' && !quote) {
76 | ++row; col = 0;
77 | continue;
78 | }
79 |
80 | // Otherwise, append the current character to the current column
81 | arr[row][col] += cc;
82 | }
83 | return arr;
84 | }
85 |
86 | }
87 |
88 | export default CSV;
--------------------------------------------------------------------------------
/src/components/FullscreenButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
92 |
93 |
--------------------------------------------------------------------------------
/src/components/datatypes/Duration.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
30 |
82 |
83 |
--------------------------------------------------------------------------------
/src/build.js:
--------------------------------------------------------------------------------
1 | const { ca, en } = require('@musement/iso-duration');
2 | const { e } = require('@radiantearth/stac-fields/helper');
3 |
4 | const fs = require('fs').promises;
5 |
6 | // Generate optimized EPSG code lists
7 | async function build_epsg() {
8 | const epsg = require('epsg-index/all.json');
9 | const names = {};
10 | const proj = {};
11 | for(const code in epsg) {
12 | names[code] = epsg[code].name;
13 |
14 | const entry = [epsg[code].proj4];
15 | const bbox = epsg[code].bbox;
16 | if (Array.isArray(bbox) && bbox[0] != 90 && bbox[1] != -180 && bbox[2] != -90 && bbox[3] != 180) {
17 | entry.push([bbox[1], bbox[2], bbox[3], bbox[0]]);
18 | }
19 | proj[code] = entry;
20 | }
21 |
22 | await fs.writeFile('src/assets/epsg-names.json', JSON.stringify(names));
23 | await fs.writeFile('src/assets/epsg-proj.json', JSON.stringify(proj));
24 | console.log("EPSG export finished!");
25 | }
26 |
27 | // Generate optimized listSpectral Indices
28 | async function build_indices() {
29 | const axios = require('axios');
30 | const response = await axios.get('https://raw.githubusercontent.com/awesome-spectral-indices/awesome-spectral-indices/main/output/spectral-indices-dict.json');
31 | const list = {
32 | domains: [],
33 | indices: []
34 | };
35 | for (const key in response.data.SpectralIndices) {
36 | const val = response.data.SpectralIndices[key];
37 | const domain = val.application_domain;
38 | if (['radar', 'kernel'].includes(domain)) {
39 | continue; // todo: Not supported right now
40 | }
41 | let dix = list.domains.indexOf(domain);
42 | if (dix === -1) {
43 | dix = list.domains.length;
44 | list.domains.push(domain);
45 | }
46 | list.indices.push([
47 | val.short_name,
48 | val.long_name,
49 | dix,
50 | val.bands,
51 | val.formula.replaceAll('**', '^'), // ** (in ASI) = power = ^ (in the Formula parser)
52 | val.reference.replace("https://doi.org/", "")
53 | ]);
54 | }
55 | await fs.writeFile('src/assets/indices.json', JSON.stringify(list));
56 | console.log("Spectral Indices export finished!");
57 | }
58 |
59 | async function ensureDir(path) {
60 | try {
61 | await fs.access(path);
62 | } catch (error) {
63 | await fs.mkdir(path, { recursive: true });
64 | }
65 | }
66 |
67 | async function copy_fonts() {
68 | // Copy Font Awesome
69 | const fontDest = 'public/fontawesome/';
70 | await ensureDir(fontDest);
71 | const faFolder = 'node_modules/@fortawesome/fontawesome-free/';
72 | const faSubfolders = ['webfonts', 'css'];
73 | for (const subfolder of faSubfolders) {
74 | const subfolderPath = faFolder + subfolder + '/';
75 | await ensureDir(fontDest + subfolder + '/');
76 | await fs.cp(subfolderPath, fontDest + subfolder + '/', { recursive: true });
77 | }
78 | console.log(`Fonts copied to ${fontDest}`);
79 | }
80 |
81 | try {
82 | Promise.all([
83 | build_epsg(),
84 | build_indices(),
85 | copy_fonts()
86 | ]);
87 | } catch (error) {
88 | console.error(error);
89 | process.exit(1);
90 | }
--------------------------------------------------------------------------------
/src/formats/format.js:
--------------------------------------------------------------------------------
1 | import Utils from '../utils.js';
2 |
3 | export class Format {
4 |
5 | constructor(asset) {
6 | Object.assign(this, asset);
7 | this.context = null;
8 | }
9 |
10 | setContext(context) {
11 | this.context = context;
12 | }
13 |
14 | getContext() {
15 | return this.context;
16 | }
17 |
18 | getUrl() {
19 | return this.href;
20 | }
21 |
22 | canGroup() {
23 | return false;
24 | }
25 |
26 | isBinary() {
27 | return true;
28 | }
29 |
30 | download(filename = null) {
31 | let tempLink = document.createElement('a');
32 | tempLink.style.display = 'none';
33 | tempLink.href = this.getUrl();
34 | tempLink.setAttribute('download', filename ? filename : Utils.makeFileName("result", this.type));
35 | tempLink.setAttribute('target', '_blank');
36 | document.body.appendChild(tempLink);
37 | tempLink.click();
38 | document.body.removeChild(tempLink);
39 | }
40 |
41 | async loadData(connection) {
42 | if (!this.loaded) {
43 | this.data = await this.fetchData(connection);
44 | this.loaded = true;
45 | }
46 | }
47 |
48 | getData() {
49 | if (!this.loaded) {
50 | throw new Error('Data must be loaded before');
51 | }
52 | return this.data;
53 | }
54 |
55 | async fetchData(connection) {
56 | let blob;
57 | let url = this.getUrl();
58 | if (url.startsWith('blob:')) {
59 | let response = await fetch(url);
60 | blob = await response.blob();
61 | }
62 | else {
63 | let auth = false;
64 | try {
65 | let apiUrl = new URL(connection.getUrl());
66 | let requestUrl = new URL(url);
67 | auth = apiUrl.origin === requestUrl.origin;
68 | } catch (error) {}
69 |
70 | blob = await connection.download(url, auth);
71 | }
72 | let promise = new Promise((resolve, reject) => {
73 | let reader = new FileReader();
74 | reader.onload = event => resolve(event.target.result);
75 | reader.onerror = reject;
76 | if (this.isBinary()) {
77 | reader.readAsBinaryString(blob);
78 | }
79 | else {
80 | reader.readAsText(blob);
81 | }
82 | });
83 | let data = await promise;
84 | return await this.parseData(data);
85 | }
86 |
87 | async parseData(data) {
88 | return data;
89 | }
90 |
91 | }
92 |
93 | export class SupportedFormat extends Format {
94 |
95 | constructor(asset, component = null, icon = 'fa-database', props = {}, events = {}) {
96 | super(asset);
97 | this.loaded = false;
98 | this.component = component;
99 | this.props = props;
100 | if (!this.props.data) {
101 | this.props.data = this;
102 | }
103 | this.icon = icon;
104 | this.events = events;
105 | }
106 |
107 | isBinary() {
108 | return false;
109 | }
110 |
111 | }
112 |
113 | export class UnsupportedFormat extends Format {
114 |
115 | constructor(asset) {
116 | super(asset);
117 | }
118 |
119 | }
120 |
121 | export class FormatCollection extends SupportedFormat {
122 |
123 | }
--------------------------------------------------------------------------------
/src/components/wizards/Download.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
90 |
--------------------------------------------------------------------------------
/src/formats/formatRegistry.js:
--------------------------------------------------------------------------------
1 | import contentType from 'content-type';
2 |
3 | import BrowserImage from '../formats/browserImage';
4 | import CSV from '../formats/csv';
5 | import GeoTIFF from '../formats/geotiff';
6 | import JSON_ from '../formats/json';
7 | import NativeType from './native';
8 | import TSV from '../formats/tsv';
9 | import { UnsupportedFormat } from './format';
10 |
11 | export default class FormatRegistry {
12 |
13 | constructor() {
14 | }
15 |
16 | createFilesFromSTAC(stac, resource = null) {
17 | let files = Object.values(stac.assets)
18 | .filter(asset => !Array.isArray(asset.roles) || !asset.roles.includes("metadata"))
19 | .map(asset => this.createFileFromAsset(asset, stac));
20 | if (resource) {
21 | files.forEach(file => file.setContext(resource));
22 | }
23 | return files;
24 | }
25 |
26 | createFilesFromBlob(data) {
27 | if (!(data instanceof Blob)) {
28 | throw new Error("Given data is not a valid Blob");
29 | }
30 | return this.createFilesFromSTAC({
31 | stac_version: "1.0.0",
32 | type: "Feature",
33 | geometry: null,
34 | properties: {},
35 | links: [],
36 | assets: {
37 | result: {
38 | href: URL.createObjectURL(data),
39 | blob: data,
40 | type: data.type
41 | }
42 | }
43 | });
44 | }
45 |
46 | createFileFromAsset(asset, stac) {
47 | try {
48 | // Detect by media type
49 | if (typeof asset.type === 'string') {
50 | let mime = contentType.parse(asset.type.toLowerCase());
51 | switch(mime.type) {
52 | case 'image/png':
53 | case 'image/jpg':
54 | case 'image/jpeg':
55 | case 'image/gif':
56 | case 'image/webp':
57 | return new BrowserImage(asset);
58 | case 'application/json':
59 | case 'text/json':
60 | case 'application/geo+json':
61 | return new JSON_(asset);
62 | case 'text/plain':
63 | return new NativeType(asset);
64 | case 'text/csv':
65 | return new CSV(asset);
66 | case 'text/tab-separated-values':
67 | return new TSV(asset);
68 | case 'image/tiff':
69 | return new GeoTIFF(asset, stac);
70 | }
71 | }
72 |
73 | // Fallback: Detect by file extension
74 | if (typeof asset.href === 'string') {
75 | let extension = asset.href.split(/[#?]/)[0].split('.').pop().trim().toLowerCase();
76 | switch(extension) {
77 | case 'png':
78 | case 'jpg':
79 | case 'jpeg':
80 | case 'gif':
81 | case 'webp':
82 | return new BrowserImage(asset);
83 | case 'json':
84 | case 'geojson':
85 | return new JSON_(asset);
86 | case 'txt':
87 | return new NativeType(asset);
88 | case 'csv':
89 | return new CSV(asset);
90 | case 'tsv':
91 | return new TSV(asset);
92 | case 'tif':
93 | case 'tiff':
94 | return new GeoTIFF(asset, stac);
95 | }
96 | }
97 |
98 | } catch (error) {
99 | console.log(error);
100 | }
101 |
102 | return new UnsupportedFormat(asset);
103 | }
104 |
105 | }
--------------------------------------------------------------------------------
/src/components/jsonSchema.js:
--------------------------------------------------------------------------------
1 | import { JsonSchemaValidator } from '@openeo/js-processgraphs';
2 | import ajv from 'ajv';
3 | import { Versions } from '@openeo/js-commons';
4 |
5 | var instance = null;
6 |
7 | export default class JsonSchema extends JsonSchemaValidator {
8 |
9 | static create(store) {
10 | if (instance === null) {
11 | instance = new JsonSchema(store);
12 | }
13 | return instance;
14 | }
15 |
16 | constructor(store) {
17 | super();
18 | this.store = store;
19 | this.setFileFormats(this.store.getters.fileFormats);
20 | }
21 |
22 | async validateBandName(data) {
23 | return data.length > 0;
24 | }
25 |
26 | async validateEpsgCode(data) {
27 | await this.store.dispatch('editor/loadEpsgCodes');
28 | if (this.store.state.editor.epsgCodes[data]) {
29 | return true;
30 | }
31 | throw new ajv.ValidationError([{
32 | message: "Invalid EPSG code '" + data + "' specified."
33 | }]);
34 | }
35 |
36 | async validateCollectionId(data) {
37 | if (this.store.state.collections.filter(c => c.id === data).length > 0) {
38 | return true;
39 | }
40 | throw new ajv.ValidationError([{
41 | message: "Collection with id '" + data + "' doesn't exist."
42 | }]);
43 | }
44 |
45 | async validateFilePath(data) {
46 | if (this.store.getters['files/getById'](data)) {
47 | return true;
48 | }
49 | throw new ajv.ValidationError([{
50 | message: "File at '" + data + "' doesn't exist."
51 | }]);
52 | }
53 |
54 | async validateInputFormatOptions(data) {
55 | throw "Not supported";
56 | }
57 |
58 | async validateOutputFormatOptions(data) {
59 | throw "Not supported";
60 | }
61 |
62 | async validateJobId(data) {
63 | if (this.store.getters['jobs/getById'](data)) {
64 | return true;
65 | }
66 | throw new ajv.ValidationError([{
67 | message: "Job with id '" + data + "' doesn't exist."
68 | }]);
69 | }
70 |
71 | async validateUri(data) {
72 | if (data.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/)) {
73 | return true;
74 | }
75 | throw new ajv.ValidationError([{
76 | message: "URI is invalid"
77 | }]);
78 | }
79 |
80 | async validateUdfCode(data) {
81 | // This is no real validation, but most data types don't have line breaks so trying this for now...
82 | if (data.match(/(\r|\n)/)) {
83 | return true;
84 | }
85 | throw new ajv.ValidationError([{
86 | message: "UDF Code is invalid"
87 | }]);
88 | }
89 |
90 | async validateUdfRuntime(data) {
91 | if (data in this.store.state.udfRuntimes) {
92 | return true;
93 | }
94 | throw new ajv.ValidationError([{
95 | message: "UDF runtime '" + data + "' is not supported."
96 | }]);
97 | }
98 |
99 | async validateUdfRuntimeVersion(data) {
100 | // Can't completely check yet whether it's a valid version as I don't know which udf runtime it's for, but for now can check that it's a valid version number
101 | if (Versions.validate(data)) {
102 | return true;
103 | }
104 | throw new ajv.ValidationError([{
105 | message: "UDF runtime version '" + data + "' is not a valid version number."
106 | }]);
107 | }
108 |
109 | }
--------------------------------------------------------------------------------
/src/components/UserWorkspace.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
66 |
67 |
--------------------------------------------------------------------------------
/src/components/datatypes/FileFormatOptionsEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
106 |
107 |
--------------------------------------------------------------------------------
/src/components/wizards/tabs/ChooseProcessingMode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Please select how you'd like to execute this workflow?
5 |
31 |
32 |
33 | The wizard has all information to create the workflow for you.
34 | Unforntunately, this back-end can't process data,
35 | You are not logged in and thus you can't process data directly,
36 | but you can insert the process into the visual model builder now.
37 |
38 |
39 |
40 |
41 |
95 |
96 |
--------------------------------------------------------------------------------
/src/export/r.js:
--------------------------------------------------------------------------------
1 | import Utils from "../utils";
2 | import Exporter from "./exporter";
3 |
4 | const KEYWORDS = [
5 | "if", "else", "repeat", "while", "function", "for", "in", "next", "break",
6 | "true", "false", "null", "inf", "nan", "na", "na_integer_", "na_real_", "na_complex_", "na_character_",
7 | // specific to this generator
8 | "openeo", "connect", "connection", "datacube", "p", "compute_result"
9 | ];
10 |
11 | export default class R extends Exporter {
12 |
13 | createProcessGraphInstance(process) {
14 | let pg = new R(process, this.processRegistry, this.getJsonSchemaValidator());
15 | return this.copyProcessGraphInstanceProperties(pg);
16 | }
17 |
18 | getKeywords() {
19 | return KEYWORDS;
20 | }
21 |
22 | makeNull() {
23 | return "NULL";
24 | }
25 | makeBoolean(val) {
26 | return val ? "TRUE" : "FALSE";
27 | }
28 | makeArray(arr) {
29 | return `list(${arr.join(', ')})`;
30 | }
31 | makeObject(obj) {
32 | let arr = Utils.mapObject(obj, (val, key) => `${this.makeString(key)} = ${val}`);
33 | return `list(${arr.join(', ')})`;
34 | }
35 |
36 | comment(comment) {
37 | this.addCode(comment, '# ');
38 | }
39 |
40 | generateImports() {
41 | this.addCode(`library(openeo)`);
42 | }
43 |
44 | generateConnection() {
45 | this.addCode(`connection = connect(host = "${this.getServerUrl()}")`);
46 | }
47 |
48 | generateAuthentication() {
49 | this.comment(`ToDo: Authentication with login()`);
50 | }
51 |
52 | generateBuilder() {
53 | this.addCode(`p = processes()`);
54 | }
55 |
56 | generateMetadataEntry(key, value) {
57 | this.comment(`${key}: ${this.e(value)}`);
58 | }
59 |
60 | async generateFunction(node) {
61 | let variable = this.var(node.id, this.varPrefix());
62 | let args = await this.generateArguments(node);
63 | if (node.namespace) {
64 | throw new Error("The R client doesn't support namespaced processes yet");
65 | // ToDo: This doesn't seem to be supported in R yet
66 | // args.namespace = this.e(node.namespace);
67 | }
68 | args = Utils.mapObject(args, (value, name) => `${name} = ${this.e(value)}`);
69 |
70 | this.comment(node.description);
71 | this.addCode(`${variable} = p$${node.process_id}(${args.join(', ')})`);
72 | }
73 |
74 | generateMissingParameter(parameter) {
75 | this.comment(parameter.description);
76 | let paramName = this.var(parameter.name, 'param');
77 | let value = typeof parameter.default !== 'undefined' ? parameter.default : null;
78 | this.addCode(`${paramName} = ${this.e(value)}`);
79 | }
80 |
81 | async generateCallback(callback, parameters, variable) {
82 | let isMathFormula = false;
83 | if (isMathFormula) {
84 | // ToDo: Use Formula class, use ExpressionModal code
85 | }
86 | else {
87 | let params = this.generateFunctionParams(parameters);
88 | this.newLine();
89 | this.addCode(`${variable} = function(${params.join(', ')}) {`);
90 | this.addCode(await callback.toCode(true), '', 1);
91 | this.addCode(`}`);
92 | }
93 | }
94 |
95 | generateResult(resultNode, callback) {
96 | if (!resultNode) {
97 | return;
98 | }
99 | let variable = this.var(resultNode.id, this.varPrefix());
100 | if (callback) {
101 | this.addCode(`return(${variable})`);
102 | }
103 | else {
104 | this.addCode(`result = compute_result(graph = ${variable})`);
105 | }
106 | }
107 |
108 | }
--------------------------------------------------------------------------------
/docs/geotiff.md:
--------------------------------------------------------------------------------
1 | # (Cloud-Optimized) GeoTiffs in the Web Editor
2 |
3 | What is required by back-ends to give users an ideal experience with GeoTiff imagery in the Web Editor?
4 |
5 | 1. All GeoTiffs must be valid GeoTiffs (including all required metadata) and should be valid cloud-optimized (COGs). If the files are not exported as COGs, the Editor needs to read the whole GeoTiff file at once which will usually fail with larger files (> 10 MB).
6 | 2. Range requests must be supported by the server to allow reading COGs.
7 | 3. [CORS](https://api.openeo.org/draft/index.html#section/Cross-Origin-Resource-Sharing-(CORS)/CORS-headers) must be sent by the server providing the files, especially you need to allow `Range` headers in `Access-Control-Expose-Headers` additionally.
8 | 4. COGs should provide overviews in the files whenever the files get larger than 1 or 2 MB in size. The overview tiles should have a size of at least 128x128 pixels and at maximum 512x512 pixels.
9 | 5. The GeoTiff must be readable by [geotiff.js](https://geotiffjs.github.io/geotiff.js/), especially the data type and the compression must be supported.
10 | 6. The GeoTiff metadata in the file should contain:
11 | 1. The no-data value in [`TIFFTAG_GDAL_NODATA`](https://gdal.org/drivers/raster/gtiff.html#nodata-value), if applicable
12 | 2. A projection, via the ["geo keys"](http://geotiff.maptools.org/spec/geotiff2.4.html) `ProjectedCSTypeGeoKey` or `GeographicTypeGeoKey` (otherwise provide at least a unit in `ProjLinearUnitsGeoKey` or `GeogAngularUnitsGeoKey`)
13 | 3. GDAL metadata should be provided in the tag [`TIFFTAG_GDAL_METADATA`](https://gdal.org/drivers/raster/gtiff.html#metadata) per band:
14 | 1. Minimum and Maximum values (tags: `STATISTICS_MINIMUM` and `STATISTICS_MAXIMUM`)
15 | 2. A band name (tag: `DESCRIPTION`)
16 | 5. The [`PhotometricInterpretation`](https://www.awaresystems.be/imaging/tiff/tifftags/photometricinterpretation.html) of the image should be set to `1` (BlackIsZero) if an RGB interpretation is not clear. If RGB is set as interpretation (`2`) and you have more than 3 samples per pixel ([`SamplesPerPixel`](https://www.awaresystems.be/imaging/tiff/tifftags/samplesperpixel.html)), the [`ExtraSamples`](https://www.awaresystems.be/imaging/tiff/tifftags/extrasamples.html) should be set.
17 | 6. [`ColorMap`](https://www.awaresystems.be/imaging/tiff/tifftags/colormap.html)s are supported.
18 | 7. For batch jobs, the STAC metadata is recommended to contain per asset:
19 | 1. The no-data value either in `file:nodata` (deprecated) or in `nodata` in `bands`
20 | 2. The `minimum` and `maximum` values per band in the `statistics` object in `bands`
21 | 3. A band `name` in `bands`
22 | 4. The projection in `proj:code` (recommended), `proj:wkt2` (not well supported by OpenLayers) or `proj:proj4` (deprecated by STAC)
23 | 5. The `type` must be set to the corresponding media type (see below)
24 | 8. For synchronous execution, the `Content-Type` in the header of the response must be set to the corresponding media type (see below)
25 |
26 | Links to the corresponding STAC extensions:
27 | - [eo](https://github.com/stac-extensions/eo)
28 | - [file](https://github.com/stac-extensions/file/tree/v1.0.0) (v1.0.0, latest is v2.1.0 but doesn't support nodata any more)
29 | - [proj](https://github.com/stac-extensions/projection)
30 | - [raster](https://github.com/stac-extensions/raster)
31 |
32 | Media Types:
33 | - COGs (`image/tiff; application=geotiff; cloud-optimized=true`)
34 | - GeoTiffs (`image/tiff; application=geotiff`)
35 |
--------------------------------------------------------------------------------
/src/components/modals/ServerInfoModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
File formats
7 |
8 |
9 |
Secondary web services
10 |
11 |
12 |
Runtimes for user-defined functions (UDF)
13 |
14 |
15 |
16 | Processing parameters
17 |
18 |
19 |
22 |
23 |
24 |
27 |
28 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
77 |
78 |
--------------------------------------------------------------------------------
/src/components/WorkPanelMixin.js:
--------------------------------------------------------------------------------
1 | import DataTable from '@openeo/vue-components/components/DataTable.vue';
2 | import Utils from '../utils.js';
3 |
4 | export default (namespace, singular, plural, loadInitially = true) => {
5 | return {
6 | components: {
7 | DataTable
8 | },
9 | data() {
10 | return {
11 | name: singular,
12 | pluralizedName: plural,
13 | syncTimer: null,
14 | lastSyncTime: null
15 | };
16 | },
17 | mounted() {
18 | if (loadInitially) {
19 | this.updateData();
20 | }
21 | },
22 | beforeDestroy() {
23 | this.stopSyncTimer();
24 | },
25 | computed: {
26 | ...Utils.mapGetters(['federation']),
27 | ...Utils.mapState(namespace, {data: namespace}),
28 | ...Utils.mapState(namespace, ['missing', 'pages', 'hasMore']),
29 | ...Utils.mapGetters(namespace, ['supportsList', 'supportsCreate', 'supportsRead', 'supportsUpdate', 'supportsDelete']),
30 | next() {
31 | return this.hasMore ? this.nextPage : null;
32 | }
33 | },
34 | methods: {
35 | ...Utils.mapActions(namespace, ['list', 'nextPage', 'create', 'read', 'update', 'delete']),
36 | getTable() { // To be overridden
37 | return this.$refs && this.$refs.table ? this.$refs.table : null;
38 | },
39 | onShow() {
40 | this.updateData().catch(error => Utils.exception(this, error, `Updating ${plural} failed`));
41 | this.startSyncTimer();
42 | },
43 | onHide() {
44 | this.stopSyncTimer();
45 | },
46 | startSyncTimer() {
47 | if (this.supportsList && this.syncTimer === null) {
48 | this.syncTimer = setInterval(this.updateData, this.getSyncInterval());
49 | }
50 | },
51 | stopSyncTimer() {
52 | if (this.syncTimer !== null) {
53 | clearInterval(this.syncTimer);
54 | this.syncTimer = null;
55 | }
56 | },
57 | getSyncInterval() {
58 | return this.$config.dataRefreshInterval*60*1000; // Refresh data every x minutes
59 | },
60 | async refreshElement(obj, callback = null) {
61 | var old = Object.assign({}, obj);
62 | try {
63 | let updated = await this.read({data: obj});
64 | if (typeof callback === 'function') {
65 | callback(updated, old);
66 | }
67 | } catch(error) {
68 | Utils.exception(this, error, "Load " + singular + " error");
69 | }
70 | },
71 | async reloadData() {
72 | return await this.updateData(true);
73 | },
74 | async updateData(force = false) {
75 | var table = this.getTable();
76 | var nextSyncTime = Date.now() - this.getSyncInterval();
77 | if (!table || (!force && this.lastSyncTime > nextSyncTime)) {
78 | return false;
79 | }
80 | else if (!this.supportsList) {
81 | table.setNoData("Sorry, listing stored " + plural + " is not supported by the server.");
82 | }
83 | else {
84 | var isUpdate = this.data.length > 0;
85 | if (!isUpdate) {
86 | table.setNoData("Loading " + plural + "...");
87 | }
88 | this.lastSyncTime = Date.now();
89 | try {
90 | let data = await this.list();
91 | if(data.length == 0) {
92 | table.setNoData("Add your first " + singular + " here...");
93 | }
94 | return true;
95 | } catch(error) {
96 | if (!isUpdate) {
97 | Utils.exception(this, error);
98 | table.setNoData("Sorry, unable to load data from the server.");
99 | }
100 | else {
101 | console.log(error);
102 | }
103 | }
104 | }
105 | return false;
106 | }
107 | }
108 | };
109 | }
--------------------------------------------------------------------------------
/src/components/datatypes/Kernel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Kernel Size (rows × columns):
5 |
6 | ×
7 |
8 |
9 |
Empty kernel
10 |
25 |
26 |
27 |
28 |
100 |
101 |
--------------------------------------------------------------------------------
/src/components/viewer/ScatterChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ error }}
4 |
5 |
6 |
7 |
8 |
152 |
153 |
--------------------------------------------------------------------------------
/src/components/viewer/LogViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Loading logs...
4 |
5 |
6 |
7 |
8 |
129 |
130 |
136 |
--------------------------------------------------------------------------------
/src/components/maps/ChannelControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
38 |
134 |
135 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // Set this to connect to a back-end automatically when opening the Web Editor,
3 | // so you could set this to https://example.com and then the Web Editor connects
4 | // to the corresponding back-end automatically.
5 | serverUrl: '',
6 |
7 | // The name of the service
8 | serviceName: 'openEO',
9 | // The name of the app
10 | appName: 'Web Editor',
11 |
12 | // Skip login and automatically load up the "discovery mode"
13 | skipLogin: false,
14 |
15 | // Default location for maps
16 | // Default to the center of the EU in Wuerzburg:
17 | // https://en.wikipedia.org/wiki/Geographical_midpoint_of_Europe#Geographic_centre_of_the_European_Union
18 | // The zoom level should show most of Europe
19 | mapLocation: [49.8, 9.9],
20 | mapZoom: 4,
21 |
22 | // OSM Nominatim compliant geocoder URL, remove to disable
23 | geocoder: "https://nominatim.openstreetmap.org/search",
24 |
25 | // A message shown on the login page
26 | loginMessage: '',
27 |
28 | // The logo to show
29 | logo: './logo.png',
30 |
31 | // Defaults for notifications
32 | snotifyDefaults: {
33 | timeout: 10000,
34 | titleMaxLength: 30,
35 | bodyMaxLength: 120,
36 | showProgressBar: true,
37 | closeOnClick: true,
38 | pauseOnHover: true
39 | },
40 |
41 | // List of supported web map services (all lower-cased)
42 | supportedMapServices: [
43 | 'xyz',
44 | 'wmts'
45 | ],
46 |
47 | // List of supported batch job sharing services
48 | supportedBatchJobSharingServices: [
49 | 'ShareEditor',
50 | 'CopyUrl',
51 | 'BlueskyShare',
52 | 'MastodonSocialShare',
53 | 'XShare'
54 | ],
55 |
56 | // List of supported web service sharing services
57 | supportedWebServiceSharingServices: [
58 | 'ShareEditor',
59 | 'CopyUrl',
60 | 'BlueskyShare',
61 | 'MastodonSocialShare',
62 | 'XShare'
63 | ],
64 |
65 | // List of supported wizards
66 | supportedWizards: [
67 | {
68 | component: 'SpectralIndices',
69 | title: 'Compute Spectral Indices',
70 | description: 'A spectral index is a mathematical equation that is applied on the various spectral bands of an image per pixel. It is often used to highlight vegetation, urban areas, snow, burn, soil, or water/drought/moisture stress. Provided by Awesome Spectral Indices (https://github.com/awesome-spectral-indices/awesome-spectral-indices).',
71 | requiredProcesses: ['reduce_dimension']
72 | }
73 | ],
74 |
75 | // Configure the (base)maps
76 | basemaps: [
77 | {
78 | // Title for the basemap
79 | title: "OpenStreetMap",
80 | // Templated URI for the XYZ basemap.
81 | url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png',
82 | // Attributon for the basemap. HTML is allowed.
83 | attributions: '© OpenStreetMap contributors.',
84 | // Maximum zoom level
85 | maxZoom: 19
86 | }
87 | ],
88 |
89 | // Import processes from openeo-community-examples repo
90 | importCommunityExamples: true,
91 |
92 | // Additional process namespaces to load by default
93 | processNamespaces: [],
94 |
95 | // Key is the OIDC provider id, value is the client ID
96 | oidcClientIds: {},
97 |
98 | // Show a warning if HTTP is used instead of HTTPS
99 | showHttpWarning: true,
100 |
101 | // refresh interval for jobs/user data/services etc. in minutes - doesn't apply to logs.
102 | // It's recommended to use a value between 1 and 5 minutes.
103 | dataRefreshInterval: 2,
104 |
105 | // Show or hide experimental and/or deprecated entites by default (e.g. processes, collections)
106 | showExperimentalByDefault: false,
107 | showDeprecatedByDefault: false,
108 |
109 | // number of items to show per page in the UI (jobs, services, files, UDPs) - null to disable pagination
110 | pageLimit: 50,
111 |
112 | };
--------------------------------------------------------------------------------
/src/components/modals/AddMapDataModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
You can add a layer with data from an external data source.
6 |
Internet Adress:
7 |
8 |
9 |
Format:
10 |
24 |
25 |
26 |
27 |
33 |
34 |
35 |
36 |
37 |
124 |
125 |
--------------------------------------------------------------------------------
/docs/oidc.md:
--------------------------------------------------------------------------------
1 | # OIDC setup guidelines for usage with the Web Editor
2 |
3 | The standard authentication mechanism in openEO is OpenID Connect (OIDC).
4 | The openEO Web Editor, being a standard web application, uses the corresponding OIDC flows such as the
5 | [Authorization Code Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth)
6 | to authenticate the user.
7 |
8 | To provide an optimal OIDC authentication user experience in the openEO Web Editor,
9 | it is important for an openEO back-end to properly set up a couple of things:
10 |
11 | - Determine what **OIDC provider(s)** to support.
12 | One can choose from existing OIDC provider services,
13 | or set up their own custom OIDC provider,
14 | (e.g. using [Keycloak](https://www.keycloak.org/)).
15 |
16 | - For at least one (and preferably all) of the supported OIDC providers:
17 | create an OIDC client that can be used as the **default OIDC client**
18 | by users that do not manage their own OIDC client.
19 |
20 | How this OIDC client has to be configured practically,
21 | heavily depends on the OIDC provider,
22 | but here are some general guidelines and constraints
23 | for the OIDC client configuration:
24 |
25 | - (At least) support the **Authorization Code Flow** without client secrets
26 | (which is sometimes labeled as a "public client").
27 | Support for PKCE (Proof Key for Code Exchange) is recommended.
28 |
29 | - **Allow-list the proper redirect URIs** related to your openEO Web Editor deployment.
30 |
31 | For example, if you host the web editor at `https://editor.openeo.example.org/`,
32 | you should allow the redirect URI `https://editor.openeo.example.org`.
33 |
34 | Note that the redirect URIs should be allow-listed *without trailing slashes*.
35 |
36 | Make sure to cover all possible Web Editor domains you want to support with the client.
37 | For example, consider allow-listing:
38 | - `http://localhost:8080` for local development of the web editor
39 | - `https://editor.openeo.org` so that your back-end can be used with the official openEO Web Editor.
40 |
41 | - **Allow-list the proper origins** of your openEO Web Editor deployment.
42 | (In Keycloak based providers, this setting is typically called "Web Origins".)
43 | This ensures that the OIDC provider sets the proper **CORS headers**
44 | so that the openEO Web Editor web app can access the tokens after authentication.
45 | In the rare case that you host the web editor and the OIDC provider on the same domain,
46 | you probably don't have to allow-list any origins.
47 |
48 | For example, if you host the web editor at `https://openeo.example.org/editor`,
49 | you should allow the origin `https://openeo.example.org`.
50 |
51 | Note that an origin by definition is only scheme + domain and optionally a port.
52 | Don't include a path (like `/editor` in the example above),
53 | not even a trailing slash.
54 |
55 | As with the redirect URIs, consider including:
56 | - `http://localhost:8080`
57 | - `https://editor.openeo.org`
58 |
59 | - Handle the `GET /credentials/oidc` endpoint in your openEO back-end,
60 | based on the OIDC providers and OIDC clients discussed above.
61 |
62 | Apart from the full details discussed
63 | in the [`GET /credentials/oidc` specification](https://api.openeo.org/#tag/Account-Management/operation/authenticate-oidc)
64 | consider these additional notes on the `default_clients` items:
65 |
66 | - Include the appropriate grant type under the `grant_types` field:
67 | - `authorization_code` for the Authorization Code Flow
68 | - `authorization_code+pkce` for the Authorization Code Flow with PKCE
69 | - `implicit` for the Implicit Flow (**discouraged**)
70 | - List the same redirect URIs discussed above again under the `redirect_urls` field.
71 | This listing allows the openEO Web Editor to hide authentication options
72 | that won't work because of the redirect URIs configuration.
73 |
--------------------------------------------------------------------------------
/src/components/modals/ExportCodeModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
28 |
29 |
30 |
31 |
102 |
103 |
--------------------------------------------------------------------------------
/src/components/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
135 |
136 |
--------------------------------------------------------------------------------
/src/components/datatypes/GeoJsonEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | To easily import your area of interest, you can drag & drop GeoJSON and KML files into this area.
13 |
14 |
15 |
16 |
17 |
124 |
125 |
126 |
127 |
--------------------------------------------------------------------------------
/src/components/viewer/TableViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
24 | No data retrieved.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
134 |
135 |
167 |
--------------------------------------------------------------------------------
/src/components/modals/CollectionModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
27 |
28 | Individual items are not available for this collection.
29 |
30 |
31 |
32 |
33 |
34 |
110 |
111 |
--------------------------------------------------------------------------------
/src/components/wizards/tabs/ChooseSpectralIndices.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Please select the spectral index you want to compute.
4 |
5 |
6 |
7 |
8 |
9 |
{{ item.summary }}
10 |
13 |
14 |
{{ item.formula }}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
114 |
115 |
--------------------------------------------------------------------------------
/src/export/python.js:
--------------------------------------------------------------------------------
1 | import { createOrUpdateFromFlatCoordinates } from "ol/extent";
2 | import Utils from "../utils";
3 | import Exporter from "./exporter";
4 |
5 | const KEYWORDS = [
6 | 'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif',
7 | 'else', 'except', 'exec', 'finally', 'for', 'from', 'global', 'if', 'import',
8 | 'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try',
9 | 'while', 'with', 'yield',
10 | // specific to this generator
11 | "openeo", "connection", "process", "builder"
12 | ];
13 |
14 | export default class Python extends Exporter {
15 |
16 | createProcessGraphInstance(process) {
17 | let pg = new Python(process, this.processRegistry, this.getJsonSchemaValidator());
18 | return this.copyProcessGraphInstanceProperties(pg);
19 | }
20 |
21 | getKeywords() {
22 | return KEYWORDS;
23 | }
24 |
25 | comment(comment) {
26 | this.addCode(comment, '# ');
27 | }
28 |
29 | generateImports() {
30 | this.addCode(`import openeo`);
31 | this.addCode(`from openeo.processes import process`);
32 | }
33 |
34 | generateConnection() {
35 | this.addCode(`connection = openeo.connect("${this.getServerUrl()}")`);
36 | }
37 |
38 | generateAuthentication() {
39 | this.comment(`ToDo: Here you need to authenticate with authenticate_basic() or authenticate_oidc()`);
40 | }
41 |
42 | generateBuilder() {}
43 |
44 | generateMetadataEntry(key, value) {
45 | this.comment(`${key}: ${this.e(value)}`);
46 | }
47 |
48 | makeNull() {
49 | return "None";
50 | }
51 | makeBoolean(val) {
52 | return val ? "True" : "False";
53 | }
54 |
55 | getTab() {
56 | return ' ';
57 | }
58 |
59 | hasCallbackParameter(node) {
60 | return Boolean(Object.values(node.arguments).find(arg => arg instanceof Python));
61 | }
62 |
63 | async generateFunction(node) {
64 | let variable = this.var(node.id, this.varPrefix());
65 | let builderName;
66 | let addProcessToArguments = true;
67 | let filterDcName = null;
68 | if (node.getParent()) {
69 | builderName = 'process';
70 | }
71 | else {
72 | let startNodes = node.getProcessGraph().getStartNodeIds();
73 | if (startNodes.includes(node.id)) {
74 | if (node.process_id === 'load_collection') {
75 | builderName = 'connection.load_collection';
76 | // Rename id to collection_id until solved: https://github.com/Open-EO/openeo-python-client/issues/223
77 | node.arguments = Object.assign({collection_id: node.arguments.id}, Utils.omitFromObject(node.arguments, ['id']));
78 | addProcessToArguments = false;
79 | }
80 | else {
81 | builderName = 'connection.datacube_from_process';
82 | }
83 | }
84 | else {
85 | let prevNodes = node.getPreviousNodes();
86 | let dcName = this.var(prevNodes[0].id, this.varPrefix());
87 | // If the process has a callback parameter, we need to call the "native" process
88 | // until https://github.com/Open-EO/openeo-python-client/issues/223 is solved
89 | if (this.hasCallbackParameter(node) || node.process_id === 'save_result') {
90 | builderName = `${dcName}.${node.process_id}`;
91 | addProcessToArguments = false;
92 | // If we call the process directly on a new data cube with dcName
93 | // we need to remove the argument that is passing this data
94 | filterDcName = (key, value) => Utils.isObject(value) && value.from_node && this.var(value.from_node, this.varPrefix()) === dcName;
95 | }
96 | else {
97 | builderName = `${dcName}.process`;
98 | }
99 | }
100 | }
101 | let args = await this.generateArguments(node, false, filterDcName);
102 | if (node.namespace) {
103 | args.namespace = this.makeString(node.namespace);
104 | }
105 | args = Utils.mapObject(args, (value, name) => `${name} = ${this.e(value)}`);
106 | if (addProcessToArguments) {
107 | args.unshift(this.makeString(node.process_id));
108 | }
109 |
110 | this.comment(node.description);
111 | this.addCode(`${variable} = ${builderName}(${args.join(', ')})`);
112 | }
113 |
114 | generateMissingParameter(parameter) {
115 | this.comment(parameter.description);
116 | let paramName = this.var(parameter.name, 'param');
117 | let value = typeof parameter.default !== 'undefined' ? parameter.default : null;
118 | this.addCode(`${paramName} = ${this.e(value)}`);
119 | }
120 |
121 | async generateCallback(callback, parameters, variable) {
122 | let params = this.generateFunctionParams(parameters);
123 | if (params.length === 0) {
124 | params.push('builder');
125 | }
126 | this.newLine();
127 | this.addCode(`def ${variable}(${params.join(', ')}):`);
128 | this.addCode(await callback.toCode(true), '', 1);
129 | this.newLine();
130 | }
131 |
132 | generateResult(resultNode, callback) {
133 | if (!resultNode) {
134 | return;
135 | }
136 | let variable = this.var(resultNode.id, this.varPrefix());
137 | if (callback) {
138 | this.addCode(`return ${variable}`);
139 | }
140 | else {
141 | this.addCode(`result = connection.execute(${variable})`);
142 | }
143 | }
144 |
145 | }
--------------------------------------------------------------------------------
/src/components/maps/GeoJsonMapEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
170 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/src/components/datatypes/TemporalPicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
164 |
165 |
--------------------------------------------------------------------------------