├── 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 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/wizards/tabs/ChooseTime.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/SyncButton.vue: -------------------------------------------------------------------------------- 1 | 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 | 16 | -------------------------------------------------------------------------------- /src/components/modals/UdfRuntimeModal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | 38 | -------------------------------------------------------------------------------- /src/components/maps/AddDataControl.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/modals/FileFormatModal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | 38 | -------------------------------------------------------------------------------- /src/components/Collection.vue: -------------------------------------------------------------------------------- 1 | 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 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/wizards/tabs/ChooseBoundingBox.vue: -------------------------------------------------------------------------------- 1 | 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 | 8 | 9 | 37 | 38 | -------------------------------------------------------------------------------- /src/components/modals/JobEstimateModal.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/wizards/components/WizardTab.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/components/modals/ServiceInfoModal.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/share/XShare.vue: -------------------------------------------------------------------------------- 1 | 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 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/share/BlueskyShare.vue: -------------------------------------------------------------------------------- 1 | 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 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/datatypes/Budget.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/share/MastodonSocialShare.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/modals/ProcessModal.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 46 | 47 | -------------------------------------------------------------------------------- /src/components/wizards/tabs/ChooseReducer.vue: -------------------------------------------------------------------------------- 1 | 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 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/TermsOfServiceConsent.vue: -------------------------------------------------------------------------------- 1 | 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 | 4 | 5 | 53 | 54 | 70 | -------------------------------------------------------------------------------- /src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/maps/GeocoderMixin.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/wizards/tabs/ChooseFormat.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/modals/ProcessParameterModal.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 9 | 10 | 58 | 59 | -------------------------------------------------------------------------------- /src/components/modals/ErrorModal.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 73 | 74 | -------------------------------------------------------------------------------- /src/components/maps/MapExtentViewer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 72 | 73 | -------------------------------------------------------------------------------- /src/components/share/CopyUrl.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 71 | 72 | -------------------------------------------------------------------------------- /src/components/share/ShareEditor.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 67 | 68 | -------------------------------------------------------------------------------- /src/components/viewer/DataViewer.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 70 | 71 | 86 | -------------------------------------------------------------------------------- /src/components/modals/ShareModal.vue: -------------------------------------------------------------------------------- 1 | 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 | 17 | 18 | 64 | 65 | -------------------------------------------------------------------------------- /src/components/maps/GeoJsonMixin.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/modals/ParameterModal.vue: -------------------------------------------------------------------------------- 1 | 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 | 27 | 28 | 73 | 74 | -------------------------------------------------------------------------------- /src/components/maps/ExtentMixin.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/modals/ListModal.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 7 | 8 | 92 | 93 | -------------------------------------------------------------------------------- /src/components/datatypes/Duration.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 17 | 18 | 66 | 67 | -------------------------------------------------------------------------------- /src/components/datatypes/FileFormatOptionsEditor.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 106 | 107 | -------------------------------------------------------------------------------- /src/components/wizards/tabs/ChooseProcessingMode.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 27 | 28 | 100 | 101 | -------------------------------------------------------------------------------- /src/components/viewer/ScatterChart.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 152 | 153 | -------------------------------------------------------------------------------- /src/components/viewer/LogViewer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 129 | 130 | 136 | -------------------------------------------------------------------------------- /src/components/maps/ChannelControl.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 30 | 31 | 102 | 103 | -------------------------------------------------------------------------------- /src/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 135 | 136 | -------------------------------------------------------------------------------- /src/components/datatypes/GeoJsonEditor.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/components/viewer/TableViewer.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 134 | 135 | 167 | -------------------------------------------------------------------------------- /src/components/modals/CollectionModal.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 110 | 111 | -------------------------------------------------------------------------------- /src/components/wizards/tabs/ChooseSpectralIndices.vue: -------------------------------------------------------------------------------- 1 | 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 | 7 | 8 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /src/components/datatypes/TemporalPicker.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 164 | 165 | --------------------------------------------------------------------------------