Mercurial > gemma
changeset 1687:54df04e9e974
Merged.
author | Sascha L. Teichmann <teichmann@intevation.de> |
---|---|
date | Sat, 29 Dec 2018 16:07:40 +0100 |
parents | 451c7d3fe6be (current diff) 8f5a5c86f2a9 (diff) |
children | 774174d09d30 |
files | client/src/components/admin/Importqueue.vue client/src/components/admin/Logs.vue client/src/components/admin/Systemconfiguration.vue client/src/components/admin/importschedule/Importschedule.vue client/src/components/admin/importschedule/Importscheduledetail.vue client/src/components/admin/usermanagement/Passwordfield.vue client/src/components/admin/usermanagement/Userdetail.vue client/src/components/admin/usermanagement/Usermanagement.vue client/src/components/map/Identify.vue client/src/components/map/Main.vue client/src/components/map/Maplayer.vue client/src/components/map/Pdftool.vue client/src/components/map/Search.vue client/src/components/map/Zoom.vue client/src/components/map/contextbox/Bottlenecks.vue client/src/components/map/contextbox/Contextbox.vue client/src/components/map/contextbox/ImportSoundingresults.vue client/src/components/map/contextbox/Staging.vue client/src/components/map/fairway/Fairwayprofile.vue client/src/components/map/fairway/Infobar.vue client/src/components/map/fairway/Profiles.vue client/src/components/map/layers/Layers.vue client/src/components/map/layers/Layerselect.vue client/src/components/map/layers/LegendElement.vue client/src/components/map/toolbar/Identify.vue client/src/components/map/toolbar/Layers.vue client/src/components/map/toolbar/Linetool.vue client/src/components/map/toolbar/Pdftool.vue client/src/components/map/toolbar/Polygontool.vue client/src/components/map/toolbar/Profiles.vue client/src/components/map/toolbar/Toolbar.vue cmd/bottlenecks/main.go |
diffstat | 139 files changed, 14083 insertions(+), 6379 deletions(-) [+] |
line wrap: on
line diff
--- a/3rdpartylibs.sh Sat Dec 29 16:06:54 2018 +0100 +++ b/3rdpartylibs.sh Sat Dec 29 16:07:40 2018 +0100 @@ -32,6 +32,9 @@ go get -u -v github.com/jonas-p/go-shp # MIT +go get -u -v github.com/robfig/cron +# MIT + ## list of additional licenses that get fetched and installed as dependencies # github.com/fsnotify/fsnotify/ BSD-3-Clause # github.com/hashicorp/hcl/ MPL-2.0
--- a/client/package.json Sat Dec 29 16:06:54 2018 +0100 +++ b/client/package.json Sat Dec 29 16:07:40 2018 +0100 @@ -1,6 +1,6 @@ { "name": "gemmajs", - "version": "1.99.0-dev", + "version": "2.0.0-dev", "license": "AGPL-3.0-or-later", "repository": { "type": "hg", @@ -11,6 +11,7 @@ "run:both": "concurrently \"../cmd/gemma/gemma\" \"vue-cli-service serve\"", "serve": "VUE_APP_HGREV=$(hg log -r . --template \"{data|shortdate}-{node|short}\") vue-cli-service serve", "build": "VUE_APP_HGREV=$(hg log -r . --template \"{data|shortdate}-{node|short}\") vue-cli-service build", + "analyze": "ANALYZE=true vue-cli-service serve", "lint": "vue-cli-service lint", "test:unit": "vue-cli-service test:unit", "test:e2e": "vue-cli-service test:e2e" @@ -21,6 +22,7 @@ "@fortawesome/free-regular-svg-icons": "^5.5.0", "@fortawesome/free-solid-svg-icons": "^5.5.0", "@fortawesome/vue-fontawesome": "^0.1.2", + "@turf/center": "^6.0.1", "@turf/distance": "^6.0.1", "@turf/helpers": "^6.1.4", "@turf/line-intersect": "^6.0.2", @@ -34,12 +36,14 @@ "locale2": "^2.2.0", "ol": "^5.3.0", "path": "^0.12.7", + "prettier": "^1.15.3", "purgecss-webpack-plugin": "^1.4.0", "v-tooltip": "^2.0.0-rc.33", "vue": "^2.5.16", "vue-clipboard2": "^0.2.1", "vue-color": "^2.6.0", "vue-highlightjs": "^1.3.3", + "vue-js-toggle-button": "^1.3.0", "vue-router": "^3.0.2", "vue-snotify": "^3.2.1", "vuex": "^3.0.1", @@ -59,7 +63,7 @@ "copy-webpack-plugin": "^4.6.0", "easygettext": "^2.7.0", "node-sass": "^4.10.0", - "pretty-quick": "^1.6.0", + "pretty-quick": "^1.8.0", "sass-loader": "^7.0.1", "vue-gettext": "^2.1.1", "vue-template-compiler": "^2.5.17",
--- a/client/src/assets/application.scss Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/assets/application.scss Sat Dec 29 16:07:40 2018 +0100 @@ -57,6 +57,11 @@ transform: translate(-50%, -50%); } +.header { + font-weight: bold; + font-size: 0.9em; +} + .ui-element { pointer-events: auto; }
--- a/client/src/components/App.vue Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/components/App.vue Sat Dec 29 16:07:40 2018 +0100 @@ -87,16 +87,16 @@ } }, components: { - Profiles: () => import("./map/fairway/Profiles"), - Infobar: () => import("./map/fairway/Infobar"), - Pdftool: () => import("./map/Pdftool"), - Zoom: () => import("./map/Zoom"), - Identify: () => import("./map/Identify"), - Layers: () => import("./map/layers/Layers"), + Profiles: () => import("./fairway/Profiles"), + Infobar: () => import("./fairway/Infobar"), + Pdftool: () => import("./Pdftool"), + Zoom: () => import("./Zoom"), + Identify: () => import("./Identify"), + Layers: () => import("./layers/Layers"), Sidebar: () => import("./Sidebar"), - Search: () => import("./map/Search"), - Contextbox: () => import("./map/contextbox/Contextbox"), - Toolbar: () => import("./map/toolbar/Toolbar") + Search: () => import("./Search"), + Contextbox: () => import("./Contextbox"), + Toolbar: () => import("./toolbar/Toolbar") } }; </script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Bottlenecks.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,323 @@ +<template> + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="ship" class="mr-2"></font-awesome-icon> + <translate>Bottlenecks</translate> + </h6> + <div class="row p-2 text-left small"> + <div class="col-5"> + <a href="#" @click="sortBy('name')" class="sort-link"> + <translate>Name</translate> + </a> + <font-awesome-icon + :icon="sortIcon" + class="ml-1" + v-if="sortColumn === 'name'" + ></font-awesome-icon> + </div> + <div class="col-2"> + <a href="#" @click="sortBy('latestMeasurement')" class="sort-link"> + <translate>Latest</translate> <br /> + <translate>Measurement</translate> + </a> + <font-awesome-icon + :icon="sortIcon" + class="ml-1" + v-if="sortColumn === 'latestMeasurement'" + ></font-awesome-icon> + </div> + <div class="col-3"> + <a href="#" @click="sortBy('chainage')" class="sort-link"> + <translate>Chainage</translate> + </a> + <font-awesome-icon + :icon="sortIcon" + class="ml-1" + v-if="sortColumn === 'chainage'" + ></font-awesome-icon> + </div> + <div class="col-2"></div> + </div> + <div + class="bottleneck-list small text-left" + :style="'max-height: ' + (showSplitscreen ? 18 : 35) + 'rem'" + v-if="filteredAndSortedBottlenecks().length" + > + <div + v-for="bottleneck in filteredAndSortedBottlenecks()" + :key="bottleneck.properties.name" + class="border-top row bottleneck-row mx-0" + > + <div class="col-5 py-2 text-left"> + <a href="#" @click="selectBottleneck(bottleneck)">{{ + bottleneck.properties.name + }}</a> + </div> + <div class="col-2 py-2"> + {{ formatSurveyDate(bottleneck.properties.current) }} + </div> + <div class="col-3 py-2"> + {{ + displayCurrentChainage( + bottleneck.properties.from, + bottleneck.properties.to + ) + }} + </div> + <div class="col-2 pr-0 text-right d-flex flex-column"> + <a + class="text-info mt-auto mb-auto mr-2" + @click="loadSurveys(bottleneck.properties.name)" + v-if="bottleneck.properties.current" + > + <font-awesome-icon + icon="spinner" + fixed-width + spin + v-if="loading === bottleneck.properties.name" + ></font-awesome-icon> + <font-awesome-icon + icon="angle-down" + fixed-width + v-if=" + loading !== bottleneck.properties.name && + openBottleneck !== bottleneck.properties.name + " + ></font-awesome-icon> + <font-awesome-icon + icon="angle-up" + fixed-width + v-if=" + loading !== bottleneck.properties.name && + openBottleneck === bottleneck.properties.name + " + ></font-awesome-icon> + </a> + </div> + <div + :class="[ + 'col-12 p-0', + 'surveys', + { open: openBottleneck === bottleneck.properties.name } + ]" + > + <a + href="#" + class="d-block px-3 py-2" + v-for="(survey, index) in openBottleneckSurveys" + :key="index" + @click="selectSurvey(survey, bottleneck)" + >{{ formatSurveyDate(survey.date_info) }}</a + > + </div> + </div> + </div> + <div v-else class="small text-center py-3 border-top"> + <translate>No results.</translate> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors.js"; +import { formatSurveyDate } from "@/lib/date.js"; + +export default { + name: "bottlenecks", + data() { + return { + sortColumn: "name", + sortDirection: "ASC", + openBottleneck: null, + openBottleneckSurveys: null, + loading: null + }; + }, + computed: { + ...mapState("application", [ + "searchQuery", + "showSearchbarLastState", + "showSplitscreen" + ]), + ...mapState("bottlenecks", ["bottlenecks"]), + sortIcon() { + return this.sortDirection === "ASC" + ? "sort-amount-down" + : "sort-amount-up"; + } + }, + methods: { + formatSurveyDate(date) { + return formatSurveyDate(date); + }, + filteredAndSortedBottlenecks() { + return this.bottlenecks + .filter(bn => { + return bn.properties.name + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + }) + .sort((bnA, bnB) => { + switch (this.sortColumn) { + case "name": + if ( + bnA.properties.name.toLowerCase() < + bnB.properties.name.toLowerCase() + ) + return this.sortDirection === "ASC" ? -1 : 1; + if ( + bnA.properties.name.toLowerCase() > + bnB.properties.name.toLowerCase() + ) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + + case "latestMeasurement": { + if ( + (bnA.properties.current || "") < (bnB.properties.current || "") + ) + return this.sortDirection === "ASC" ? -1 : 1; + if ( + (bnA.properties.current || "") > (bnB.properties.current || "") + ) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + } + + case "chainage": + if (bnA.properties.from < bnB.properties.from) + return this.sortDirection === "ASC" ? -1 : 1; + if (bnA.properties.from > bnB.properties.from) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + + default: + return 0; + } + }); + }, + selectSurvey(survey, bottleneck) { + this.$store + .dispatch( + "bottlenecks/setSelectedBottleneck", + bottleneck.properties.name + ) + .then(() => { + this.$store.commit("bottlenecks/selectedSurvey", survey); + }) + .then(() => { + this.$store.commit("map/moveMap", { + coordinates: bottleneck.geometry.coordinates, + zoom: 17, + preventZoomOut: true + }); + }); + }, + selectBottleneck(bottleneck) { + this.$store + .dispatch( + "bottlenecks/setSelectedBottleneck", + bottleneck.properties.name + ) + .then(() => { + this.$store.commit("bottlenecks/setFirstSurveySelected"); + }) + .then(() => { + this.$store.commit("map/moveMap", { + coordinates: bottleneck.geometry.coordinates, + zoom: 17, + preventZoomOut: true + }); + }); + }, + sortBy(column) { + this.sortColumn = column; + this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC"; + }, + loadSurveys(name) { + this.openBottleneckSurveys = null; + if (name === this.openBottleneck) { + this.openBottleneck = null; + } else { + this.openBottleneck = name; + this.loading = name; + + HTTP.get("/surveys/" + name, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + this.openBottleneckSurveys = response.data.surveys.sort((a, b) => { + return a.date_info < b.date_info ? 1 : -1; + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => (this.loading = null)); + } + }, + displayCurrentChainage(from, to) { + return from / 10 + " - " + to / 10; + } + }, + mounted() { + this.$store.dispatch("bottlenecks/loadBottlenecks"); + } +}; +</script> + +<style lang="scss" scoped> +.bottleneck-list { + overflow-y: auto; +} + +.bottleneck-list .bottleneck-row a { + text-decoration: none; +} + +.bottleneck-list .bottleneck-row:hover { + background: #fbfbfb; +} + +.surveys { + max-height: 0; + min-height: 0; + overflow: hidden; +} + +.surveys a:hover { + background: #f3f3f3; +} + +.surveys.open { + max-height: 250px; + overflow: auto; +} + +.sort-link { + color: #444; + font-weight: bold; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Contextbox.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,94 @@ +<template> + <div :class="style"> + <div @click="close" class="ui-element close-contextbox text-muted"> + <font-awesome-icon icon="times"></font-awesome-icon> + </div> + <Bottlenecks v-if="contextBoxContent === 'bottlenecks'"></Bottlenecks> + <Staging v-if="contextBoxContent === 'staging'"></Staging> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "contextbox", + components: { + Bottlenecks: () => import("./Bottlenecks"), + Staging: () => import("./staging/Staging.vue") + }, + computed: { + ...mapState("application", [ + "showSearchbarLastState", + "contextBoxContent", + "showContextBox" + ]), + style() { + return [ + "ui-element shadow-xs contextbox", + { + contextboxcollapsed: !this.showContextBox, + contextboxextended: this.showContextBox, + "rounded-bottom": this.contextBoxContent !== "imports", + rounded: this.contextBoxContent === "imports" + } + ]; + } + }, + methods: { + close() { + this.$store.commit("application/showContextBox", false); + this.$store.commit( + "application/showSearchbar", + this.showSearchbarLastState + ); + } + } +}; +</script> + +<style lang="scss" scoped> +.contextbox { + position: relative; + background-color: #ffffff; + opacity: $slight-transparent; + transition: max-width 0.3s, max-height 0.3s; + overflow: hidden; + background: #fff; +} +.contextbox > div:last-child { + width: 600px; +} + +.contextboxcollapsed { + max-width: 0; + max-height: 0; +} + +.contextboxextended { + max-width: 700px; + max-height: 640px; +} + +.close-contextbox { + position: absolute; + z-index: 2; + right: 0; + top: 7px; + height: $icon-width; + width: $icon-height; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Identify.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,124 @@ +<template> + <div + :class="[ + 'box ui-element rounded bg-white text-nowrap', + { expanded: showIdentify } + ]" + > + <div style="width: 20rem"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="info" class="mr-2"></font-awesome-icon> + <translate>Identified</translate> + <font-awesome-icon + icon="times" + class="ml-auto text-muted" + @click="$store.commit('application/showIdentify', false)" + ></font-awesome-icon> + </h6> + <div class="d-flex flex-column features p-3 flex-grow-1 text-left"> + <div v-if="currentMeasurement"> + <b> + {{ currentMeasurement.quantity }} ({{ + currentMeasurement.unitSymbol + }}): + </b> + <br /> + <small>{{ currentMeasurement.value }}</small> + </div> + <div v-for="(feature, i) of identifiedFeatures" :key="feature.getId()"> + <div v-if="feature.getId()" :class="{ 'mt-2': i }"> + <strong> + {{ + feature.getId().replace(/[.][^.]*$/, "") + /* cut away everything from the last . to the end */ + }}: + </strong> + <small + v-for="(value, key) in prepareProperties(feature)" + :key="key" + > + <div v-if="value">{{ key }}:{{ value }}</div> + </small> + </div> + </div> + <div + v-if="!currentMeasurement && !identifiedFeatures.length" + class="text-muted small text-center my-auto" + > + <translate>No features identified.</translate> + </div> + </div> + <div class="versioninfo border-top p-3 text-left"> + <span v-translate="{ license: 'AGPL-3.0-or-later' }"> + This app uses <i>gemma</i>, which is Free Software under <br /> + %{ license } without warranty, see docs for details. + </span> + <br /> + <a href="https://hg.intevation.de/gemma/file/tip"> + <translate>source-code</translate> + </a> + {{ versionStr }} <br />© via donau. ⓔ Intevation. <br /> + <span v-translate="{ name: 'OpenSteetMap' }" + >Some data © + <a href="https://www.openstreetmap.org/copyright">%{ name }</a> + contributors. + </span> + <p v-translate="{ geoLicense: 'CC-BY-4.0' }"> + Uses + <a href="https://download.geonames.org/export/dump/readme.txt" + >GeoNames</a + > + under %{ geoLicense }. + </p> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.features { + max-height: 19rem; + overflow-y: auto; +} + +.versioninfo { + font-size: 60%; + white-space: normal; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Bernhard E. Reiter <bernhard.reiter@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; + +export default { + name: "identify", + computed: { + ...mapGetters("application", ["versionStr"]), + ...mapState("application", ["showIdentify"]), + ...mapState("map", ["identifiedFeatures", "currentMeasurement"]) + }, + methods: { + prepareProperties(feature) { + // return dict object with propertyname:plainvalue prepared for display + var properties = feature.getProperties(); + delete properties[feature.getGeometryName()]; + return properties; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/ImportSoundingresults.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,394 @@ +<template> + <div class="main d-flex flex-column"> + <div class="d-flex flex-row"> + <Spacer></Spacer> + <div class="card shadow-xs mt-3 mr-3 w-100 importsoundingresultscard"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon icon="upload" class="mr-2"></font-awesome-icon> + <translate class="headline">Import Soundingresults</translate> + </h6> + <div v-if="editState" class="ml-auto mr-auto mt-4 w-95"> + <div class="d-flex flex-column"> + <div class="d-flex flex-row"> + <div class="mt-1 text-left w-50 ml-2 mr-4"> + <small class="text-muted"> + <translate>Bottleneck</translate> + </small> + <select v-model="bottleneck" class="custom-select"> + <option + v-for="bottleneck in availableBottlenecks" + :key="bottleneck" + >{{ bottleneck }}</option + > + </select> + <span class="text-danger"> + <small v-if="!bottleneck"> + <translate>Please select a bottleneck</translate> + </small> + </span> + </div> + <div class="d-flex flex-column mt-1 text-left w-50 mr-2"> + <small class="text-muted"> + <translate>Projection</translate> (EPSG) + </small> + <input + class="form-control" + v-model="projection" + value="4326" + placeholder="e.g. 4326" + type="number" + /> + <span class="text-left text-danger"> + <small v-if="!projection"> + <translate>Please enter a projection</translate> + </small> + </span> + </div> + </div> + <div class="d-flex flex-row"> + <div class="mt-1 text-left w-50 ml-2 mr-4"> + <small class="text-muted"> + <translate>Depthreference</translate> + </small> + <select + v-model="depthReference" + class="custom-select" + id="depthreference" + > + <option + v-for="option in this.$options.depthReferenceOptions" + :key="option" + >{{ option }}</option + > + </select> + <span class="text-left text-danger"> + <small v-if="!depthReference"> + <translate>Please enter a reference</translate> + </small> + </span> + </div> + <div class="mt-1 text-left w-50 mr-2"> + <small class="text-muted"> <translate>Date</translate> </small> + <input + id="importdate" + type="date" + class="form-control" + placeholder="Date of import" + aria-label="bottleneck" + aria-describedby="bottlenecklabel" + v-model="importDate" + /> + <span class="text-left text-danger"> + <small v-if="!importDate"> + <translate>Please enter a date</translate> + </small> + </span> + </div> + </div> + </div> + <div class="ml-2 mt-2 text-left"> + <small v-for="(message, index) in messages" :key="index"> + {{ message }} + </small> + </div> + </div> + <div class="w-95 ml-auto mr-auto mt-4 mb-4"> + <div v-if="uploadState" class="d-flex flex-row input-group mb-4"> + <div class="custom-file"> + <input + accept=".zip" + type="file" + @change="fileSelected" + class="custom-file-input" + id="uploadFile" + /> + <label class="custom-file-label" for="uploadFile"> + {{ uploadLabel }} + </label> + </div> + </div> + <div class="buttons text-right"> + <a + v-if="editState" + download="meta.json" + :href="dataLink" + class="btn btn-outline-info pull-left mt-4" + > + <translate>Download Meta.json</translate> + </a> + <button + v-if="editState" + @click="deleteTempData" + class="btn btn-danger mt-4" + type="button" + > + <translate>Cancel Upload</translate> + </button> + <button + :disabled="disableUploadButton" + @click="submit" + class="btn btn-info mt-4" + type="button" + > + {{ uploadState ? Upload : Confirm }} + </button> + </div> + </div> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { HTTP } from "@/lib/http"; +import { displayError, displayInfo } from "@/lib/errors.js"; +import { mapState } from "vuex"; +import Spacer from "./Spacer"; + +const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; + +export default { + name: "imports", + components: { + Spacer + }, + data() { + return { + importState: IMPORTSTATE.UPLOAD, + depthReference: "", + bottleneck: "", + projection: "", + importDate: "", + uploadLabel: this.$gettext("choose .zip- file"), + uploadFile: null, + disableUpload: false, + token: null, + messages: [] + }; + }, + methods: { + initialState() { + this.importState = IMPORTSTATE.UPLOAD; + this.depthReference = ""; + this.bottleneck = ""; + this.projection = ""; + this.importDate = ""; + this.uploadLabel = this.$gettext("choose .zip- file"); + this.uploadFile = null; + this.disableUpload = false; + this.token = null; + this.messages = []; + }, + fileSelected(e) { + const files = e.target.files || e.dataTransfer.files; + if (!files) return; + this.uploadLabel = files[0].name; + this.uploadFile = files[0]; + }, + deleteTempData() { + HTTP.delete("/imports/soundingresult-upload/" + this.token, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + } + }) + .then(() => { + this.initialState(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + submit() { + if (!this.uploadFile || this.disableUpload) return; + if (this.importState === IMPORTSTATE.UPLOAD) { + this.upload(); + } else { + this.confirm(); + } + }, + upload() { + let formData = new FormData(); + formData.append("soundingresult", this.uploadFile); + HTTP.post("/imports/soundingresult-upload", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(response => { + if (response.data.meta) { + const { bottleneck, date, epsg } = response.data.meta; + const depthReference = response.data.meta["depth-reference"]; + this.bottleneck = bottleneck; + this.depthReference = depthReference; + this.importDate = new Date(date).toISOString().split("T")[0]; + this.projection = epsg; + } + this.importState = IMPORTSTATE.EDIT; + this.token = response.data.token; + this.messages = response.data.messages; + }) + .catch(error => { + const { status, data } = error.response; + const messages = data.messages ? data.messages.join(", ") : ""; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${messages}` + }); + }); + }, + confirm() { + let formData = new FormData(); + formData.append("token", this.token); + if (this.bottleneck) formData.append("bottleneck", this.bottleneck); + if (this.importDate) + formData.append("date", this.importDate.split("T")[0]); + if (this.depthReference) + formData.append("depth-reference", this.depthReference); + if (this.projection) formData.append("", this.projection); + + HTTP.post("/imports/soundingresult", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(() => { + displayInfo({ + title: this.$gettext("Import"), + message: this.$gettext("Starting import for ") + this.bottleneck + }); + this.initialState(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + mounted() { + this.$store.dispatch("bottlenecks/loadBottlenecks"); + }, + watch: { + showContextBox() { + if (!this.showContextBox && this.token) this.deleteTempData(); + } + }, + computed: { + ...mapState("application", ["showContextBox"]), + ...mapState("bottlenecks", ["bottlenecks"]), + disableUploadButton() { + if (this.importState === IMPORTSTATE.UPLOAD) return this.disableUpload; + if ( + !this.bottleneck || + !this.importDate || + !this.depthReference || + !this.projection + ) + return true; + return this.disableUpload; + }, + availableBottlenecks() { + return this.bottlenecks.map(x => x.properties.name); + }, + editState() { + return this.importState === IMPORTSTATE.EDIT; + }, + uploadState() { + return this.importState === IMPORTSTATE.UPLOAD; + }, + Upload() { + return this.$gettext("Upload"); + }, + Confirm() { + return this.$gettext("Confirm"); + }, + dataLink() { + return ( + "data:text/json;charset=utf-8," + + encodeURIComponent( + JSON.stringify({ + depthReference: this.depthReference, + bottleneck: this.bottleneck, + date: this.importDate + }) + ) + ); + } + }, + depthReferenceOptions: [ + "", + // "NAP", + // "KP", + // "FZP", + // "ADR", + // "TAW", + // "PUL", + // "NGM", + // "ETRS", + // "POT", + // "LDC", + // "HDC", + // "ZPG", + // "GLW", + // "HSW", + // "LNW", + // "HNW", + // "IGN", + // "WGS", + "RN" //, + // "HBO" + ] +}; +</script> + +<style lang="scss" scoped> +.importsoundingresultscard { + height: 100%; +} + +.projectionLabel { + margin-left: $small-offset; +} + +.depthreferencelabel { + margin-left: $small-offset; +} + +.offset-r { + margin-right: $small-offset; +} + +.buttons button { + margin-left: $offset !important; +} + +.label-text { + width: 5rem; + text-align: left; + line-height: 2.25rem; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/ImportStretches.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,85 @@ +<template> + <div class="d-flex flex-row"> + <Spacer></Spacer> + <div class="card sysconfig mt-3 shadow-xs w-100 h-100 mr-3"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon + icon="cloud-upload-alt" + class="mr-2" + ></font-awesome-icon> + <translate class="headline">Import streches</translate> + </h6> + <div class="card-body stretches-card"> + <div class="w-95 ml-auto mr-auto mt-4 mb-4"> + <div class="d-flex flex-row input-group mb-4"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div><input class="form-control" type="url" /></div> + </div> + </div> + <div class="buttons text-right"> + <button + :disabled="disableUploadButton" + @click="submit" + class="btn btn-info mt-4" + type="button" + > + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="play" + ></font-awesome-icon> + <translate>Trigger import</translate> + </button> + </div> + </div> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { displayInfo } from "@/lib/errors.js"; + +export default { + name: "importstretches", + data() { + return { + disableUploadButton: false, + uploadLabel: "", + uploadFile: null + }; + }, + methods: { + submit() { + displayInfo({ + title: this.$gettext("Import stretches"), + message: this.$gettext("under construction") + }); + } + }, + components: { + Spacer: () => import("./Spacer") + } +}; +</script> + +<style lang="scss" scoped></style>
--- a/client/src/components/Login.vue Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/components/Login.vue Sat Dec 29 16:07:40 2018 +0100 @@ -3,7 +3,7 @@ <div class="m-5"> <!-- logo section --> <div class="d-flex flex-row justify-content-center mb-3"> - <div class="logo mr-3"><img src="../assets/logo.png" /></div> + <div class="logo mr-3"><img src="@/assets/logo.png" /></div> <div class="title"> <h1>{{ appTitle }}</h1> </div> @@ -127,8 +127,8 @@ * Markus Kottländer <markus@intevation.de > */ import { mapState } from "vuex"; -import { HTTP } from "../lib/http.js"; -import { displayError } from "../lib/errors.js"; +import { HTTP } from "@/lib/http.js"; +import { displayError } from "@/lib/errors.js"; const UNAUTHORIZED = 401;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Logs.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,179 @@ +<template> + <div class="main d-flex flex-column"> + <div class="d-flex flex-row"> + <Spacer></Spacer> + <div class="card logs shadow-xs mt-3 mr-3"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon class="mr-2 fa-fw" icon="book"></font-awesome-icon> + <translate class="headline">Logs</translate> + </h6> + <div class="logoutput text-left bg-white"> + <pre id="code" v-highlightjs="logs"> + <code class="bash hljs hljs-string"></code> + </pre> + </div> + <div class="logmenu"> + <div class="d-flex align-self-center"> + <ul class="nav nav-pills"> + <li class="nav-item"> + <a + :class="accesslogStyle" + @click="fetch('system/log/apache2/access.log', 'accesslog')" + href="#" + > + <translate>Accesslog</translate> + </a> + </li> + <li class="nav-item"> + <a + :class="errorlogStyle" + @click="fetch('system/log/apache2/error.log', 'errorlog')" + href="#" + > + <translate>Errorlog</translate> + </a> + </li> + </ul> + </div> + <div class="statuscontainer d-flex flex-row mb-3"> + <div class="statusline align-self-center"> + <h3><translate>Last refresh:</translate> {{ refreshed }}</h3> + </div> + <div class="refresh"> + <button + @click="fetch(currentFile, currentLog)" + class="btn btn-dark" + > + <translate>Refresh</translate> + </button> + </div> + </div> + </div> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.statuscontainer { + width: 87%; + position: relative; +} + +.logmenu { + position: relative; + margin-left: $offset; + margin-top: $offset; +} + +.logs { + height: 85vh; +} + +#code { + overflow: auto; +} + +.refresh { + position: absolute; + right: $offset; + bottom: 0; +} + +.logoutput { + margin-left: $offset; + margin-right: $offset; + margin-top: $offset; + height: 90%; + overflow: auto; + transition: $transition-fast; +} + +.statusline { + position: absolute; + right: 0; + margin-right: 9rem; + bottom: -0.5rem; +} + +.statuscontainer { + width: 100%; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { mapState } from "vuex"; +import { HTTP } from "@/lib/http.js"; +import "../../node_modules/highlight.js/styles/paraiso-dark.css"; +import Vue from "vue"; +import VueHighlightJS from "vue-highlightjs"; +Vue.use(VueHighlightJS); + +const ACCESSLOG = "accesslog"; +const ERRORLOG = "errorlog"; + +export default { + name: "logs", + components: { + Spacer: () => import("./Spacer") + }, + mounted() { + this.fetch("system/log/apache2/access.log", ACCESSLOG); + }, + data() { + return { + logs: null, + currentLog: null, + currentFile: null, + refreshed: null + }; + }, + methods: { + fetch(file, type) { + HTTP.get(file, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.logs = response.data.content; + this.currentLog = type; + this.refreshed = new Date().toLocaleString(); + this.currentFile = file; + }) + .catch(); + }, + disallow(e) { + e.target.blur(); + } + }, + computed: { + ...mapState("application", ["showSidebar"]), + accesslogStyle() { + return { + active: this.currentLog == ACCESSLOG, + "nav-link": true + }; + }, + errorlogStyle() { + return { + active: this.currentLog == ERRORLOG, + "nav-link": true + }; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Main.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,30 @@ +<template> + <div class="main d-flex flex-column"> + <Maplayer></Maplayer> + <FairwayProfile></FairwayProfile> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +export default { + name: "mainview", + components: { + Maplayer: () => import("./Maplayer"), + FairwayProfile: () => import("./fairway/Fairwayprofile") + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Maplayer.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,383 @@ +<template> + <div id="map" :class="mapStyle"></div> +</template> + +<style lang="scss" scoped> +.nocursor { + cursor: none; +} + +.mapsplit { + height: 50vh; +} + +.mapfull { + height: 100vh; +} + +// the following css part is for browser-printing based pdf generation +@page { + size: A4 landscape !important; + margin: 4mm !important; + // according to https://www.w3.org/TR/css-page-3/#page-size-prop + // we shall now have 210 - 2*4 = 202 mm width and 297 - 2*4 = 289 mm height +} + +@media print { + .mapfull { + width: 2000px; + height: 2828px; + } + .mapsplit { + width: 2000px; + height: 2828px; + } +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * * Thomas Junk <thomas.junk@intevation.de> + * * Bernhard E. Reiter <bernhard.reiter@intevation.de> + */ +import { HTTP } from "@/lib/http"; +import { mapGetters, mapState } from "vuex"; +import "ol/ol.css"; +import { Map, View } from "ol"; +import { WFS, GeoJSON } from "ol/format.js"; +import { Stroke, Style, Fill } from "ol/style.js"; + +/* for the sake of debugging */ +/* eslint-disable no-console */ +export default { + name: "maplayer", + data() { + return { + projection: "EPSG:3857" + }; + }, + computed: { + ...mapGetters("map", ["getLayerByName", "getVSourceByName"]), + ...mapState("map", [ + "extent", + "layers", + "openLayersMap", + "lineTool", + "polygonTool", + "cutTool" + ]), + ...mapState("bottlenecks", ["selectedSurvey"]), + ...mapState("application", ["showSplitscreen"]), + mapStyle() { + return { + mapfull: !this.showSplitscreen, + mapsplit: this.showSplitscreen, + nocursor: this.hasActiveInteractions + }; + }, + hasActiveInteractions() { + return ( + (this.lineTool && this.lineTool.getActive()) || + (this.polygonTool && this.polygonTool.getActive()) || + (this.cutTool && this.cutTool.getActive()) + ); + } + }, + methods: { + buildVectorLoader(featureRequestOptions, endpoint, vectorSource) { + // build a function to be used for VectorSource.setLoader() + // make use of WFS().writeGetFeature to build the request + // and use our HTTP library to actually do it + // NOTE: a) the geometryName has to be given in featureRequestOptions, + // because we want to load depending on the bbox + // b) the VectorSource has to have the option strategy: bbox + featureRequestOptions["outputFormat"] = "application/json"; + var loader = function(extent, resolution, projection) { + featureRequestOptions["bbox"] = extent; + featureRequestOptions["srsName"] = projection.getCode(); + var featureRequest = new WFS().writeGetFeature(featureRequestOptions); + // DEBUG console.log(featureRequest); + HTTP.post( + endpoint, + new XMLSerializer().serializeToString(featureRequest), + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + } + ) + .then(response => { + var features = new GeoJSON().readFeatures( + JSON.stringify(response.data) + ); + vectorSource.addFeatures(features); + // console.log( + // "loaded", + // features.length, + // featureRequestOptions.featureTypes, + // "features" + // ); + // DEBUG console.log("loaded ", features, "for", vectorSource); + // eslint-disable-next-line + }) + .catch(() => { + vectorSource.removeLoadedExtent(extent); + }); + }; + return loader; + }, + updateBottleneckFilter(bottleneck_id, datestr) { + console.log("updating filter with", bottleneck_id, datestr); + const layer = this.getLayerByName("Bottleneck isolines"); + const wmsSrc = layer.data.getSource(); + const exists = bottleneck_id != "does_not_exist"; + + if (exists) { + wmsSrc.updateParams({ + cql_filter: + "date_info='" + + datestr + + "' AND bottleneck_id='" + + bottleneck_id + + "'" + }); + } + layer.isVisible = exists; + layer.data.setVisible(exists); + }, + onBeforePrint(/* evt */) { + // console.log("onBeforePrint(", evt ,")"); + // + // the following code shows how to get the current map canvas + // and change it, however this does not work well enough, as + // another mechanism seems to update the size again before the rendering + // for printing is done: + // console.log(this.openLayersMap.getViewport()); + // var canvas = this.openLayersMap.getViewport().getElementsByTagName("canvas")[0]; + // console.log(canvas); + // canvas.width=1000; + // canvas.height=1414; + // + // An experiment which also did not work: + // this.openLayersMap.setSize([1000, 1414]); // estimate portait DIN A4 + // + // according to documentation + // http://openlayers.org/en/latest/apidoc/module-ol_PluggableMap-PluggableMap.html#updateSize + // "Force a recalculation of the map viewport size. This should be called when third-party code changes the size of the map viewport." + // but did not help + // this.openLayersMap.updateSize(); + }, + onAfterPrint(/* evt */) { + // could be used to undo changes that have been done for printing + // though https://www.tjvantoll.com/2012/06/15/detecting-print-requests-with-javascript/ + // reported that this was not feasable (back then). + // console.log("onAfterPrint(", evt, ")"); + } + }, + watch: { + showSplitscreen() { + const map = this.openLayersMap; + this.$nextTick(() => { + map && map.updateSize(); + }); + }, + selectedSurvey(newSelectedSurvey) { + if (newSelectedSurvey) { + this.updateBottleneckFilter( + newSelectedSurvey.bottleneck_id, + newSelectedSurvey.date_info + ); + } else { + this.updateBottleneckFilter("does_not_exist", "1999-10-01"); + } + } + }, + mounted() { + let map = new Map({ + layers: [...this.layers.map(x => x.data)], + target: "map", + controls: [], + view: new View({ + center: [this.extent.lon, this.extent.lat], + zoom: this.extent.zoom, + projection: this.projection + }) + }); + map.on("moveend", event => { + const center = event.map.getView().getCenter(); + this.$store.commit("map/extent", { + lat: center[1], + lon: center[0], + zoom: event.map.getView().getZoom() + }); + }); + this.$store.dispatch("map/openLayersMap", map); + + // TODO make display of layers more dynamic, e.g. from a list + + // loading the full WFS layer, by not setting the loader function + // and without bboxStrategy + var featureRequest = new WFS().writeGetFeature({ + srsName: "EPSG:3857", + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["fairway_dimensions"], + outputFormat: "application/json" + }); + + // NOTE: loading the full fairway_dimensions makes sure + // that all are available for the intersection with the profile + HTTP.post( + "/internal/wfs", + new XMLSerializer().serializeToString(featureRequest), + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + } + ).then(response => { + this.getVSourceByName("Fairway Dimensions").addFeatures( + new GeoJSON().readFeatures(JSON.stringify(response.data)) + ); + // would scale to the extend of all resulting features + // this.openLayersMap.getView().fit(vectorSrc.getExtent()); + }); + + // load following layers with bboxStrategy (using our request builder) + var layer = null; + + layer = this.getLayerByName("Waterway Area"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featurePrefix: "ws-wamos", + featureTypes: ["ienc_wtware"], + geometryName: "geom" + }, + "/external/d4d", + layer.data.getSource() + ) + ); + + layer = this.getLayerByName("Waterway Axis"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featurePrefix: "ws-wamos", + featureTypes: ["ienc_wtwaxs"], + geometryName: "geom" + }, + "/external/d4d", + layer.data.getSource() + ) + ); + + layer = this.getLayerByName("Distance marks"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featurePrefix: "ws-wamos", + featureTypes: ["ienc_dismar"], + geometryName: "geom" //, + /* restrict loading approximately to extend of danube in Austria */ + // filter: bboxFilter("geom", [13.3, 48.0, 17.1, 48.6], "EPSG:4326") + }, + "/external/d4d", + layer.data.getSource() + ) + ); + layer.data.setVisible(layer.isVisible); + + layer = this.getLayerByName("Distance marks, Axis"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["distance_marks_geoserver"], + geometryName: "geom" + }, + "/internal/wfs", + layer.data.getSource() + ) + ); + + layer = this.getLayerByName("Waterway Area, named"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["hydro_seaare"], + geometryName: "geom" + }, + "/external/d4d", + layer.data.getSource() + ) + ); + layer.data.setVisible(layer.isVisible); + + layer = this.getLayerByName("Bottlenecks"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["bottlenecks"], + geometryName: "area" + }, + "/internal/wfs", + layer.data.getSource() + ) + ); + HTTP.get("/system/style/Bottlenecks/stroke", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.btlnStrokeC = response.data.code; + HTTP.get("/system/style/Bottlenecks/fill", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.btlnFillC = response.data.code; + var newstyle = new Style({ + stroke: new Stroke({ + color: this.btlnStrokeC, + width: 4 + }), + fill: new Fill({ + color: this.btlnFillC + }) + }); + layer.data.setStyle(newstyle); + }) + .catch(error => { + console.log(error); + }); + }) + .catch(error => { + console.log(error); + }); + + window.addEventListener("beforeprint", this.onBeforePrint); + window.addEventListener("afterprint", this.onAfterPrint); + + // so none is shown + this.updateBottleneckFilter("does_not_exist", "1999-10-01"); + this.$store.dispatch("map/enableIdentifyTool"); + this.$store.dispatch("bottlenecks/loadBottlenecks"); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Pdftool.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,130 @@ +<template> + <div + :class="[ + 'box ui-element rounded bg-white text-nowrap', + { expanded: showPdfTool } + ]" + > + <div style="width: 20rem"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="file-pdf" class="mr-2"></font-awesome-icon + ><translate>Generate PDF</translate> + <font-awesome-icon + icon="times" + class="ml-auto text-muted" + @click="$store.commit('application/showPdfTool', false)" + ></font-awesome-icon> + </h6> + <div class="p-3"> + <b><translate>Chose format:</translate></b> + <select v-model="form.format" class="form-control d-block w-100"> + <option value="landscape"><translate>landscape</translate></option> + <option value="portrait"><translate>portrait</translate></option> + </select> + <select v-model="form.paperSize" class="form-control d-block w-100"> + <option value="a3"><translate>ISO A3</translate></option> + <option value="a4"><translate>ISO A4</translate></option> + </select> + <small class="d-block my-2"> + <input + type="radio" + id="pdfexport-downloadtype-download" + value="download" + v-model="form.downloadType" + selected + /> + <label for="pdfexport-downloadtype-download" class="ml-1 mr-2" + ><translate>Download</translate></label + > + <input + type="radio" + id="pdfexport-downloadtype-open" + value="open" + v-model="form.downloadType" + /> + <label for="pdfexport-downloadtype-open" class="ml-1" + ><translate>Open in new window</translate></label + > + </small> + <button + @click="download" + type="button" + class="btn btn-sm btn-info d-block w-100" + > + <translate>Generate PDF</translate> + </button> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * * Markus Kottländer <markus.kottlaender@intevation.de> + * * Bernhard E. Reiter <bernhard@intevation.de> + */ +import { mapState } from "vuex"; + +var paperSizes = { + // in millimeter, landscape [width, height] + a3: [420, 297], + a4: [297, 210] +}; + +export default { + name: "pdftool", + data() { + return { + form: { + format: "landscape", + paperSize: "a4", + downloadType: "download" + } + }; + }, + computed: { + ...mapState("application", ["showPdfTool"]), + ...mapState("bottlenecks", ["selectedSurvey"]) + }, + methods: { + isLandscape() { + return this.form.format !== "portrait"; + }, + download() { + /* eslint-disable no-unused-vars */ + const width = this.isLandscape() + ? paperSizes[this.form.paperSize][0] + : paperSizes[this.form.paperSize][1]; + const height = this.isLandscape() + ? paperSizes[this.form.paperSize][1] + : paperSizes[this.form.paperSize][0]; + + // TODO: replace this src with an API reponse after actually generating PDFs + let src = !this.isLandscape() + ? "/img/PrintTemplate-Var2-Landscape.pdf" + : "/img/PrintTemplate-Var2-Portrait.pdf"; + + let a = document.createElement("a"); + a.href = src; + + if (this.form.downloadType === "download") + a.download = src.substr(src.lastIndexOf("/") + 1); + else a.target = "_blank"; + + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Search.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,290 @@ +<template> + <div :class="searchbarContainerStyle"> + <div class="input-group-prepend m-0 d-print-none"> + <span @click="toggleSearchbar" :class="searchButtonStyle" for="search"> + <font-awesome-icon icon="search"></font-awesome-icon> + </span> + </div> + <div + :class="[ + 'searchgroup', + { + 'searchgroup-collapsed': !showSearchbar, + big: + showContextBox && + ['bottlenecks', 'staging'].indexOf(contextBoxContent) !== -1 + } + ]" + > + <input + @keyup.enter="takeFirstSearchresult" + id="search" + v-model="searchQuery" + type="text" + :class="searchInputStyle" + /> + </div> + <div + v-if="showSearchbar && searchResults !== null && !showContextBox" + class="searchresults border-top ui-element bg-white rounded-bottom d-print-none position-absolute" + > + <div + v-for="entry of searchResults" + :key="entry.name" + class="border-top text-left" + > + <a + href="#" + @click.prevent="moveToSearchResult(entry)" + class="p-2 d-block text-nowrap" + > + <font-awesome-icon + icon="ship" + v-if="entry.type === 'bottleneck'" + class="mr-1" + fixed-width + /> + <font-awesome-icon + icon="water" + v-if="entry.type === 'rhm'" + class="mr-1" + fixed-width + /> + <font-awesome-icon + icon="city" + v-if="entry.type === 'city'" + class="mr-1" + fixed-width + /> + {{ entry.name }} + </a> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.searchcontainer { + opacity: 0.96; +} + +.searchcontainer .searchbar { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.searchgroup { + margin-left: -3px; + transition: width 0.3s; + width: 300px; + overflow: hidden; +} + +.searchgroup.big { + width: 571px; +} + +.searchgroup-collapsed { + width: 0; +} + +.searchbar { + height: 2rem !important; + box-shadow: none !important; +} + +.searchbar.rounded-top-right { + border-radius: 0 !important; + border-top-right-radius: 0.25rem !important; +} + +.searchlabel.rounded-top-left { + border-radius: 0 !important; + border-top-left-radius: 0.25rem !important; +} + +.input-group-text { + height: 2rem; + width: 2rem; +} + +.input-group-prepend svg path { + fill: #666; +} + +.searchresults { + box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2); + top: 2rem; + left: 0; + right: 0; + max-height: 24rem; + overflow: auto; +} + +.searchresults > div:first-child { + border-top: 0 !important; +} + +.searchresults a { + text-decoration: none; +} + +.searchresults a:hover { + background: #f8f8f8; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import debounce from "lodash.debounce"; +import { mapState } from "vuex"; + +import { displayError } from "@/lib/errors.js"; +import { HTTP } from "@/lib/http"; + +const setFocus = () => document.querySelector("#search").focus(); + +export default { + name: "search", + data() { + return { + searchQueryIsDirty: false, + searchResults: null, + isSearching: false + }; + }, + computed: { + ...mapState("application", [ + "showSearchbar", + "showContextBox", + "contextBoxContent" + ]), + searchQuery: { + get() { + return this.$store.state.application.searchQuery; + }, + set(value) { + this.$store.commit("application/searchQuery", value); + } + }, + searchIndicator: function() { + if (this.isSearching) { + return "⟳"; + } else if (this.searchQueryIsDirty) { + return ""; + } else { + return "✓"; + } + }, + searchbarContainerStyle() { + return [ + "input-group searchcontainer shadow-xs", + { + "d-flex": this.contextBoxContent !== "imports", + "d-none": this.contextBoxContent === "imports" && this.showContextBox + } + ]; + }, + searchInputStyle() { + return [ + "form-control ui-element search searchbar d-print-none border-0", + { "rounded-top-right": this.showContextBox || this.searchResults } + ]; + }, + searchButtonStyle() { + return [ + "ui-element input-group-text p-0 d-flex border-0 justify-content-center searchlabel bg-white d-print-none", + { + rounded: !this.showSearchbar, + "rounded-left": this.showSearchbar, + "rounded-top-left": + this.showSearchbar && (this.showContextBox || this.searchResults) + } + ]; + } + }, + watch: { + searchQuery: function() { + this.searchQueryIsDirty = true; + this.triggerSearch(); + } + }, + methods: { + takeFirstSearchresult() { + if (!this.searchResults || this.searchResults.length != 1) return; + this.moveToSearchResult(this.searchResults[0]); + }, + triggerSearch: debounce(function() { + this.doSearch(); + }, 500), + doSearch() { + this.isCalculating = true; + this.searchResults = null; + + if (this.searchQuery == "") { + return; + } + + HTTP.post( + "/search", + { string: this.searchQuery }, + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + } + ) + .then(response => { + this.searchResults = response.data; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + + this.isCalculating = false; + this.searchQueryIsDirty = false; + }, + moveToSearchResult(resultEntry) { + if (resultEntry.geom.type == "Point") { + let zoom = 11; + if (resultEntry.type === "bottleneck") zoom = 17; + if (resultEntry.type === "rhm") zoom = 15; + if (resultEntry.type === "city") zoom = 13; + + this.$store.commit("map/moveMap", { + coordinates: resultEntry.geom.coordinates, + zoom, + preventZoomOut: true + }); + } + // this.searchQuery = ""; // clear search query again + this.toggleSearchbar(); + }, + toggleSearchbar() { + if (!this.showContextBox) { + if (!this.showSearchbar) { + setTimeout(setFocus, 300); + } + this.$store.commit("application/showSearchbar", !this.showSearchbar); + } + } + } +}; +</script>
--- a/client/src/components/Sidebar.vue Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/components/Sidebar.vue Sat Dec 29 16:07:40 2018 +0100 @@ -4,11 +4,12 @@ @click="$store.commit('application/showSidebar', !showSidebar)" class="menubutton ui-element d-print-none p-2 bg-white rounded position-absolute d-flex justify-content-center" > - <font-awesome-icon icon="bars"></font-awesome-icon> + <font-awesome-icon class="fa-fw" icon="bars"></font-awesome-icon> </div> <div class="menu text-nowrap text-left"> <router-link to="/"> <font-awesome-icon + class="fa-fw mr-2" fixed-width icon="map-marked-alt" ></font-awesome-icon> @@ -19,61 +20,100 @@ @click="toggleContextBox('bottlenecks')" href="#" > - <font-awesome-icon fixed-width icon="ship"></font-awesome-icon> + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="ship" + ></font-awesome-icon> <span class="fix-trans-space" v-translate>Bottlenecks</span> </a> <div v-if="isWaterwayAdmin"> <a - :class="['secondary', { active: isActive('imports') }]" - @click="toggleContextBox('imports')" - href="#" - > - <font-awesome-icon fixed-width icon="upload"></font-awesome-icon> - <span class="fix-trans-space" v-translate - >Import soundingresults</span - > - </a> - <a :class="['secondary', { active: isActive('staging') }]" @click="toggleContextBox('staging')" href="#" > <font-awesome-icon + class="fa-fw mr-2" fixed-width icon="clipboard-check" ></font-awesome-icon> <span class="fix-trans-space" v-translate>Staging area</span> </a> + <small class="text-muted pl-3"> <translate>Import</translate> </small> + <hr class="m-0" /> + <router-link to="/importsoundingresults"> + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="upload" + ></font-awesome-icon> + <span class="fix-trans-space" v-translate + >Import soundingresults</span + > + </router-link> + <router-link to="/importstretches" v-if="this.$options.IMPORTSTRETCHES"> + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="cloud-upload-alt" + ></font-awesome-icon> + <span class="fix-trans-space" v-translate>Import stretches</span> + </router-link> + <router-link to="importschedule" v-if="this.$options.IMPORTSCHEDULE"> + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="clock" + ></font-awesome-icon> + <translate class="fix-trans-space">Imports</translate> + </router-link> <small class="text-muted pl-3"> <translate>Systemadministration</translate> </small> <hr class="m-0" /> <router-link to="usermanagement"> - <font-awesome-icon fixed-width icon="users-cog"></font-awesome-icon> + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="users-cog" + ></font-awesome-icon> <span class="fix-trans-space" v-translate>Users</span> </router-link> </div> <div v-if="isSysAdmin"> <router-link to="systemconfiguration"> - <font-awesome-icon fixed-width icon="wrench"></font-awesome-icon> + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="wrench" + ></font-awesome-icon> <span class="fix-trans-space" v-translate>Configuration</span> </router-link> <router-link to="logs"> - <font-awesome-icon fixed-width icon="book"></font-awesome-icon> + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="book" + ></font-awesome-icon> <span class="fix-trans-space" v-translate>Logs</span> </router-link> <router-link to="importqueue"> - <font-awesome-icon fixed-width icon="tasks"></font-awesome-icon> + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="tasks" + ></font-awesome-icon> <span class="fix-trans-space" v-translate>Importqueue</span> </router-link> - <router-link to="importschedule" v-if="this.$options.IMPORTSCHEDULE"> - <font-awesome-icon fixed-width icon="clock"></font-awesome-icon> - <translate class="fix-trans-space">Importschedule</translate> - </router-link> </div> <hr class="m-0" /> <a @click="logoff" href="#"> - <font-awesome-icon fixed-width icon="power-off"></font-awesome-icon> + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="power-off" + ></font-awesome-icon> <span class="fix-trans-space" v-translate>Logout</span> {{ user }} </a> </div> @@ -96,11 +136,22 @@ * Markus Kottländer <markus.kottlaender@intevation.de> */ import { mapGetters, mapState } from "vuex"; -import app from "../main"; +import app from "@/main"; export default { name: "sidebar", props: ["routeName"], + watch: { + $route() { + const { review } = this.$route.query; + if (review) { + this.toggleContextBox("staging"); + this.$store.commit("imports/setImportToReview", review); + } else { + this.$store.commit("imports/setImportToReview", -99); + } + } + }, computed: { ...mapGetters("user", ["isSysAdmin", "isWaterwayAdmin"]), ...mapState("user", ["user"]), @@ -121,6 +172,7 @@ } }, IMPORTSCHEDULE: process.env.VUE_APP_FEATURE_IMPORTSCHEDULE, + IMPORTSTRETCHES: process.env.VUE_APP_FEATURE_IMPORTSTRETCHES, methods: { logoff() { app.$snotify.clear(); @@ -129,7 +181,7 @@ this.$router.push("/login"); }, toggleContextBox(context) { - this.$router.push("/"); + if (this.$route.path !== "/") this.$router.push("/"); this.$store.commit("application/showContextBox", true); this.$store.commit("application/contextBoxContent", context); this.$store.commit("application/showSearchbar", true);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Spacer.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,50 @@ +<template> + <div :class="room"></div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { mapState } from "vuex"; +export default { + name: "spacer", + computed: { + ...mapState("application", ["showSidebar"]), + room() { + return [ + "spacer ml-3", + { + "spacer-expanded": this.showSidebar, + "spacer-collapsed": !this.showSidebar + } + ]; + } + } +}; +</script> + +<style lang="scss" scoped> +.spacer { + height: 90vh; +} + +.spacer-collapsed { + min-width: $icon-width + $offset; + transition: $transition-fast; +} + +.spacer-expanded { + min-width: $sidebar-width + $offset; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Systemconfiguration.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,171 @@ +<template> + <div class="d-flex flex-row"> + <Spacer></Spacer> + <div class="card sysconfig mt-3 shadow-xs"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon icon="wrench" class="mr-2"></font-awesome-icon> + <translate class="headline">Systemconfiguration</translate> + </h6> + <div class="card-body config"> + <section class="configsection"> + <h4 class="card-title"> + <translate>Bottleneck Areas stroke-color</translate> + </h4> + <compact-picker v-model="strokeColor" /> + </section> + <section> + <h4 class="card-title"> + <translate>Bottleneck Areas fill-color</translate> + </h4> + <chrome-picker v-model="fillColor" /> + </section> + <div class="sendbutton"> + <a @click.prevent="submit" class="btn btn-info text-white"> + <translate>Send</translate> + </a> + </div> + </div> + <!-- card-body --> + </div> + </div> +</template> + +<style scoped lang="scss"> +.config { + text-align: left; +} + +.configsection { + margin-bottom: $large-offset; +} + +.sendbutton { + position: absolute; + right: $offset; + bottom: $offset; +} + +.inputs { + margin-left: auto; + margin-right: auto; +} + +.sysconfig { + margin-right: $offset; + width: 100%; + height: 100%; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Bernhard Reiter <bernhard@intevation.de> + */ +import { Chrome } from "vue-color"; +import { Compact } from "vue-color"; + +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors.js"; +import { mapState } from "vuex"; + +export default { + name: "systemconfiguration", + data() { + return { + sent: false, + strokeColor: { r: 0, g: 0, b: 0, a: 1.0 }, + fillColor: { r: 0, g: 0, b: 0, a: 1.0 }, + currentConfig: null + }; + }, + components: { + "chrome-picker": Chrome, + "compact-picker": Compact, + Spacer: () => import("./Spacer") + }, + computed: { + ...mapState("application", ["showSidebar"]) + }, + methods: { + submit() { + HTTP.put("/system/style/Bottlenecks/stroke", this.strokeColor.rgba, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then() + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + + HTTP.put("/system/style/Bottlenecks/fill", this.fillColor.rgba, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then() + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + mounted() { + HTTP.get("/system/style/Bottlenecks/stroke", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + this.strokeColor = response.data.colour; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + + HTTP.get("/system/style/Bottlenecks/fill", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + this.fillColor = response.data.colour; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Zoom.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,76 @@ +<template> + <div + class="d-flex buttoncontainer shadow-xs mb-3 position-absolute" + :style="showSplitscreen ? 'margin-bottom: 51vh !important' : ''" + > + <button + class="zoomButton border-0 bg-white rounded-left ui-element" + @click="zoomOut" + > + <font-awesome-icon icon="minus"></font-awesome-icon> + </button> + <button + class="zoomButton border-0 bg-white rounded-right ui-element border-right" + @click="zoomIn" + > + <font-awesome-icon icon="plus"></font-awesome-icon> + </button> + </div> +</template> + +<style lang="scss" scoped> +.buttoncontainer { + bottom: 0; + left: 50%; + margin-left: -$icon-width; +} + +.zoomButton { + min-height: $icon-width; + min-width: $icon-width; + z-index: 1; + outline: none; + color: #666; +} +</style> +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus@intevation.de> + * Thomas Junk <thomas.junk@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "zoom", + computed: { + ...mapState("map", ["openLayersMap"]), + ...mapState("application", ["showSplitscreen"]), + zoomLevel: { + get() { + return this.openLayersMap.getView().getZoom(); + }, + set(value) { + this.openLayersMap.getView().animate({ zoom: value, duration: 300 }); + } + } + }, + methods: { + zoomIn() { + this.zoomLevel = this.zoomLevel + 1; + }, + zoomOut() { + this.zoomLevel = this.zoomLevel - 1; + } + } +}; +</script>
--- a/client/src/components/admin/Importqueue.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,283 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <div :class="spacerStyle"></div> - <div class="mt-3"> - <div class="card importqueuecard shadow-xs"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon icon="tasks" class="mr-2"></font-awesome-icon> - <translate class="headline">Importqueue</translate> - </h6> - <div class="card-body importcardbody"> - <div class="card-body importcardbody"> - <div class="searchandfilter d-flex flex-row"> - <div class="searchgroup input-group"> - <div class="input-group-prepend"> - <span class="input-group-text" id="search"> - <font-awesome-icon icon="search"></font-awesome-icon> - </span> - </div> - <input - v-model="searchQuery" - type="text" - class="form-control" - placeholder - aria-label="Search" - aria-describedby="search" - /> - </div> - <div class="filters"> - <button - @click="setFilter('successful')" - :class="successfulStyle" - > - <translate>Successful</translate> - </button> - <button @click="setFilter('failed')" :class="failedStyle"> - <translate>Failed</translate> - </button> - <button @click="setFilter('pending')" :class="pendingStyle"> - <translate>Pending</translate> - </button> - <button @click="setFilter('rejected')" :class="rejectedStyle"> - <translate>Rejected</translate> - </button> - <button @click="setFilter('accepted')" :class="acceptedStyle"> - <translate>Accepted</translate> - </button> - </div> - </div> - <table class="table"> - <thead> - <tr> - <th><translate>Enqueued</translate></th> - <th><translate>Kind</translate></th> - <th><translate>User</translate></th> - <th><translate>Signer</translate></th> - <th><translate>State</translate></th> - </tr> - </thead> - <tbody> - <tr v-for="job in filteredImports" :key="job.id"> - <td>{{ job.enqueued }}</td> - <td>{{ job.kind }}</td> - <td>{{ job.user }}</td> - <td>{{ job.signer }}</td> - <td>{{ job.state }}</td> - </tr> - </tbody> - </table> - <div> - <button @click="refresh" class="btn btn-info refresh"> - <translate>Refresh</translate> - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus@intevation.de> - */ -import { displayError } from "../../lib/errors.js"; -import { mapState } from "vuex"; - -export default { - name: "importqueue", - data() { - return { - searchQuery: "", - successful: false, - failed: false, - pending: false, - rejected: false, - accepted: false - }; - }, - mounted() { - this.loadQueue(); - }, - methods: { - setFilter(name) { - this[name] = !this[name]; - const allSet = - this.successful && - this.failed && - this.pending && - this.accepted && - this.rejected; - if (allSet) { - this.successful = false; - this.failed = false; - this.pending = false; - this.accepted = false; - this.rejected = false; - } - }, - loadQueue() { - this.$store.dispatch("imports/getImports").catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - refresh() { - this.loadQueue(); - } - }, - computed: { - ...mapState("imports", ["imports"]), - ...mapState("application", ["showSidebar"]), - filteredImports() { - const filtered = this.imports - .filter(element => { - if (!this.searchQuery) return true; - return [(element.kind, element.user, element.enqueued)].some(x => { - return x.toLowerCase().includes(this.searchQuery.toLowerCase()); - }); - }) - .filter(y => { - if ( - !this.successful && - !this.failed && - !this.pending && - !this.accepted && - !this.rejected - ) - return true; - let filterCriteria = []; - if (this.successful) filterCriteria.push("successful"); - if (this.failed) filterCriteria.push("failed"); - if (this.pending) filterCriteria.push("pending"); - if (this.accepted) filterCriteria.push("accepted"); - if (this.rejected) filterCriteria.push("rejected"); - const result = filterCriteria.map(selectedState => { - return y.state === selectedState; - }); - return result.some(x => x); - }); - return filtered; - }, - spacerStyle() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - }, - successfulStyle() { - return { - btn: true, - "btn-light": !this.successful, - "btn-dark": this.successful - }; - }, - pendingStyle() { - return { - btn: true, - "btn-light": !this.pending, - "btn-dark": this.pending - }; - }, - failedStyle() { - return { - btn: true, - "btn-light": !this.failed, - "btn-dark": this.failed - }; - }, - rejectedStyle() { - return { - btn: true, - "btn-light": !this.rejected, - "btn-dark": this.rejected - }; - }, - acceptedStyle() { - return { - btn: true, - "btn-light": !this.accepted, - "btn-dark": this.accepted - }; - } - } -}; -</script> - -<style lang="scss" scoped> -.refresh { - position: absolute; - right: $offset; - bottom: $offset; -} - -.spacer { - height: 100vh; -} - -.spacer-collapsed { - min-width: $icon-width + $offset; - transition: $transition-fast; -} - -.spacer-expanded { - min-width: $sidebar-width + $offset; -} - -.importqueuecard { - width: 75vw; - min-height: 20rem; -} - -.card-body { - width: 100%; - margin-left: auto; - margin-right: auto; -} - -.searchandfilter { - position: relative; - margin-bottom: $xx-large-offset; -} - -.filters { - position: absolute; - right: 0; -} - -.filters button { - margin-right: $small-offset; -} - -.table td, -.table th { - border-top: 0 !important; - text-align: left; - padding: $small-offset !important; -} - -.searchgroup { - position: absolute; - left: 0; - width: 45%; -} -</style>
--- a/client/src/components/admin/Logs.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,198 +0,0 @@ -<template> - <div class="main d-flex flex-column"> - <div class="d-flex flex-row"> - <div :class="spacer"></div> - <div class="card logs shadow-xs mt-3 mr-3"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon class="mr-2" icon="book"></font-awesome-icon> - <translate class="headline">Logs</translate> - </h6> - <div class="logoutput text-left bg-white"> - <pre id="code" v-highlightjs="logs"> - <code class="bash hljs hljs-string"></code> - </pre> - </div> - <div class="logmenu"> - <div class="d-flex align-self-center"> - <ul class="nav nav-pills"> - <li class="nav-item"> - <a - :class="accesslogStyle" - @click="fetch('system/log/apache2/access.log', 'accesslog')" - href="#" - > - <translate>Accesslog</translate> - </a> - </li> - <li class="nav-item"> - <a - :class="errorlogStyle" - @click="fetch('system/log/apache2/error.log', 'errorlog')" - href="#" - > - <translate>Errorlog</translate> - </a> - </li> - </ul> - </div> - <div class="statuscontainer d-flex flex-row mb-3"> - <div class="statusline align-self-center"> - <h3><translate>Last refresh:</translate> {{ refreshed }}</h3> - </div> - <div class="refresh"> - <button - @click="fetch(currentFile, currentLog)" - class="btn btn-dark" - > - <translate>Refresh</translate> - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.statuscontainer { - width: 87%; - position: relative; -} - -.logmenu { - position: relative; - margin-left: $offset; - margin-top: $offset; -} - -.logs { - height: 85vh; -} - -#code { - overflow: auto; -} - -.refresh { - position: absolute; - right: $offset; - bottom: 0; -} - -.logoutput { - margin-left: $offset; - margin-right: $offset; - margin-top: $offset; - height: 90%; - overflow: auto; - transition: $transition-fast; -} - -.spacer { - height: 90vh; -} - -.spacer-collapsed { - min-width: $icon-width + $offset; - transition: $transition-fast; -} - -.spacer-expanded { - min-width: $sidebar-width + $offset; -} - -.statusline { - position: absolute; - right: 0; - margin-right: 9rem; - bottom: -0.5rem; -} - -.statuscontainer { - width: 100%; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { mapState } from "vuex"; -import { HTTP } from "../../lib/http.js"; -import "../../../node_modules/highlight.js/styles/paraiso-dark.css"; -import Vue from "vue"; -import VueHighlightJS from "vue-highlightjs"; -Vue.use(VueHighlightJS); - -const ACCESSLOG = "accesslog"; -const ERRORLOG = "errorlog"; - -export default { - name: "logs", - mounted() { - this.fetch("system/log/apache2/access.log", ACCESSLOG); - }, - data() { - return { - logs: null, - currentLog: null, - currentFile: null, - refreshed: null - }; - }, - methods: { - fetch(file, type) { - HTTP.get(file, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - this.logs = response.data.content; - this.currentLog = type; - this.refreshed = new Date().toLocaleString(); - this.currentFile = file; - }) - .catch(); - }, - disallow(e) { - e.target.blur(); - } - }, - computed: { - ...mapState("application", ["showSidebar"]), - accesslogStyle() { - return { - active: this.currentLog == ACCESSLOG, - "nav-link": true - }; - }, - errorlogStyle() { - return { - active: this.currentLog == ERRORLOG, - "nav-link": true - }; - }, - spacer() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - } - } -}; -</script>
--- a/client/src/components/admin/Systemconfiguration.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,190 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <div :class="spacerStyle"></div> - <div class="card sysconfig mt-3 shadow-xs"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon icon="wrench" class="mr-2"></font-awesome-icon> - <translate class="headline">Systemconfiguration</translate> - </h6> - <div class="card-body config"> - <section class="configsection"> - <h4 class="card-title"> - <translate>Bottleneck Areas stroke-color</translate> - </h4> - <compact-picker v-model="strokeColor" /> - </section> - <section> - <h4 class="card-title"> - <translate>Bottleneck Areas fill-color</translate> - </h4> - <chrome-picker v-model="fillColor" /> - </section> - <div class="sendbutton"> - <a @click.prevent="submit" class="btn btn-info text-white"> - <translate>Send</translate> - </a> - </div> - </div> - <!-- card-body --> - </div> - </div> -</template> - -<style scoped lang="scss"> -.config { - text-align: left; -} - -.configsection { - margin-bottom: $large-offset; -} - -.sendbutton { - position: absolute; - right: $offset; - bottom: $offset; -} - -.inputs { - margin-left: auto; - margin-right: auto; -} - -.sysconfig { - width: 30vw; - height: 100%; -} - -.spacer { - height: 100vh; -} - -.spacer-collapsed { - min-width: $icon-width + $offset; - transition: $transition-fast; -} - -.spacer-expanded { - min-width: $sidebar-width + $offset; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Bernhard Reiter <bernhard@intevation.de> - */ -import { Chrome } from "vue-color"; -import { Compact } from "vue-color"; - -import { HTTP } from "../../lib/http"; -import { displayError } from "../../lib/errors.js"; -import { mapState } from "vuex"; -export default { - name: "systemconfiguration", - data() { - return { - sent: false, - strokeColor: { r: 0, g: 0, b: 0, a: 1.0 }, - fillColor: { r: 0, g: 0, b: 0, a: 1.0 }, - currentConfig: null - }; - }, - components: { - "chrome-picker": Chrome, - "compact-picker": Compact - }, - computed: { - ...mapState("application", ["showSidebar"]), - spacerStyle() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - } - }, - methods: { - submit() { - HTTP.put("/system/style/Bottlenecks/stroke", this.strokeColor.rgba, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then() - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - - HTTP.put("/system/style/Bottlenecks/fill", this.fillColor.rgba, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then() - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - mounted() { - HTTP.get("/system/style/Bottlenecks/stroke", { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - this.strokeColor = response.data.colour; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - - HTTP.get("/system/style/Bottlenecks/fill", { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - this.fillColor = response.data.colour; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } -}; -</script>
--- a/client/src/components/admin/importschedule/Importschedule.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <div :class="spacerStyle"></div> - <div class="mt-3"> - <div class="card schedulecard shadow-xs"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon icon="clock" class="mr-2"></font-awesome-icon> - <translate class="headline">Importschedule</translate> - </h6> - <div class="card-body schedulecardbody"> - <div class="card-body schedulecardbody"> - <div class="searchandfilter d-flex flex-row"> - <div class="searchgroup input-group"> - <div class="input-group-prepend"> - <span class="input-group-text" id="search"> - <font-awesome-icon icon="search"></font-awesome-icon> - </span> - </div> - <input - v-model="searchQuery" - type="text" - class="form-control" - placeholder - aria-label="Search" - aria-describedby="search" - /> - </div> - </div> - <table v-if="schedules.length" class="table"> - <thead> - <tr> - <th><translate>Import</translate></th> - <th><translate>Type</translate></th> - <th><translate>Author</translate></th> - <th><translate>Schedule</translate></th> - <th><translate>Email</translate></th> - <th> </th> - <th> </th> - </tr> - </thead> - <tbody> - <tr v-for="(schedule, index) in schedules" :key="index"> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - <td> - <font-awesome-icon - icon="pencil-alt" - fixed-width - ></font-awesome-icon> - </td> - <td> - <font-awesome-icon - @click="deleteSchedule" - icon="trash" - fixed-width - ></font-awesome-icon> - </td> - </tr> - </tbody> - </table> - <div v-else class="mt-4 small text-center py-3"> - <translate>No schedules</translate> - </div> - <button - @click="newImport" - class="btn btn-info position-absolute newbutton" - > - <translate>New Import</translate> - </button> - </div> - </div> - </div> - </div> - <Importscheduledetail></Importscheduledetail> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import { mapState } from "vuex"; -import Importscheduledetail from "./Importscheduledetail"; -//import { SCHEDULES } from "../../store/imports.js"; - -export default { - name: "importschedule", - components: { - Importscheduledetail - }, - data() { - return { - searchQuery: "" - }; - }, - methods: { - newImport() { - this.$store.commit("imports/setImportScheduleDetailVisible"); - }, - deleteSchedule(index) { - this.$store.commit("imports/deleteSchedule", index); - } - }, - computed: { - ...mapState("application", ["showSidebar"]), - ...mapState("imports", ["schedules"]), - spacerStyle() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - } - } -}; -</script> - -<style lang="scss" scoped> -.spacer { - height: 100vh; -} - -.spacer-collapsed { - min-width: $icon-width + $offset; - transition: $transition-fast; -} - -.spacer-expanded { - min-width: $sidebar-width + $offset; -} - -.schedulecard { - width: 40vw; - min-height: 20rem; -} - -.schedulecard-body { - width: 100%; - margin-left: auto; - margin-right: auto; -} - -.newbutton { - position: absolute; - bottom: $offset; - right: $offset; -} -</style>
--- a/client/src/components/admin/importschedule/Importscheduledetail.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -<template> - <div class="importscheduledetails" v-if="importScheduleDetailVisible"> - <div class="card shadow-xs"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <translate>New import</translate> - <span @click="closeDetailview" class="closebutton"> - <font-awesome-icon icon="times"></font-awesome-icon> - </span> - </h6> - <div class="card-body"> - <h1>lalala</h1> - <form @submit.prevent="save" class="ml-3"></form> - </div> - </div> - </div> -</template> - -<script> -import { mapState } from "vuex"; - -export default { - name: "importscheduledetail", - computed: { - ...mapState("imports", ["importScheduleDetailVisible"]) - }, - methods: { - closeDetailview() { - this.$store.commit("imports/clearImportScheduleDetail"); - this.$store.commit("imports/setImportScheduleDetailInvisible"); - } - } -}; -</script> - -<style lang="scss" scoped> -.importscheduledetails { - margin-top: $offset; - margin-left: $offset; -} -</style>
--- a/client/src/components/admin/usermanagement/Passwordfield.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +0,0 @@ -<template> - <div> - <label for="password">{{ this.label }}</label> - <div class="d-flex d-row"> - <input - :type="isPasswordVisible" - @change="fieldChanged" - class="form-control" - :placeholder="placeholder" - :required="required" - /> - <span class="input-group-text" @click="showPassword"> - <font-awesome-icon - :icon="readablePassword ? 'eye-slash' : 'eye'" - ></font-awesome-icon> - </span> - </div> - <div v-show="passworderrors" class="text-danger"> - <small> - <font-awesome-icon icon="exclamation-triangle"></font-awesome-icon> - {{ this.passworderrors }} - </small> - </div> - </div> -</template> - -<style> -/* FIXME does not work here, unclear why, so added to Login.vue -input[type="password"]::-ms-reveal { - display: none; -} */ -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -export default { - name: "passwordfield", - props: ["model", "placeholder", "label", "passworderrors", "required"], - data() { - return { - password: "", - readablePassword: false - }; - }, - methods: { - showPassword() { - this.readablePassword = !this.readablePassword; - }, - fieldChanged(e) { - this.$emit("fieldchange", e.target.value); - } - }, - computed: { - isPasswordVisible() { - return this.readablePassword ? "text" : "password"; - } - } -}; -</script>
--- a/client/src/components/admin/usermanagement/Userdetail.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,384 +0,0 @@ -<template> - <div class="userdetails h-100 mt-3 mr-auto shadow fadeIn animated"> - <div class="card"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - {{ this.cardHeader }} - <span @click="closeDetailview" class="closebutton"> - <font-awesome-icon icon="times"></font-awesome-icon> - </span> - </h6> - <div class="card-body"> - <form @submit.prevent="save" class="ml-3"> - <div class="formfields"> - <div v-if="currentUser.isNew" class="form-group row"> - <label for="user"> <translate>Username</translate> </label> - <input - type="user" - :placeholder="userNamePlaceholder" - class="form-control form-control-sm" - id="user" - aria-describedby="userHelp" - v-model="currentUser.user" - /> - <div v-show="errors.user" class="text-danger"> - <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> - {{ errors.user }} - </small> - </div> - </div> - <div class="form-group row"> - <label for="country"> <translate>Country</translate> </label> - <select - class="form-control form-control-sm" - v-on:change="validateCountry" - v-model="currentUser.country" - > - <option disabled value> - <translate>Please select one</translate> - </option> - <option - v-for="country in countries" - v-bind:value="country" - v-bind:key="country" - >{{ country }}</option - > - </select> - <div v-show="errors.country" class="text-danger"> - <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> - {{ errors.country }} - </small> - </div> - </div> - <div class="form-group row"> - <label for="email"> <translate>Email address</translate> </label> - <input - type="email" - v-on:change="validateEmailaddress" - class="form-control form-control-sm" - id="email" - aria-describedby="emailHelp" - v-model="currentUser.email" - /> - <div v-show="errors.email" class="text-danger"> - <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> - {{ errors.email }} - </small> - </div> - </div> - <div class="form-group row"> - <label for="role"> <translate>Role</translate> </label> - <select - class="form-control form-control-sm" - v-on:change="validateRole" - v-model="currentUser.role" - > - <option disabled value> - <translate>Please select one</translate> - </option> - <option value="sys_admin"> - <translate>Sysadmin</translate> - </option> - <option value="waterway_admin"> - <translate>Waterway Admin</translate> - </option> - <option value="waterway_user"> - <translate>Waterway User</translate> - </option> - </select> - <div v-show="errors.role" class="text-danger"> - <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> - {{ errors.role }} - </small> - </div> - </div> - <div class="form-group row"> - <PasswordField - @fieldchange="passwordChanged" - :placeholder="passwordPlaceholder" - :label="passwordLabel" - :passworderrors="errors.password" - ></PasswordField> - </div> - <div class="form-group row"> - <PasswordField - @fieldchange="passwordReChanged" - :placeholder="passwordRePlaceholder" - :label="passwordReLabel" - :passworderrors="errors.passwordre" - ></PasswordField> - </div> - </div> - <div> - <button - type="submit" - :disabled="submitted" - class="shadow-sm btn btn-info pull-right" - > - <translate>Submit</translate> - </button> - </div> - <div - v-if="currentUser.role != 'waterway_user'" - class="form-group row d-flex flex-row justify-content-start mailbutton" - > - <a @click="sendTestMail" class="btn btn-light"> - <font-awesome-icon icon="paper-plane"></font-awesome-icon> - <translate>Send testmail</translate> - </a> - <div v-if="mailsent"><translate>Mail was sent</translate></div> - </div> - </form> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.mailbutton { - width: 12vw; -} - -.formfields { - width: 10vw; -} - -.userdetails { - min-width: 40vw; -} - -form { - font-size: $smaller; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { HTTP } from "../../../lib/http"; -import { displayError } from "../../../lib/errors.js"; -import { mapState } from "vuex"; -import PasswordField from "./Passwordfield"; - -const emptyErrormessages = () => { - return { - email: "", - country: "", - role: "", - password: "", - passwordre: "" - }; -}; - -const isEmailValid = email => { - /** - * - * For convenience purposes the same regex used as in the go code - * cf. types.go - * - */ - // eslint-disable-next-line - return /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test( - email - ); -}; - -const violatedPasswordRules = password => { - return ( - // rules according to issue 70 - password.length < 7 || - /\W/.test(password) == false || - /\d/.test(password) == false - ); -}; - -export default { - name: "userdetail", - components: { - PasswordField - }, - data() { - return { - mailsent: false, - passwordLabel: this.$gettext("Password"), - passwordReLabel: this.$gettext("Repeat Password"), - passwordPlaceholder: this.$gettext("password"), - passwordRePlaceholder: this.$gettext("password again"), - password: "", - passwordre: "", - currentUser: {}, - path: null, - submitted: false, - errors: { - email: "", - country: "", - role: "", - password: "", - passwordre: "" - } - }; - }, - mounted() { - this.currentUser = { ...this.user }; - this.path = this.user.name; - }, - watch: { - user() { - this.currentUser = { ...this.user }; - this.path = this.user.name; - this.clearPassword(); - this.clearErrors(); - } - }, - computed: { - cardHeader() { - if (this.currentUser.isNew) return "N.N"; - return this.currentUser.user; - }, - userNamePlaceholder() { - if (this.currentUser.isNew) return "N.N"; - return ""; - }, - ...mapState("application", ["countries"]), - user() { - return this.$store.getters["usermanagement/currentUser"]; - }, - isFormValid() { - return ( - isEmailValid(this.currentUser.email) && - this.currentUser.country && - this.password === this.passwordre && - (this.password === "" || !violatedPasswordRules(this.password)) - ); - } - }, - methods: { - sendTestMail() { - if (this.mailsent) return; - HTTP.get("/testmail/" + this.currentUser.user, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - }) - .then(() => { - this.mailsent = true; - }) - .catch(error => { - this.loginFailed = true; - this.submitted = false; - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - passwordChanged(value) { - this.password = value; - this.validatePassword(); - }, - passwordReChanged(value) { - this.passwordre = value; - this.validatePassword(); - }, - clearErrors() { - this.errors = emptyErrormessages(); - }, - clearPassword() { - this.password = ""; - this.passwordre = ""; - }, - closeDetailview() { - this.$store.commit("usermanagement/clearCurrentUser"); - this.$store.commit("usermanagement/setUserDetailsInvisible"); - }, - validateCountry() { - this.errors.country = this.currentUser.country - ? "" - : this.$gettext("Please choose a country"); - }, - validateRole() { - this.errors.role = this.currentUser.role - ? "" - : this.$gettext("Please choose a role"); - }, - validatePassword() { - this.errors.passwordre = - this.password === this.passwordre - ? "" - : this.$gettext("Passwords do not match!"); - this.errors.password = - this.password === "" || !violatedPasswordRules(this.password) - ? "" - : this.$gettext( - "Password should at least be 8 char long including 1 digit and 1 special char like $" - ); - }, - validateEmailaddress() { - this.errors.email = isEmailValid(this.currentUser.email) - ? "" - : this.$gettext("invalid email"); - }, - validate() { - this.validateCountry(); - this.validateRole(); - this.validatePassword(); - this.validateEmailaddress(); - }, - save() { - this.validate(); - if (!this.isFormValid) return; - if (this.password) this.currentUser.password = this.password; - this.submitted = true; - this.$store - .dispatch("usermanagement/saveCurrentUser", { - path: this.user.user, - user: this.currentUser - }) - .then(() => { - this.submitted = false; - this.$store.dispatch("usermanagement/loadUsers").catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - this.submitted = false; - const { status, data } = error.response; - displayError({ - title: this.$gettext("Error while saving user"), - message: `${status}: ${data.message || data}` - }); - }); - } - } -}; -</script>
--- a/client/src/components/admin/usermanagement/Usermanagement.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,345 +0,0 @@ -<template> - <div class="main d-flex flex-row"> - <div :class="spacerStyle"></div> - <div class="d-flex content flex-column"> - <div class="d-flex flex-row"> - <div :class="userlistStyle"> - <div class="card"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon - icon="users-cog" - class="mr-2" - ></font-awesome-icon> - <translate class="headline">Users</translate> - </h6> - <div class="card-body"> - <table id="datatable" :class="tableStyle"> - <thead> - <tr> - <th scope="col" @click="sortBy('user')"> - <span - >Username - <font-awesome-icon - v-if="sortCriterion == 'user'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col" @click="sortBy('country')"> - <span - >Country - <font-awesome-icon - v-if="sortCriterion == 'country'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col" @click="sortBy('email')"> - <span - >Email - <font-awesome-icon - v-if="sortCriterion == 'email'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col" @click="sortBy('role')"> - <span - >Role - <font-awesome-icon - v-if="sortCriterion == 'role'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col"></th> - </tr> - </thead> - <tbody> - <tr - v-for="user in users" - :key="user.user" - @click="selectUser(user.user)" - > - <td>{{ user.user }}</td> - <td>{{ user.country }}</td> - <td>{{ user.email }}</td> - <td> - <font-awesome-icon - :icon="roleIcon(user.role)" - @click="deleteUser(user.user)" - ></font-awesome-icon> - </td> - <td> - <font-awesome-icon - icon="trash" - @click="deleteUser(user.user)" - ></font-awesome-icon> - </td> - </tr> - </tbody> - </table> - </div> - <div class="d-flex mx-auto align-items-center"> - <button - @click="prevPage" - v-if="this.currentPage !== 1" - class="mr-2 btn btn-sm btn-light align-self-center" - > - <font-awesome-icon icon="angle-left"></font-awesome-icon> - </button> - {{ this.currentPage }} / {{ this.pages }} - <button - @click="nextPage" - v-if="this.currentPage !== this.pages" - class="ml-2 btn btn-sm btn-light align-self-center" - > - <font-awesome-icon icon="angle-right"></font-awesome-icon> - </button> - </div> - <div class="mr-3 pb-3"> - <button - @click="addUser" - class="btn btn-info pull-right shadow-sm" - > - <translate>Add User</translate> - </button> - </div> - </div> - </div> - <Userdetail v-if="isUserDetailsVisible"></Userdetail> - </div> - </div> - </div> -</template> - -<style scoped lang="scss"> -@import "../../../assets/tooltip.scss"; - -.spacer { - height: 100vh; - margin-left: $offset; -} - -.spacer-collapsed { - min-width: $icon-width + $offset; - transition: $transition-fast; -} - -.spacer-expanded { - min-width: $sidebar-width + $offset; -} - -.main { - height: 100vh; -} - -.icon { - font-size: large; -} - -.userlist { - min-width: 520px; - height: 100%; -} - -.userlistsmall { - width: 30vw; -} - -.userlistextended { - width: 70vw; -} - -.table { - width: 90% !important; - margin: auto; -} - -.table th { - cursor: pointer; -} - -.table th, -td { - font-size: $smaller; - border-top: 0px !important; - text-align: left; - padding: $small-offset !important; -} - -.table td { - font-size: $smaller; - cursor: pointer; -} - -tr span { - display: flex; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import Userdetail from "./Userdetail"; -import store from "../../../store"; -import { mapGetters, mapState } from "vuex"; -import { displayError } from "../../../lib/errors.js"; - -export default { - name: "userview", - data() { - return { - sortCriterion: "user", - pageSize: 10, - currentPage: 1 - }; - }, - components: { - Userdetail - }, - computed: { - ...mapGetters("usermanagement", ["isUserDetailsVisible"]), - ...mapState("application", ["showSidebar"]), - spacerStyle() { - return [ - "spacer", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - }, - users() { - let users = [...this.$store.getters["usermanagement/users"]]; - users.sort((a, b) => { - if ( - a[this.sortCriterion].toLowerCase() < - b[this.sortCriterion].toLowerCase() - ) - return -1; - if ( - a[this.sortCriterion].toLowerCase() > - b[this.sortCriterion].toLowerCase() - ) - return 1; - return 0; - }); - const start = (this.currentPage - 1) * this.pageSize; - return users.slice(start, start + this.pageSize); - }, - pages() { - let users = [...this.$store.getters["usermanagement/users"]]; - return Math.ceil(users.length / this.pageSize); - }, - tableStyle() { - return { - table: true, - "table-hover": true, - "table-sm": this.isUserDetailsVisible, - fadeIn: true, - animated: true - }; - }, - userlistStyle() { - return [ - "userlist mt-3 mr-3 shadow-xs", - { - userlistsmall: this.isUserDetailsVisible, - userlistextended: !this.isUserDetailsVisible - } - ]; - } - }, - methods: { - tween() {}, - nextPage() { - if (this.currentPage < this.pages) { - document.querySelector("#datatable").classList.add("fadeOut"); - setTimeout(() => { - document.querySelector("#datatable").classList.remove("fadeOut"); - this.currentPage += 1; - }, 10); - } - return; - }, - prevPage() { - if (this.currentPage > 0) { - document.querySelector("#datatable").classList.add("fadeOut"); - setTimeout(() => { - document.querySelector("#datatable").classList.remove("fadeOut"); - this.currentPage -= 1; - }, 10); - } - return; - }, - sortBy(criterion) { - this.sortCriterion = criterion; - }, - deleteUser(name) { - this.$store - .dispatch("usermanagement/deleteUser", { name: name }) - .then(() => { - this.submitted = false; - this.$store.dispatch("usermanagement/loadUsers").catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - addUser() { - this.$store.commit("usermanagement/clearCurrentUser"); - this.$store.commit("usermanagement/setUserDetailsVisible"); - }, - selectUser(name) { - const user = this.$store.getters["usermanagement/getUserByName"](name); - this.$store.commit("usermanagement/setCurrentUser", user); - }, - roleIcon(role) { - if (role === "sys_admin") return "star"; - if (role === "waterway_admin") return ["fab", "adn"]; - return "user"; - } - }, - beforeRouteEnter(to, from, next) { - store - .dispatch("usermanagement/loadUsers") - .then(next) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data}` - }); - }); - }, - beforeRouteLeave(to, from, next) { - store.commit("usermanagement/clearCurrentUser"); - store.commit("usermanagement/setUserDetailsInvisible"); - next(); - } -}; -</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/Fairwayprofile.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,414 @@ +<template> + <div :class="['position-relative', { show: showSplitscreen }]"> + <button + class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle" + @click="$store.commit('application/showSplitscreen', false)" + v-if="showSplitscreen" + > + <font-awesome-icon icon="angle-down" /> + </button> + <button + class="rounded-bottom bg-white border-0 position-absolute clear-selection" + @click="$store.dispatch('fairwayprofile/clearSelection')" + v-if="showSplitscreen" + > + <font-awesome-icon icon="times" /> + </button> + <div class="profile bg-white position-relative d-flex flex-column"> + <h5 + class="headline border-bottom mb-0 py-2" + v-if="selectedBottleneck && selectedSurvey" + > + {{ selectedBottleneck }} ({{ selectedSurvey.date_info }}) + </h5> + <div class="d-flex flex-fill"> + <div + class="loading d-flex justify-content-center align-items-center" + v-if="surveysLoading || profileLoading" + > + <font-awesome-icon icon="spinner" spin /> + </div> + <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.profile { + width: 100vw; + height: 0; + overflow: hidden; + z-index: 2; +} + +.splitscreen-toggle, +.clear-selection { + width: 2rem; + height: 2rem; + margin-top: 8px; + z-index: 3; + outline: none; +} + +.splitscreen-toggle svg path, +.clear-selection svg path { + fill: #666; +} + +.splitscreen-toggle { + right: 2.5rem; +} + +.clear-selection { + right: 0.5rem; +} + +.show .profile { + height: 50vh; +} + +.loading { + background: rgba(255, 255, 255, 0.96); + position: absolute; + z-index: 99; + top: 0; + right: 0; + bottom: 0; + left: 0; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import * as d3 from "d3"; +import { mapState, mapGetters } from "vuex"; +import debounce from "debounce"; + +const GROUND_COLOR = "#4A2F06"; + +export default { + name: "fairwayprofile", + data() { + return { + coordinatesInput: "", + coordinatesSelect: null, + cutLabel: "", + showLabelInput: false, + width: null, + height: null, + margin: { + top: 20, + right: 40, + bottom: 30, + left: 40 + } + }; + }, + computed: { + ...mapGetters("fairwayprofile", ["totalLength"]), + ...mapState("application", ["showSplitscreen"]), + ...mapState("fairwayprofile", [ + "startPoint", + "endPoint", + "currentProfile", + "additionalSurvey", + "minAlt", + "maxAlt", + "fairwayCoordinates", + "waterLevels", + "selectedWaterLevel", + "profileLoading" + ]), + ...mapState("bottlenecks", [ + "selectedBottleneck", + "selectedSurvey", + "surveysLoading" + ]), + currentData() { + if ( + !this.selectedSurvey || + !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info) + ) + return []; + return this.currentProfile[this.selectedSurvey.date_info].points; + }, + additionalData() { + if ( + !this.additionalSurvey || + !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info) + ) + return []; + return this.currentProfile[this.additionalSurvey.date_info].points; + }, + waterColor() { + const result = this.waterLevels.find( + x => x.level === this.selectedWaterLevel + ); + return result.color; + }, + xScale() { + return [0, this.totalLength]; + }, + yScaleLeft() { + const hi = Math.max(this.maxAlt, this.selectedWaterLevel); + return [this.minAlt, hi]; + }, + yScaleRight() { + const DELTA = this.maxAlt * 1.1 - this.maxAlt; + return [this.maxAlt * 1 + DELTA, -DELTA]; + } + }, + watch: { + currentData() { + this.drawDiagram(); + }, + additionalData() { + this.drawDiagram(); + }, + width() { + this.drawDiagram(); + }, + height() { + this.drawDiagram(); + }, + waterLevels() { + this.drawDiagram(); + }, + selectedWaterLevel() { + this.drawDiagram(); + }, + fairwayCoordinates() { + this.drawDiagram(); + } + }, + methods: { + drawDiagram() { + this.coordinatesSelect = null; + const chartDiv = document.querySelector(".fairwayprofile"); + d3.select(".fairwayprofile svg").remove(); + this.scaleFairwayProfile(); + let svg = d3.select(chartDiv).append("svg"); + svg.attr("width", this.width); + svg.attr("height", this.height); + const width = this.width - this.margin.right - 1.5 * this.margin.left; + const height = this.height - this.margin.top - 2 * this.margin.bottom; + const currentData = this.currentData; + const additionalData = this.additionalData; + const { xScale, yScaleRight, graph } = this.generateCoordinates( + svg, + height, + width + ); + if (!this.height || !this.width) return; // do not try to render when height and width are unknown + this.drawWaterlevel({ graph, xScale, yScaleRight, height }); + this.drawLabels({ graph, height }); + this.drawFairway({ graph, xScale, yScaleRight }); + if (currentData) { + this.drawProfile({ + graph, + xScale, + yScaleRight, + currentData, + height, + color: GROUND_COLOR, + strokeColor: "black", + opacity: 1 + }); + } + if (additionalData) { + this.drawProfile({ + graph, + xScale, + yScaleRight, + currentData: additionalData, + height, + color: GROUND_COLOR, + strokeColor: "#943007", + opacity: 0.6 + }); + } + }, + drawFairway({ graph, xScale, yScaleRight }) { + for (let coordinates of this.fairwayCoordinates) { + const [startPoint, endPoint, depth] = coordinates; + let fairwayArea = d3 + .area() + .x(function(d) { + return xScale(d.x); + }) + .y0(yScaleRight(0)) + .y1(function(d) { + return yScaleRight(d.y); + }); + graph + .append("path") + .datum([{ x: startPoint, y: depth }, { x: endPoint, y: depth }]) + .attr("fill", "#002AFF") + .attr("stroke-opacity", 0.65) + .attr("fill-opacity", 0.65) + .attr("stroke", "#FFD20D") + .attr("d", fairwayArea); + } + }, + drawLabels({ graph, height }) { + graph + .append("text") + .attr("transform", ["rotate(-90)"]) + .attr("y", this.width - 60) + .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2) + .attr("dy", "1em") + .attr("fill", "black") + .style("text-anchor", "middle") + .text("Depth [m]"); + graph + .append("text") + .attr("y", 0 - this.margin.left) + .attr("x", 0 - height / 4) + .attr("dy", "1em") + .attr("fill", "black") + .style("text-anchor", "middle") + .attr("transform", [ + "translate(" + this.width / 2 + "," + this.height + ")", + "rotate(0)" + ]) + .text("Width [m]"); + }, + generateCoordinates(svg, height, width) { + let xScale = d3 + .scaleLinear() + .domain(this.xScale) + .rangeRound([0, width]); + + xScale.ticks(5); + let yScaleLeft = d3 + .scaleLinear() + .domain(this.yScaleLeft) + .rangeRound([height, 0]); + + let yScaleRight = d3 + .scaleLinear() + .domain(this.yScaleRight) + .rangeRound([height, 0]); + + let xAxis = d3.axisBottom(xScale); + let yAxis2 = d3.axisRight(yScaleRight); + let graph = svg + .append("g") + .attr( + "transform", + "translate(" + this.margin.left + "," + this.margin.top + ")" + ); + graph + .append("g") + .attr("transform", "translate(0," + height + ")") + .call(xAxis.ticks(5)); + graph + .append("g") + .attr("transform", "translate(" + width + ",0)") + .call(yAxis2); + return { xScale, yScaleLeft, yScaleRight, graph }; + }, + drawWaterlevel({ graph, xScale, yScaleRight, height }) { + let waterArea = d3 + .area() + .x(function(d) { + return xScale(d.x); + }) + .y0(height) + .y1(function(d) { + return yScaleRight(d.y); + }); + graph + .append("path") + .datum([{ x: 0, y: 0 }, { x: this.totalLength, y: 0 }]) + .attr("fill", this.waterColor) + .attr("stroke", this.waterColor) + .attr("d", waterArea); + }, + drawProfile({ + graph, + xScale, + yScaleRight, + currentData, + height, + color, + strokeColor, + opacity + }) { + for (let part of currentData) { + let profileLine = d3 + .line() + .x(d => { + return xScale(d.x); + }) + .y(d => { + return yScaleRight(d.y); + }); + let profileArea = d3 + .area() + .x(function(d) { + return xScale(d.x); + }) + .y0(height) + .y1(function(d) { + return yScaleRight(d.y); + }); + graph + .append("path") + .datum(part) + .attr("fill", color) + .attr("stroke", color) + .attr("stroke-width", 3) + .attr("stroke-opacity", opacity) + .attr("fill-opacity", opacity) + .attr("d", profileArea); + graph + .append("path") + .datum(part) + .attr("fill", "none") + .attr("stroke", strokeColor) + .attr("stroke-linejoin", "round") + .attr("stroke-linecap", "round") + .attr("stroke-width", 3) + .attr("stroke-opacity", opacity) + .attr("fill-opacity", opacity) + .attr("d", profileLine); + } + }, + scaleFairwayProfile() { + if (!document.querySelector(".fairwayprofile")) return; + const clientHeight = document.querySelector(".fairwayprofile") + .clientHeight; + const clientWidth = document.querySelector(".fairwayprofile").clientWidth; + if (!clientHeight || !clientWidth) return; + this.height = clientHeight; + this.width = clientWidth; + } + }, + created() { + window.addEventListener("resize", debounce(this.drawDiagram), 100); + }, + mounted() { + this.drawDiagram(); + }, + updated() { + this.scaleFairwayProfile(); + }, + destroyed() { + window.removeEventListener("resize", debounce(this.drawDiagram)); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/Infobar.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,61 @@ +<template> + <div + v-if="Object.keys(currentProfile).length && !showSplitscreen" + class="ui-element shadow-xs infobar rounded bg-white ml-auto mb-3 mr-3" + > + <div class="d-flex flex-row justify-content-between h-100"> + <h6 class="my-auto px-2"> + {{ selectedBottleneck }} ({{ selectedSurvey.date_info }}) + </h6> + <span + class="p-2 border-left d-flex align-items-center" + @click="$store.commit('application/showSplitscreen', true)" + > + <font-awesome-icon icon="angle-up"></font-awesome-icon> + </span> + <span + class="p-2 border-left d-flex align-items-center" + @click="$store.dispatch('fairwayprofile/clearSelection')" + > + <font-awesome-icon icon="times"></font-awesome-icon> + </span> + </div> + </div> +</template> + +<style lang="scss" scoped> +.infobar { + height: 2.2rem; + z-index: 2; +} + +.infobar svg path { + fill: #666; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "infobar", + computed: { + ...mapState("application", ["showSplitscreen"]), + ...mapState("fairwayprofile", ["currentProfile"]), + ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]) + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/Profiles.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,471 @@ +<template> + <div + :class="[ + 'box ui-element rounded bg-white text-nowrap', + { expanded: showProfiles } + ]" + > + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="chart-area" class="mr-2"></font-awesome-icon> + <translate>Profiles</translate> + <font-awesome-icon + icon="times" + class="ml-auto text-muted" + @click="$store.commit('application/showProfiles', false)" + ></font-awesome-icon> + </h6> + <div + class="d-flex flex-column p-3 flex-grow-1 text-left position-relative" + > + <div + class="loading d-flex justify-content-center align-items-center" + v-if="surveysLoading || profileLoading" + > + <font-awesome-icon icon="spinner" spin /> + </div> + <select + @click="moveToBottleneck" + v-model="selectedBottleneck" + class="form-control font-weight-bold" + > + <option :value="null"> + <translate>Select Bottleneck</translate> + </option> + <option + v-for="bn in bottlenecks" + :key="bn.properties.name" + :value="bn.properties.name" + >{{ bn.properties.name }}</option + > + </select> + <div v-if="selectedBottleneck"> + <div class="d-flex mt-2"> + <div class="flex-fill"> + <small class="text-muted"> + <translate>Sounding Result</translate>: + </small> + <select + v-model="selectedSurvey" + class="form-control form-control-sm" + > + <option + v-for="survey in surveys" + :key="survey.date_info" + :value="survey" + >{{ formatSurveyDate(survey.date_info) }}</option + > + </select> + </div> + <div + class="flex-fill ml-3" + v-if="selectedSurvey && surveys.length > 1" + > + <small class="text-muted mt-1"> + <translate>Compare with</translate>: + </small> + <select + v-model="additionalSurvey" + class="form-control form-control-sm" + > + <option :value="null">None</option> + <option + v-for="survey in additionalSurveys" + :key="survey.date_info" + :value="survey" + >{{ formatSurveyDate(survey.date_info) }}</option + > + </select> + </div> + </div> + <hr class="w-100 mb-0" /> + <small class="text-muted d-block mt-2"> + <translate>Saved cross profiles</translate>: + </small> + <div class="d-flex"> + <select + :class="[ + 'form-control form-control-sm flex-fill', + { 'rounded-left-only': selectedCut } + ]" + v-model="selectedCut" + > + <option></option> + <option + v-for="(cut, index) in previousCuts" + :value="cut" + :key="index" + >{{ cut.label }}</option + > + </select> + <button + class="btn btn-sm btn-danger input-button-right" + @click="confirmDeleteSelectedCut = true" + v-if="selectedCut && !confirmDeleteSelectedCut" + > + <font-awesome-icon icon="trash" /> + </button> + <button + class="btn btn-sm btn-info rounded-0" + @click="confirmDeleteSelectedCut = false" + v-if="selectedCut && confirmDeleteSelectedCut" + > + <font-awesome-icon icon="times" /> + </button> + <button + class="btn btn-sm btn-danger input-button-right" + @click="deleteSelectedCut" + v-if="selectedCut && confirmDeleteSelectedCut" + > + <font-awesome-icon icon="check" /> + </button> + </div> + <small class="text-muted d-block mt-2"> + <translate>Enter coordinates manually</translate>: + </small> + <div class="position-relative"> + <input + class="form-control form-control-sm pr-5" + placeholder="Lat,Lon,Lat,Lon" + v-model="coordinatesInput" + /> + <button + class="btn btn-sm btn-info position-absolute input-button-right" + @click="applyManualCoordinates" + style="top: 0; right: 0;" + v-if="coordinatesInputIsValid" + > + <font-awesome-icon icon="check" /> + </button> + </div> + <small class="d-flex text-left mt-2" v-if="startPoint && endPoint"> + <div class="text-nowrap mr-3"> + <b> <translate>Start</translate>: </b> <br /> + Lat: {{ startPoint[1] }} <br /> + Lon: {{ startPoint[0] }} + </div> + <div class="text-nowrap"> + <b>End:</b> <br /> + Lat: {{ endPoint[1] }} <br /> + Lon: {{ endPoint[0] }} + </div> + <button + v-clipboard:copy="coordinatesForClipboard" + v-clipboard:success="onCopyCoordinates" + class="btn btn-info btn-sm ml-auto mt-auto" + > + <font-awesome-icon icon="copy" /> + </button> + </small> + <div class="d-flex mt-3"> + <div + class="pr-3 w-50" + v-if="startPoint && endPoint && !selectedCut" + > + <button + class="btn btn-info btn-sm w-100" + @click="showLabelInput = !showLabelInput" + > + <font-awesome-icon :icon="showLabelInput ? 'times' : 'check'" /> + {{ showLabelInput ? "Cancel" : "Save" }} + </button> + </div> + <div + :class="startPoint && endPoint && !selectedCut ? 'w-50' : 'w-100'" + > + <button class="btn btn-info btn-sm w-100" @click="toggleCutTool"> + <font-awesome-icon + :icon="cutTool && cutTool.getActive() ? 'times' : 'plus'" + ></font-awesome-icon> + {{ cutTool && cutTool.getActive() ? "Cancel" : "New" }} + </button> + </div> + </div> + <div v-if="showLabelInput" class="mt-2"> + <small class="text-muted"> + <translate>Enter label for cross profile</translate>: + </small> + <div class="position-relative"> + <input + class="form-control form-control-sm pr-5" + v-model="cutLabel" + /> + <button + class="btn btn-sm btn-info position-absolute input-button-right" + @click="saveCut" + v-if="cutLabel" + style="top: 0; right: 0;" + > + <font-awesome-icon icon="check" /> + </button> + </div> + </div> + </div> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.loading { + background: rgba(255, 255, 255, 0.9); + position: absolute; + z-index: 99; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.input-button-right { + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.rounded-left-only { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; +import Feature from "ol/Feature"; +import LineString from "ol/geom/LineString"; +import { displayError, displayInfo } from "@/lib/errors.js"; +import { formatSurveyDate } from "@/lib/date.js"; + +export default { + name: "profiles", + data() { + return { + coordinatesInput: "", + cutLabel: "", + showLabelInput: false, + confirmDeleteSelectedCut: false + }; + }, + computed: { + ...mapGetters("map", ["getVSourceByName"]), + ...mapState("application", ["showProfiles"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), + ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]), + ...mapState("fairwayprofile", [ + "previousCuts", + "startPoint", + "endPoint", + "profileLoading" + ]), + selectedBottleneck: { + get() { + return this.$store.state.bottlenecks.selectedBottleneck; + }, + set(name) { + this.$store + .dispatch("bottlenecks/setSelectedBottleneck", name) + .then(() => { + this.$store.commit("bottlenecks/setFirstSurveySelected"); + }); + } + }, + selectedSurvey: { + get() { + return this.$store.state.bottlenecks.selectedSurvey; + }, + set(survey) { + this.$store.commit("fairwayprofile/additionalSurvey", null); + this.$store.commit("bottlenecks/selectedSurvey", survey); + } + }, + additionalSurvey: { + get() { + return this.$store.state.fairwayprofile.additionalSurvey; + }, + set(survey) { + this.$store.commit("fairwayprofile/additionalSurvey", survey); + } + }, + selectedCut: { + get() { + return this.$store.state.fairwayprofile.selectedCut; + }, + set(cut) { + this.$store.commit("fairwayprofile/selectedCut", cut); + if (!cut) { + this.$store.commit("fairwayprofile/clearCurrentProfile"); + this.$store.commit("application/showSplitscreen", false); + this.getVSourceByName("Cut Tool").clear(); + } + } + }, + additionalSurveys() { + return this.surveys.filter(survey => survey !== this.selectedSurvey); + }, + coordinatesForClipboard() { + return ( + this.startPoint[1] + + "," + + this.startPoint[0] + + "," + + this.endPoint[1] + + "," + + this.endPoint[0] + ); + }, + coordinatesInputIsValid() { + const coordinates = this.coordinatesInput + .split(",") + .map(coord => parseFloat(coord.trim())) + .filter(c => Number(c) === c); + return coordinates.length === 4; + } + }, + watch: { + selectedBottleneck() { + this.$store.dispatch("fairwayprofile/previousCuts"); + this.cutLabel = + this.selectedBottleneck + " (" + new Date().toISOString() + ")"; + }, + selectedSurvey(survey) { + this.loadProfile(survey); + }, + additionalSurvey(survey) { + this.loadProfile(survey); + }, + selectedCut(cut) { + if (cut) { + this.confirmDeleteSelectedCut = false; + this.applyCoordinates(cut.coordinates); + } + } + }, + methods: { + formatSurveyDate(date) { + return formatSurveyDate(date); + }, + loadProfile(survey) { + if (survey) { + this.$store.commit("fairwayprofile/profileLoading", true); + this.$store + .dispatch("fairwayprofile/loadProfile", survey) + .finally(() => + this.$store.commit("fairwayprofile/profileLoading", false) + ); + } + }, + toggleCutTool() { + this.cutTool.setActive(!this.cutTool.getActive()); + this.lineTool.setActive(false); + this.polygonTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + }, + onCopyCoordinates() { + displayInfo({ + title: this.$gettext("Success"), + message: this.$gettext("Coordinates copied to clipboard!") + }); + }, + applyManualCoordinates() { + const coordinates = this.coordinatesInput + .split(",") + .map(coord => parseFloat(coord.trim())); + this.selectedCut = null; + this.coordinatesInput = ""; + this.applyCoordinates([ + coordinates[1], + coordinates[0], + coordinates[3], + coordinates[2] + ]); + }, + applyCoordinates(coordinates) { + // allow only numbers + coordinates = coordinates.filter(c => Number(c) === c); + if (coordinates.length === 4) { + // draw line on map + this.getVSourceByName("Cut Tool").clear(); + const cut = new Feature({ + geometry: new LineString([ + [coordinates[0], coordinates[1]], + [coordinates[2], coordinates[3]] + ]).transform("EPSG:4326", "EPSG:3857") + }); + this.getVSourceByName("Cut Tool").addFeature(cut); + + // draw diagram + this.$store.dispatch("fairwayprofile/cut", cut); + } else { + displayError({ + title: this.$gettext("Invalid input"), + message: this.$gettext( + "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" + ) + }); + } + }, + saveCut() { + const previousCuts = + JSON.parse(localStorage.getItem("previousCuts")) || []; + const newEntry = { + label: this.cutLabel, + bottleneckName: this.selectedBottleneck, + coordinates: [...this.startPoint, ...this.endPoint], + timestamp: new Date().getTime() + }; + const existingEntry = previousCuts.find(cut => { + return JSON.stringify(cut) === JSON.stringify(newEntry); + }); + if (!existingEntry) previousCuts.push(newEntry); + if (previousCuts.length > 100) previousCuts.shift(); + localStorage.setItem("previousCuts", JSON.stringify(previousCuts)); + this.$store.dispatch("fairwayprofile/previousCuts"); + + this.showLabelInput = false; + displayInfo({ + title: this.$gettext("Profile saved!"), + message: this.$gettext( + 'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.' + ) + }); + }, + deleteSelectedCut() { + let previousCuts = JSON.parse(localStorage.getItem("previousCuts")) || []; + previousCuts = previousCuts.filter(cut => { + return JSON.stringify(cut) !== JSON.stringify(this.selectedCut); + }); + localStorage.setItem("previousCuts", JSON.stringify(previousCuts)); + this.$store.commit("fairwayprofile/selectedCut", null); + this.$store.dispatch("fairwayprofile/previousCuts"); + displayInfo({ title: this.$gettext("Profile deleted!") }); + }, + moveToBottleneck() { + const bottleneck = this.bottlenecks.find( + bn => bn.properties.name === this.selectedBottleneck + ); + if (!bottleneck) return; + this.$store.commit("map/moveMap", { + coordinates: bottleneck.geometry.coordinates, + zoom: 17, + preventZoomOut: true + }); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importqueue/Importqueue.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,357 @@ +<template> + <div class="d-flex flex-row"> + <Spacer></Spacer> + <div class="mt-3 importqueuecard flex-grow-1"> + <div class="card shadow-xs"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon icon="tasks" class="mr-2"></font-awesome-icon> + <translate class="headline">Importqueue</translate> + </h6> + <div class="card-body importcardbody"> + <div class="card-body importcardbody"> + <div class="searchandfilter d-flex flex-row"> + <div class="searchgroup input-group"> + <div class="input-group-prepend"> + <span class="input-group-text" id="search"> + <font-awesome-icon icon="search"></font-awesome-icon> + </span> + </div> + <input + v-model="searchQuery" + type="text" + class="form-control" + placeholder + aria-label="Search" + aria-describedby="search" + /> + </div> + <div class="filters"> + <button + @click="setFilter('successful')" + :class="successfulStyle" + > + <translate>Successful</translate> + </button> + <button @click="setFilter('failed')" :class="failedStyle"> + <translate>Failed</translate> + </button> + <button @click="setFilter('pending')" :class="pendingStyle"> + <translate>Pending</translate> + </button> + <button @click="setFilter('rejected')" :class="rejectedStyle"> + <translate>Rejected</translate> + </button> + <button @click="setFilter('accepted')" :class="acceptedStyle"> + <translate>Accepted</translate> + </button> + </div> + </div> + <div class="text-left d-flex flex-row border-bottom entries"> + <div class="header py-1 jobid mr-2"> + <translate>Id</translate> + </div> + <div class="header py-1 enqueued mr-2"> + <translate>Enqueued</translate> + </div> + <div class="header py-1 kind mr-2"> + <translate>Kind</translate> + </div> + <div class="header py-1 user mr-2"> + <translate>User</translate> + </div> + <div class="header py-1 signer mr-2"> + <translate>Signer</translate> + </div> + <div class="header py-1 state mr-2"> + <translate>State</translate> + </div> + </div> + <div class="importqueuedetail"> + <div + class="text-left" + v-for="job in filteredImports" + :key="job.id" + > + <Importqueuedetail + :reload="reload" + :job="job" + ></Importqueuedetail> + </div> + </div> + <div> + <button @click="refresh" class="btn btn-info refresh"> + <translate>Refresh</translate> + </button> + </div> + </div> + </div> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus@intevation.de> + */ +import { displayError } from "@/lib/errors.js"; +import { mapState } from "vuex"; +import { HTTP } from "@/lib/http.js"; + +export default { + name: "importqueue", + components: { + Importqueuedetail: () => import("./Importqueuedetail"), + Spacer: () => import("@/components/Spacer") + }, + data() { + return { + reload: false, + searchQuery: "", + successful: false, + failed: false, + pending: false, + rejected: false, + accepted: false + }; + }, + mounted() { + this.loadQueue(); + }, + methods: { + setFilter(name) { + this[name] = !this[name]; + const allSet = + this.successful && + this.failed && + this.pending && + this.accepted && + this.rejected; + if (allSet) { + this.successful = false; + this.failed = false; + this.pending = false; + this.accepted = false; + this.rejected = false; + } + }, + loadQueue() { + this.reload = true; + this.$store + .dispatch("imports/getImports") + .then(() => { + this.reload = false; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + refresh() { + this.loadQueue(); + }, + showDetails(id) { + HTTP.get("/imports/" + id, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + const { entries } = response.data; + this.entries = entries; + this.$modal.show("details"); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + close() { + this.$modal.hide("details"); + } + }, + computed: { + ...mapState("imports", ["imports"]), + ...mapState("application", ["showSidebar"]), + sortIcon() { + return this.sortAsc ? "sort-amount-down" : "sort-amount-up"; + }, + filteredImports() { + const filtered = this.imports + .filter(element => { + if (!this.searchQuery) return true; + return [(element.kind, element.user, element.enqueued)].some(x => { + return x.toLowerCase().includes(this.searchQuery.toLowerCase()); + }); + }) + .filter(y => { + if ( + !this.successful && + !this.failed && + !this.pending && + !this.accepted && + !this.rejected + ) + return true; + let filterCriteria = []; + if (this.successful) filterCriteria.push("successful"); + if (this.failed) filterCriteria.push("failed"); + if (this.pending) filterCriteria.push("pending"); + if (this.accepted) filterCriteria.push("accepted"); + if (this.rejected) filterCriteria.push("rejected"); + const result = filterCriteria.map(selectedState => { + return y.state === selectedState; + }); + return result.some(x => x); + }); + return filtered; + }, + successfulStyle() { + return { + btn: true, + "btn-light": !this.successful, + "btn-dark": this.successful + }; + }, + pendingStyle() { + return { + btn: true, + "btn-light": !this.pending, + "btn-dark": this.pending + }; + }, + failedStyle() { + return { + btn: true, + "btn-light": !this.failed, + "btn-dark": this.failed + }; + }, + rejectedStyle() { + return { + btn: true, + "btn-light": !this.rejected, + "btn-dark": this.rejected + }; + }, + acceptedStyle() { + return { + btn: true, + "btn-light": !this.accepted, + "btn-dark": this.accepted + }; + } + } +}; +</script> + +<style lang="scss" scoped> +.importqueuedetail { + margin-bottom: 3rem; +} +.entries { + width: 100%; +} + +.jobid { + width: 15%; +} + +.enqueued { + width: 20%; +} + +.user { + width: 15%; +} + +.signer { + width: 20%; +} + +.kind { + width: 10%; +} + +.state { + width: 20%; +} + +.details thead { + display: block; +} +.details tbody { + display: block; +} + +.details tbody { + height: 260px; + overflow-y: auto; + overflow-x: hidden; +} + +.closebutton { + top: $small-offset; +} + +.refresh { + position: absolute; + right: $offset; + bottom: $offset; +} + +.importqueuecard { + width: 97%; + margin-right: $offset; + min-height: 20rem; +} + +.card-body { + width: 100%; + margin-left: auto; + margin-right: auto; +} + +.searchandfilter { + position: relative; + margin-bottom: $xx-large-offset; +} + +.filters { + position: absolute; + right: 0; +} + +.filters button { + margin-right: $small-offset; +} + +.table td, +.table th { + border-top: 0 !important; + text-align: left; + padding: $small-offset !important; +} + +.searchgroup { + position: absolute; + left: 0; + width: 45%; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importqueue/Importqueuedetail.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,307 @@ +<template> + <div class="entry d-flex flex-column py-1 border-bottom"> + <div class="d-flex flex-row position-relative"> + <div @click="showDetails(job.id)" class="jobid ml-2 mt-1 mr-2"> + {{ job.id }} + </div> + <div @click="showDetails(job.id)" class="enqueued mt-1 mr-2"> + {{ formatDate(job.enqueued) }} + </div> + <div @click="showDetails(job.id)" class="kind mt-1 mr-2"> + {{ job.kind }} + </div> + <div @click="showDetails(job.id)" class="user mt-1 mr-2"> + {{ job.user }} + </div> + <div @click="showDetails(job.id)" class="signer mt-1 mr-2"> + {{ job.signer }} + </div> + <div @click="showDetails(job.id)" class="state mt-1 mr-2"> + {{ job.state }} + </div> + <div @click="showDetails(job.id)" class="mt-1 text-info detailsbutton"> + <font-awesome-icon + v-if="show" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + v-if="loading" + icon="spinner" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + v-if="!show && !loading" + icon="angle-down" + fixed-width + ></font-awesome-icon> + </div> + </div> + <div class="detailstable d-flex flex-row"> + <div :class="collapse"> + <table class="table table-responsive"> + <thead> + <tr> + <th class="type pb-0"> + <small class="condensed"><translate>Kind</translate></small> + </th> + <th class="datetime pb-0"> + <a href="#" @click="sortAsc = !sortAsc" class="sort-link" + ><small class="condensed"><translate>Date</translate></small> + <small class="condensed" + ><font-awesome-icon + :icon="sortIcon" + class="ml-1" + ></font-awesome-icon></small + ></a> + </th> + <th class="message pb-0"> + <small class="condensed"><translate>Message</translate></small> + </th> + </tr> + </thead> + <tbody> + <tr + v-for="(entry, index) in sortedEntries" + :key="index" + class="detailsrow" + > + <td class="type"> + <span class="condensed">{{ entry.kind }}</span> + </td> + <td class="datetime"> + <span class="condensed">{{ formatDateTime(entry.time) }}</span> + </td> + <td class="message"> + <span class="condensed">{{ entry.message }}</span> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { HTTP } from "@/lib/http.js"; +import { displayError } from "@/lib/errors.js"; +import locale2 from "locale2"; + +export default { + name: "importqueuedetail", + props: ["job", "reload"], + data() { + return { + loading: false, + show: false, + entries: [], + sortAsc: true + }; + }, + watch: { + reload() { + if (this.reload) { + this.entries = []; + this.show = false; + } + } + }, + methods: { + formatDate(date) { + return date + ? new Date(date).toLocaleDateString(locale2, { + day: "2-digit", + month: "2-digit", + year: "numeric" + }) + : ""; + }, + formatDateTime(date) { + if (!date) return ""; + const d = new Date(date); + return ( + d.toLocaleDateString(locale2, { + day: "2-digit", + month: "2-digit", + year: "numeric" + }) + + " - " + + d.toLocaleTimeString(locale2, { + hour12: false + }) + ); + }, + showDetails(id) { + if (this.show) { + this.show = false; + return; + } + if (this.entries.length === 0) { + this.loading = true; + HTTP.get("/imports/" + id, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + const { entries } = response.data; + this.entries = entries; + this.show = true; + this.loading = false; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } else { + this.show = true; + } + } + }, + computed: { + sortedEntries() { + let sorted = this.entries.slice(); + sorted.sort((r1, r2) => { + let d1 = new Date(r1.time); + let d2 = new Date(r2.time); + if (d2 < d1) { + return !this.sortAsc ? -1 : 1; + } + if (d2 > d1) { + return !this.sortAsc ? 1 : -1; + } + return 0; + }); + return sorted; + }, + sortIcon() { + return this.sortAsc ? "sort-amount-down" : "sort-amount-up"; + }, + icon() { + return { + "angle-up": !this.show, + "angle-down": this.show + }; + }, + collapse() { + return { + details: true, + collapse: true, + show: this.show, + "w-100": true + }; + } + } +}; +</script> + +<style lang="scss" scoped> +.condensed { + font-stretch: condensed; +} + +.entry { + background-color: white; + cursor: pointer; + width: 100%; +} + +.entry:hover { + background-color: #efefef; + transition: 1.6s; +} + +.detailstable { + margin-left: $offset; + margin-right: $large-offset; +} + +.detailsbutton { + position: absolute; + top: 0; + right: 0; + height: 100%; +} +.jobid { + width: 15%; +} + +.enqueued { + width: 20%; +} + +.user { + width: 15%; +} + +.signer { + width: 20%; +} + +.kind { + width: 10%; +} + +.state { + width: 20%; +} + +.details { + width: 50%; +} + +.detailsrow { + line-height: 0.1em; +} + +.type { + width: 65px; + white-space: nowrap; + padding-left: 0px; + border-top: 0px; + padding-bottom: $small-offset; +} + +.datetime { + width: 200px; + white-space: nowrap; + padding-left: 0px; + border-top: 0px; + padding-bottom: $small-offset; +} + +.message { + min-width: 700px; + white-space: nowrap; + padding-left: 0px; + border-top: 0px; + padding-bottom: $small-offset; +} + +thead, +tbody { + display: block; +} + +tbody { + height: 150px; + overflow-y: auto; + overflow-x: auto; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importschedule/Importschedule.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,152 @@ +<template> + <div class="d-flex flex-row"> + <Spacer></Spacer> + <div class="mt-3 w-100"> + <div class="card flex-grow-1 schedulecard shadow-xs"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon icon="clock" class="mr-2"></font-awesome-icon> + <translate class="headline">Imports</translate> + </h6> + <div class="card-body schedulecardbody"> + <div class="card-body schedulecardbody"> + <div class="searchandfilter w-50 d-flex flex-row"> + <div class="searchgroup input-group"> + <div class="input-group-prepend"> + <span class="input-group-text" id="search"> + <font-awesome-icon icon="search"></font-awesome-icon> + </span> + </div> + <input + v-model="searchQuery" + type="text" + class="form-control" + placeholder + aria-label="Search" + aria-describedby="search" + /> + </div> + </div> + <table v-if="schedules.length" class="table"> + <thead> + <tr> + <th><translate>Import</translate></th> + <th><translate>Type</translate></th> + <th><translate>Author</translate></th> + <th><translate>Schedule</translate></th> + <th><translate>Email</translate></th> + <th> </th> + <th> </th> + </tr> + </thead> + <tbody> + <tr v-for="(schedule, index) in schedules" :key="index"> + <td></td> + <td></td> + <td></td> + <td></td> + <td></td> + <td> + <font-awesome-icon + icon="pencil-alt" + fixed-width + ></font-awesome-icon> + </td> + <td> + <font-awesome-icon + @click="deleteSchedule" + icon="trash" + fixed-width + ></font-awesome-icon> + </td> + </tr> + </tbody> + </table> + <div v-else class="mt-4 small text-center py-3"> + <translate>No schedules</translate> + </div> + <button + @click="newImport" + class="btn btn-info position-absolute newbutton" + > + <translate>New Import</translate> + </button> + </div> + </div> + </div> + </div> + <Importscheduledetail></Importscheduledetail> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { mapState } from "vuex"; + +export default { + name: "importschedule", + components: { + Importscheduledetail: () => import("./Importscheduledetail"), + Spacer: () => import("@/components/Spacer") + }, + data() { + return { + searchQuery: "" + }; + }, + methods: { + newImport() { + this.$store.commit("imports/setImportScheduleDetailVisible"); + }, + deleteSchedule(index) { + this.$store.commit("imports/deleteSchedule", index); + } + }, + computed: { + ...mapState("application", ["showSidebar"]), + ...mapState("imports", ["schedules"]), + spacerStyle() { + return [ + "spacer ml-3", + { + "spacer-expanded": this.showSidebar, + "spacer-collapsed": !this.showSidebar + } + ]; + } + } +}; +</script> + +<style lang="scss" scoped> +.schedulecard { + margin-right: $offset; + min-height: 20rem; +} + +.schedulecard-body { + width: 100%; + margin-left: auto; + margin-right: auto; +} + +.newbutton { + position: absolute; + bottom: $offset; + right: $offset; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importschedule/Importscheduledetail.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,511 @@ +<template> + <div + class="importscheduledetails fadeIn animated" + v-if="importScheduleDetailVisible" + > + <div class="card shadow-xs importscheduledetailscard pb-5"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <translate>New import</translate> + <span @click="closeDetailview" class="closebutton"> + <font-awesome-icon icon="times"></font-awesome-icon> + </span> + </h6> + <div class="card-body"> + <form @submit.prevent="save" class="ml-2 mr-2"> + <div class="d-flex flex-row w-100"> + <div class="flex-column w-50 mr-3"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Imports</translate> + </small> + </div> + <select v-model="import_" class="custom-select" id="importtype"> + <option :value="$options.IMPORTTYPES.BOTTLENECK" + ><translate>Bottlenecks</translate></option + > + </select> + </div> + <div v-if="import_" class="flex-column w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Importtype</translate> + </small> + </div> + <select + :disabled="fixedSource" + v-model="importSource" + class="custom-select" + id="importsource" + > + <option :value="this.$options.IMPORTSOURCES.SOAP" + ><translate>SOAP</translate></option + > + </select> + </div> + </div> + <div v-if="isURLRequired"> + <div class="d-flex flex-row"> + <div class="flex-column mt-3 mr-3 w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div class="w-100"> + <input v-model="url" class="form-control" type="url" /> + </div> + </div> + <div class="flex-column mt-3 text-left"> + <div class="d-flex flex-row"> + <small class="text-muted mr-2" + ><translate>Insecure</translate> + </small> + </div> + <div class="d-flex flex-row"> + <toggle-button + v-model="insecure" + class="mt-2" + :speed="100" + :color="{ + checked: '#FF0000', + unchecked: '#E9ECEF', + disabled: '#CCCCCC' + }" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="60" + :height="30" + /> + </div> + </div> + </div> + </div> + <div class="flex-column mt-3 w-100 mr-2"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Simple Schedule</translate> + </small> + </div> + <div class="flex-flex-row text-left"> + <toggle-button + v-model="easyCron" + class="mt-2" + :speed="100" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="60" + :height="30" + /> + </div> + </div> + <div class="flex-column w-100 mr-2"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Schedule</translate> + </small> + </div> + <div v-if="easyCron" class="text-left w-50"> + <select v-model="simple" class="form-control" + ><option value="weekly"><translate>Weekly</translate></option> + <option value="monthly"><translate>Monthly</translate></option> + </select> + </div> + <div v-if="!easyCron" class="text-left w-100"> + <div class="d-flex flex-row"> + <h4 class="mt-auto mb-auto mr-2">{{ $options.EVERY }}</h4> + <select + style="width: 130px;" + v-model="cronMode" + class="form-control" + @change="clearInputs" + > + <option + v-for="(option, key) in $options.CRONMODE" + :value="key" + :key="key" + >{{ option }}</option + > + </select> + <div v-if="cronMode == 'hour'" class="ml-1 d-flex flex-row"> + <h4 class="mt-auto mb-auto">{{ $options.ON }}</h4> + <input + v-model="minutes" + class="cronfield ml-1 mr-1 form-control" + type="number" + /> + <h4 class="mt-auto mb-auto">{{ $options.MINUTESPAST }}</h4> + </div> + <div v-if="cronMode == 'day'" class="ml-1 d-flex flex-row"> + <h4 class="mt-auto mb-auto">{{ $options.AT }}</h4> + <input + v-model="hour" + class="cronfield ml-1 mr-1 form-control" + type="number" + /> + <input + v-model="minutes" + class="cronfield ml-1 mr-1 form-control" + type="number" + /> + <h4 class="mt-auto mb-auto">{{ $options.OCLOCK }}</h4> + </div> + <div v-if="cronMode == 'week'" class="ml-1 d-flex flex-row"> + <h4 class="ml-1 mr-1 mt-auto mb-auto">{{ $options.ON }}</h4> + <select v-model="day" class="form-control"> + <option + v-for="(option, key) in $options.DAYSOFWEEK" + :key="key" + :value="key" + >{{ option }}</option + > + </select> + <h4 class="ml-1 mt-auto mb-auto">{{ $options.AT }}</h4> + <input + v-model="hour" + class="cronfield ml-1 mr-1 form-control" + type="number" + /> + <input + v-model="minutes" + class="cronfield ml-1 mr-1 form-control" + type="number" + /> + </div> + <div v-if="cronMode == 'month'" class="ml-1 d-flex flex-row"> + <h4 class="ml-1 mt-auto mb-auto">{{ $options.ON }}</h4> + <input + v-model="dayOfMonth" + class="cronfield ml-1 mr-1 form-control" + type="number" + /> + <h4 class="mt-auto mb-auto">{{ $options.AT }}</h4> + <input + v-model="hour" + class="cronfield ml-1 mr-2 form-control" + type="number" + /> + <input + v-model="minutes" + class="cronfield ml-1 mr-2 form-control" + type="number" + /> + <h4 class="mt-auto mb-auto">{{ $options.OCLOCK }}</h4> + </div> + <div v-if="cronMode == 'year'" class="ml-1 d-flex flex-row"> + <h4 class="ml-1 mt-auto mb-auto">{{ $options.ON }}</h4> + <input + v-model="dayOfMonth" + class="cronfield ml-1 mr-1 form-control" + type="number" + /> + <h4 class="mt-auto mb-auto">{{ $options.OF }}</h4> + <select v-model="month" class="ml-1 mr-1 form-control"> + <option + v-for="(option, key) in $options.MONTHS" + :value="key" + :key="key" + >{{ option }}</option + > + </select> + <h4 class="mt-auto mb-auto">{{ $options.ON }}</h4> + <input + v-model="hour" + class="cronfield ml-1 mr-1 form-control" + type="number" + /> + <input + v-model="minutes" + class="cronfield ml-1 mr-1 form-control" + type="number" + /> + </div> + </div> + <div class="mt-3 w-50 d-flex flex-row"> + <h5 class="mt-auto mb-auto mr-2"> + <translate>Cronstring</translate> + </h5> + <input class="form-control" :value="cronString" type="text" /> + </div> + </div> + </div> + <div class="flex-column mt-3 w-100 mr-2"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Email Notification</translate> + </small> + </div> + <div class="flex-flex-row text-left"> + <toggle-button + v-model="eMailNotification" + class="mt-2" + :speed="100" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="60" + :height="30" + /> + </div> + </div> + <div class="flex-column w-100 mr-2"> + <div class="flex-row text-left"> + <small class="text-muted"><translate>Email</translate> </small> + </div> + <input + :disabled="!eMailNotification" + class="form-control" + type="email" + /> + </div> + <button type="submit" class="shadow-sm btn btn-info submit-button"> + <translate>Submit</translate> + </button> + <button + @click="triggerManualImport" + type="button" + class="shadow-sm btn btn-outline-info trigger" + :disabled="!triggerActive" + > + <font-awesome-icon + class="fa-fw mr-2" + fixed-width + icon="play" + ></font-awesome-icon + ><translate>Trigger import</translate> + </button> + </form> + </div> + </div> + </div> +</template> + +<script> +import { mapState } from "vuex"; +import { displayInfo, displayError } from "@/lib/errors.js"; +import app from "@/main.js"; +import { HTTP } from "@/lib/http.js"; + +export default { + name: "importscheduledetail", + data() { + return { + importType: null, + schedule: null, + import_: null, + importSource: null, + eMailNotification: false, + easyCron: true, + cronMode: "", + minutes: null, + month: null, + hour: null, + day: null, + dayOfMonth: null, + simple: null, + url: null, + insecure: false, + triggerActive: true + }; + }, + IMPORTTYPES: { + BOTTLENECK: "BOTTLENECK" + }, + IMPORTSOURCES: { + SOAP: "SOAP" + }, + EVERY: app.$gettext("Every"), + MINUTESPAST: app.$gettext("minutes past"), + ON: app.$gettext("on"), + OF: app.$gettext("of"), + AT: app.$gettext("at"), + OCLOCK: app.$gettext("o' clock"), + CRONMODE: { + "15minutes": app.$gettext("15 minutes"), + hour: app.$gettext("hour"), + day: app.$gettext("day"), + week: app.$gettext("week"), + month: app.$gettext("month"), + year: app.$gettext("year") + }, + DAYSOFWEEK: { + 1: app.$gettext("Monday"), + 2: app.$gettext("Tuesday"), + 3: app.$gettext("Wednesday"), + 4: app.$gettext("Thursday"), + 5: app.$gettext("Friday"), + 6: app.$gettext("Saturday"), + 0: app.$gettext("Sunday") + }, + MONTHS: { + 1: app.$gettext("January"), + 2: app.$gettext("February"), + 3: app.$gettext("March"), + 4: app.$gettext("April"), + 5: app.$gettext("May"), + 6: app.$gettext("June"), + 7: app.$gettext("July"), + 8: app.$gettext("August"), + 9: app.$gettext("September"), + 10: app.$gettext("October"), + 11: app.$gettext("November"), + 12: app.$gettext("December") + }, + watch: { + cronString() { + if (this.isWeekly(this.cronString)) { + this.simple = "weekly"; + } + if (this.isMonthly(this.cronString)) { + this.simple = "monthly"; + } + }, + import_() { + if (this.import_ === this.$options.IMPORTTYPES.BOTTLENECK) { + this.importSource = this.$options.IMPORTSOURCES.SOAP; + } + } + }, + computed: { + ...mapState("imports", ["importScheduleDetailVisible"]), + isURLRequired() { + if (this.import_ === this.$options.IMPORTTYPES.BOTTLENECK) return true; + return false; + }, + cronString: { + get() { + let getValue = value => { + return this[value] ? this[value] : "*"; + }; + if (this.cronMode === "15minutes") return "*/15 * * * *"; + const min = getValue("minutes"); + const h = getValue("hour"); + const dm = getValue("dayOfMonth"); + const m = getValue("month"); + const wd = getValue("day"); + return `${min} ${h} ${dm} ${m} ${wd}`; + } + }, + fixedSource() { + if (this.import_ === this.$options.IMPORTTYPES.BOTTLENECK) return true; + return false; + } + }, + methods: { + isWeekly(cron) { + return /\d{1,2} \d{1,2} \* \* \d{1}/.test(cron); + }, + isMonthly(cron) { + return /\d{1,2} \d{1,2} \d{1,2} \* \*/.test(cron); + }, + initialize() { + this.importType = null; + this.schedule = null; + this.import_ = null; + this.importSource = null; + this.eMailNotification = false; + this.easyCron = true; + this.cronMode = ""; + this.minutes = null; + this.month = null; + this.hour = null; + this.day = null; + this.dayOfMonth = null; + this.simple = null; + this.url = null; + this.insecure = false; + }, + clearInputs() { + this.minutes = null; + this.month = null; + this.hour = null; + this.day = null; + this.dayOfMonth = null; + }, + triggerManualImport() { + if (!this.triggerActive) return; + let data = {}; + if (this.import_ === this.$options.IMPORTTYPES.BOTTLENECK) { + if (!this.url) return; + data["url"] = this.url; + data["insecure"] = this.insecure; + } + const importTypes = { + BOTTLENECK: "bottleneck" + }; + this.triggerActive = false; + HTTP.post("imports/" + importTypes[this.import_], data, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + } + }) + .then(response => { + const { id } = response.data; + displayInfo({ + title: this.$gettext("Import"), + message: this.$gettext("Manually triggered import: #") + id + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => { + this.triggerActive = true; + }); + }, + save() { + displayInfo({ + title: this.$gettext("Import"), + message: this.$gettext("under construction") + }); + }, + closeDetailview() { + this.initialize(); + this.$store.commit("imports/setImportScheduleDetailInvisible"); + } + }, + imports: [], + on: "on", + off: "off", + periods: { + DAILY: "daily", + MONTHLY: "monthly" + } +}; +</script> + +<style lang="scss" scoped> +.cronfield { + width: 55px; +} + +.importscheduledetailscard { + min-height: 550px; +} + +.importscheduledetails { + width: 100%; + margin-top: $offset; + margin-right: $offset; +} + +.trigger { + position: absolute; + left: $large-offset; + bottom: $offset; +} + +.submit-button { + position: absolute; + right: $large-offset; + bottom: $offset; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/layers/Layers.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,63 @@ +<template> + <div + :class="[ + 'box ui-element rounded bg-white text-nowrap', + { expanded: showLayers } + ]" + > + <div style="width: 20rem"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="layer-group" class="mr-2"></font-awesome-icon + ><translate>Layers</translate> + <font-awesome-icon + icon="times" + class="ml-auto text-muted" + @click="$store.commit('application/showLayers', false)" + ></font-awesome-icon> + </h6> + <div class="d-flex flex-column p-3 small"> + <Layerselect + v-for="(layer, index) in layersForLegend" + :layerindex="index" + :layername="layer.name" + :key="layer.name" + :isVisible="layer.isVisible" + @visibilityToggled="visibilityToggled" + ></Layerselect> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapGetters, mapState } from "vuex"; +export default { + name: "layers", + components: { + Layerselect: () => import("./Layerselect") + }, + computed: { + ...mapGetters("map", ["layersForLegend"]), + ...mapState("application", ["showLayers"]) + }, + methods: { + visibilityToggled(layer) { + this.$store.commit("map/toggleVisibility", layer); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/layers/Layerselect.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,84 @@ +<template> + <div> + <div class="form-check d-flex flex-row flex-start selection"> + <input + class="form-check-input" + @change="visibilityToggled" + :id="layername" + type="checkbox" + :checked="isVisible" + /> + <LegendElement + :layername="layername" + :layerindex="layerindex" + ></LegendElement> + <label class="layername form-check-label" @click="visibilityToggled">{{ + layername + }}</label> + </div> + <div v-if="isVisible && layername == 'Bottleneck isolines'"> + <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl" /> + </div> + </div> +</template> + +<style lang="scss" scoped> +.selection { + text-align: left; +} +.layername { + margin-left: $small-offset; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { HTTP } from "@/lib/http"; +export default { + props: ["layername", "layerindex", "isVisible"], + name: "layerselect", + data() { + return { + isolinesLegendImgUrl: "" + }; + }, + components: { + LegendElement: () => import("./LegendElement.vue") + }, + methods: { + visibilityToggled() { + this.$emit("visibilityToggled", this.layerindex); + } + }, + created() { + // fetch legend image for bottleneck isolines + // TODO: move to store + if (this.layername == "Bottleneck isolines") { + const src = + "/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.0.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=sounding_results_contour_lines_geoserver&legend_options=columns:4;fontAntiAliasing:true"; + HTTP.get(src, { + headers: { + Accept: "image/png", + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + var urlCreator = window.URL || window.webkitURL; + this.isolinesLegendImgUrl = urlCreator.createObjectURL(response.data); + }); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/layers/LegendElement.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,124 @@ +<template> + <div :id="id" class="legendelement"></div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { mapGetters } from "vuex"; + +import { Map, View } from "ol"; +import Feature from "ol/Feature"; +import { Vector as VectorLayer } from "ol/layer.js"; +import { Vector as VectorSource } from "ol/source.js"; +import LineString from "ol/geom/LineString.js"; +import Point from "ol/geom/Point"; + +export default { + name: "legendelement", + props: ["layername", "layerindex"], + data: function() { + return { + myMap: null, + mapLayer: null + }; + }, + computed: { + ...mapGetters("map", ["getLayerByName"]), + id() { + return "legendelement" + this.layerindex; + }, + mstyle() { + if (this.mapLayer && this.mapLayer.data.getStyle) { + return this.mapLayer.data.getStyle(); + } + } + }, + watch: { + mstyle(newStyle, oldStyle) { + // only recreate if there already was a style before + if (oldStyle) { + let vector = this.createVectorLayer(); + + this.myMap.removeLayer(this.myMap.getLayers()[0]); + this.myMap.addLayer(vector); + } + } + }, + mounted() { + this.mapLayer = this.getLayerByName(this.layername); + if (this.mapLayer.data.getType() == "VECTOR") { + this.initMap(); + } else { + // TODO other tiles + } + }, + methods: { + initMap() { + let vector = this.createVectorLayer(); + + this.myMap = new Map({ + layers: [vector], + target: this.id, + controls: [], + interactions: [], + view: new View({ + center: [0, 0], + zoom: 3, + projection: "EPSG:4326" + }) + }); + }, + createVectorLayer() { + let mapStyle = this.mapLayer.data.getStyle(); + + let feature = new Feature({ + geometry: new LineString([[-1, 0.5], [0, 0], [0.7, 0], [1.3, -0.7]]) + }); + + // special case if we need to call the style function with a special + // parameter or to detect a point layer + if (this.mapLayer["forLegendStyle"]) { + if (this.mapLayer.forLegendStyle.point) { + feature.setGeometry(new Point([0, 0])); + } + mapStyle = this.mapLayer.data.getStyleFunction()( + feature, + this.mapLayer.forLegendStyle.resolution + ); + } + + // we could add extra properties here, if they are needed for + // the styling function in the future. An idea is to extend the + // this.mapLayer["forLegendStyle"] for it. + // FIXME, this is a special case for the Fairway Dimensions style + feature.set("level_of_service", ""); + return new VectorLayer({ + source: new VectorSource({ + features: [feature], + wrapX: false + }), + style: mapStyle + }); + } + } +}; +</script> + +<style lang="scss" scoped> +.legendelement { + max-height: 1.5rem; + width: 2rem; +} +</style>
--- a/client/src/components/map/Identify.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,125 +0,0 @@ -<template> - <div - :class="[ - 'box ui-element rounded bg-white text-nowrap', - { expanded: showIdentify } - ]" - > - <div style="width: 20rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="info" class="mr-2"></font-awesome-icon> - <translate>Identified</translate> - <font-awesome-icon - icon="times" - class="ml-auto text-muted" - @click="$store.commit('application/showIdentify', false)" - ></font-awesome-icon> - </h6> - <div class="d-flex flex-column features p-3 flex-grow-1 text-left"> - <div v-if="currentMeasurement"> - <b> - {{ currentMeasurement.quantity }} ({{ - currentMeasurement.unitSymbol - }}): - </b> - <br /> - <small>{{ currentMeasurement.value }}</small> - </div> - <div v-for="(feature, i) of identifiedFeatures" :key="feature.getId()"> - <div v-if="feature.getId()" :class="{ 'mt-2': i }"> - <b - >{{ - feature - .getId() - .replace( - /[.][^.]*$/, - "" - ) /* cut away everything from the last . to the end */ - }}:</b> - <small - v-for="(value, key) in prepareProperties(feature)" - :key="key" - > - <div v-if="value">{{ key }}:{{ value }}</div> - </small> - </div> - </div> - <div - v-if="!currentMeasurement && !identifiedFeatures.length" - class="text-muted small text-center my-auto" - > - <translate>No features identified.</translate> - </div> - </div> - <div class="versioninfo border-top p-3 text-left"> - <span v-translate="{ license: 'AGPL-3.0-or-later' }"> - This app uses <i>gemma</i>, which is Free Software under <br /> - %{ license } without warranty, see docs for details. - </span> - <br /> - <a href="https://hg.intevation.de/gemma/file/tip"> - <translate>source-code</translate> - </a> - {{ versionStr }} <br />© via donau. ⓔ Intevation. <br /> - <span v-translate="{ name: 'OpenSteetMap' }" - >Some data © - <a href="https://www.openstreetmap.org/copyright">%{ name }</a> - contributors. - </span> - <p v-translate="{geoLicense: 'CC-BY-4.0'}"> - Uses - <a href="https://download.geonames.org/export/dump/readme.txt" - >GeoNames</a> under %{ geoLicense }. - </p> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.features { - max-height: 19rem; - overflow-y: auto; -} - -.versioninfo { - font-size: 60%; - white-space: normal; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Bernhard E. Reiter <bernhard.reiter@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; - -export default { - name: "identify", - computed: { - ...mapGetters("application", ["versionStr"]), - ...mapState("application", ["showIdentify"]), - ...mapState("map", ["identifiedFeatures", "currentMeasurement"]) - }, - methods: { - prepareProperties(feature) { - // return dict object with propertyname:plainvalue prepared for display - var properties = feature.getProperties(); - delete properties[feature.getGeometryName()]; - return properties; - } - } -}; -</script>
--- a/client/src/components/map/Main.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ -<template> - <div class="main d-flex flex-column"> - <Maplayer></Maplayer> - <FairwayProfile></FairwayProfile> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import Maplayer from "./Maplayer"; -import FairwayProfile from "./fairway/Fairwayprofile"; - -export default { - name: "mainview", - components: { - Maplayer, - FairwayProfile - } -}; -</script>
--- a/client/src/components/map/Maplayer.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,375 +0,0 @@ -<template> - <div id="map" :class="mapStyle"></div> -</template> - -<style lang="scss" scoped> -.nocursor { - cursor: none; -} - -.mapsplit { - height: 50vh; -} - -.mapfull { - height: 100vh; -} - -@media print { - .mapfull { - width: 2000px; - height: 2828px; - } - .mapsplit { - width: 2000px; - height: 2828px; - } -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * * Thomas Junk <thomas.junk@intevation.de> - * * Bernhard E. Reiter <bernhard.reiter@intevation.de> - */ -import { HTTP } from "../../lib/http"; -import { mapGetters, mapState } from "vuex"; -import "ol/ol.css"; -import { Map, View } from "ol"; -import { WFS, GeoJSON } from "ol/format.js"; -import { Stroke, Style, Fill } from "ol/style.js"; - -/* for the sake of debugging */ -/* eslint-disable no-console */ -export default { - name: "maplayer", - data() { - return { - projection: "EPSG:3857" - }; - }, - computed: { - ...mapGetters("map", ["getLayerByName", "getVSourceByName"]), - ...mapState("map", [ - "extent", - "layers", - "openLayersMap", - "lineTool", - "polygonTool", - "cutTool" - ]), - ...mapState("bottlenecks", ["selectedSurvey"]), - ...mapState("application", ["showSplitscreen"]), - mapStyle() { - return { - mapfull: !this.showSplitscreen, - mapsplit: this.showSplitscreen, - nocursor: this.hasActiveInteractions - }; - }, - hasActiveInteractions() { - return ( - (this.lineTool && this.lineTool.getActive()) || - (this.polygonTool && this.polygonTool.getActive()) || - (this.cutTool && this.cutTool.getActive()) - ); - } - }, - methods: { - buildVectorLoader(featureRequestOptions, endpoint, vectorSource) { - // build a function to be used for VectorSource.setLoader() - // make use of WFS().writeGetFeature to build the request - // and use our HTTP library to actually do it - // NOTE: a) the geometryName has to be given in featureRequestOptions, - // because we want to load depending on the bbox - // b) the VectorSource has to have the option strategy: bbox - featureRequestOptions["outputFormat"] = "application/json"; - var loader = function(extent, resolution, projection) { - featureRequestOptions["bbox"] = extent; - featureRequestOptions["srsName"] = projection.getCode(); - var featureRequest = new WFS().writeGetFeature(featureRequestOptions); - // DEBUG console.log(featureRequest); - HTTP.post( - endpoint, - new XMLSerializer().serializeToString(featureRequest), - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - } - ) - .then(response => { - var features = new GeoJSON().readFeatures( - JSON.stringify(response.data) - ); - vectorSource.addFeatures(features); - // console.log( - // "loaded", - // features.length, - // featureRequestOptions.featureTypes, - // "features" - // ); - // DEBUG console.log("loaded ", features, "for", vectorSource); - // eslint-disable-next-line - }) - .catch(() => { - vectorSource.removeLoadedExtent(extent); - }); - }; - return loader; - }, - updateBottleneckFilter(bottleneck_id, datestr) { - console.log("updating filter with", bottleneck_id, datestr); - const layer = this.getLayerByName("Bottleneck isolines"); - const wmsSrc = layer.data.getSource(); - const exists = bottleneck_id != "does_not_exist"; - - if (exists) { - wmsSrc.updateParams({ - cql_filter: - "date_info='" + - datestr + - "' AND bottleneck_id='" + - bottleneck_id + - "'" - }); - } - layer.isVisible = exists; - layer.data.setVisible(exists); - }, - onBeforePrint(/* evt */) { - // console.log("onBeforePrint(", evt ,")"); - // - // the following code shows how to get the current map canvas - // and change it, however this does not work well enough, as - // another mechanism seems to update the size again before the rendering - // for printing is done: - // console.log(this.openLayersMap.getViewport()); - // var canvas = this.openLayersMap.getViewport().getElementsByTagName("canvas")[0]; - // console.log(canvas); - // canvas.width=1000; - // canvas.height=1414; - // - // An experiment which also did not work: - // this.openLayersMap.setSize([1000, 1414]); // estimate portait DIN A4 - // - // according to documentation - // http://openlayers.org/en/latest/apidoc/module-ol_PluggableMap-PluggableMap.html#updateSize - // "Force a recalculation of the map viewport size. This should be called when third-party code changes the size of the map viewport." - // but did not help - // this.openLayersMap.updateSize(); - }, - onAfterPrint(/* evt */) { - // could be used to undo changes that have been done for printing - // though https://www.tjvantoll.com/2012/06/15/detecting-print-requests-with-javascript/ - // reported that this was not feasable (back then). - // console.log("onAfterPrint(", evt, ")"); - } - }, - watch: { - showSplitscreen() { - const map = this.openLayersMap; - this.$nextTick(() => { - map && map.updateSize(); - }); - }, - selectedSurvey(newSelectedSurvey) { - if (newSelectedSurvey) { - this.updateBottleneckFilter( - newSelectedSurvey.bottleneck_id, - newSelectedSurvey.date_info - ); - } else { - this.updateBottleneckFilter("does_not_exist", "1999-10-01"); - } - } - }, - mounted() { - let map = new Map({ - layers: [...this.layers.map(x => x.data)], - target: "map", - controls: [], - view: new View({ - center: [this.extent.lon, this.extent.lat], - zoom: this.extent.zoom, - projection: this.projection - }) - }); - map.on("moveend", event => { - const center = event.map.getView().getCenter(); - this.$store.commit("map/extent", { - lat: center[1], - lon: center[0], - zoom: event.map.getView().getZoom() - }); - }); - this.$store.dispatch("map/openLayersMap", map); - - // TODO make display of layers more dynamic, e.g. from a list - - // loading the full WFS layer, by not setting the loader function - // and without bboxStrategy - var featureRequest = new WFS().writeGetFeature({ - srsName: "EPSG:3857", - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["fairway_dimensions"], - outputFormat: "application/json" - }); - - // NOTE: loading the full fairway_dimensions makes sure - // that all are available for the intersection with the profile - HTTP.post( - "/internal/wfs", - new XMLSerializer().serializeToString(featureRequest), - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - } - ).then(response => { - this.getVSourceByName("Fairway Dimensions").addFeatures( - new GeoJSON().readFeatures(JSON.stringify(response.data)) - ); - // would scale to the extend of all resulting features - // this.openLayersMap.getView().fit(vectorSrc.getExtent()); - }); - - // load following layers with bboxStrategy (using our request builder) - var layer = null; - - layer = this.getLayerByName("Waterway Area"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featurePrefix: "ws-wamos", - featureTypes: ["ienc_wtware"], - geometryName: "geom" - }, - "/external/d4d", - layer.data.getSource() - ) - ); - - layer = this.getLayerByName("Waterway Axis"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featurePrefix: "ws-wamos", - featureTypes: ["ienc_wtwaxs"], - geometryName: "geom" - }, - "/external/d4d", - layer.data.getSource() - ) - ); - - layer = this.getLayerByName("Distance marks"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featurePrefix: "ws-wamos", - featureTypes: ["ienc_dismar"], - geometryName: "geom" //, - /* restrict loading approximately to extend of danube in Austria */ - // filter: bboxFilter("geom", [13.3, 48.0, 17.1, 48.6], "EPSG:4326") - }, - "/external/d4d", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName("Distance marks, Axis"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["distance_marks_geoserver"], - geometryName: "geom" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - - layer = this.getLayerByName("Waterway Area, named"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["hydro_seaare"], - geometryName: "geom" - }, - "/external/d4d", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName("Bottlenecks"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["bottlenecks"], - geometryName: "area" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - HTTP.get("/system/style/Bottlenecks/stroke", { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - this.btlnStrokeC = response.data.code; - HTTP.get("/system/style/Bottlenecks/fill", { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - this.btlnFillC = response.data.code; - var newstyle = new Style({ - stroke: new Stroke({ - color: this.btlnStrokeC, - width: 4 - }), - fill: new Fill({ - color: this.btlnFillC - }) - }); - layer.data.setStyle(newstyle); - }) - .catch(error => { - console.log(error); - }); - }) - .catch(error => { - console.log(error); - }); - - window.addEventListener("beforeprint", this.onBeforePrint); - window.addEventListener("afterprint", this.onAfterPrint); - - // so none is shown - this.updateBottleneckFilter("does_not_exist", "1999-10-01"); - this.$store.dispatch("map/enableIdentifyTool"); - this.$store.dispatch("bottlenecks/loadBottlenecks"); - } -}; -</script>
--- a/client/src/components/map/Pdftool.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ -<template> - <div - :class="[ - 'box ui-element rounded bg-white text-nowrap', - { expanded: showPdfTool } - ]" - > - <div style="width: 20rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="file-pdf" class="mr-2"></font-awesome-icon - ><translate>Generate PDF</translate> - <font-awesome-icon - icon="times" - class="ml-auto text-muted" - @click="$store.commit('application/showPdfTool', false)" - ></font-awesome-icon> - </h6> - <div class="p-3"> - <b><translate>Chose format:</translate></b> - <select v-model="form.format" class="form-control d-block w-100"> - <option><translate>landscape</translate></option> - <option><translate>portrait</translate></option> - </select> - <small class="d-block my-2"> - <input - type="radio" - id="pdfexport-downloadtype-download" - value="download" - v-model="form.downloadType" - selected - /> - <label for="pdfexport-downloadtype-download" class="ml-1 mr-2" - ><translate>Download</translate></label - > - <input - type="radio" - id="pdfexport-downloadtype-open" - value="open" - v-model="form.downloadType" - /> - <label for="pdfexport-downloadtype-open" class="ml-1" - ><translate>Open in new window</translate></label - > - </small> - <button - @click="download" - type="button" - class="btn btn-sm btn-info d-block w-100" - > - <translate>Generate PDF</translate> - </button> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; -//import { HTTP } from "../application/lib/http"; - -export default { - name: "pdftool", - data() { - return { - form: { - format: "landscape", - downloadType: "download" - } - }; - }, - computed: { - ...mapState("application", ["showPdfTool"]), - ...mapState("bottlenecks", ["selectedSurvey"]) - }, - methods: { - download() { - // generate PDF and open it - // TODO: replace this src with an API reponse after actually generating PDFs - let src = - this.form.format === "landscape" - ? "/img/PrintTemplate-Var2-Landscape.pdf" - : "/img/PrintTemplate-Var2-Portrait.pdf"; - - let a = document.createElement("a"); - a.href = src; - - if (this.form.downloadType === "download") - a.download = src.substr(src.lastIndexOf("/") + 1); - else a.target = "_blank"; - - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - } - } -}; -</script>
--- a/client/src/components/map/Search.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,292 +0,0 @@ -<template> - <div :class="searchbarContainerStyle"> - <div class="input-group-prepend m-0 d-print-none"> - <span @click="toggleSearchbar" :class="searchButtonStyle" for="search"> - <font-awesome-icon icon="search"></font-awesome-icon> - </span> - </div> - <div - :class="[ - 'searchgroup', - { - 'searchgroup-collapsed': !showSearchbar, - big: - showContextBox && - ['bottlenecks', 'staging'].indexOf(contextBoxContent) !== -1 - } - ]" - > - <input - @keyup.enter="takeFirstSearchresult" - id="search" - v-model="searchQuery" - type="text" - :class="searchInputStyle" - /> - </div> - <div - v-if="showSearchbar && searchResults !== null && !showContextBox" - class="searchresults border-top ui-element bg-white rounded-bottom d-print-none position-absolute" - > - <div - v-for="entry of searchResults" - :key="entry.name" - class="border-top text-left" - > - <a - href="#" - @click.prevent="moveToSearchResult(entry)" - class="p-2 d-block text-nowrap" - > - <font-awesome-icon - icon="ship" - v-if="entry.type === 'bottleneck'" - class="mr-1" - fixed-width - /> - <font-awesome-icon - icon="water" - v-if="entry.type === 'rhm'" - class="mr-1" - fixed-width - /> - <font-awesome-icon - icon="city" - v-if="entry.type === 'city'" - class="mr-1" - fixed-width - /> - {{ entry.name }} - </a> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.searchcontainer { - opacity: 0.96; -} - -.searchcontainer .searchbar { - border-top-left-radius: 0 !important; - border-bottom-left-radius: 0 !important; -} - -.searchgroup { - margin-left: -3px; - transition: width 0.3s; - width: 300px; - overflow: hidden; -} - -.searchgroup.big { - width: 571px; -} - -.searchgroup-collapsed { - width: 0; -} - -.searchbar { - height: 2rem !important; - box-shadow: none !important; -} - -.searchbar.rounded-top-right { - border-radius: 0 !important; - border-top-right-radius: 0.25rem !important; -} - -.searchlabel.rounded-top-left { - border-radius: 0 !important; - border-top-left-radius: 0.25rem !important; -} - -.input-group-text { - height: 2rem; - width: 2rem; -} - -.input-group-prepend svg path { - fill: #666; -} - -.searchresults { - box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2); - top: 2rem; - left: 0; - right: 0; - max-height: 24rem; - overflow: auto; -} - -.searchresults > div:first-child { - border-top: 0 !important; -} - -.searchresults a { - text-decoration: none; -} - -.searchresults a:hover { - background: #f8f8f8; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import debounce from "lodash.debounce"; -import { mapState } from "vuex"; - -import { displayError } from "../../lib/errors.js"; -import { HTTP } from "../../lib/http"; - -const setFocus = () => document.querySelector("#search").focus(); - -export default { - name: "search", - data() { - return { - searchQueryIsDirty: false, - searchResults: null, - isSearching: false - }; - }, - computed: { - ...mapState("application", [ - "showSearchbar", - "showContextBox", - "contextBoxContent" - ]), - searchQuery: { - get() { - return this.$store.state.application.searchQuery; - }, - set(value) { - this.$store.commit("application/searchQuery", value); - } - }, - searchIndicator: function() { - if (this.isSearching) { - return "⟳"; - } else if (this.searchQueryIsDirty) { - return ""; - } else { - return "✓"; - } - }, - searchbarContainerStyle() { - return [ - "input-group searchcontainer shadow-xs", - { - "d-flex": this.contextBoxContent !== "imports", - "d-none": this.contextBoxContent === "imports" && this.showContextBox - } - ]; - }, - searchInputStyle() { - return [ - "form-control ui-element search searchbar d-print-none border-0", - { "rounded-top-right": this.showContextBox || this.searchResults } - ]; - }, - searchButtonStyle() { - return [ - "ui-element input-group-text p-0 d-flex border-0 justify-content-center searchlabel bg-white d-print-none", - { - rounded: !this.showSearchbar, - "rounded-left": this.showSearchbar, - "rounded-top-left": - this.showSearchbar && (this.showContextBox || this.searchResults) - } - ]; - } - }, - watch: { - searchQuery: function() { - this.searchQueryIsDirty = true; - this.triggerSearch(); - } - }, - methods: { - takeFirstSearchresult() { - if (!this.searchResults || this.searchResults.length != 1) return; - this.moveToSearchResult(this.searchResults[0]); - }, - triggerSearch: debounce(function() { - this.doSearch(); - }, 500), - doSearch() { - this.isCalculating = true; - this.searchResults = null; - - if (this.searchQuery == "") { - return; - } - - HTTP.post( - "/search", - { string: this.searchQuery }, - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - } - ) - .then(response => { - // console.log("got:", response.data); - this.searchResults = response.data; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - - this.isCalculating = false; - this.searchQueryIsDirty = false; - }, - moveToSearchResult(resultEntry) { - // DEBUG console.log("Moving to", resultEntry); - if (resultEntry.geom.type == "Point") { - let zoom = 11; - if (resultEntry.type === "bottleneck") zoom = 17; - if (resultEntry.type === "rhm") zoom = 15; - if (resultEntry.type === "city") zoom = 13; - - this.$store.commit("map/moveMap", { - coordinates: resultEntry.geom.coordinates, - zoom, - preventZoomOut: true - }); - } - // this.searchQuery = ""; // clear search query again - this.toggleSearchbar(); - }, - toggleSearchbar() { - if (!this.showContextBox) { - if (!this.showSearchbar) { - setTimeout(setFocus, 300); - } - this.$store.commit("application/showSearchbar", !this.showSearchbar); - } - } - } -}; -</script>
--- a/client/src/components/map/Zoom.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -<template> - <div - class="d-flex buttoncontainer shadow-xs mb-3 position-absolute" - :style="showSplitscreen ? 'margin-bottom: 51vh !important' : ''" - > - <button - class="zoomButton border-0 bg-white rounded-left ui-element" - @click="zoomOut" - > - <font-awesome-icon icon="minus"></font-awesome-icon> - </button> - <button - class="zoomButton border-0 bg-white rounded-right ui-element border-right" - @click="zoomIn" - > - <font-awesome-icon icon="plus"></font-awesome-icon> - </button> - </div> -</template> - -<style lang="scss" scoped> -.buttoncontainer { - bottom: 0; - left: 50%; - margin-left: -$icon-width; -} - -.zoomButton { - min-height: $icon-width; - min-width: $icon-width; - z-index: 1; - outline: none; - color: #666; -} -</style> -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus@intevation.de> - * Thomas Junk <thomas.junk@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "zoom", - computed: { - ...mapState("map", ["openLayersMap"]), - ...mapState("application", ["showSplitscreen"]), - zoomLevel: { - get() { - return this.openLayersMap.getView().getZoom(); - }, - set(value) { - this.openLayersMap.getView().animate({ zoom: value, duration: 300 }); - } - } - }, - methods: { - zoomIn() { - this.zoomLevel = this.zoomLevel + 1; - }, - zoomOut() { - this.zoomLevel = this.zoomLevel - 1; - } - } -}; -</script>
--- a/client/src/components/map/contextbox/Bottlenecks.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,304 +0,0 @@ -<template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="ship" class="mr-2"></font-awesome-icon> - <translate>Bottlenecks</translate> - </h6> - <div class="row p-2 text-left small"> - <div class="col-5"> - <a href="#" @click="sortBy('name')" class="sort-link"> - <translate>Name</translate> - </a> - <font-awesome-icon - :icon="sortIcon" - class="ml-1" - v-if="sortColumn === 'name'" - ></font-awesome-icon> - </div> - <div class="col-2"> - <a href="#" @click="sortBy('latestMeasurement')" class="sort-link"> - <translate>Latest</translate> <br /> - <translate>Measurement</translate> - </a> - <font-awesome-icon - :icon="sortIcon" - class="ml-1" - v-if="sortColumn === 'latestMeasurement'" - ></font-awesome-icon> - </div> - <div class="col-3"> - <a href="#" @click="sortBy('chainage')" class="sort-link"> - <translate>Chainage</translate> - </a> - <font-awesome-icon - :icon="sortIcon" - class="ml-1" - v-if="sortColumn === 'chainage'" - ></font-awesome-icon> - </div> - <div class="col-2"></div> - </div> - <div - class="bottleneck-list small text-left" - :style="'max-height: ' + (showSplitscreen ? 18 : 35) + 'rem'" - v-if="filteredAndSortedBottlenecks().length" - > - <div - v-for="bottleneck in filteredAndSortedBottlenecks()" - :key="bottleneck.properties.name" - class="border-top row bottleneck-row mx-0" - > - <div class="col-5 py-2 text-left"> - <a href="#" @click="selectBottleneck(bottleneck)">{{ - bottleneck.properties.name - }}</a> - </div> - <div class="col-2 py-2"> - {{ displayCurrentSurvey(bottleneck.properties.current) }} - </div> - <div class="col-3 py-2"> - {{ - displayCurrentChainage( - bottleneck.properties.from, - bottleneck.properties.to - ) - }} - </div> - <div class="col-2 pr-0 text-right"> - <button - type="button" - class="btn btn-sm btn-info rounded-0 h-100" - @click="loadSurveys(bottleneck.properties.name)" - v-if="bottleneck.properties.current" - > - <font-awesome-icon - icon="spinner" - fixed-width - spin - v-if="loading === bottleneck.properties.name" - ></font-awesome-icon> - <font-awesome-icon - icon="angle-down" - fixed-width - v-if=" - loading !== bottleneck.properties.name && - openBottleneck !== bottleneck.properties.name - " - ></font-awesome-icon> - <font-awesome-icon - icon="angle-up" - fixed-width - v-if=" - loading !== bottleneck.properties.name && - openBottleneck === bottleneck.properties.name - " - ></font-awesome-icon> - </button> - </div> - <div - :class="[ - 'col-12 p-0', - 'surveys', - { open: openBottleneck === bottleneck.properties.name } - ]" - > - <a - href="#" - class="d-block px-3 py-2" - v-for="(survey, index) in openBottleneckSurveys" - :key="index" - @click="selectSurvey(survey, bottleneck)" - >{{ survey.date_info }}</a - > - </div> - </div> - </div> - <div v-else class="small text-center py-3 border-top"> - <translate>No results.</translate> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; -import { HTTP } from "../../../lib/http"; -import { displayError } from "../../../lib/errors.js"; - -export default { - name: "bottlenecks", - data() { - return { - sortColumn: "name", - sortDirection: "ASC", - openBottleneck: null, - openBottleneckSurveys: null, - loading: null - }; - }, - computed: { - ...mapState("application", [ - "searchQuery", - "showSearchbarLastState", - "showSplitscreen" - ]), - ...mapState("bottlenecks", ["bottlenecks"]), - sortIcon() { - return this.sortDirection === "ASC" - ? "sort-amount-down" - : "sort-amount-up"; - } - }, - methods: { - filteredAndSortedBottlenecks() { - return this.bottlenecks - .filter(bn => { - return bn.properties.name - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - }) - .sort((bnA, bnB) => { - switch (this.sortColumn) { - case "name": - if ( - bnA.properties.name.toLowerCase() < - bnB.properties.name.toLowerCase() - ) - return this.sortDirection === "ASC" ? -1 : 1; - if ( - bnA.properties.name.toLowerCase() > - bnB.properties.name.toLowerCase() - ) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - - case "latestMeasurement": { - if ( - (bnA.properties.current || "") < (bnB.properties.current || "") - ) - return this.sortDirection === "ASC" ? -1 : 1; - if ( - (bnA.properties.current || "") > (bnB.properties.current || "") - ) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - } - - case "chainage": - if (bnA.properties.from < bnB.properties.from) - return this.sortDirection === "ASC" ? -1 : 1; - if (bnA.properties.from > bnB.properties.from) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - - default: - return 0; - } - }); - }, - selectSurvey(survey, bottleneck) { - this.selectBottleneck(bottleneck); - this.$store.commit("bottlenecks/selectedSurvey", survey); - }, - selectBottleneck(bottleneck) { - this.$store.dispatch( - "bottlenecks/setSelectedBottleneck", - bottleneck.properties.name - ); - this.$store.commit("map/moveMap", { - coordinates: bottleneck.geometry.coordinates, - zoom: 17, - preventZoomOut: true - }); - }, - sortBy(column) { - this.sortColumn = column; - this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC"; - }, - loadSurveys(name) { - this.openBottleneckSurveys = null; - if (name === this.openBottleneck) { - this.openBottleneck = null; - } else { - this.openBottleneck = name; - this.loading = name; - - HTTP.get("/surveys/" + name, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - }) - .then(response => { - this.openBottleneckSurveys = response.data.surveys.sort((a, b) => { - return a.date_info < b.date_info ? 1 : -1; - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }) - .finally(() => (this.loading = null)); - } - }, - displayCurrentSurvey(current) { - return current ? current.substr(0, current.length - 1) : ""; - }, - displayCurrentChainage(from, to) { - return from / 10 + " - " + to / 10; - } - }, - mounted() { - this.$store.dispatch("bottlenecks/loadBottlenecks"); - } -}; -</script> - -<style lang="scss" scoped> -.bottleneck-list { - overflow-y: auto; -} - -.bottleneck-list .bottleneck-row a { - text-decoration: none; -} - -.bottleneck-list .bottleneck-row:hover { - background: #fbfbfb; -} - -.surveys { - max-height: 0; - min-height: 0; - overflow: hidden; -} - -.surveys a:hover { - background: #f3f3f3; -} - -.surveys.open { - max-height: 250px; - overflow: auto; -} - -.sort-link { - color: #444; - font-weight: bold; -} -</style>
--- a/client/src/components/map/contextbox/Contextbox.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -<template> - <div :class="style"> - <div @click="close" class="ui-element close-contextbox text-muted"> - <font-awesome-icon icon="times"></font-awesome-icon> - </div> - <Bottlenecks v-if="contextBoxContent === 'bottlenecks'"></Bottlenecks> - <Importsounding v-if="contextBoxContent === 'imports'"></Importsounding> - <Staging v-if="contextBoxContent === 'staging'"></Staging> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "contextbox", - components: { - Bottlenecks: () => import("./Bottlenecks"), - Importsounding: () => import("./ImportSoundingresults.vue"), - Staging: () => import("./Staging.vue") - }, - computed: { - ...mapState("application", [ - "showSearchbarLastState", - "contextBoxContent", - "showContextBox" - ]), - style() { - return [ - "ui-element shadow-xs contextbox", - { - contextboxcollapsed: !this.showContextBox, - contextboxextended: this.showContextBox, - "rounded-bottom": this.contextBoxContent !== "imports", - rounded: this.contextBoxContent === "imports" - } - ]; - } - }, - methods: { - close() { - this.$store.commit("application/showContextBox", false); - this.$store.commit( - "application/showSearchbar", - this.showSearchbarLastState - ); - } - } -}; -</script> - -<style lang="scss" scoped> -.contextbox { - position: relative; - background-color: #ffffff; - opacity: $slight-transparent; - transition: max-width 0.3s, max-height 0.3s; - overflow: hidden; - background: #fff; -} -.contextbox > div:last-child { - width: 600px; -} - -.contextboxcollapsed { - max-width: 0; - max-height: 0; -} - -.contextboxextended { - max-width: 600px; - max-height: 640px; -} - -.close-contextbox { - position: absolute; - z-index: 2; - right: 0; - top: 7px; - height: $icon-width; - width: $icon-height; -} -</style>
--- a/client/src/components/map/contextbox/ImportSoundingresults.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,379 +0,0 @@ -<template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="upload" class="mr-2"></font-awesome-icon> - <translate>Import Soundingresults</translate> - </h6> - <div v-if="editState" class="ml-auto mr-auto mt-4 w-95"> - <div class="d-flex flex-column"> - <div class="d-flex flex-row"> - <div class="mt-1 text-left w-50 ml-2 mr-4"> - <small class="text-muted"> - <translate>Bottleneck</translate> - </small> - <select v-model="bottleneck" class="custom-select"> - <option - v-for="bottleneck in availableBottlenecks" - :key="bottleneck" - >{{ bottleneck }}</option - > - </select> - <span class="text-danger"> - <small v-if="!bottleneck"> - <translate>Please select a bottleneck</translate> - </small> - </span> - </div> - <div class="d-flex flex-column mt-1 text-left w-50 mr-2"> - <small class="text-muted"> - <translate>Projection</translate> (EPSG) - </small> - <input - class="form-control" - v-model="projection" - value="4326" - placeholder="e.g. 4326" - type="number" - /> - <span class="text-left text-danger"> - <small v-if="!projection"> - <translate>Please enter a projection</translate> - </small> - </span> - </div> - </div> - <div class="d-flex flex-row"> - <div class="mt-1 text-left w-50 ml-2 mr-4"> - <small class="text-muted"> - <translate>Depthreference</translate> - </small> - <select - v-model="depthReference" - class="custom-select" - id="depthreference" - > - <option - v-for="option in this.$options.depthReferenceOptions" - :key="option" - >{{ option }}</option - > - </select> - <span class="text-left text-danger"> - <small v-if="!depthReference"> - <translate>Please enter a reference</translate> - </small> - </span> - </div> - <div class="mt-1 text-left w-50 mr-2"> - <small class="text-muted"> <translate>Date</translate> </small> - <input - id="importdate" - type="date" - class="form-control" - placeholder="Date of import" - aria-label="bottleneck" - aria-describedby="bottlenecklabel" - v-model="importDate" - /> - <span class="text-left text-danger"> - <small v-if="!importDate"> - <translate>Please enter a date</translate> - </small> - </span> - </div> - </div> - </div> - <div class="ml-2 mt-2 text-left"> - <small v-for="(message, index) in messages" :key="index"> - {{ message }} - </small> - </div> - </div> - <div class="w-95 ml-auto mr-auto mt-4 mb-4"> - <div v-if="uploadState" class="d-flex flex-row input-group mb-4"> - <div class="custom-file"> - <input - accept=".zip" - type="file" - @change="fileSelected" - class="custom-file-input" - id="uploadFile" - /> - <label class="custom-file-label" for="uploadFile"> - {{ uploadLabel }} - </label> - </div> - </div> - <div class="buttons text-right"> - <a - v-if="editState" - download="meta.json" - :href="dataLink" - class="btn btn-outline-info pull-left" - > - <translate>Download Meta.json</translate> - </a> - <button - v-if="editState" - @click="deleteTempData" - class="btn btn-danger" - type="button" - > - <translate>Cancel Upload</translate> - </button> - <button - :disabled="disableUploadButton" - @click="submit" - class="btn btn-info" - type="button" - > - {{ uploadState ? Upload : Confirm }} - </button> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { HTTP } from "../../../lib/http"; -import { displayError, displayInfo } from "../../../lib/errors.js"; -import { mapState } from "vuex"; - -const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; - -export default { - name: "imports", - data() { - return { - importState: IMPORTSTATE.UPLOAD, - depthReference: "", - bottleneck: "", - projection: "", - importDate: "", - uploadLabel: this.$gettext("choose .zip- file"), - uploadFile: null, - disableUpload: false, - token: null, - messages: [] - }; - }, - methods: { - initialState() { - this.importState = IMPORTSTATE.UPLOAD; - this.depthReference = ""; - this.bottleneck = ""; - this.projection = ""; - this.importDate = ""; - this.uploadLabel = this.$gettext("choose .zip- file"); - this.uploadFile = null; - this.disableUpload = false; - this.token = null; - this.messages = []; - }, - fileSelected(e) { - const files = e.target.files || e.dataTransfer.files; - if (!files) return; - this.uploadLabel = files[0].name; - this.uploadFile = files[0]; - }, - deleteTempData() { - HTTP.delete("/imports/soundingresult-upload/" + this.token, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - } - }) - .then(() => { - this.initialState(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - submit() { - if (!this.uploadFile || this.disableUpload) return; - if (this.importState === IMPORTSTATE.UPLOAD) { - this.upload(); - } else { - this.confirm(); - } - }, - upload() { - let formData = new FormData(); - formData.append("soundingresult", this.uploadFile); - HTTP.post("/imports/soundingresult-upload", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(response => { - if (response.data.meta) { - const { bottleneck, date, epsg } = response.data.meta; - const depthReference = response.data.meta["depth-reference"]; - this.bottleneck = bottleneck; - this.depthReference = depthReference; - this.importDate = new Date(date).toISOString().split("T")[0]; - this.projection = epsg; - } - this.importState = IMPORTSTATE.EDIT; - this.token = response.data.token; - this.messages = response.data.messages; - }) - .catch(error => { - const { status, data } = error.response; - const messages = data.messages ? data.messages.join(", ") : ""; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${messages}` - }); - }); - }, - confirm() { - let formData = new FormData(); - formData.append("token", this.token); - if (this.bottleneck) formData.append("bottleneck", this.bottleneck); - if (this.importDate) - formData.append("date", this.importDate.split("T")[0]); - if (this.depthReference) - formData.append("depth-reference", this.depthReference); - if (this.projection) formData.append("", this.projection); - - HTTP.post("/imports/soundingresult", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(() => { - displayInfo({ - title: this.$gettext("Import"), - message: this.$gettext("Starting import for ") + this.bottleneck - }); - this.initialState(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - mounted() { - this.$store.dispatch("bottlenecks/loadBottlenecks"); - }, - watch: { - showContextBox() { - if (!this.showContextBox && this.token) this.deleteTempData(); - } - }, - computed: { - ...mapState("application", ["showContextBox"]), - ...mapState("bottlenecks", ["bottlenecks"]), - disableUploadButton() { - if (this.importState === IMPORTSTATE.UPLOAD) return this.disableUpload; - if ( - !this.bottleneck || - !this.importDate || - !this.depthReference || - !this.projection - ) - return true; - return this.disableUpload; - }, - availableBottlenecks() { - return this.bottlenecks.map(x => x.properties.name); - }, - editState() { - return this.importState === IMPORTSTATE.EDIT; - }, - uploadState() { - return this.importState === IMPORTSTATE.UPLOAD; - }, - Upload() { - return this.$gettext("Upload"); - }, - Confirm() { - return this.$gettext("Confirm"); - }, - dataLink() { - return ( - "data:text/json;charset=utf-8," + - encodeURIComponent( - JSON.stringify({ - depthReference: this.depthReference, - bottleneck: this.bottleneck, - date: this.importDate - }) - ) - ); - } - }, - depthReferenceOptions: [ - "", - // "NAP", - // "KP", - // "FZP", - // "ADR", - // "TAW", - // "PUL", - // "NGM", - // "ETRS", - // "POT", - // "LDC", - // "HDC", - // "ZPG", - // "GLW", - // "HSW", - // "LNW", - // "HNW", - // "IGN", - // "WGS", - "RN" //, - // "HBO" - ] -}; -</script> - -<style lang="scss" scoped> -.projectionLabel { - margin-left: $small-offset; -} - -.depthreferencelabel { - margin-left: $small-offset; -} - -.offset-r { - margin-right: $small-offset; -} - -.buttons button { - margin-left: $offset !important; -} - -.label-text { - width: 5rem; - text-align: left; - line-height: 2.25rem; -} -</style>
--- a/client/src/components/map/contextbox/Staging.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,232 +0,0 @@ -<template> - <div class="w-90 stagingcard"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon - class="mr-2" - icon="clipboard-check" - ></font-awesome-icon> - <translate>Staging Area</translate> - </h6> - <table class="table"> - <thead> - <tr> - <th><translate>Name</translate></th> - <th><translate>Type</translate></th> - <th><translate>Date</translate></th> - <th><translate>Imported</translate></th> - <th><translate>Username</translate></th> - <th> </th> - <th> </th> - </tr> - </thead> - <tbody v-if="filteredData.length"> - <tr :key="data.id" v-for="data in filteredData"> - <td> - <a @click="zoomTo(data.id)" href="#">{{ - data.summary.bottleneck - }}</a> - </td> - <td>{{ data.kind.toUpperCase() }}</td> - <td>{{ data.summary.date }}</td> - <td>{{ data.enqueued.split("T")[0] }}</td> - <td>{{ data.user }}</td> - <td> - <button - :class="{ - btn: true, - 'btn-sm': true, - 'btn-outline-success': needsApproval(data) || isRejected(data), - 'btn-success': isApproved(data) - }" - @click="toggleApproval(data.id, $options.STATES.APPROVED)" - > - <font-awesome-icon icon="check"></font-awesome-icon> - </button> - </td> - <td> - <button - :class="{ - btn: true, - 'btn-sm': true, - 'btn-outline-danger': needsApproval(data) || isApproved(data), - 'btn-danger': isRejected(data) - }" - @click="toggleApproval(data.id, $options.STATES.REJECTED)" - > - <font-awesome-icon icon="times"></font-awesome-icon> - </button> - </td> - </tr> - </tbody> - <tbody v-else> - <tr> - <td class="text-center" colspan="6"> - <translate>No results.</translate> - </td> - </tr> - </tbody> - </table> - <div class="p-3" v-if="filteredData.length"> - <button @click="confirmReview" class="confirm-button btn btn-info"> - <translate>Confirm</translate> - </button> - </div> - <div class="p-3"> - <button @click="loadData" class="refresh btn btn-dark">Refresh</button> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus@intevation.de> - */ -import { mapState } from "vuex"; -import { HTTP } from "../../../lib/http.js"; -import { STATES } from "../../../store/imports.js"; -import { displayError, displayInfo } from "../../../lib/errors.js"; - -export default { - data() { - return {}; - }, - mounted() { - this.loadData(); - }, - computed: { - ...mapState("application", ["searchQuery"]), - ...mapState("imports", ["staging"]), - filteredData() { - return this.staging.filter(data => { - const result = [data.id + "", data.enqueued, data.kind, data.user].some( - x => x.toLowerCase().includes(this.searchQuery.toLowerCase()) - ); - return result; - }); - } - }, - STATES: STATES, - methods: { - loadData() { - this.$store.dispatch("imports/getStaging").catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }, - confirmReview() { - const reviewResults = this.staging - .filter(x => x.status !== STATES.NEEDSAPPROVAL) - .map(r => { - return { - id: r.id, - state: r.status - }; - }); - if (!reviewResults.length) return; - HTTP.patch("/imports", reviewResults, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - const messages = response.data - .map(x => { - if (x.message) return x.message; - if (x.error) return x.error; - }) - .join("\n\n"); - displayInfo({ - title: "Staging Area", - message: messages, - options: { - timeout: 0, - buttons: [{ text: "Ok", action: null, bold: true }] - } - }); - this.loadData(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }, - needsApproval(item) { - return item.status === STATES.NEEDSAPPROVAL; - }, - isRejected(item) { - return item.status === STATES.REJECTED; - }, - isApproved(item) { - return item.status === STATES.APPROVED; - }, - zoomTo(id) { - if (!id) return; - const soundingResult = this.filteredData.filter(x => x.id == id)[0]; - const { lat, lon, bottleneck, date } = soundingResult.summary; - const coordinates = [lat, lon]; - - this.$store.commit("map/moveMap", { - coordinates: coordinates, - zoom: 17, - preventZoomOut: true - }); - this.$store.dispatch( - "bottlenecks/setSelectedBottleneck", - bottleneck, - date - ); - }, - toggleApproval(id, newStatus) { - this.$store.commit("imports/toggleApproval", { - id: id, - newStatus: newStatus - }); - } - } -}; -</script> - -<style lang="scss" scoped> -.refresh { - position: absolute; - left: $offset; - bottom: $offset; -} -.table th, -td { - font-size: 0.9rem; - border-top: 0px !important; - border-bottom-width: 1px; - text-align: left; - padding: 0.5rem !important; -} - -.stagingcard { - position: relative; - min-height: 150px; -} - -.confirm-button { - position: absolute; - right: $offset; - bottom: $offset; -} -</style>
--- a/client/src/components/map/fairway/Fairwayprofile.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,413 +0,0 @@ -<template> - <div :class="['position-relative', { show: showSplitscreen }]"> - <button - class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle" - @click="$store.commit('application/showSplitscreen', false)" - v-if="showSplitscreen" - > - <font-awesome-icon icon="angle-down" /> - </button> - <button - class="rounded-bottom bg-white border-0 position-absolute clear-selection" - @click="$store.dispatch('fairwayprofile/clearSelection')" - v-if="showSplitscreen" - > - <font-awesome-icon icon="times" /> - </button> - <div class="profile bg-white position-relative d-flex flex-column"> - <h5 - class="headline border-bottom mb-0 py-2" - v-if="selectedBottleneck && selectedSurvey" - > - {{ selectedBottleneck }} ({{ selectedSurvey.date_info }}) - </h5> - <div class="d-flex flex-fill"> - <div - class="loading d-flex justify-content-center align-items-center" - v-if="surveysLoading || profileLoading" - > - <font-awesome-icon icon="spinner" spin /> - </div> - <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.profile { - width: 100vw; - height: 0; - overflow: hidden; - z-index: 2; -} - -.splitscreen-toggle, -.clear-selection { - width: 2rem; - height: 2rem; - margin-top: 8px; - z-index: 3; - outline: none; -} - -.splitscreen-toggle svg path, -.clear-selection svg path { - fill: #666; -} - -.splitscreen-toggle { - right: 2.5rem; -} - -.clear-selection { - right: 0.5rem; -} - -.show .profile { - height: 50vh; -} - -.loading { - background: rgba(255, 255, 255, 0.96); - position: absolute; - z-index: 99; - top: 0; - right: 0; - bottom: 0; - left: 0; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import * as d3 from "d3"; -import { mapState, mapGetters } from "vuex"; -import debounce from "debounce"; - -const GROUND_COLOR = "#4A2F06"; - -export default { - name: "fairwayprofile", - data() { - return { - coordinatesInput: "", - coordinatesSelect: null, - cutLabel: "", - showLabelInput: false, - width: null, - height: null, - margin: { - top: 20, - right: 40, - bottom: 30, - left: 40 - } - }; - }, - computed: { - ...mapGetters("fairwayprofile", ["totalLength"]), - ...mapState("application", ["showSplitscreen"]), - ...mapState("fairwayprofile", [ - "startPoint", - "endPoint", - "currentProfile", - "additionalSurvey", - "minAlt", - "maxAlt", - "fairwayCoordinates", - "waterLevels", - "selectedWaterLevel", - "profileLoading" - ]), - ...mapState("bottlenecks", [ - "selectedBottleneck", - "selectedSurvey", - "surveysLoading" - ]), - currentData() { - if ( - !this.selectedSurvey || - !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info) - ) - return []; - return this.currentProfile[this.selectedSurvey.date_info].points; - }, - additionalData() { - if ( - !this.additionalSurvey || - !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info) - ) - return []; - return this.currentProfile[this.additionalSurvey.date_info].points; - }, - waterColor() { - const result = this.waterLevels.find( - x => x.level === this.selectedWaterLevel - ); - return result.color; - }, - xScale() { - return [0, this.totalLength]; - }, - yScaleLeft() { - const hi = Math.max(this.maxAlt, this.selectedWaterLevel); - return [this.minAlt, hi]; - }, - yScaleRight() { - const DELTA = this.maxAlt * 1.1 - this.maxAlt; - return [this.maxAlt * 1 + DELTA, -DELTA]; - } - }, - watch: { - currentData() { - this.drawDiagram(); - }, - additionalData() { - this.drawDiagram(); - }, - width() { - this.drawDiagram(); - }, - height() { - this.drawDiagram(); - }, - waterLevels() { - this.drawDiagram(); - }, - selectedWaterLevel() { - this.drawDiagram(); - }, - fairwayCoordinates() { - this.drawDiagram(); - } - }, - methods: { - drawDiagram() { - this.coordinatesSelect = null; - const chartDiv = document.querySelector(".fairwayprofile"); - d3.select(".fairwayprofile svg").remove(); - this.scaleFairwayProfile(); - let svg = d3.select(chartDiv).append("svg"); - svg.attr("width", this.width); - svg.attr("height", this.height); - const width = this.width - this.margin.right - 1.5 * this.margin.left; - const height = this.height - this.margin.top - 2 * this.margin.bottom; - const currentData = this.currentData; - const additionalData = this.additionalData; - const { xScale, yScaleRight, graph } = this.generateCoordinates( - svg, - height, - width - ); - this.drawWaterlevel({ graph, xScale, yScaleRight, height }); - this.drawLabels({ graph, height }); - this.drawFairway({ graph, xScale, yScaleRight }); - if (currentData) { - this.drawProfile({ - graph, - xScale, - yScaleRight, - currentData, - height, - color: GROUND_COLOR, - strokeColor: "black", - opacity: 1 - }); - } - if (additionalData) { - this.drawProfile({ - graph, - xScale, - yScaleRight, - currentData: additionalData, - height, - color: GROUND_COLOR, - strokeColor: "#943007", - opacity: 0.6 - }); - } - }, - drawFairway({ graph, xScale, yScaleRight }) { - for (let coordinates of this.fairwayCoordinates) { - const [startPoint, endPoint, depth] = coordinates; - let fairwayArea = d3 - .area() - .x(function(d) { - return xScale(d.x); - }) - .y0(yScaleRight(0)) - .y1(function(d) { - return yScaleRight(d.y); - }); - graph - .append("path") - .datum([{ x: startPoint, y: depth }, { x: endPoint, y: depth }]) - .attr("fill", "#002AFF") - .attr("stroke-opacity", 0.65) - .attr("fill-opacity", 0.65) - .attr("stroke", "#FFD20D") - .attr("d", fairwayArea); - } - }, - drawLabels({ graph, height }) { - graph - .append("text") - .attr("transform", ["rotate(-90)"]) - .attr("y", this.width - 60) - .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2) - .attr("dy", "1em") - .attr("fill", "black") - .style("text-anchor", "middle") - .text("Depth [m]"); - graph - .append("text") - .attr("y", 0 - this.margin.left) - .attr("x", 0 - height / 4) - .attr("dy", "1em") - .attr("fill", "black") - .style("text-anchor", "middle") - .attr("transform", [ - "translate(" + this.width / 2 + "," + this.height + ")", - "rotate(0)" - ]) - .text("Width [m]"); - }, - generateCoordinates(svg, height, width) { - let xScale = d3 - .scaleLinear() - .domain(this.xScale) - .rangeRound([0, width]); - - xScale.ticks(5); - let yScaleLeft = d3 - .scaleLinear() - .domain(this.yScaleLeft) - .rangeRound([height, 0]); - - let yScaleRight = d3 - .scaleLinear() - .domain(this.yScaleRight) - .rangeRound([height, 0]); - - let xAxis = d3.axisBottom(xScale); - let yAxis2 = d3.axisRight(yScaleRight); - let graph = svg - .append("g") - .attr( - "transform", - "translate(" + this.margin.left + "," + this.margin.top + ")" - ); - graph - .append("g") - .attr("transform", "translate(0," + height + ")") - .call(xAxis.ticks(5)); - graph - .append("g") - .attr("transform", "translate(" + width + ",0)") - .call(yAxis2); - return { xScale, yScaleLeft, yScaleRight, graph }; - }, - drawWaterlevel({ graph, xScale, yScaleRight, height }) { - let waterArea = d3 - .area() - .x(function(d) { - return xScale(d.x); - }) - .y0(height) - .y1(function(d) { - return yScaleRight(d.y); - }); - graph - .append("path") - .datum([{ x: 0, y: 0 }, { x: this.totalLength, y: 0 }]) - .attr("fill", this.waterColor) - .attr("stroke", this.waterColor) - .attr("d", waterArea); - }, - drawProfile({ - graph, - xScale, - yScaleRight, - currentData, - height, - color, - strokeColor, - opacity - }) { - for (let part of currentData) { - let profileLine = d3 - .line() - .x(d => { - return xScale(d.x); - }) - .y(d => { - return yScaleRight(d.y); - }); - let profileArea = d3 - .area() - .x(function(d) { - return xScale(d.x); - }) - .y0(height) - .y1(function(d) { - return yScaleRight(d.y); - }); - graph - .append("path") - .datum(part) - .attr("fill", color) - .attr("stroke", color) - .attr("stroke-width", 3) - .attr("stroke-opacity", opacity) - .attr("fill-opacity", opacity) - .attr("d", profileArea); - graph - .append("path") - .datum(part) - .attr("fill", "none") - .attr("stroke", strokeColor) - .attr("stroke-linejoin", "round") - .attr("stroke-linecap", "round") - .attr("stroke-width", 3) - .attr("stroke-opacity", opacity) - .attr("fill-opacity", opacity) - .attr("d", profileLine); - } - }, - scaleFairwayProfile() { - if (!document.querySelector(".fairwayprofile")) return; - const clientHeight = document.querySelector(".fairwayprofile") - .clientHeight; - const clientWidth = document.querySelector(".fairwayprofile").clientWidth; - if (!clientHeight || !clientWidth) return; - this.height = clientHeight; - this.width = clientWidth; - } - }, - created() { - window.addEventListener("resize", debounce(this.drawDiagram), 100); - }, - mounted() { - this.drawDiagram(); - }, - updated() { - this.scaleFairwayProfile(); - }, - destroyed() { - window.removeEventListener("resize", debounce(this.drawDiagram)); - } -}; -</script>
--- a/client/src/components/map/fairway/Infobar.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +0,0 @@ -<template> - <div - v-if="Object.keys(currentProfile).length && !showSplitscreen" - class="ui-element shadow-xs infobar rounded bg-white ml-auto mb-3 mr-3" - > - <div class="d-flex flex-row justify-content-between h-100"> - <h6 class="my-auto px-2"> - {{ selectedBottleneck }} ({{ selectedSurvey.date_info }}) - </h6> - <span - class="p-2 border-left d-flex align-items-center" - @click="$store.commit('application/showSplitscreen', true)" - > - <font-awesome-icon icon="angle-up"></font-awesome-icon> - </span> - <span - class="p-2 border-left d-flex align-items-center" - @click="$store.dispatch('fairwayprofile/clearSelection')" - > - <font-awesome-icon icon="times"></font-awesome-icon> - </span> - </div> - </div> -</template> - -<style lang="scss" scoped> -.infobar { - height: 2.2rem; - z-index: 2; -} - -.infobar svg path { - fill: #666; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "infobar", - computed: { - ...mapState("application", ["showSplitscreen"]), - ...mapState("fairwayprofile", ["currentProfile"]), - ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]) - } -}; -</script>
--- a/client/src/components/map/fairway/Profiles.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,463 +0,0 @@ -<template> - <div - :class="[ - 'box ui-element rounded bg-white text-nowrap', - { expanded: showProfiles } - ]" - > - <div style="width: 20rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="chart-area" class="mr-2"></font-awesome-icon> - <translate>Profiles</translate> - <font-awesome-icon - icon="times" - class="ml-auto text-muted" - @click="$store.commit('application/showProfiles', false)" - ></font-awesome-icon> - </h6> - <div - class="d-flex flex-column p-3 flex-grow-1 text-left position-relative" - > - <div - class="loading d-flex justify-content-center align-items-center" - v-if="surveysLoading || profileLoading" - > - <font-awesome-icon icon="spinner" spin /> - </div> - <select - @click="moveToBottleneck" - v-model="selectedBottleneck" - class="form-control font-weight-bold" - > - <option :value="null"> - <translate>Select Bottleneck</translate> - </option> - <option - v-for="bn in bottlenecks" - :key="bn.properties.name" - :value="bn.properties.name" - >{{ bn.properties.name }}</option - > - </select> - <div v-if="selectedBottleneck"> - <div class="d-flex mt-2"> - <div class="flex-fill"> - <small class="text-muted"> - <translate>Sounding Result</translate>: - </small> - <select - v-model="selectedSurvey" - class="form-control form-control-sm" - > - <option - v-for="survey in surveys" - :key="survey.date_info" - :value="survey" - >{{ survey.date_info }}</option - > - </select> - </div> - <div - class="flex-fill ml-3" - v-if="selectedSurvey && surveys.length > 1" - > - <small class="text-muted mt-1"> - <translate>Compare with</translate>: - </small> - <select - v-model="additionalSurvey" - class="form-control form-control-sm" - > - <option :value="null">None</option> - <option - v-for="survey in additionalSurveys" - :key="survey.date_info" - :value="survey" - >{{ survey.date_info }}</option - > - </select> - </div> - </div> - <hr class="w-100 mb-0" /> - <small class="text-muted d-block mt-2"> - <translate>Saved cross profiles</translate>: - </small> - <div class="d-flex"> - <select - :class="[ - 'form-control form-control-sm flex-fill', - { 'rounded-left-only': selectedCut } - ]" - v-model="selectedCut" - > - <option></option> - <option - v-for="(cut, index) in previousCuts" - :value="cut" - :key="index" - >{{ cut.label }}</option - > - </select> - <button - class="btn btn-sm btn-danger input-button-right" - @click="confirmDeleteSelectedCut = true" - v-if="selectedCut && !confirmDeleteSelectedCut" - > - <font-awesome-icon icon="trash" /> - </button> - <button - class="btn btn-sm btn-info rounded-0" - @click="confirmDeleteSelectedCut = false" - v-if="selectedCut && confirmDeleteSelectedCut" - > - <font-awesome-icon icon="times" /> - </button> - <button - class="btn btn-sm btn-danger input-button-right" - @click="deleteSelectedCut" - v-if="selectedCut && confirmDeleteSelectedCut" - > - <font-awesome-icon icon="check" /> - </button> - </div> - <small class="text-muted d-block mt-2"> - <translate>Enter coordinates manually</translate>: - </small> - <div class="position-relative"> - <input - class="form-control form-control-sm pr-5" - placeholder="Lat,Lon,Lat,Lon" - v-model="coordinatesInput" - /> - <button - class="btn btn-sm btn-info position-absolute input-button-right" - @click="applyManualCoordinates" - style="top: 0; right: 0;" - v-if="coordinatesInputIsValid" - > - <font-awesome-icon icon="check" /> - </button> - </div> - <small class="d-flex text-left mt-2" v-if="startPoint && endPoint"> - <div class="text-nowrap mr-3"> - <b> <translate>Start</translate>: </b> <br /> - Lat: {{ startPoint[1] }} <br /> - Lon: {{ startPoint[0] }} - </div> - <div class="text-nowrap"> - <b>End:</b> <br /> - Lat: {{ endPoint[1] }} <br /> - Lon: {{ endPoint[0] }} - </div> - <button - v-clipboard:copy="coordinatesForClipboard" - v-clipboard:success="onCopyCoordinates" - class="btn btn-info btn-sm ml-auto mt-auto" - > - <font-awesome-icon icon="copy" /> - </button> - </small> - <div class="d-flex mt-3"> - <div - class="pr-3 w-50" - v-if="startPoint && endPoint && !selectedCut" - > - <button - class="btn btn-info btn-sm w-100" - @click="showLabelInput = !showLabelInput" - > - <font-awesome-icon :icon="showLabelInput ? 'times' : 'check'" /> - {{ showLabelInput ? "Cancel" : "Save" }} - </button> - </div> - <div - :class="startPoint && endPoint && !selectedCut ? 'w-50' : 'w-100'" - > - <button class="btn btn-info btn-sm w-100" @click="toggleCutTool"> - <font-awesome-icon - :icon="cutTool && cutTool.getActive() ? 'times' : 'plus'" - ></font-awesome-icon> - {{ cutTool && cutTool.getActive() ? "Cancel" : "New" }} - </button> - </div> - </div> - <div v-if="showLabelInput" class="mt-2"> - <small class="text-muted"> - <translate>Enter label for cross profile</translate>: - </small> - <div class="position-relative"> - <input - class="form-control form-control-sm pr-5" - v-model="cutLabel" - /> - <button - class="btn btn-sm btn-info position-absolute input-button-right" - @click="saveCut" - v-if="cutLabel" - style="top: 0; right: 0;" - > - <font-awesome-icon icon="check" /> - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.loading { - background: rgba(255, 255, 255, 0.9); - position: absolute; - z-index: 99; - top: 0; - right: 0; - bottom: 0; - left: 0; -} - -.input-button-right { - border-top-right-radius: $border-radius; - border-bottom-right-radius: $border-radius; - border-top-left-radius: 0 !important; - border-bottom-left-radius: 0 !important; -} - -.rounded-left-only { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; - border-top-left-radius: $border-radius; - border-bottom-left-radius: $border-radius; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; -import Feature from "ol/Feature"; -import LineString from "ol/geom/LineString"; -import { displayError, displayInfo } from "../../../lib/errors.js"; - -export default { - name: "profiles", - data() { - return { - coordinatesInput: "", - cutLabel: "", - showLabelInput: false, - confirmDeleteSelectedCut: false - }; - }, - computed: { - ...mapGetters("map", ["getVSourceByName"]), - ...mapState("application", ["showProfiles"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), - ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]), - ...mapState("fairwayprofile", [ - "previousCuts", - "startPoint", - "endPoint", - "profileLoading" - ]), - selectedBottleneck: { - get() { - return this.$store.state.bottlenecks.selectedBottleneck; - }, - set(name) { - this.$store.dispatch("bottlenecks/setSelectedBottleneck", name); - } - }, - selectedSurvey: { - get() { - return this.$store.state.bottlenecks.selectedSurvey; - }, - set(survey) { - this.$store.commit("fairwayprofile/additionalSurvey", null); - this.$store.commit("bottlenecks/selectedSurvey", survey); - } - }, - additionalSurvey: { - get() { - return this.$store.state.fairwayprofile.additionalSurvey; - }, - set(survey) { - this.$store.commit("fairwayprofile/additionalSurvey", survey); - } - }, - selectedCut: { - get() { - return this.$store.state.fairwayprofile.selectedCut; - }, - set(cut) { - this.$store.commit("fairwayprofile/selectedCut", cut); - if (!cut) { - this.$store.commit("fairwayprofile/clearCurrentProfile"); - this.$store.commit("application/showSplitscreen", false); - this.getVSourceByName("Cut Tool").clear(); - } - } - }, - additionalSurveys() { - return this.surveys.filter(survey => survey !== this.selectedSurvey); - }, - coordinatesForClipboard() { - return ( - this.startPoint[1] + - "," + - this.startPoint[0] + - "," + - this.endPoint[1] + - "," + - this.endPoint[0] - ); - }, - coordinatesInputIsValid() { - const coordinates = this.coordinatesInput - .split(",") - .map(coord => parseFloat(coord.trim())) - .filter(c => Number(c) === c); - return coordinates.length === 4; - } - }, - watch: { - selectedBottleneck() { - this.$store.dispatch("fairwayprofile/previousCuts"); - this.cutLabel = - this.selectedBottleneck + " (" + new Date().toISOString() + ")"; - }, - selectedSurvey(survey) { - this.loadProfile(survey); - }, - additionalSurvey(survey) { - this.loadProfile(survey); - }, - selectedCut(cut) { - if (cut) { - this.confirmDeleteSelectedCut = false; - this.applyCoordinates(cut.coordinates); - } - } - }, - methods: { - loadProfile(survey) { - if (survey) { - this.$store.commit("fairwayprofile/profileLoading", true); - this.$store - .dispatch("fairwayprofile/loadProfile", survey) - .finally(() => - this.$store.commit("fairwayprofile/profileLoading", false) - ); - } - }, - toggleCutTool() { - this.cutTool.setActive(!this.cutTool.getActive()); - this.lineTool.setActive(false); - this.polygonTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - }, - onCopyCoordinates() { - displayInfo({ - title: this.$gettext("Success"), - message: this.$gettext("Coordinates copied to clipboard!") - }); - }, - applyManualCoordinates() { - const coordinates = this.coordinatesInput - .split(",") - .map(coord => parseFloat(coord.trim())); - this.selectedCut = null; - this.coordinatesInput = ""; - this.applyCoordinates([ - coordinates[1], - coordinates[0], - coordinates[3], - coordinates[2] - ]); - }, - applyCoordinates(coordinates) { - // allow only numbers - coordinates = coordinates.filter(c => Number(c) === c); - if (coordinates.length === 4) { - // draw line on map - this.getVSourceByName("Cut Tool").clear(); - const cut = new Feature({ - geometry: new LineString([ - [coordinates[0], coordinates[1]], - [coordinates[2], coordinates[3]] - ]).transform("EPSG:4326", "EPSG:3857") - }); - this.getVSourceByName("Cut Tool").addFeature(cut); - - // draw diagram - this.$store.dispatch("fairwayprofile/cut", cut); - } else { - displayError({ - title: this.$gettext("Invalid input"), - message: this.$gettext( - "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" - ) - }); - } - }, - saveCut() { - const previousCuts = - JSON.parse(localStorage.getItem("previousCuts")) || []; - const newEntry = { - label: this.cutLabel, - bottleneckName: this.selectedBottleneck, - coordinates: [...this.startPoint, ...this.endPoint], - timestamp: new Date().getTime() - }; - const existingEntry = previousCuts.find(cut => { - return JSON.stringify(cut) === JSON.stringify(newEntry); - }); - if (!existingEntry) previousCuts.push(newEntry); - if (previousCuts.length > 100) previousCuts.shift(); - localStorage.setItem("previousCuts", JSON.stringify(previousCuts)); - this.$store.dispatch("fairwayprofile/previousCuts"); - - this.showLabelInput = false; - displayInfo({ - title: this.$gettext("Profile saved!"), - message: this.$gettext( - 'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.' - ) - }); - }, - deleteSelectedCut() { - let previousCuts = JSON.parse(localStorage.getItem("previousCuts")) || []; - previousCuts = previousCuts.filter(cut => { - return JSON.stringify(cut) !== JSON.stringify(this.selectedCut); - }); - localStorage.setItem("previousCuts", JSON.stringify(previousCuts)); - this.$store.commit("fairwayprofile/selectedCut", null); - this.$store.dispatch("fairwayprofile/previousCuts"); - displayInfo({ title: this.$gettext("Profile deleted!") }); - }, - moveToBottleneck() { - const bottleneck = this.bottlenecks.find( - bn => bn.properties.name === this.selectedBottleneck - ); - if (!bottleneck) return; - this.$store.commit("map/moveMap", { - coordinates: bottleneck.geometry.coordinates, - zoom: 17, - preventZoomOut: true - }); - } - } -}; -</script>
--- a/client/src/components/map/layers/Layers.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -<template> - <div - :class="[ - 'box ui-element rounded bg-white text-nowrap', - { expanded: showLayers } - ]" - > - <div style="width: 20rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="layer-group" class="mr-2"></font-awesome-icon - ><translate>Layers</translate> - <font-awesome-icon - icon="times" - class="ml-auto text-muted" - @click="$store.commit('application/showLayers', false)" - ></font-awesome-icon> - </h6> - <div class="d-flex flex-column p-3 small"> - <Layerselect - v-for="(layer, index) in layersForLegend" - :layerindex="index" - :layername="layer.name" - :key="layer.name" - :isVisible="layer.isVisible" - @visibilityToggled="visibilityToggled" - ></Layerselect> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import Layerselect from "./Layerselect"; -import { mapGetters, mapState } from "vuex"; -export default { - name: "layers", - components: { - Layerselect - }, - computed: { - ...mapGetters("map", ["layersForLegend"]), - ...mapState("application", ["showLayers"]) - }, - methods: { - visibilityToggled(layer) { - this.$store.commit("map/toggleVisibility", layer); - } - } -}; -</script>
--- a/client/src/components/map/layers/Layerselect.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,85 +0,0 @@ -<template> - <div> - <div class="form-check d-flex flex-row flex-start selection"> - <input - class="form-check-input" - @change="visibilityToggled" - :id="layername" - type="checkbox" - :checked="isVisible" - /> - <LegendElement - :layername="layername" - :layerindex="layerindex" - ></LegendElement> - <label class="layername form-check-label" @click="visibilityToggled">{{ - layername - }}</label> - </div> - <div v-if="isVisible && layername == 'Bottleneck isolines'"> - <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl" /> - </div> - </div> -</template> - -<style lang="scss" scoped> -.selection { - text-align: left; -} -.layername { - margin-left: $small-offset; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { HTTP } from "../../../lib/http"; -import LegendElement from "./LegendElement.vue"; -export default { - props: ["layername", "layerindex", "isVisible"], - name: "layerselect", - data() { - return { - isolinesLegendImgUrl: "" - }; - }, - components: { - LegendElement - }, - methods: { - visibilityToggled() { - this.$emit("visibilityToggled", this.layerindex); - } - }, - created() { - // fetch legend image for bottleneck isolines - // TODO: move to store - if (this.layername == "Bottleneck isolines") { - const src = - "/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.0.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=sounding_results_contour_lines_geoserver&legend_options=columns:4;fontAntiAliasing:true"; - HTTP.get(src, { - headers: { - Accept: "image/png", - "X-Gemma-Auth": localStorage.getItem("token") - }, - responseType: "blob" - }).then(response => { - var urlCreator = window.URL || window.webkitURL; - this.isolinesLegendImgUrl = urlCreator.createObjectURL(response.data); - }); - } - } -}; -</script>
--- a/client/src/components/map/layers/LegendElement.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,124 +0,0 @@ -<template> - <div :id="id" class="legendelement"></div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { mapGetters } from "vuex"; - -import { Map, View } from "ol"; -import Feature from "ol/Feature"; -import { Vector as VectorLayer } from "ol/layer.js"; -import { Vector as VectorSource } from "ol/source.js"; -import LineString from "ol/geom/LineString.js"; -import Point from "ol/geom/Point"; - -export default { - name: "legendelement", - props: ["layername", "layerindex"], - data: function() { - return { - myMap: null, - mapLayer: null - }; - }, - computed: { - ...mapGetters("map", ["getLayerByName"]), - id() { - return "legendelement" + this.layerindex; - }, - mstyle() { - if (this.mapLayer && this.mapLayer.data.getStyle) { - return this.mapLayer.data.getStyle(); - } - } - }, - watch: { - mstyle(newStyle, oldStyle) { - // only recreate if there already was a style before - if (oldStyle) { - let vector = this.createVectorLayer(); - - this.myMap.removeLayer(this.myMap.getLayers()[0]); - this.myMap.addLayer(vector); - } - } - }, - mounted() { - this.mapLayer = this.getLayerByName(this.layername); - if (this.mapLayer.data.getType() == "VECTOR") { - this.initMap(); - } else { - // TODO other tiles - } - }, - methods: { - initMap() { - let vector = this.createVectorLayer(); - - this.myMap = new Map({ - layers: [vector], - target: this.id, - controls: [], - interactions: [], - view: new View({ - center: [0, 0], - zoom: 3, - projection: "EPSG:4326" - }) - }); - }, - createVectorLayer() { - let mapStyle = this.mapLayer.data.getStyle(); - - let feature = new Feature({ - geometry: new LineString([[-1, 0.5], [0, 0], [0.7, 0], [1.3, -0.7]]) - }); - - // special case if we need to call the style function with a special - // parameter or to detect a point layer - if (this.mapLayer["forLegendStyle"]) { - if (this.mapLayer.forLegendStyle.point) { - feature.setGeometry(new Point([0, 0])); - } - mapStyle = this.mapLayer.data.getStyleFunction()( - feature, - this.mapLayer.forLegendStyle.resolution - ); - } - - // we could add extra properties here, if they are needed for - // the styling function in the future. An idea is to extend the - // this.mapLayer["forLegendStyle"] for it. - // FIXME, this is a special case for the Fairway Dimensions style - feature.set("level_of_service", ""); - return new VectorLayer({ - source: new VectorSource({ - features: [feature], - wrapX: false - }), - style: mapStyle - }); - } - } -}; -</script> - -<style lang="scss" scoped> -.legendelement { - max-height: 1.5rem; - width: 2rem; -} -</style>
--- a/client/src/components/map/toolbar/Identify.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -<template> - <div - @click="$store.commit('application/showIdentify', !showIdentify)" - class="toolbar-button" - > - <font-awesome-icon - icon="info" - :class="{ 'text-info': showIdentify }" - ></font-awesome-icon> - <span - :class="[ - 'indicator', - { - show: - !showIdentify && (identifiedFeatures.length || currentMeasurement) - } - ]" - > - {{ badgeCount }} - </span> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "identify", - computed: { - ...mapState("application", ["showIdentify"]), - ...mapState("map", ["identifiedFeatures", "currentMeasurement"]), - badgeCount() { - return this.identifiedFeatures.length + !!this.currentMeasurement; - } - } -}; -</script>
--- a/client/src/components/map/toolbar/Layers.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +0,0 @@ -<template> - <div - @click="$store.commit('application/showLayers', !showLayers)" - class="toolbar-button" - > - <font-awesome-icon - icon="layer-group" - :class="{ 'text-info': showLayers }" - ></font-awesome-icon> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "layers", - computed: { - ...mapState("application", ["showLayers"]) - } -}; -</script>
--- a/client/src/components/map/toolbar/Linetool.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -<template> - <div @click="toggleLineTool" class="toolbar-button"> - <font-awesome-icon - icon="ruler" - :class="{ 'text-info': lineTool && lineTool.getActive() }" - ></font-awesome-icon> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; - -export default { - name: "linetool", - computed: { - ...mapGetters("map", ["getLayerByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]) - }, - methods: { - toggleLineTool() { - this.lineTool.setActive(!this.lineTool.getActive()); - this.polygonTool.setActive(false); - this.cutTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - this.getVSourceByName("Draw Tool").clear(); - } - } -}; -</script>
--- a/client/src/components/map/toolbar/Pdftool.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +0,0 @@ -<template> - <div - @click="$store.commit('application/showPdfTool', !showPdfTool)" - class="toolbar-button" - > - <font-awesome-icon - icon="file-pdf" - :class="{ 'text-info': showPdfTool }" - ></font-awesome-icon> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "pdftool", - computed: { - ...mapState("application", ["showPdfTool"]) - } -}; -</script>
--- a/client/src/components/map/toolbar/Polygontool.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -<template> - <div @click="togglePolygonTool" class="toolbar-button"> - <font-awesome-icon - icon="draw-polygon" - :class="{ 'text-info': polygonTool && polygonTool.getActive() }" - ></font-awesome-icon> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; - -export default { - name: "polygontool", - computed: { - ...mapGetters("map", ["getLayerByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]) - }, - methods: { - togglePolygonTool() { - this.polygonTool.setActive(!this.polygonTool.getActive()); - this.lineTool.setActive(false); - this.cutTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - this.getVSourceByName("Draw Tool").clear(); - } - } -}; -</script>
--- a/client/src/components/map/toolbar/Profiles.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +0,0 @@ -<template> - <div - @click="$store.commit('application/showProfiles', !showProfiles)" - class="toolbar-button" - > - <font-awesome-icon - icon="chart-area" - :class="{ 'text-info': showProfiles }" - ></font-awesome-icon> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "profiles", - computed: { - ...mapState("application", ["showProfiles"]) - } -}; -</script>
--- a/client/src/components/map/toolbar/Toolbar.vue Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,142 +0,0 @@ -<template> - <div class="ml-2"> - <div - :class=" - 'rounded-top toolbar toolbar-' + - (expandToolbar ? 'expanded' : 'collapsed') - " - > - <Identify></Identify> - <Layers></Layers> - <Profiles></Profiles> - <Linetool></Linetool> - <Polygontool></Polygontool> - <Pdftool></Pdftool> - </div> - <div - @click="$store.commit('application/expandToolbar', !expandToolbar)" - class="toolbar-button toolbar-toggle rounded-bottom bg-info text-white" - > - <font-awesome-icon - :icon="expandToolbar ? 'angle-up' : 'angle-down'" - ></font-awesome-icon> - </div> - </div> -</template> - -<style lang="scss"> -// not scoped to affect nested components -// doen't work when put in application/assets/application.sass... why??? o_O -.toolbar { - box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2); - overflow: hidden; - transition: max-height 0.4s; - margin-bottom: auto; -} - -.toolbar-collapsed { - max-height: 6rem; -} - -.toolbar-expanded { - max-height: 100%; -} - -.toolbar-button { - opacity: 0.96; - color: #666; - height: 2rem; - width: 2rem; - align-items: center; - justify-content: center; - display: flex; - background: #fff; - border-bottom: 1px solid #dee2e6; - z-index: 2; - pointer-events: auto; - position: relative; - overflow: hidden; -} - -.toolbar-button:last-child { - border-bottom: none; -} - -.toolbar-button .inverted { - color: #17a2b8; -} - -.toolbar-button .grey { - color: #ddd; -} - -.toolbar-button .indicator { - color: #fff; - background: #17a2b8; - position: absolute; - bottom: -14px; - left: -14px; - padding: 2px 4px 1px; - font-size: 11px; - line-height: 11px; - border-top-right-radius: 0.25rem; - transition: bottom 0.3s, left 0.3s; -} - -.toolbar-button .indicator.show { - left: 0; - bottom: 0; -} - -.toolbar-toggle { - height: 1.2rem; - border-bottom: none; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; - -export default { - name: "toolbar", - components: { - Identify: () => import("./Identify.vue"), - Layers: () => import("./Layers.vue"), - Linetool: () => import("./Linetool.vue"), - Polygontool: () => import("./Polygontool.vue"), - Profiles: () => import("./Profiles.vue"), - Pdftool: () => import("./Pdftool.vue") - }, - computed: { - ...mapGetters("map", ["getVSourceByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), - ...mapState("application", ["expandToolbar"]) - }, - mounted() { - window.addEventListener("keydown", e => { - // Escape - if (e.keyCode === 27) { - this.lineTool.setActive(false); - this.polygonTool.setActive(false); - this.cutTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - this.$store.dispatch("map/enableIdentifyTool"); - this.getVSourceByName("Draw Tool").clear(); - } - }); - } -}; -</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/staging/Staging.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,191 @@ +<template> + <div class="w-90 stagingcard"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon + class="mr-2" + icon="clipboard-check" + ></font-awesome-icon> + <translate>Staging Area</translate> + </h6> + <div class="mt-3 pl-3 pr-3"> + <div class="mt-3 text-left flex-row d-flex border-bottom"> + <div class="header text-left name"><translate>Name</translate></div> + <div class="header text-left type"><translate>Type</translate></div> + <div class="header text-left date"><translate>Date</translate></div> + <div class="header text-left imported"> + <translate>Imported</translate> + </div> + <div class="header text-left username"> + <translate>Username</translate> + </div> + <div class="ml-3 controls"></div> + </div> + <div class="mt-3" v-if="filteredData.length > 0"> + <StagingDetail + class="mb-3 border-bottom" + :key="data.id" + v-for="data in filteredData" + :data="data" + ></StagingDetail> + </div> + </div> + <div class="mt-3 p-3" v-if="filteredData.length > 0"> + <button @click="confirmReview" class="confirm-button btn btn-info"> + <translate>Confirm</translate> + </button> + </div> + <div v-else class="mr-auto ml-auto"><translate>No results.</translate></div> + <div class="mt-1 p-3"> + <button @click="loadData" class="refresh btn btn-dark">Refresh</button> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus@intevation.de> + */ +import { mapState } from "vuex"; +import { HTTP } from "@/lib/http.js"; +import { displayError, displayInfo } from "@/lib/errors.js"; +import { STATES } from "@/store/imports.js"; + +export default { + data() { + return {}; + }, + components: { + StagingDetail: () => import("./StagingDetail") + }, + mounted() { + this.loadData(); + }, + computed: { + ...mapState("application", ["searchQuery"]), + ...mapState("imports", ["staging"]), + filteredData() { + return this.staging.filter(data => { + const result = [data.id + "", data.enqueued, data.kind, data.user].some( + x => x.toLowerCase().includes(this.searchQuery.toLowerCase()) + ); + return result; + }); + } + }, + methods: { + loadData() { + this.$store.dispatch("imports/getStaging").catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }, + confirmReview() { + const reviewResults = this.staging + .filter(x => x.status !== STATES.NEEDSAPPROVAL) + .map(r => { + return { + id: r.id, + state: r.status + }; + }); + if (!reviewResults.length) return; + HTTP.patch("/imports", reviewResults, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + const messages = response.data + .map(x => { + if (x.message) return x.message; + if (x.error) return x.error; + }) + .join("\n\n"); + displayInfo({ + title: "Staging Area", + message: messages, + options: { + timeout: 0, + buttons: [{ text: "Ok", action: null, bold: true }] + } + }); + this.loadData(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } + }, + STATES: STATES +}; +</script> + +<style lang="scss" scoped> +.name { + width: 180px; +} + +.date { + width: 90px; +} + +.type { + width: 40px; +} + +.imported { + width: 90px; +} + +.username { + width: 150px; +} + +.controls { + width: 60px; +} + +.refresh { + position: absolute; + left: $offset; + bottom: $offset; +} +.table th, +td { + font-size: 0.9rem; + border-top: 0px !important; + border-bottom-width: 1px; + text-align: left; + padding: 0.5rem !important; +} + +.stagingcard { + position: relative; + min-height: 150px; +} + +.confirm-button { + position: absolute; + right: $offset; + bottom: $offset; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/staging/StagingDetail.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,334 @@ +<template> + <div :class="detail"> + <div class="d-flex flex-row"> + <div class="mt-auto d-flex flex-row mb-auto small name text-left"> + <a + v-if="!isBottleneck(data.kind.toUpperCase())" + @click="zoomTo()" + href="#" + >{{ data.summary.bottleneck }}</a + > + <span v-else class="text-left" + ><translate>Bottlenecks</translate> ({{ + data.summary.bottlenecks.length + }})</span + > + </div> + <div class="mt-auto mb-auto small text-left type"> + {{ data.kind.toUpperCase() }} + </div> + <div class="mt-auto mb-auto small text-left date"> + {{ formatSurveyDate(data.summary.date) }} + </div> + <div class="mt-auto mb-auto small text-left imported"> + {{ formatSurveyDate(data.enqueued.split("T")[0]) }} + </div> + <div class="mt-auto mb-auto small text-left username"> + {{ data.user }} + </div> + <div class="controls d-flex flex-row justify-content-end"> + <div> + <button + :class="{ + 'ml-3': true, + 'mr-3': true, + btn: true, + 'btn-sm': true, + 'btn-outline-success': needsApproval(data) || isRejected(data), + 'btn-success': isApproved(data) + }" + @click="toggleApproval(data.id, $options.STATES.APPROVED)" + > + <font-awesome-icon icon="check"></font-awesome-icon> + </button> + </div> + <div> + <button + :class="{ + 'mr-3': true, + btn: true, + 'btn-sm': true, + 'btn-outline-danger': needsApproval(data) || isApproved(data), + 'btn-danger': isRejected(data) + }" + @click="toggleApproval(data.id, $options.STATES.REJECTED)" + > + <font-awesome-icon icon="times"></font-awesome-icon> + </button> + </div> + <div v-if="isBottleneck(data.kind.toUpperCase())"> + <div + @click="showDetails()" + class="mt-auto mb-auto text-info text-left" + > + <font-awesome-icon + v-if="show" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + v-if="loading" + icon="spinner" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + v-if="!show && !loading" + icon="angle-down" + fixed-width + ></font-awesome-icon> + </div> + </div> + <div v-else class="empty"></div> + </div> + </div> + <div + v-for="(bottleneck, index) in bottlenecks" + :key="index" + class="d-flex flex-row" + v-if="show && bottlenecks.length > 0" + > + <div class="d-flex flex-column"> + <div class="d-flex flex-row"> + <a @click="moveToBottleneck(index)" class="small" href="#">{{ + bottleneck.properties.objnam + }}</a> + <div + @click="showFull = !showFull" + class="small mt-auto mb-auto text-info text-left" + > + <font-awesome-icon + v-if="showFull" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + v-if="!showFull" + icon="angle-down" + fixed-width + ></font-awesome-icon> + </div> + </div> + + <div class="d-flex flex-row" v-if="showFull"> + <table> + <tr + v-for="(info, index) in Object.keys(bottleneck.properties)" + :key="index" + class="mr-1 small text-muted" + > + <td class="condensed text-left">{{ info }}</td> + <td class="condensed pl-3 text-left"> + {{ bottleneck.properties[info] }} + </td> + </tr> + </table> + </div> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { formatSurveyDate } from "@/lib/date.js"; +import { STATES } from "@/store/imports.js"; +import { HTTP } from "@/lib/http"; +import { WFS } from "ol/format.js"; +import { or as orFilter, equalTo as equalToFilter } from "ol/format/filter.js"; +import { displayError } from "@/lib/errors.js"; +import { mapState } from "vuex"; +import center from "@turf/center"; + +export default { + name: "stagingdetail", + props: ["data"], + data() { + return { + showFull: false, + show: false, + loading: false, + bottlenecks: [] + }; + }, + mounted() { + this.bottlenecks = []; + if (this.open) this.showDetails(); + }, + computed: { + ...mapState("imports", ["importToReview"]), + open() { + return this.importToReview == this.data.id; + }, + detail() { + return [ + "pb-2", + "pt-2", + "d-flex", + "flex-column", + "w-100", + { + highlight: this.open && this.needsApproval(this.data) + } + ]; + } + }, + watch: { + open() { + const { review } = this.$route.query; + if (review) { + this.showDetails(); + } + } + }, + methods: { + showDetails() { + if (this.data.kind.toUpperCase() !== "BN") return; + if (this.show) { + this.show = false; + return; + } + if (this.bottlenecks.length > 0) { + this.show = true; + return; + } + this.loading = true; + const generateFilter = () => { + const { bottlenecks } = this.data.summary; + if (bottlenecks.length === 1) + return equalToFilter("bottleneck_id", bottlenecks[0]); + const orExpressions = bottlenecks.map(x => { + equalToFilter("bottleneck_id", x); + }); + return orFilter(...orExpressions); + }; + const filterExpression = generateFilter(); + const bottleneckFeatureCollectionRequest = new WFS().writeGetFeature({ + srsName: "EPSG:4326", + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["bottlenecks"], + outputFormat: "application/json", + filter: filterExpression + }); + HTTP.post( + "/internal/wfs", + new XMLSerializer().serializeToString( + bottleneckFeatureCollectionRequest + ), + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + } + ) + .then(response => { + this.bottlenecks = response.data.features; + this.show = true; + this.loading = false; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + isBottleneck(kind) { + return kind === "BN"; + }, + formatSurveyDate(date) { + return formatSurveyDate(date); + }, + needsApproval(item) { + return item.status === STATES.NEEDSAPPROVAL; + }, + isRejected(item) { + return item.status === STATES.REJECTED; + }, + isApproved(item) { + return item.status === STATES.APPROVED; + }, + moveToBottleneck(index) { + const { coordinates } = center(this.bottlenecks[index]).geometry; + this.moveMap(coordinates); + }, + moveMap(coordinates) { + this.$store.commit("map/moveMap", { + coordinates: coordinates, + zoom: 17, + preventZoomOut: true + }); + }, + zoomTo() { + const { lat, lon, bottleneck, date } = this.data.summary; + const coordinates = [lat, lon]; + this.moveMap(coordinates); + this.$store + .dispatch("bottlenecks/setSelectedBottleneck", bottleneck) + .then(() => { + this.$store.commit("bottlenecks/setSelectedSurveyByDate", date); + }); + }, + toggleApproval(id, newStatus) { + this.$store.commit("imports/toggleApproval", { + id: id, + newStatus: newStatus + }); + } + }, + STATES: STATES +}; +</script> + +<style lang="scss" scoped> +.highlight { + background-color: #f9f9f9; +} + +.condensed { + font-stretch: condensed; +} + +.empty { + margin-right: 20px; +} + +.name { + width: 180px; +} + +.date { + width: 90px; +} + +.type { + width: 40px; +} + +.imported { + width: 90px; +} + +.username { + width: 150px; +} + +.controls { + width: 60px; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Identify.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,50 @@ +<template> + <div + @click="$store.commit('application/showIdentify', !showIdentify)" + class="toolbar-button" + > + <font-awesome-icon + icon="info" + :class="{ 'text-info': showIdentify }" + ></font-awesome-icon> + <span + :class="[ + 'indicator', + { + show: + !showIdentify && (identifiedFeatures.length || currentMeasurement) + } + ]" + > + {{ badgeCount }} + </span> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "identify", + computed: { + ...mapState("application", ["showIdentify"]), + ...mapState("map", ["identifiedFeatures", "currentMeasurement"]), + badgeCount() { + return this.identifiedFeatures.length + !!this.currentMeasurement; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Layers.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,35 @@ +<template> + <div + @click="$store.commit('application/showLayers', !showLayers)" + class="toolbar-button" + > + <font-awesome-icon + icon="layer-group" + :class="{ 'text-info': showLayers }" + ></font-awesome-icon> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "layers", + computed: { + ...mapState("application", ["showLayers"]) + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Linetool.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,42 @@ +<template> + <div @click="toggleLineTool" class="toolbar-button"> + <font-awesome-icon + icon="ruler" + :class="{ 'text-info': lineTool && lineTool.getActive() }" + ></font-awesome-icon> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; + +export default { + name: "linetool", + computed: { + ...mapGetters("map", ["getLayerByName"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool"]) + }, + methods: { + toggleLineTool() { + this.lineTool.setActive(!this.lineTool.getActive()); + this.polygonTool.setActive(false); + this.cutTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + this.getVSourceByName("Draw Tool").clear(); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Pdftool.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,35 @@ +<template> + <div + @click="$store.commit('application/showPdfTool', !showPdfTool)" + class="toolbar-button" + > + <font-awesome-icon + icon="file-pdf" + :class="{ 'text-info': showPdfTool }" + ></font-awesome-icon> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "pdftool", + computed: { + ...mapState("application", ["showPdfTool"]) + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Polygontool.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,42 @@ +<template> + <div @click="togglePolygonTool" class="toolbar-button"> + <font-awesome-icon + icon="draw-polygon" + :class="{ 'text-info': polygonTool && polygonTool.getActive() }" + ></font-awesome-icon> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; + +export default { + name: "polygontool", + computed: { + ...mapGetters("map", ["getLayerByName"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool"]) + }, + methods: { + togglePolygonTool() { + this.polygonTool.setActive(!this.polygonTool.getActive()); + this.lineTool.setActive(false); + this.cutTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + this.getVSourceByName("Draw Tool").clear(); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Profiles.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,35 @@ +<template> + <div + @click="$store.commit('application/showProfiles', !showProfiles)" + class="toolbar-button" + > + <font-awesome-icon + icon="chart-area" + :class="{ 'text-info': showProfiles }" + ></font-awesome-icon> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "profiles", + computed: { + ...mapState("application", ["showProfiles"]) + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Toolbar.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,142 @@ +<template> + <div class="ml-2"> + <div + :class=" + 'rounded-top toolbar toolbar-' + + (expandToolbar ? 'expanded' : 'collapsed') + " + > + <Identify></Identify> + <Layers></Layers> + <Profiles></Profiles> + <Linetool></Linetool> + <Polygontool></Polygontool> + <Pdftool></Pdftool> + </div> + <div + @click="$store.commit('application/expandToolbar', !expandToolbar)" + class="toolbar-button toolbar-toggle rounded-bottom bg-info text-white" + > + <font-awesome-icon + :icon="expandToolbar ? 'angle-up' : 'angle-down'" + ></font-awesome-icon> + </div> + </div> +</template> + +<style lang="scss"> +// not scoped to affect nested components +// doen't work when put in application/assets/application.sass... why??? o_O +.toolbar { + box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2); + overflow: hidden; + transition: max-height 0.4s; + margin-bottom: auto; +} + +.toolbar-collapsed { + max-height: 6rem; +} + +.toolbar-expanded { + max-height: 100%; +} + +.toolbar-button { + opacity: 0.96; + color: #666; + height: 2rem; + width: 2rem; + align-items: center; + justify-content: center; + display: flex; + background: #fff; + border-bottom: 1px solid #dee2e6; + z-index: 2; + pointer-events: auto; + position: relative; + overflow: hidden; +} + +.toolbar-button:last-child { + border-bottom: none; +} + +.toolbar-button .inverted { + color: #17a2b8; +} + +.toolbar-button .grey { + color: #ddd; +} + +.toolbar-button .indicator { + color: #fff; + background: #17a2b8; + position: absolute; + bottom: -14px; + left: -14px; + padding: 2px 4px 1px; + font-size: 11px; + line-height: 11px; + border-top-right-radius: 0.25rem; + transition: bottom 0.3s, left 0.3s; +} + +.toolbar-button .indicator.show { + left: 0; + bottom: 0; +} + +.toolbar-toggle { + height: 1.2rem; + border-bottom: none; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; + +export default { + name: "toolbar", + components: { + Identify: () => import("./Identify.vue"), + Layers: () => import("./Layers.vue"), + Linetool: () => import("./Linetool.vue"), + Polygontool: () => import("./Polygontool.vue"), + Profiles: () => import("./Profiles.vue"), + Pdftool: () => import("./Pdftool.vue") + }, + computed: { + ...mapGetters("map", ["getVSourceByName"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), + ...mapState("application", ["expandToolbar"]) + }, + mounted() { + window.addEventListener("keydown", e => { + // Escape + if (e.keyCode === 27) { + this.lineTool.setActive(false); + this.polygonTool.setActive(false); + this.cutTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + this.$store.dispatch("map/enableIdentifyTool"); + this.getVSourceByName("Draw Tool").clear(); + } + }); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/usermanagement/Passwordfield.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,73 @@ +<template> + <div class="w-100"> + <div class="d-flex flex-row"> + <label for="password">{{ this.label }}</label> + </div> + <div class="d-flex d-row"> + <input + :type="isPasswordVisible" + @change="fieldChanged" + class="form-control" + :placeholder="placeholder" + :required="required" + /> + <span class="input-group-text" @click="showPassword"> + <font-awesome-icon + :icon="readablePassword ? 'eye-slash' : 'eye'" + ></font-awesome-icon> + </span> + </div> + <div v-show="passworderrors" class="text-danger"> + <small> + <font-awesome-icon icon="exclamation-triangle"></font-awesome-icon> + {{ this.passworderrors }} + </small> + </div> + </div> +</template> + +<style> +/* FIXME does not work here, unclear why, so added to Login.vue +input[type="password"]::-ms-reveal { + display: none; +} */ +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +export default { + name: "passwordfield", + props: ["model", "placeholder", "label", "passworderrors", "required"], + data() { + return { + password: "", + readablePassword: false + }; + }, + methods: { + showPassword() { + this.readablePassword = !this.readablePassword; + }, + fieldChanged(e) { + this.$emit("fieldchange", e.target.value); + } + }, + computed: { + isPasswordVisible() { + return this.readablePassword ? "text" : "password"; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/usermanagement/Userdetail.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,392 @@ +<template> + <div class="userdetails mt-3 shadow fadeIn animated card"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + {{ this.cardHeader }} + <span @click="closeDetailview" class="closebutton"> + <font-awesome-icon icon="times"></font-awesome-icon> + </span> + </h6> + <div class="card-body"> + <form @submit.prevent="save" class="ml-3"> + <div class="formfields"> + <div v-if="currentUser.isNew" class="form-group row"> + <label for="user"> <translate>Username</translate> </label> + <input + type="user" + :placeholder="userNamePlaceholder" + class="form-control form-control-sm" + id="user" + aria-describedby="userHelp" + v-model="currentUser.user" + /> + <div v-show="errors.user" class="text-danger"> + <small> + <font-awesome-icon + icon="exclamation-triangle" + ></font-awesome-icon> + {{ errors.user }} + </small> + </div> + </div> + <div class="form-group row"> + <label for="country"> <translate>Country</translate> </label> + <select + class="form-control form-control-sm" + v-on:change="validateCountry" + v-model="currentUser.country" + > + <option disabled value> + <translate>Please select one</translate> + </option> + <option + v-for="country in countries" + v-bind:value="country" + v-bind:key="country" + >{{ country }}</option + > + </select> + <div v-show="errors.country" class="text-danger"> + <small> + <font-awesome-icon + icon="exclamation-triangle" + ></font-awesome-icon> + {{ errors.country }} + </small> + </div> + </div> + <div class="form-group row"> + <label for="email"> <translate>Email address</translate> </label> + <input + type="email" + v-on:change="validateEmailaddress" + class="form-control form-control-sm" + id="email" + aria-describedby="emailHelp" + v-model="currentUser.email" + /> + <div v-show="errors.email" class="text-danger"> + <small> + <font-awesome-icon + icon="exclamation-triangle" + ></font-awesome-icon> + {{ errors.email }} + </small> + </div> + </div> + <div class="form-group row"> + <label for="role"> <translate>Role</translate> </label> + <select + class="form-control form-control-sm" + v-on:change="validateRole" + v-model="currentUser.role" + > + <option disabled value> + <translate>Please select one</translate> + </option> + <option value="sys_admin"> + <translate>Sysadmin</translate> + </option> + <option value="waterway_admin"> + <translate>Waterway Admin</translate> + </option> + <option value="waterway_user"> + <translate>Waterway User</translate> + </option> + </select> + <div v-show="errors.role" class="text-danger"> + <small> + <font-awesome-icon + icon="exclamation-triangle" + ></font-awesome-icon> + {{ errors.role }} + </small> + </div> + </div> + <div class="form-group row"> + <PasswordField + @fieldchange="passwordChanged" + :placeholder="passwordPlaceholder" + :label="passwordLabel" + :passworderrors="errors.password" + ></PasswordField> + </div> + <div class="form-group row"> + <PasswordField + @fieldchange="passwordReChanged" + :placeholder="passwordRePlaceholder" + :label="passwordReLabel" + :passworderrors="errors.passwordre" + ></PasswordField> + </div> + </div> + <div> + <button + type="submit" + :disabled="submitted" + class="shadow-sm btn btn-info submit-button" + > + <translate>Submit</translate> + </button> + </div> + <div + v-if="currentUser.role != 'waterway_user'" + class="form-group row d-flex flex-row justify-content-start mailbutton" + > + <a @click="sendTestMail" class="btn btn-light"> + <font-awesome-icon icon="paper-plane"></font-awesome-icon> + <translate>Send testmail</translate> + </a> + <div v-if="mailsent"><translate>Mail was sent</translate></div> + </div> + </form> + </div> + </div> +</template> + +<style lang="scss" scoped> +.submit-button { + position: absolute; + right: $offset; + bottom: $offset; +} +.mailbutton { + width: 12vw; + position: absolute; + left: $large-offset; + bottom: 0; +} + +.formfields { + width: 60%; +} + +.userdetails { + height: 600px; + margin-top: $offset; + margin-left: $offset; + margin-right: $offset; +} + +form { + font-size: $smaller; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors.js"; +import { mapState } from "vuex"; + +const emptyErrormessages = () => { + return { + email: "", + country: "", + role: "", + password: "", + passwordre: "" + }; +}; + +const isEmailValid = email => { + /** + * + * For convenience purposes the same regex used as in the go code + * cf. types.go + * + */ + // eslint-disable-next-line + return /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test( + email + ); +}; + +const violatedPasswordRules = password => { + return ( + // rules according to issue 70 + password.length < 7 || + /\W/.test(password) == false || + /\d/.test(password) == false + ); +}; + +export default { + name: "userdetail", + components: { + PasswordField: () => import("./Passwordfield") + }, + data() { + return { + mailsent: false, + passwordLabel: this.$gettext("Password"), + passwordReLabel: this.$gettext("Repeat Password"), + passwordPlaceholder: this.$gettext("password"), + passwordRePlaceholder: this.$gettext("password again"), + password: "", + passwordre: "", + currentUser: {}, + path: null, + submitted: false, + errors: { + email: "", + country: "", + role: "", + password: "", + passwordre: "" + } + }; + }, + mounted() { + this.currentUser = { ...this.user }; + this.path = this.user.name; + }, + watch: { + user() { + this.currentUser = { ...this.user }; + this.path = this.user.name; + this.clearPassword(); + this.clearErrors(); + } + }, + computed: { + cardHeader() { + if (this.currentUser.isNew) return "N.N"; + return this.currentUser.user; + }, + userNamePlaceholder() { + if (this.currentUser.isNew) return "N.N"; + return ""; + }, + ...mapState("application", ["countries"]), + user() { + return this.$store.getters["usermanagement/currentUser"]; + }, + isFormValid() { + return ( + isEmailValid(this.currentUser.email) && + this.currentUser.country && + this.password === this.passwordre && + (this.password === "" || !violatedPasswordRules(this.password)) + ); + } + }, + methods: { + sendTestMail() { + if (this.mailsent) return; + HTTP.get("/testmail/" + this.currentUser.user, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(() => { + this.mailsent = true; + }) + .catch(error => { + this.loginFailed = true; + this.submitted = false; + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + passwordChanged(value) { + this.password = value; + this.validatePassword(); + }, + passwordReChanged(value) { + this.passwordre = value; + this.validatePassword(); + }, + clearErrors() { + this.errors = emptyErrormessages(); + }, + clearPassword() { + this.password = ""; + this.passwordre = ""; + }, + closeDetailview() { + this.$store.commit("usermanagement/clearCurrentUser"); + this.$store.commit("usermanagement/setUserDetailsInvisible"); + }, + validateCountry() { + this.errors.country = this.currentUser.country + ? "" + : this.$gettext("Please choose a country"); + }, + validateRole() { + this.errors.role = this.currentUser.role + ? "" + : this.$gettext("Please choose a role"); + }, + validatePassword() { + this.errors.passwordre = + this.password === this.passwordre + ? "" + : this.$gettext("Passwords do not match!"); + this.errors.password = + this.password === "" || !violatedPasswordRules(this.password) + ? "" + : this.$gettext( + "Password should at least be 8 char long including 1 digit and 1 special char like $" + ); + }, + validateEmailaddress() { + this.errors.email = isEmailValid(this.currentUser.email) + ? "" + : this.$gettext("invalid email"); + }, + validate() { + this.validateCountry(); + this.validateRole(); + this.validatePassword(); + this.validateEmailaddress(); + }, + save() { + this.validate(); + if (!this.isFormValid) return; + if (this.password) this.currentUser.password = this.password; + this.submitted = true; + this.$store + .dispatch("usermanagement/saveCurrentUser", { + path: this.user.user, + user: this.currentUser + }) + .then(() => { + this.submitted = false; + this.$store.dispatch("usermanagement/loadUsers").catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + this.submitted = false; + const { status, data } = error.response; + displayError({ + title: this.$gettext("Error while saving user"), + message: `${status}: ${data.message || data}` + }); + }); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/usermanagement/Usermanagement.vue Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,351 @@ +<template> + <div class="main d-flex flex-row"> + <Spacer></Spacer> + <div class="d-flex content flex-column"> + <div class="d-flex flex-row"> + <div :class="userlistStyle"> + <div class="card"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon + icon="users-cog" + class="mr-2 fa-fw" + ></font-awesome-icon> + <translate class="headline">Users</translate> + </h6> + <div class="card-body"> + <table id="datatable" :class="tableStyle"> + <thead> + <tr> + <th scope="col" @click="sortBy('user')"> + <span + >Username + <font-awesome-icon + v-if="sortCriterion == 'user'" + icon="angle-down" + ></font-awesome-icon> + </span> + </th> + <th scope="col" @click="sortBy('country')"> + <span + >Country + <font-awesome-icon + v-if="sortCriterion == 'country'" + icon="angle-down" + ></font-awesome-icon> + </span> + </th> + <th scope="col" @click="sortBy('email')"> + <span + >Email + <font-awesome-icon + v-if="sortCriterion == 'email'" + icon="angle-down" + ></font-awesome-icon> + </span> + </th> + <th scope="col" @click="sortBy('role')"> + <span + >Role + <font-awesome-icon + v-if="sortCriterion == 'role'" + icon="angle-down" + ></font-awesome-icon> + </span> + </th> + <th scope="col"></th> + </tr> + </thead> + <tbody> + <tr + v-for="user in users" + :key="user.user" + @click="selectUser(user.user)" + > + <td>{{ user.user }}</td> + <td>{{ user.country }}</td> + <td>{{ user.email }}</td> + <td> + <font-awesome-icon + v-tooltip="roleLabel(user.role)" + :icon="roleIcon(user.role)" + @click="deleteUser(user.user)" + ></font-awesome-icon> + </td> + <td> + <font-awesome-icon + icon="trash" + @click="deleteUser(user.user)" + ></font-awesome-icon> + </td> + </tr> + </tbody> + </table> + </div> + <div class="d-flex mx-auto align-items-center"> + <button + @click="prevPage" + v-if="this.currentPage !== 1" + class="mr-2 btn btn-sm btn-light align-self-center" + > + <font-awesome-icon icon="angle-left"></font-awesome-icon> + </button> + {{ this.currentPage }} / {{ this.pages }} + <button + @click="nextPage" + v-if="this.currentPage !== this.pages" + class="ml-2 btn btn-sm btn-light align-self-center" + > + <font-awesome-icon icon="angle-right"></font-awesome-icon> + </button> + </div> + <div class="mr-3 pb-3"> + <button @click="addUser" class="btn btn-info addbutton shadow-sm"> + <translate>Add User</translate> + </button> + </div> + </div> + </div> + <Userdetail + class="d-flex userdetails" + v-if="isUserDetailsVisible" + ></Userdetail> + </div> + </div> + </div> +</template> + +<style lang="scss"> +@import "@/assets/tooltip.scss"; + +.addbutton { + position: absolute; + bottom: $offset; + right: $offset; +} + +.content { + width: 100%; +} + +.userdetails { + width: 50%; +} + +.main { + height: 100vh; +} + +.icon { + font-size: large; +} + +.userlist { + min-width: 520px; + height: 100%; +} + +.userlistsmall { + width: 100%; +} + +.userlistextended { + width: 100%; +} + +.table { + width: 90% !important; + margin: auto; +} + +.table th { + cursor: pointer; +} + +.table th, +td { + font-size: $smaller; + border-top: 0px !important; + text-align: left; + padding: $small-offset !important; +} + +.table td { + font-size: $smaller; + cursor: pointer; +} + +tr span { + display: flex; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import store from "@/store"; +import { mapGetters, mapState } from "vuex"; +import { displayError } from "@/lib/errors.js"; +import Vue from "vue"; +import { VTooltip, VPopover, VClosePopover } from "v-tooltip"; + +Vue.directive("tooltip", VTooltip); +Vue.directive("close-popover", VClosePopover); +Vue.component("v-popover", VPopover); + +export default { + name: "userview", + data() { + return { + sortCriterion: "user", + pageSize: 20, + currentPage: 1 + }; + }, + components: { + Userdetail: () => import("./Userdetail"), + Spacer: () => import("@/components/Spacer") + }, + computed: { + ...mapGetters("usermanagement", ["isUserDetailsVisible"]), + ...mapState("application", ["showSidebar"]), + users() { + let users = [...this.$store.getters["usermanagement/users"]]; + users.sort((a, b) => { + if ( + a[this.sortCriterion].toLowerCase() < + b[this.sortCriterion].toLowerCase() + ) + return -1; + if ( + a[this.sortCriterion].toLowerCase() > + b[this.sortCriterion].toLowerCase() + ) + return 1; + return 0; + }); + const start = (this.currentPage - 1) * this.pageSize; + return users.slice(start, start + this.pageSize); + }, + pages() { + let users = [...this.$store.getters["usermanagement/users"]]; + return Math.ceil(users.length / this.pageSize); + }, + tableStyle() { + return { + table: true, + "table-hover": true, + "table-sm": this.isUserDetailsVisible, + fadeIn: true, + animated: true + }; + }, + userlistStyle() { + return [ + "userlist mt-3 mr-3 shadow-xs", + { + userlistsmall: this.isUserDetailsVisible, + userlistextended: !this.isUserDetailsVisible + } + ]; + } + }, + methods: { + tween() {}, + nextPage() { + if (this.currentPage < this.pages) { + document.querySelector("#datatable").classList.add("fadeOut"); + setTimeout(() => { + document.querySelector("#datatable").classList.remove("fadeOut"); + this.currentPage += 1; + }, 10); + } + return; + }, + prevPage() { + if (this.currentPage > 0) { + document.querySelector("#datatable").classList.add("fadeOut"); + setTimeout(() => { + document.querySelector("#datatable").classList.remove("fadeOut"); + this.currentPage -= 1; + }, 10); + } + return; + }, + sortBy(criterion) { + this.sortCriterion = criterion; + }, + deleteUser(name) { + this.$store + .dispatch("usermanagement/deleteUser", { name: name }) + .then(() => { + this.submitted = false; + this.$store.dispatch("usermanagement/loadUsers").catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + addUser() { + this.$store.commit("usermanagement/clearCurrentUser"); + this.$store.commit("usermanagement/setUserDetailsVisible"); + }, + selectUser(name) { + const user = this.$store.getters["usermanagement/getUserByName"](name); + this.$store.commit("usermanagement/setCurrentUser", user); + }, + roleIcon(role) { + if (role === "sys_admin") return "star"; + if (role === "waterway_admin") return ["fab", "adn"]; + return "user"; + }, + roleLabel(role) { + const labels = { + sys_admin: this.$gettext("System-Administrator"), + waterway_admin: this.$gettext("Waterway Admin"), + waterway_user: this.$gettext("Waterway User") + }; + return labels[role]; + } + }, + beforeRouteEnter(to, from, next) { + store + .dispatch("usermanagement/loadUsers") + .then(next) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data}` + }); + }); + }, + beforeRouteLeave(to, from, next) { + store.commit("usermanagement/clearCurrentUser"); + store.commit("usermanagement/setUserDetailsInvisible"); + next(); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/fontawesome.js Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,100 @@ +import Vue from "vue"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { + faAngleDown, + faAngleLeft, + faAngleRight, + faAngleUp, + faBars, + faBook, + faChartArea, + faCheck, + faCity, + faClipboardCheck, + faClock, + faCloudUploadAlt, + faCopy, + faDrawPolygon, + faExclamationTriangle, + faEye, + faEyeSlash, + faFilePdf, + faFolderPlus, + faInfo, + faLayerGroup, + faMapMarkedAlt, + faMinus, + faPaperPlane, + faPencilAlt, + faPlay, + faPlus, + faPowerOff, + faRuler, + faSearch, + faShip, + faSortAmountDown, + faSortAmountUp, + faSpinner, + faStar, + faTasks, + faTimes, + faTrash, + faUpload, + faUser, + faUsersCog, + faWater, + faWrench +} from "@fortawesome/free-solid-svg-icons"; +import { faAdn } from "@fortawesome/free-brands-svg-icons"; + +library.add( + faAdn, + faAngleDown, + faAngleLeft, + faAngleRight, + faAngleUp, + faBars, + faBook, + faChartArea, + faCheck, + faCity, + faClipboardCheck, + faClock, + faCloudUploadAlt, + faCopy, + faDrawPolygon, + faExclamationTriangle, + faEye, + faEyeSlash, + faFilePdf, + faFolderPlus, + faInfo, + faLayerGroup, + faMapMarkedAlt, + faMinus, + faPaperPlane, + faPencilAlt, + faPlay, + faPlus, + faPowerOff, + faRuler, + faSearch, + faShip, + faSortAmountDown, + faSortAmountUp, + faSpinner, + faStar, + faTasks, + faTimes, + faTrash, + faUpload, + faUser, + faUsersCog, + faWater, + faWrench +); + +import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; +Vue.component("font-awesome-icon", FontAwesomeIcon); + +export { Vue };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/lib/date.js Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,27 @@ +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import locale2 from "locale2"; + +const formatSurveyDate = current => { + return current + ? new Date(current).toLocaleDateString(locale2, { + day: "2-digit", + month: "2-digit", + year: "numeric" + }) + : ""; +}; + +export { formatSurveyDate };
--- a/client/src/locale/bg_BG/LC_MESSAGES/app.po Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/locale/bg_BG/LC_MESSAGES/app.po Sat Dec 29 16:07:40 2018 +0100 @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: gemmajs 1.99.0-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-12-05 14:14+0100\n" +"POT-Creation-Date: 2018-12-10 15:30+0100\n" "PO-Revision-Date: 2018-12-05 12:23+0100\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -25,7 +25,7 @@ msgid "Accesslog" msgstr "" -#: src/components/admin/usermanagement/Usermanagement.vue:106 +#: src/components/admin/usermanagement/Usermanagement.vue:103 msgid "Add User" msgstr "" @@ -38,17 +38,17 @@ msgstr "" #: src/components/admin/Importqueue.vue:136 -#: src/components/admin/Systemconfiguration.vue:133 -#: src/components/admin/Systemconfiguration.vue:148 -#: src/components/admin/Systemconfiguration.vue:167 -#: src/components/admin/Systemconfiguration.vue:184 -#: src/components/admin/usermanagement/Userdetail.vue:296 -#: src/components/admin/usermanagement/Userdetail.vue:368 -#: src/components/admin/usermanagement/Usermanagement.vue:300 -#: src/components/admin/usermanagement/Usermanagement.vue:308 -#: src/components/admin/usermanagement/Usermanagement.vue:334 +#: src/components/admin/Systemconfiguration.vue:134 +#: src/components/admin/Systemconfiguration.vue:149 +#: src/components/admin/Systemconfiguration.vue:168 +#: src/components/admin/Systemconfiguration.vue:185 +#: src/components/admin/usermanagement/Userdetail.vue:305 +#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Usermanagement.vue:313 +#: src/components/admin/usermanagement/Usermanagement.vue:321 +#: src/components/admin/usermanagement/Usermanagement.vue:347 #: src/components/map/Search.vue:257 -#: src/components/map/contextbox/Bottlenecks.vue:252 +#: src/components/map/contextbox/Bottlenecks.vue:275 #: src/components/map/contextbox/ImportSoundingresults.vue:205 #: src/components/map/contextbox/ImportSoundingresults.vue:244 #: src/components/map/contextbox/ImportSoundingresults.vue:275 @@ -67,7 +67,7 @@ msgid "Bottleneck Areas stroke-color" msgstr "" -#: src/components/Sidebar.vue:22 +#: src/components/Sidebar.vue:27 #: src/components/map/contextbox/Bottlenecks.vue:4 msgid "Bottlenecks" msgstr "" @@ -93,7 +93,7 @@ msgid "Compare with" msgstr "" -#: src/components/Sidebar.vue:58 +#: src/components/Sidebar.vue:76 msgid "Configuration" msgstr "" @@ -102,11 +102,11 @@ msgid "Confirm" msgstr "" -#: src/components/map/fairway/Profiles.vue:374 +#: src/components/map/fairway/Profiles.vue:378 msgid "Coordinates copied to clipboard!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:34 +#: src/components/admin/usermanagement/Userdetail.vue:33 msgid "Country" msgstr "" @@ -128,13 +128,18 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:37 +#: src/components/admin/importschedule/Importscheduledetail.vue:80 msgid "Email" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:60 +#: src/components/admin/usermanagement/Userdetail.vue:59 msgid "Email address" msgstr "" +#: src/components/admin/importschedule/Importscheduledetail.vue:61 +msgid "Email Notification" +msgstr "" + #: src/components/admin/Importqueue.vue:53 msgid "Enqueued" msgstr "" @@ -155,7 +160,7 @@ msgid "Enter username" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Userdetail.vue:386 msgid "Error while saving user" msgstr "" @@ -184,7 +189,7 @@ msgid "Import" msgstr "" -#: src/components/Sidebar.vue:31 +#: src/components/Sidebar.vue:40 msgid "Import soundingresults" msgstr "" @@ -196,20 +201,28 @@ msgid "Imported" msgstr "" -#: src/components/Sidebar.vue:66 src/components/admin/Importqueue.vue:9 +#: src/components/Sidebar.vue:92 src/components/admin/Importqueue.vue:9 msgid "Importqueue" msgstr "" -#: src/components/Sidebar.vue:70 +#: src/components/admin/importschedule/Importscheduledetail.vue:20 +msgid "Imports" +msgstr "" + +#: src/components/Sidebar.vue:100 #: src/components/admin/importschedule/Importschedule.vue:9 msgid "Importschedule" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:345 +#: src/components/admin/importschedule/Importscheduledetail.vue:34 +msgid "Importtype" +msgstr "" + +#: src/components/admin/usermanagement/Userdetail.vue:354 msgid "invalid email" msgstr "" -#: src/components/map/fairway/Profiles.vue:408 +#: src/components/map/fairway/Profiles.vue:412 msgid "Invalid input" msgstr "" @@ -241,19 +254,19 @@ msgid "Login failed" msgstr "" -#: src/components/Sidebar.vue:76 +#: src/components/Sidebar.vue:110 msgid "Logout" msgstr "" -#: src/components/Sidebar.vue:62 src/components/admin/Logs.vue:9 +#: src/components/Sidebar.vue:84 src/components/admin/Logs.vue:9 msgid "Logs" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:141 +#: src/components/admin/usermanagement/Userdetail.vue:140 msgid "Mail was sent" msgstr "" -#: src/components/Sidebar.vue:14 +#: src/components/Sidebar.vue:15 msgid "Map" msgstr "" @@ -266,7 +279,7 @@ msgid "Name" msgstr "" -#: src/components/admin/importschedule/Importscheduledetail.vue:6 +#: src/components/admin/importschedule/Importscheduledetail.vue:9 msgid "New import" msgstr "" @@ -274,7 +287,7 @@ msgid "New Import" msgstr "" -#: src/components/map/Identify.vue:50 +#: src/components/map/Identify.vue:47 msgid "No features identified." msgstr "" @@ -291,15 +304,15 @@ msgid "Open in new window" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:229 +#: src/components/admin/usermanagement/Userdetail.vue:238 msgid "password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:227 +#: src/components/admin/usermanagement/Userdetail.vue:236 msgid "Password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:230 +#: src/components/admin/usermanagement/Userdetail.vue:239 msgid "password again" msgstr "" @@ -307,12 +320,12 @@ msgid "Password reset requested!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:338 -#: src/components/admin/usermanagement/Userdetail.vue:339 +#: src/components/admin/usermanagement/Userdetail.vue:347 +#: src/components/admin/usermanagement/Userdetail.vue:348 msgid "Password should at least be 8 char long including 1 digit and 1 special char like $" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:334 +#: src/components/admin/usermanagement/Userdetail.vue:343 msgid "Passwords do not match!" msgstr "" @@ -320,11 +333,11 @@ msgid "Pending" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:323 +#: src/components/admin/usermanagement/Userdetail.vue:332 msgid "Please choose a country" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:328 +#: src/components/admin/usermanagement/Userdetail.vue:337 msgid "Please choose a role" msgstr "" @@ -340,8 +353,8 @@ msgid "Please enter a reference" msgstr "" -#: src/components/map/fairway/Profiles.vue:409 -#: src/components/map/fairway/Profiles.vue:410 +#: src/components/map/fairway/Profiles.vue:413 +#: src/components/map/fairway/Profiles.vue:414 msgid "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" msgstr "" @@ -349,8 +362,8 @@ msgid "Please select a bottleneck" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:41 -#: src/components/admin/usermanagement/Userdetail.vue:86 +#: src/components/admin/usermanagement/Userdetail.vue:40 +#: src/components/admin/usermanagement/Userdetail.vue:85 msgid "Please select one" msgstr "" @@ -358,11 +371,11 @@ msgid "portrait" msgstr "" -#: src/components/map/fairway/Profiles.vue:448 +#: src/components/map/fairway/Profiles.vue:452 msgid "Profile deleted!" msgstr "" -#: src/components/map/fairway/Profiles.vue:434 +#: src/components/map/fairway/Profiles.vue:438 msgid "Profile saved!" msgstr "" @@ -382,7 +395,7 @@ msgid "Rejected" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:228 +#: src/components/admin/usermanagement/Userdetail.vue:237 msgid "Repeat Password" msgstr "" @@ -390,7 +403,7 @@ msgid "Request password reset!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:79 +#: src/components/admin/usermanagement/Userdetail.vue:78 msgid "Role" msgstr "" @@ -399,6 +412,7 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:36 +#: src/components/admin/importschedule/Importscheduledetail.vue:48 msgid "Schedule" msgstr "" @@ -410,7 +424,7 @@ msgid "Send" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:139 +#: src/components/admin/usermanagement/Userdetail.vue:138 msgid "Send testmail" msgstr "" @@ -418,7 +432,7 @@ msgid "Signer" msgstr "" -#: src/components/map/Identify.vue:63 +#: src/components/map/Identify.vue:60 msgid "" "Some data ©\n" " <a href=\"https://www.openstreetmap.org/copyright\">%{ name }</a>\n" @@ -429,11 +443,11 @@ msgid "Sounding Result" msgstr "" -#: src/components/map/Identify.vue:60 +#: src/components/map/Identify.vue:57 msgid "source-code" msgstr "" -#: src/components/Sidebar.vue:44 +#: src/components/Sidebar.vue:54 msgid "Staging area" msgstr "" @@ -453,11 +467,12 @@ msgid "State" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:130 +#: src/components/admin/importschedule/Importscheduledetail.vue:85 +#: src/components/admin/usermanagement/Userdetail.vue:129 msgid "Submit" msgstr "" -#: src/components/map/fairway/Profiles.vue:373 +#: src/components/map/fairway/Profiles.vue:377 msgid "Success" msgstr "" @@ -465,11 +480,11 @@ msgid "Successful" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:89 +#: src/components/admin/usermanagement/Userdetail.vue:88 msgid "Sysadmin" msgstr "" -#: src/components/Sidebar.vue:47 +#: src/components/Sidebar.vue:57 msgid "Systemadministration" msgstr "" @@ -477,7 +492,7 @@ msgid "Systemconfiguration" msgstr "" -#: src/components/map/Identify.vue:54 +#: src/components/map/Identify.vue:51 msgid "" "This app uses <i>gemma</i>, which is Free Software under <br/>\n" " %{ license } without warranty, see docs for details." @@ -496,31 +511,40 @@ msgid "User" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:15 +#: src/components/admin/usermanagement/Userdetail.vue:14 #: src/components/map/contextbox/Staging.vue:16 msgid "Username" msgstr "" -#: src/components/Sidebar.vue:52 +#: src/components/Sidebar.vue:66 #: src/components/admin/usermanagement/Usermanagement.vue:14 msgid "Users" msgstr "" -#: src/components/map/Identify.vue:68 +#: src/components/map/Identify.vue:65 msgid "" "Uses\n" -" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a> under %{ geoLicense }." +" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a>\n" +" under %{ geoLicense }." msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:92 +#: src/components/admin/usermanagement/Userdetail.vue:91 msgid "Waterway Admin" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:95 +#: src/components/admin/usermanagement/Userdetail.vue:94 msgid "Waterway User" msgstr "" -#: src/components/map/fairway/Profiles.vue:435 -#: src/components/map/fairway/Profiles.vue:436 +#: src/components/map/fairway/Profiles.vue:439 +#: src/components/map/fairway/Profiles.vue:440 msgid "You can now select these coordinates from the \"Saved cross profiles\" menu to restore this cross profile." msgstr "" + +#: src/store/map.js:415 +msgid "Length" +msgstr "" + +#: src/store/map.js:436 +msgid "Area" +msgstr ""
--- a/client/src/locale/de_AT/LC_MESSAGES/app.po Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/locale/de_AT/LC_MESSAGES/app.po Sat Dec 29 16:07:40 2018 +0100 @@ -8,10 +8,11 @@ msgstr "" "Project-Id-Version: wamosjs 0.1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-12-05 14:14+0100\n" -"PO-Revision-Date: 2018-12-05 13:13+0000\n" +"POT-Creation-Date: 2018-12-10 15:30+0100\n" +"PO-Revision-Date: 2018-12-11 17:08+0000\n" "Last-Translator: Sascha L. Teichmann <sascha.teichmann@intevation.de>\n" -"Language-Team: Austrian German <https://hosted.weblate.org/projects/gemma/client/de_AT/>\n" +"Language-Team: Austrian German <https://hosted.weblate.org/projects/gemma/" +"client/de_AT/>\n" "Language: de_AT\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -27,7 +28,7 @@ msgid "Accesslog" msgstr "Zugriffs-Protokoll" -#: src/components/admin/usermanagement/Usermanagement.vue:106 +#: src/components/admin/usermanagement/Usermanagement.vue:103 msgid "Add User" msgstr "Benutzer hinzufügen" @@ -40,17 +41,17 @@ msgstr "zurück zur Anmeldung" #: src/components/admin/Importqueue.vue:136 -#: src/components/admin/Systemconfiguration.vue:133 -#: src/components/admin/Systemconfiguration.vue:148 -#: src/components/admin/Systemconfiguration.vue:167 -#: src/components/admin/Systemconfiguration.vue:184 -#: src/components/admin/usermanagement/Userdetail.vue:296 -#: src/components/admin/usermanagement/Userdetail.vue:368 -#: src/components/admin/usermanagement/Usermanagement.vue:300 -#: src/components/admin/usermanagement/Usermanagement.vue:308 -#: src/components/admin/usermanagement/Usermanagement.vue:334 +#: src/components/admin/Systemconfiguration.vue:134 +#: src/components/admin/Systemconfiguration.vue:149 +#: src/components/admin/Systemconfiguration.vue:168 +#: src/components/admin/Systemconfiguration.vue:185 +#: src/components/admin/usermanagement/Userdetail.vue:305 +#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Usermanagement.vue:313 +#: src/components/admin/usermanagement/Usermanagement.vue:321 +#: src/components/admin/usermanagement/Usermanagement.vue:347 #: src/components/map/Search.vue:257 -#: src/components/map/contextbox/Bottlenecks.vue:252 +#: src/components/map/contextbox/Bottlenecks.vue:275 #: src/components/map/contextbox/ImportSoundingresults.vue:205 #: src/components/map/contextbox/ImportSoundingresults.vue:244 #: src/components/map/contextbox/ImportSoundingresults.vue:275 @@ -69,7 +70,7 @@ msgid "Bottleneck Areas stroke-color" msgstr "Flächenumrandungsfarbe Seichtstelle" -#: src/components/Sidebar.vue:22 +#: src/components/Sidebar.vue:27 #: src/components/map/contextbox/Bottlenecks.vue:4 msgid "Bottlenecks" msgstr "Seichtstellen" @@ -80,7 +81,7 @@ #: src/components/map/contextbox/Bottlenecks.vue:30 msgid "Chainage" -msgstr "Verkettung" +msgstr "Stationierung" #: src/components/map/contextbox/ImportSoundingresults.vue:167 #: src/components/map/contextbox/ImportSoundingresults.vue:181 @@ -95,7 +96,7 @@ msgid "Compare with" msgstr "Vergleiche mit" -#: src/components/Sidebar.vue:58 +#: src/components/Sidebar.vue:76 msgid "Configuration" msgstr "Konfiguration" @@ -104,11 +105,11 @@ msgid "Confirm" msgstr "Bestätigen" -#: src/components/map/fairway/Profiles.vue:374 +#: src/components/map/fairway/Profiles.vue:378 msgid "Coordinates copied to clipboard!" msgstr "Koordinaten auf die Zwischenablage kopiert!" -#: src/components/admin/usermanagement/Userdetail.vue:34 +#: src/components/admin/usermanagement/Userdetail.vue:33 msgid "Country" msgstr "Land" @@ -130,13 +131,18 @@ msgstr "Meta.json Herunterladen" #: src/components/admin/importschedule/Importschedule.vue:37 +#: src/components/admin/importschedule/Importscheduledetail.vue:80 msgid "Email" msgstr "E-Mail" -#: src/components/admin/usermanagement/Userdetail.vue:60 +#: src/components/admin/usermanagement/Userdetail.vue:59 msgid "Email address" msgstr "E-Mail Adresse" +#: src/components/admin/importschedule/Importscheduledetail.vue:61 +msgid "Email Notification" +msgstr "E-Mail Benachrichtigung" + #: src/components/admin/Importqueue.vue:53 msgid "Enqueued" msgstr "Hinzugefügt" @@ -157,7 +163,7 @@ msgid "Enter username" msgstr "Benutzername eingeben" -#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Userdetail.vue:386 msgid "Error while saving user" msgstr "Während des Speicherns der Nutzerdaten trat ein Fehler auf" @@ -186,7 +192,7 @@ msgid "Import" msgstr "Daten-Import" -#: src/components/Sidebar.vue:31 +#: src/components/Sidebar.vue:40 msgid "Import soundingresults" msgstr "Seichtstellenmessungen importieren" @@ -198,20 +204,28 @@ msgid "Imported" msgstr "Importiert" -#: src/components/Sidebar.vue:66 src/components/admin/Importqueue.vue:9 +#: src/components/Sidebar.vue:92 src/components/admin/Importqueue.vue:9 msgid "Importqueue" msgstr "Import-Warteschlange" -#: src/components/Sidebar.vue:70 +#: src/components/admin/importschedule/Importscheduledetail.vue:20 +msgid "Imports" +msgstr "Daten-Import" + +#: src/components/Sidebar.vue:100 #: src/components/admin/importschedule/Importschedule.vue:9 msgid "Importschedule" msgstr "Import-Zeitplan" -#: src/components/admin/usermanagement/Userdetail.vue:345 +#: src/components/admin/importschedule/Importscheduledetail.vue:34 +msgid "Importtype" +msgstr "Art des Imports" + +#: src/components/admin/usermanagement/Userdetail.vue:354 msgid "invalid email" msgstr "Ungültige E-Mail" -#: src/components/map/fairway/Profiles.vue:408 +#: src/components/map/fairway/Profiles.vue:412 msgid "Invalid input" msgstr "Ungültige Eingabe" @@ -229,7 +243,7 @@ #: src/components/map/contextbox/Bottlenecks.vue:19 msgid "Latest" -msgstr "Neustes" +msgstr "Neuste" #: src/components/map/layers/Layers.vue:10 msgid "Layers" @@ -243,19 +257,19 @@ msgid "Login failed" msgstr "Login fehlgeschlagen" -#: src/components/Sidebar.vue:76 +#: src/components/Sidebar.vue:110 msgid "Logout" msgstr "Abmelden" -#: src/components/Sidebar.vue:62 src/components/admin/Logs.vue:9 +#: src/components/Sidebar.vue:84 src/components/admin/Logs.vue:9 msgid "Logs" msgstr "Protokolle" -#: src/components/admin/usermanagement/Userdetail.vue:141 +#: src/components/admin/usermanagement/Userdetail.vue:140 msgid "Mail was sent" msgstr "E-Mail wurde gesendet" -#: src/components/Sidebar.vue:14 +#: src/components/Sidebar.vue:15 msgid "Map" msgstr "Karte" @@ -268,7 +282,7 @@ msgid "Name" msgstr "Name" -#: src/components/admin/importschedule/Importscheduledetail.vue:6 +#: src/components/admin/importschedule/Importscheduledetail.vue:9 msgid "New import" msgstr "Neuer Import" @@ -276,7 +290,7 @@ msgid "New Import" msgstr "Neuer Import" -#: src/components/map/Identify.vue:50 +#: src/components/map/Identify.vue:47 msgid "No features identified." msgstr "Keine Objekte identifiziert." @@ -293,15 +307,15 @@ msgid "Open in new window" msgstr "In neuem Fenster öffnen" -#: src/components/admin/usermanagement/Userdetail.vue:229 +#: src/components/admin/usermanagement/Userdetail.vue:238 msgid "password" msgstr "Passwort" -#: src/components/admin/usermanagement/Userdetail.vue:227 +#: src/components/admin/usermanagement/Userdetail.vue:236 msgid "Password" msgstr "Passwort" -#: src/components/admin/usermanagement/Userdetail.vue:230 +#: src/components/admin/usermanagement/Userdetail.vue:239 msgid "password again" msgstr "Noch einmal das Passwort" @@ -309,12 +323,12 @@ msgid "Password reset requested!" msgstr "Passwort Zurücksetzung angefragt!" -#: src/components/admin/usermanagement/Userdetail.vue:338 -#: src/components/admin/usermanagement/Userdetail.vue:339 +#: src/components/admin/usermanagement/Userdetail.vue:347 +#: src/components/admin/usermanagement/Userdetail.vue:348 msgid "Password should at least be 8 char long including 1 digit and 1 special char like $" msgstr "Das Passwort sollte mindestens 8 Zeichen lang sein, eine Zahlenziffer und ein Sonderzeichen wie etwa $ enthalten" -#: src/components/admin/usermanagement/Userdetail.vue:334 +#: src/components/admin/usermanagement/Userdetail.vue:343 msgid "Passwords do not match!" msgstr "Die Passwörter stimmen nicht überein!" @@ -322,11 +336,11 @@ msgid "Pending" msgstr "Ausstehend" -#: src/components/admin/usermanagement/Userdetail.vue:323 +#: src/components/admin/usermanagement/Userdetail.vue:332 msgid "Please choose a country" msgstr "Bitte wählen Sie ein Land aus" -#: src/components/admin/usermanagement/Userdetail.vue:328 +#: src/components/admin/usermanagement/Userdetail.vue:337 msgid "Please choose a role" msgstr "Bitte wählen Sie eine Rolle aus" @@ -342,8 +356,8 @@ msgid "Please enter a reference" msgstr "Bitte ein Höhenreferenzsystem eingeben" -#: src/components/map/fairway/Profiles.vue:409 -#: src/components/map/fairway/Profiles.vue:410 +#: src/components/map/fairway/Profiles.vue:413 +#: src/components/map/fairway/Profiles.vue:414 msgid "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" msgstr "Bitte geben Sie die Koordinaten in folgendem Format an: Lat,Lon,Lat,Lon" @@ -351,8 +365,8 @@ msgid "Please select a bottleneck" msgstr "Bitte eine Seichtstelle wählen" -#: src/components/admin/usermanagement/Userdetail.vue:41 -#: src/components/admin/usermanagement/Userdetail.vue:86 +#: src/components/admin/usermanagement/Userdetail.vue:40 +#: src/components/admin/usermanagement/Userdetail.vue:85 msgid "Please select one" msgstr "Bitte auswählen" @@ -360,11 +374,11 @@ msgid "portrait" msgstr "Hochformat" -#: src/components/map/fairway/Profiles.vue:448 +#: src/components/map/fairway/Profiles.vue:452 msgid "Profile deleted!" msgstr "Profil gelöscht!" -#: src/components/map/fairway/Profiles.vue:434 +#: src/components/map/fairway/Profiles.vue:438 msgid "Profile saved!" msgstr "Profil gespeichert!" @@ -384,7 +398,7 @@ msgid "Rejected" msgstr "Abgelehnt" -#: src/components/admin/usermanagement/Userdetail.vue:228 +#: src/components/admin/usermanagement/Userdetail.vue:237 msgid "Repeat Password" msgstr "Passwort erneut eingeben" @@ -392,7 +406,7 @@ msgid "Request password reset!" msgstr "Passwort-Zurücksetzung anfragen!" -#: src/components/admin/usermanagement/Userdetail.vue:79 +#: src/components/admin/usermanagement/Userdetail.vue:78 msgid "Role" msgstr "Rolle" @@ -401,26 +415,27 @@ msgstr "Gespeicherte Profile" #: src/components/admin/importschedule/Importschedule.vue:36 +#: src/components/admin/importschedule/Importscheduledetail.vue:48 msgid "Schedule" msgstr "Zeitplan" #: src/components/map/fairway/Profiles.vue:32 msgid "Select Bottleneck" -msgstr "Wähle Engstelle" +msgstr "Wähle Seichtstelle" #: src/components/admin/Systemconfiguration.vue:25 msgid "Send" msgstr "Absenden" -#: src/components/admin/usermanagement/Userdetail.vue:139 +#: src/components/admin/usermanagement/Userdetail.vue:138 msgid "Send testmail" msgstr "Test-E-Mail versenden" #: src/components/admin/Importqueue.vue:56 msgid "Signer" -msgstr "" +msgstr "Überprüft durch" -#: src/components/map/Identify.vue:63 +#: src/components/map/Identify.vue:60 msgid "" "Some data ©\n" " <a href=\"https://www.openstreetmap.org/copyright\">%{ name }</a>\n" @@ -429,13 +444,13 @@ #: src/components/map/fairway/Profiles.vue:45 msgid "Sounding Result" -msgstr "Seichtstellenvermessungsergebnisse" +msgstr "Seichtstellenvermessung" -#: src/components/map/Identify.vue:60 +#: src/components/map/Identify.vue:57 msgid "source-code" msgstr "Quelltext" -#: src/components/Sidebar.vue:44 +#: src/components/Sidebar.vue:54 msgid "Staging area" msgstr "Import-Überprüfung" @@ -455,11 +470,12 @@ msgid "State" msgstr "Zustand" -#: src/components/admin/usermanagement/Userdetail.vue:130 +#: src/components/admin/importschedule/Importscheduledetail.vue:85 +#: src/components/admin/usermanagement/Userdetail.vue:129 msgid "Submit" msgstr "Abschicken" -#: src/components/map/fairway/Profiles.vue:373 +#: src/components/map/fairway/Profiles.vue:377 msgid "Success" msgstr "Erfolg" @@ -467,11 +483,11 @@ msgid "Successful" msgstr "Erfolgreich" -#: src/components/admin/usermanagement/Userdetail.vue:89 +#: src/components/admin/usermanagement/Userdetail.vue:88 msgid "Sysadmin" msgstr "Sys-Admin" -#: src/components/Sidebar.vue:47 +#: src/components/Sidebar.vue:57 msgid "Systemadministration" msgstr "System-Administration" @@ -479,7 +495,7 @@ msgid "Systemconfiguration" msgstr "System-Konfiguation" -#: src/components/map/Identify.vue:54 +#: src/components/map/Identify.vue:51 msgid "" "This app uses <i>gemma</i>, which is Free Software under <br/>\n" " %{ license } without warranty, see docs for details." @@ -498,31 +514,40 @@ msgid "User" msgstr "Benutzer" -#: src/components/admin/usermanagement/Userdetail.vue:15 +#: src/components/admin/usermanagement/Userdetail.vue:14 #: src/components/map/contextbox/Staging.vue:16 msgid "Username" msgstr "Benutzername" -#: src/components/Sidebar.vue:52 +#: src/components/Sidebar.vue:66 #: src/components/admin/usermanagement/Usermanagement.vue:14 msgid "Users" msgstr "Benutzer" -#: src/components/map/Identify.vue:68 +#: src/components/map/Identify.vue:65 msgid "" "Uses\n" -" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a> under %{ geoLicense }." +" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a>\n" +" under %{ geoLicense }." msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:92 +#: src/components/admin/usermanagement/Userdetail.vue:91 msgid "Waterway Admin" msgstr "Waterway-Admin" -#: src/components/admin/usermanagement/Userdetail.vue:95 +#: src/components/admin/usermanagement/Userdetail.vue:94 msgid "Waterway User" msgstr "Waterway-Benutzer" -#: src/components/map/fairway/Profiles.vue:435 -#: src/components/map/fairway/Profiles.vue:436 +#: src/components/map/fairway/Profiles.vue:439 +#: src/components/map/fairway/Profiles.vue:440 msgid "You can now select these coordinates from the \"Saved cross profiles\" menu to restore this cross profile." msgstr "Sie können diese Koordinaten aus dem \"Gespeicherte Profile\"-Menü auswählen, um diesen Profilschnitt wieder herzustellen." + +#: src/store/map.js:415 +msgid "Length" +msgstr "Länge" + +#: src/store/map.js:436 +msgid "Area" +msgstr "Fläche"
--- a/client/src/locale/en_GB/LC_MESSAGES/app.po Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/locale/en_GB/LC_MESSAGES/app.po Sat Dec 29 16:07:40 2018 +0100 @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: wamosjs 0.1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-12-05 14:14+0100\n" +"POT-Creation-Date: 2018-12-10 15:30+0100\n" "PO-Revision-Date: 2018-07-03 17:18+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -25,7 +25,7 @@ msgid "Accesslog" msgstr "" -#: src/components/admin/usermanagement/Usermanagement.vue:106 +#: src/components/admin/usermanagement/Usermanagement.vue:103 msgid "Add User" msgstr "" @@ -38,17 +38,17 @@ msgstr "" #: src/components/admin/Importqueue.vue:136 -#: src/components/admin/Systemconfiguration.vue:133 -#: src/components/admin/Systemconfiguration.vue:148 -#: src/components/admin/Systemconfiguration.vue:167 -#: src/components/admin/Systemconfiguration.vue:184 -#: src/components/admin/usermanagement/Userdetail.vue:296 -#: src/components/admin/usermanagement/Userdetail.vue:368 -#: src/components/admin/usermanagement/Usermanagement.vue:300 -#: src/components/admin/usermanagement/Usermanagement.vue:308 -#: src/components/admin/usermanagement/Usermanagement.vue:334 +#: src/components/admin/Systemconfiguration.vue:134 +#: src/components/admin/Systemconfiguration.vue:149 +#: src/components/admin/Systemconfiguration.vue:168 +#: src/components/admin/Systemconfiguration.vue:185 +#: src/components/admin/usermanagement/Userdetail.vue:305 +#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Usermanagement.vue:313 +#: src/components/admin/usermanagement/Usermanagement.vue:321 +#: src/components/admin/usermanagement/Usermanagement.vue:347 #: src/components/map/Search.vue:257 -#: src/components/map/contextbox/Bottlenecks.vue:252 +#: src/components/map/contextbox/Bottlenecks.vue:275 #: src/components/map/contextbox/ImportSoundingresults.vue:205 #: src/components/map/contextbox/ImportSoundingresults.vue:244 #: src/components/map/contextbox/ImportSoundingresults.vue:275 @@ -67,7 +67,7 @@ msgid "Bottleneck Areas stroke-color" msgstr "" -#: src/components/Sidebar.vue:22 +#: src/components/Sidebar.vue:27 #: src/components/map/contextbox/Bottlenecks.vue:4 msgid "Bottlenecks" msgstr "" @@ -93,7 +93,7 @@ msgid "Compare with" msgstr "" -#: src/components/Sidebar.vue:58 +#: src/components/Sidebar.vue:76 msgid "Configuration" msgstr "" @@ -102,11 +102,11 @@ msgid "Confirm" msgstr "" -#: src/components/map/fairway/Profiles.vue:374 +#: src/components/map/fairway/Profiles.vue:378 msgid "Coordinates copied to clipboard!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:34 +#: src/components/admin/usermanagement/Userdetail.vue:33 msgid "Country" msgstr "" @@ -128,13 +128,18 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:37 +#: src/components/admin/importschedule/Importscheduledetail.vue:80 msgid "Email" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:60 +#: src/components/admin/usermanagement/Userdetail.vue:59 msgid "Email address" msgstr "" +#: src/components/admin/importschedule/Importscheduledetail.vue:61 +msgid "Email Notification" +msgstr "" + #: src/components/admin/Importqueue.vue:53 msgid "Enqueued" msgstr "" @@ -155,7 +160,7 @@ msgid "Enter username" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Userdetail.vue:386 msgid "Error while saving user" msgstr "" @@ -184,7 +189,7 @@ msgid "Import" msgstr "" -#: src/components/Sidebar.vue:31 +#: src/components/Sidebar.vue:40 msgid "Import soundingresults" msgstr "" @@ -196,20 +201,28 @@ msgid "Imported" msgstr "" -#: src/components/Sidebar.vue:66 src/components/admin/Importqueue.vue:9 +#: src/components/Sidebar.vue:92 src/components/admin/Importqueue.vue:9 msgid "Importqueue" msgstr "" -#: src/components/Sidebar.vue:70 +#: src/components/admin/importschedule/Importscheduledetail.vue:20 +msgid "Imports" +msgstr "" + +#: src/components/Sidebar.vue:100 #: src/components/admin/importschedule/Importschedule.vue:9 msgid "Importschedule" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:345 +#: src/components/admin/importschedule/Importscheduledetail.vue:34 +msgid "Importtype" +msgstr "" + +#: src/components/admin/usermanagement/Userdetail.vue:354 msgid "invalid email" msgstr "" -#: src/components/map/fairway/Profiles.vue:408 +#: src/components/map/fairway/Profiles.vue:412 msgid "Invalid input" msgstr "" @@ -241,19 +254,19 @@ msgid "Login failed" msgstr "" -#: src/components/Sidebar.vue:76 +#: src/components/Sidebar.vue:110 msgid "Logout" msgstr "" -#: src/components/Sidebar.vue:62 src/components/admin/Logs.vue:9 +#: src/components/Sidebar.vue:84 src/components/admin/Logs.vue:9 msgid "Logs" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:141 +#: src/components/admin/usermanagement/Userdetail.vue:140 msgid "Mail was sent" msgstr "" -#: src/components/Sidebar.vue:14 +#: src/components/Sidebar.vue:15 msgid "Map" msgstr "" @@ -266,7 +279,7 @@ msgid "Name" msgstr "" -#: src/components/admin/importschedule/Importscheduledetail.vue:6 +#: src/components/admin/importschedule/Importscheduledetail.vue:9 msgid "New import" msgstr "" @@ -274,7 +287,7 @@ msgid "New Import" msgstr "" -#: src/components/map/Identify.vue:50 +#: src/components/map/Identify.vue:47 msgid "No features identified." msgstr "" @@ -291,15 +304,15 @@ msgid "Open in new window" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:229 +#: src/components/admin/usermanagement/Userdetail.vue:238 msgid "password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:227 +#: src/components/admin/usermanagement/Userdetail.vue:236 msgid "Password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:230 +#: src/components/admin/usermanagement/Userdetail.vue:239 msgid "password again" msgstr "" @@ -307,12 +320,12 @@ msgid "Password reset requested!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:338 -#: src/components/admin/usermanagement/Userdetail.vue:339 +#: src/components/admin/usermanagement/Userdetail.vue:347 +#: src/components/admin/usermanagement/Userdetail.vue:348 msgid "Password should at least be 8 char long including 1 digit and 1 special char like $" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:334 +#: src/components/admin/usermanagement/Userdetail.vue:343 msgid "Passwords do not match!" msgstr "" @@ -320,11 +333,11 @@ msgid "Pending" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:323 +#: src/components/admin/usermanagement/Userdetail.vue:332 msgid "Please choose a country" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:328 +#: src/components/admin/usermanagement/Userdetail.vue:337 msgid "Please choose a role" msgstr "" @@ -340,8 +353,8 @@ msgid "Please enter a reference" msgstr "" -#: src/components/map/fairway/Profiles.vue:409 -#: src/components/map/fairway/Profiles.vue:410 +#: src/components/map/fairway/Profiles.vue:413 +#: src/components/map/fairway/Profiles.vue:414 msgid "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" msgstr "" @@ -349,8 +362,8 @@ msgid "Please select a bottleneck" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:41 -#: src/components/admin/usermanagement/Userdetail.vue:86 +#: src/components/admin/usermanagement/Userdetail.vue:40 +#: src/components/admin/usermanagement/Userdetail.vue:85 msgid "Please select one" msgstr "" @@ -358,11 +371,11 @@ msgid "portrait" msgstr "" -#: src/components/map/fairway/Profiles.vue:448 +#: src/components/map/fairway/Profiles.vue:452 msgid "Profile deleted!" msgstr "" -#: src/components/map/fairway/Profiles.vue:434 +#: src/components/map/fairway/Profiles.vue:438 msgid "Profile saved!" msgstr "" @@ -382,7 +395,7 @@ msgid "Rejected" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:228 +#: src/components/admin/usermanagement/Userdetail.vue:237 msgid "Repeat Password" msgstr "" @@ -390,7 +403,7 @@ msgid "Request password reset!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:79 +#: src/components/admin/usermanagement/Userdetail.vue:78 msgid "Role" msgstr "" @@ -399,6 +412,7 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:36 +#: src/components/admin/importschedule/Importscheduledetail.vue:48 msgid "Schedule" msgstr "" @@ -410,7 +424,7 @@ msgid "Send" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:139 +#: src/components/admin/usermanagement/Userdetail.vue:138 msgid "Send testmail" msgstr "" @@ -418,7 +432,7 @@ msgid "Signer" msgstr "" -#: src/components/map/Identify.vue:63 +#: src/components/map/Identify.vue:60 msgid "" "Some data ©\n" " <a href=\"https://www.openstreetmap.org/copyright\">%{ name }</a>\n" @@ -429,11 +443,11 @@ msgid "Sounding Result" msgstr "" -#: src/components/map/Identify.vue:60 +#: src/components/map/Identify.vue:57 msgid "source-code" msgstr "" -#: src/components/Sidebar.vue:44 +#: src/components/Sidebar.vue:54 msgid "Staging area" msgstr "" @@ -453,11 +467,12 @@ msgid "State" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:130 +#: src/components/admin/importschedule/Importscheduledetail.vue:85 +#: src/components/admin/usermanagement/Userdetail.vue:129 msgid "Submit" msgstr "" -#: src/components/map/fairway/Profiles.vue:373 +#: src/components/map/fairway/Profiles.vue:377 msgid "Success" msgstr "" @@ -465,11 +480,11 @@ msgid "Successful" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:89 +#: src/components/admin/usermanagement/Userdetail.vue:88 msgid "Sysadmin" msgstr "" -#: src/components/Sidebar.vue:47 +#: src/components/Sidebar.vue:57 msgid "Systemadministration" msgstr "" @@ -477,7 +492,7 @@ msgid "Systemconfiguration" msgstr "" -#: src/components/map/Identify.vue:54 +#: src/components/map/Identify.vue:51 msgid "" "This app uses <i>gemma</i>, which is Free Software under <br/>\n" " %{ license } without warranty, see docs for details." @@ -496,31 +511,40 @@ msgid "User" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:15 +#: src/components/admin/usermanagement/Userdetail.vue:14 #: src/components/map/contextbox/Staging.vue:16 msgid "Username" msgstr "" -#: src/components/Sidebar.vue:52 +#: src/components/Sidebar.vue:66 #: src/components/admin/usermanagement/Usermanagement.vue:14 msgid "Users" msgstr "" -#: src/components/map/Identify.vue:68 +#: src/components/map/Identify.vue:65 msgid "" "Uses\n" -" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a> under %{ geoLicense }." +" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a>\n" +" under %{ geoLicense }." msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:92 +#: src/components/admin/usermanagement/Userdetail.vue:91 msgid "Waterway Admin" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:95 +#: src/components/admin/usermanagement/Userdetail.vue:94 msgid "Waterway User" msgstr "" -#: src/components/map/fairway/Profiles.vue:435 -#: src/components/map/fairway/Profiles.vue:436 +#: src/components/map/fairway/Profiles.vue:439 +#: src/components/map/fairway/Profiles.vue:440 msgid "You can now select these coordinates from the \"Saved cross profiles\" menu to restore this cross profile." msgstr "" + +#: src/store/map.js:415 +msgid "Length" +msgstr "" + +#: src/store/map.js:436 +msgid "Area" +msgstr ""
--- a/client/src/locale/hr_HR/LC_MESSAGES/app.po Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/locale/hr_HR/LC_MESSAGES/app.po Sat Dec 29 16:07:40 2018 +0100 @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: gemmajs 1.99.0-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-12-05 14:14+0100\n" +"POT-Creation-Date: 2018-12-10 15:30+0100\n" "PO-Revision-Date: 2018-12-05 12:23+0100\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -25,7 +25,7 @@ msgid "Accesslog" msgstr "" -#: src/components/admin/usermanagement/Usermanagement.vue:106 +#: src/components/admin/usermanagement/Usermanagement.vue:103 msgid "Add User" msgstr "" @@ -38,17 +38,17 @@ msgstr "" #: src/components/admin/Importqueue.vue:136 -#: src/components/admin/Systemconfiguration.vue:133 -#: src/components/admin/Systemconfiguration.vue:148 -#: src/components/admin/Systemconfiguration.vue:167 -#: src/components/admin/Systemconfiguration.vue:184 -#: src/components/admin/usermanagement/Userdetail.vue:296 -#: src/components/admin/usermanagement/Userdetail.vue:368 -#: src/components/admin/usermanagement/Usermanagement.vue:300 -#: src/components/admin/usermanagement/Usermanagement.vue:308 -#: src/components/admin/usermanagement/Usermanagement.vue:334 +#: src/components/admin/Systemconfiguration.vue:134 +#: src/components/admin/Systemconfiguration.vue:149 +#: src/components/admin/Systemconfiguration.vue:168 +#: src/components/admin/Systemconfiguration.vue:185 +#: src/components/admin/usermanagement/Userdetail.vue:305 +#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Usermanagement.vue:313 +#: src/components/admin/usermanagement/Usermanagement.vue:321 +#: src/components/admin/usermanagement/Usermanagement.vue:347 #: src/components/map/Search.vue:257 -#: src/components/map/contextbox/Bottlenecks.vue:252 +#: src/components/map/contextbox/Bottlenecks.vue:275 #: src/components/map/contextbox/ImportSoundingresults.vue:205 #: src/components/map/contextbox/ImportSoundingresults.vue:244 #: src/components/map/contextbox/ImportSoundingresults.vue:275 @@ -67,7 +67,7 @@ msgid "Bottleneck Areas stroke-color" msgstr "" -#: src/components/Sidebar.vue:22 +#: src/components/Sidebar.vue:27 #: src/components/map/contextbox/Bottlenecks.vue:4 msgid "Bottlenecks" msgstr "" @@ -93,7 +93,7 @@ msgid "Compare with" msgstr "" -#: src/components/Sidebar.vue:58 +#: src/components/Sidebar.vue:76 msgid "Configuration" msgstr "" @@ -102,11 +102,11 @@ msgid "Confirm" msgstr "" -#: src/components/map/fairway/Profiles.vue:374 +#: src/components/map/fairway/Profiles.vue:378 msgid "Coordinates copied to clipboard!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:34 +#: src/components/admin/usermanagement/Userdetail.vue:33 msgid "Country" msgstr "" @@ -128,13 +128,18 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:37 +#: src/components/admin/importschedule/Importscheduledetail.vue:80 msgid "Email" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:60 +#: src/components/admin/usermanagement/Userdetail.vue:59 msgid "Email address" msgstr "" +#: src/components/admin/importschedule/Importscheduledetail.vue:61 +msgid "Email Notification" +msgstr "" + #: src/components/admin/Importqueue.vue:53 msgid "Enqueued" msgstr "" @@ -155,7 +160,7 @@ msgid "Enter username" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Userdetail.vue:386 msgid "Error while saving user" msgstr "" @@ -184,7 +189,7 @@ msgid "Import" msgstr "" -#: src/components/Sidebar.vue:31 +#: src/components/Sidebar.vue:40 msgid "Import soundingresults" msgstr "" @@ -196,20 +201,28 @@ msgid "Imported" msgstr "" -#: src/components/Sidebar.vue:66 src/components/admin/Importqueue.vue:9 +#: src/components/Sidebar.vue:92 src/components/admin/Importqueue.vue:9 msgid "Importqueue" msgstr "" -#: src/components/Sidebar.vue:70 +#: src/components/admin/importschedule/Importscheduledetail.vue:20 +msgid "Imports" +msgstr "" + +#: src/components/Sidebar.vue:100 #: src/components/admin/importschedule/Importschedule.vue:9 msgid "Importschedule" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:345 +#: src/components/admin/importschedule/Importscheduledetail.vue:34 +msgid "Importtype" +msgstr "" + +#: src/components/admin/usermanagement/Userdetail.vue:354 msgid "invalid email" msgstr "" -#: src/components/map/fairway/Profiles.vue:408 +#: src/components/map/fairway/Profiles.vue:412 msgid "Invalid input" msgstr "" @@ -241,19 +254,19 @@ msgid "Login failed" msgstr "" -#: src/components/Sidebar.vue:76 +#: src/components/Sidebar.vue:110 msgid "Logout" msgstr "" -#: src/components/Sidebar.vue:62 src/components/admin/Logs.vue:9 +#: src/components/Sidebar.vue:84 src/components/admin/Logs.vue:9 msgid "Logs" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:141 +#: src/components/admin/usermanagement/Userdetail.vue:140 msgid "Mail was sent" msgstr "" -#: src/components/Sidebar.vue:14 +#: src/components/Sidebar.vue:15 msgid "Map" msgstr "" @@ -266,7 +279,7 @@ msgid "Name" msgstr "" -#: src/components/admin/importschedule/Importscheduledetail.vue:6 +#: src/components/admin/importschedule/Importscheduledetail.vue:9 msgid "New import" msgstr "" @@ -274,7 +287,7 @@ msgid "New Import" msgstr "" -#: src/components/map/Identify.vue:50 +#: src/components/map/Identify.vue:47 msgid "No features identified." msgstr "" @@ -291,15 +304,15 @@ msgid "Open in new window" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:229 +#: src/components/admin/usermanagement/Userdetail.vue:238 msgid "password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:227 +#: src/components/admin/usermanagement/Userdetail.vue:236 msgid "Password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:230 +#: src/components/admin/usermanagement/Userdetail.vue:239 msgid "password again" msgstr "" @@ -307,12 +320,12 @@ msgid "Password reset requested!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:338 -#: src/components/admin/usermanagement/Userdetail.vue:339 +#: src/components/admin/usermanagement/Userdetail.vue:347 +#: src/components/admin/usermanagement/Userdetail.vue:348 msgid "Password should at least be 8 char long including 1 digit and 1 special char like $" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:334 +#: src/components/admin/usermanagement/Userdetail.vue:343 msgid "Passwords do not match!" msgstr "" @@ -320,11 +333,11 @@ msgid "Pending" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:323 +#: src/components/admin/usermanagement/Userdetail.vue:332 msgid "Please choose a country" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:328 +#: src/components/admin/usermanagement/Userdetail.vue:337 msgid "Please choose a role" msgstr "" @@ -340,8 +353,8 @@ msgid "Please enter a reference" msgstr "" -#: src/components/map/fairway/Profiles.vue:409 -#: src/components/map/fairway/Profiles.vue:410 +#: src/components/map/fairway/Profiles.vue:413 +#: src/components/map/fairway/Profiles.vue:414 msgid "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" msgstr "" @@ -349,8 +362,8 @@ msgid "Please select a bottleneck" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:41 -#: src/components/admin/usermanagement/Userdetail.vue:86 +#: src/components/admin/usermanagement/Userdetail.vue:40 +#: src/components/admin/usermanagement/Userdetail.vue:85 msgid "Please select one" msgstr "" @@ -358,11 +371,11 @@ msgid "portrait" msgstr "" -#: src/components/map/fairway/Profiles.vue:448 +#: src/components/map/fairway/Profiles.vue:452 msgid "Profile deleted!" msgstr "" -#: src/components/map/fairway/Profiles.vue:434 +#: src/components/map/fairway/Profiles.vue:438 msgid "Profile saved!" msgstr "" @@ -382,7 +395,7 @@ msgid "Rejected" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:228 +#: src/components/admin/usermanagement/Userdetail.vue:237 msgid "Repeat Password" msgstr "" @@ -390,7 +403,7 @@ msgid "Request password reset!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:79 +#: src/components/admin/usermanagement/Userdetail.vue:78 msgid "Role" msgstr "" @@ -399,6 +412,7 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:36 +#: src/components/admin/importschedule/Importscheduledetail.vue:48 msgid "Schedule" msgstr "" @@ -410,7 +424,7 @@ msgid "Send" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:139 +#: src/components/admin/usermanagement/Userdetail.vue:138 msgid "Send testmail" msgstr "" @@ -418,7 +432,7 @@ msgid "Signer" msgstr "" -#: src/components/map/Identify.vue:63 +#: src/components/map/Identify.vue:60 msgid "" "Some data ©\n" " <a href=\"https://www.openstreetmap.org/copyright\">%{ name }</a>\n" @@ -429,11 +443,11 @@ msgid "Sounding Result" msgstr "" -#: src/components/map/Identify.vue:60 +#: src/components/map/Identify.vue:57 msgid "source-code" msgstr "" -#: src/components/Sidebar.vue:44 +#: src/components/Sidebar.vue:54 msgid "Staging area" msgstr "" @@ -453,11 +467,12 @@ msgid "State" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:130 +#: src/components/admin/importschedule/Importscheduledetail.vue:85 +#: src/components/admin/usermanagement/Userdetail.vue:129 msgid "Submit" msgstr "" -#: src/components/map/fairway/Profiles.vue:373 +#: src/components/map/fairway/Profiles.vue:377 msgid "Success" msgstr "" @@ -465,11 +480,11 @@ msgid "Successful" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:89 +#: src/components/admin/usermanagement/Userdetail.vue:88 msgid "Sysadmin" msgstr "" -#: src/components/Sidebar.vue:47 +#: src/components/Sidebar.vue:57 msgid "Systemadministration" msgstr "" @@ -477,7 +492,7 @@ msgid "Systemconfiguration" msgstr "" -#: src/components/map/Identify.vue:54 +#: src/components/map/Identify.vue:51 msgid "" "This app uses <i>gemma</i>, which is Free Software under <br/>\n" " %{ license } without warranty, see docs for details." @@ -496,31 +511,40 @@ msgid "User" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:15 +#: src/components/admin/usermanagement/Userdetail.vue:14 #: src/components/map/contextbox/Staging.vue:16 msgid "Username" msgstr "" -#: src/components/Sidebar.vue:52 +#: src/components/Sidebar.vue:66 #: src/components/admin/usermanagement/Usermanagement.vue:14 msgid "Users" msgstr "" -#: src/components/map/Identify.vue:68 +#: src/components/map/Identify.vue:65 msgid "" "Uses\n" -" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a> under %{ geoLicense }." +" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a>\n" +" under %{ geoLicense }." msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:92 +#: src/components/admin/usermanagement/Userdetail.vue:91 msgid "Waterway Admin" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:95 +#: src/components/admin/usermanagement/Userdetail.vue:94 msgid "Waterway User" msgstr "" -#: src/components/map/fairway/Profiles.vue:435 -#: src/components/map/fairway/Profiles.vue:436 +#: src/components/map/fairway/Profiles.vue:439 +#: src/components/map/fairway/Profiles.vue:440 msgid "You can now select these coordinates from the \"Saved cross profiles\" menu to restore this cross profile." msgstr "" + +#: src/store/map.js:415 +msgid "Length" +msgstr "" + +#: src/store/map.js:436 +msgid "Area" +msgstr ""
--- a/client/src/locale/hu_HU/LC_MESSAGES/app.po Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/locale/hu_HU/LC_MESSAGES/app.po Sat Dec 29 16:07:40 2018 +0100 @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: gemmajs 1.99.0-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-12-05 14:14+0100\n" +"POT-Creation-Date: 2018-12-10 15:30+0100\n" "PO-Revision-Date: 2018-12-05 12:23+0100\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -25,7 +25,7 @@ msgid "Accesslog" msgstr "" -#: src/components/admin/usermanagement/Usermanagement.vue:106 +#: src/components/admin/usermanagement/Usermanagement.vue:103 msgid "Add User" msgstr "" @@ -38,17 +38,17 @@ msgstr "" #: src/components/admin/Importqueue.vue:136 -#: src/components/admin/Systemconfiguration.vue:133 -#: src/components/admin/Systemconfiguration.vue:148 -#: src/components/admin/Systemconfiguration.vue:167 -#: src/components/admin/Systemconfiguration.vue:184 -#: src/components/admin/usermanagement/Userdetail.vue:296 -#: src/components/admin/usermanagement/Userdetail.vue:368 -#: src/components/admin/usermanagement/Usermanagement.vue:300 -#: src/components/admin/usermanagement/Usermanagement.vue:308 -#: src/components/admin/usermanagement/Usermanagement.vue:334 +#: src/components/admin/Systemconfiguration.vue:134 +#: src/components/admin/Systemconfiguration.vue:149 +#: src/components/admin/Systemconfiguration.vue:168 +#: src/components/admin/Systemconfiguration.vue:185 +#: src/components/admin/usermanagement/Userdetail.vue:305 +#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Usermanagement.vue:313 +#: src/components/admin/usermanagement/Usermanagement.vue:321 +#: src/components/admin/usermanagement/Usermanagement.vue:347 #: src/components/map/Search.vue:257 -#: src/components/map/contextbox/Bottlenecks.vue:252 +#: src/components/map/contextbox/Bottlenecks.vue:275 #: src/components/map/contextbox/ImportSoundingresults.vue:205 #: src/components/map/contextbox/ImportSoundingresults.vue:244 #: src/components/map/contextbox/ImportSoundingresults.vue:275 @@ -67,7 +67,7 @@ msgid "Bottleneck Areas stroke-color" msgstr "" -#: src/components/Sidebar.vue:22 +#: src/components/Sidebar.vue:27 #: src/components/map/contextbox/Bottlenecks.vue:4 msgid "Bottlenecks" msgstr "" @@ -93,7 +93,7 @@ msgid "Compare with" msgstr "" -#: src/components/Sidebar.vue:58 +#: src/components/Sidebar.vue:76 msgid "Configuration" msgstr "" @@ -102,11 +102,11 @@ msgid "Confirm" msgstr "" -#: src/components/map/fairway/Profiles.vue:374 +#: src/components/map/fairway/Profiles.vue:378 msgid "Coordinates copied to clipboard!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:34 +#: src/components/admin/usermanagement/Userdetail.vue:33 msgid "Country" msgstr "" @@ -128,13 +128,18 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:37 +#: src/components/admin/importschedule/Importscheduledetail.vue:80 msgid "Email" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:60 +#: src/components/admin/usermanagement/Userdetail.vue:59 msgid "Email address" msgstr "" +#: src/components/admin/importschedule/Importscheduledetail.vue:61 +msgid "Email Notification" +msgstr "" + #: src/components/admin/Importqueue.vue:53 msgid "Enqueued" msgstr "" @@ -155,7 +160,7 @@ msgid "Enter username" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Userdetail.vue:386 msgid "Error while saving user" msgstr "" @@ -184,7 +189,7 @@ msgid "Import" msgstr "" -#: src/components/Sidebar.vue:31 +#: src/components/Sidebar.vue:40 msgid "Import soundingresults" msgstr "" @@ -196,20 +201,28 @@ msgid "Imported" msgstr "" -#: src/components/Sidebar.vue:66 src/components/admin/Importqueue.vue:9 +#: src/components/Sidebar.vue:92 src/components/admin/Importqueue.vue:9 msgid "Importqueue" msgstr "" -#: src/components/Sidebar.vue:70 +#: src/components/admin/importschedule/Importscheduledetail.vue:20 +msgid "Imports" +msgstr "" + +#: src/components/Sidebar.vue:100 #: src/components/admin/importschedule/Importschedule.vue:9 msgid "Importschedule" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:345 +#: src/components/admin/importschedule/Importscheduledetail.vue:34 +msgid "Importtype" +msgstr "" + +#: src/components/admin/usermanagement/Userdetail.vue:354 msgid "invalid email" msgstr "" -#: src/components/map/fairway/Profiles.vue:408 +#: src/components/map/fairway/Profiles.vue:412 msgid "Invalid input" msgstr "" @@ -241,19 +254,19 @@ msgid "Login failed" msgstr "" -#: src/components/Sidebar.vue:76 +#: src/components/Sidebar.vue:110 msgid "Logout" msgstr "" -#: src/components/Sidebar.vue:62 src/components/admin/Logs.vue:9 +#: src/components/Sidebar.vue:84 src/components/admin/Logs.vue:9 msgid "Logs" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:141 +#: src/components/admin/usermanagement/Userdetail.vue:140 msgid "Mail was sent" msgstr "" -#: src/components/Sidebar.vue:14 +#: src/components/Sidebar.vue:15 msgid "Map" msgstr "" @@ -266,7 +279,7 @@ msgid "Name" msgstr "" -#: src/components/admin/importschedule/Importscheduledetail.vue:6 +#: src/components/admin/importschedule/Importscheduledetail.vue:9 msgid "New import" msgstr "" @@ -274,7 +287,7 @@ msgid "New Import" msgstr "" -#: src/components/map/Identify.vue:50 +#: src/components/map/Identify.vue:47 msgid "No features identified." msgstr "" @@ -291,15 +304,15 @@ msgid "Open in new window" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:229 +#: src/components/admin/usermanagement/Userdetail.vue:238 msgid "password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:227 +#: src/components/admin/usermanagement/Userdetail.vue:236 msgid "Password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:230 +#: src/components/admin/usermanagement/Userdetail.vue:239 msgid "password again" msgstr "" @@ -307,12 +320,12 @@ msgid "Password reset requested!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:338 -#: src/components/admin/usermanagement/Userdetail.vue:339 +#: src/components/admin/usermanagement/Userdetail.vue:347 +#: src/components/admin/usermanagement/Userdetail.vue:348 msgid "Password should at least be 8 char long including 1 digit and 1 special char like $" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:334 +#: src/components/admin/usermanagement/Userdetail.vue:343 msgid "Passwords do not match!" msgstr "" @@ -320,11 +333,11 @@ msgid "Pending" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:323 +#: src/components/admin/usermanagement/Userdetail.vue:332 msgid "Please choose a country" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:328 +#: src/components/admin/usermanagement/Userdetail.vue:337 msgid "Please choose a role" msgstr "" @@ -340,8 +353,8 @@ msgid "Please enter a reference" msgstr "" -#: src/components/map/fairway/Profiles.vue:409 -#: src/components/map/fairway/Profiles.vue:410 +#: src/components/map/fairway/Profiles.vue:413 +#: src/components/map/fairway/Profiles.vue:414 msgid "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" msgstr "" @@ -349,8 +362,8 @@ msgid "Please select a bottleneck" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:41 -#: src/components/admin/usermanagement/Userdetail.vue:86 +#: src/components/admin/usermanagement/Userdetail.vue:40 +#: src/components/admin/usermanagement/Userdetail.vue:85 msgid "Please select one" msgstr "" @@ -358,11 +371,11 @@ msgid "portrait" msgstr "" -#: src/components/map/fairway/Profiles.vue:448 +#: src/components/map/fairway/Profiles.vue:452 msgid "Profile deleted!" msgstr "" -#: src/components/map/fairway/Profiles.vue:434 +#: src/components/map/fairway/Profiles.vue:438 msgid "Profile saved!" msgstr "" @@ -382,7 +395,7 @@ msgid "Rejected" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:228 +#: src/components/admin/usermanagement/Userdetail.vue:237 msgid "Repeat Password" msgstr "" @@ -390,7 +403,7 @@ msgid "Request password reset!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:79 +#: src/components/admin/usermanagement/Userdetail.vue:78 msgid "Role" msgstr "" @@ -399,6 +412,7 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:36 +#: src/components/admin/importschedule/Importscheduledetail.vue:48 msgid "Schedule" msgstr "" @@ -410,7 +424,7 @@ msgid "Send" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:139 +#: src/components/admin/usermanagement/Userdetail.vue:138 msgid "Send testmail" msgstr "" @@ -418,7 +432,7 @@ msgid "Signer" msgstr "" -#: src/components/map/Identify.vue:63 +#: src/components/map/Identify.vue:60 msgid "" "Some data ©\n" " <a href=\"https://www.openstreetmap.org/copyright\">%{ name }</a>\n" @@ -429,11 +443,11 @@ msgid "Sounding Result" msgstr "" -#: src/components/map/Identify.vue:60 +#: src/components/map/Identify.vue:57 msgid "source-code" msgstr "" -#: src/components/Sidebar.vue:44 +#: src/components/Sidebar.vue:54 msgid "Staging area" msgstr "" @@ -453,11 +467,12 @@ msgid "State" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:130 +#: src/components/admin/importschedule/Importscheduledetail.vue:85 +#: src/components/admin/usermanagement/Userdetail.vue:129 msgid "Submit" msgstr "" -#: src/components/map/fairway/Profiles.vue:373 +#: src/components/map/fairway/Profiles.vue:377 msgid "Success" msgstr "" @@ -465,11 +480,11 @@ msgid "Successful" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:89 +#: src/components/admin/usermanagement/Userdetail.vue:88 msgid "Sysadmin" msgstr "" -#: src/components/Sidebar.vue:47 +#: src/components/Sidebar.vue:57 msgid "Systemadministration" msgstr "" @@ -477,7 +492,7 @@ msgid "Systemconfiguration" msgstr "" -#: src/components/map/Identify.vue:54 +#: src/components/map/Identify.vue:51 msgid "" "This app uses <i>gemma</i>, which is Free Software under <br/>\n" " %{ license } without warranty, see docs for details." @@ -496,31 +511,40 @@ msgid "User" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:15 +#: src/components/admin/usermanagement/Userdetail.vue:14 #: src/components/map/contextbox/Staging.vue:16 msgid "Username" msgstr "" -#: src/components/Sidebar.vue:52 +#: src/components/Sidebar.vue:66 #: src/components/admin/usermanagement/Usermanagement.vue:14 msgid "Users" msgstr "" -#: src/components/map/Identify.vue:68 +#: src/components/map/Identify.vue:65 msgid "" "Uses\n" -" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a> under %{ geoLicense }." +" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a>\n" +" under %{ geoLicense }." msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:92 +#: src/components/admin/usermanagement/Userdetail.vue:91 msgid "Waterway Admin" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:95 +#: src/components/admin/usermanagement/Userdetail.vue:94 msgid "Waterway User" msgstr "" -#: src/components/map/fairway/Profiles.vue:435 -#: src/components/map/fairway/Profiles.vue:436 +#: src/components/map/fairway/Profiles.vue:439 +#: src/components/map/fairway/Profiles.vue:440 msgid "You can now select these coordinates from the \"Saved cross profiles\" menu to restore this cross profile." msgstr "" + +#: src/store/map.js:415 +msgid "Length" +msgstr "" + +#: src/store/map.js:436 +msgid "Area" +msgstr ""
--- a/client/src/locale/ro_RO/LC_MESSAGES/app.po Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/locale/ro_RO/LC_MESSAGES/app.po Sat Dec 29 16:07:40 2018 +0100 @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: gemmajs 1.99.0-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-12-05 14:14+0100\n" +"POT-Creation-Date: 2018-12-10 15:30+0100\n" "PO-Revision-Date: 2018-12-05 12:23+0100\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -25,7 +25,7 @@ msgid "Accesslog" msgstr "" -#: src/components/admin/usermanagement/Usermanagement.vue:106 +#: src/components/admin/usermanagement/Usermanagement.vue:103 msgid "Add User" msgstr "" @@ -38,17 +38,17 @@ msgstr "" #: src/components/admin/Importqueue.vue:136 -#: src/components/admin/Systemconfiguration.vue:133 -#: src/components/admin/Systemconfiguration.vue:148 -#: src/components/admin/Systemconfiguration.vue:167 -#: src/components/admin/Systemconfiguration.vue:184 -#: src/components/admin/usermanagement/Userdetail.vue:296 -#: src/components/admin/usermanagement/Userdetail.vue:368 -#: src/components/admin/usermanagement/Usermanagement.vue:300 -#: src/components/admin/usermanagement/Usermanagement.vue:308 -#: src/components/admin/usermanagement/Usermanagement.vue:334 +#: src/components/admin/Systemconfiguration.vue:134 +#: src/components/admin/Systemconfiguration.vue:149 +#: src/components/admin/Systemconfiguration.vue:168 +#: src/components/admin/Systemconfiguration.vue:185 +#: src/components/admin/usermanagement/Userdetail.vue:305 +#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Usermanagement.vue:313 +#: src/components/admin/usermanagement/Usermanagement.vue:321 +#: src/components/admin/usermanagement/Usermanagement.vue:347 #: src/components/map/Search.vue:257 -#: src/components/map/contextbox/Bottlenecks.vue:252 +#: src/components/map/contextbox/Bottlenecks.vue:275 #: src/components/map/contextbox/ImportSoundingresults.vue:205 #: src/components/map/contextbox/ImportSoundingresults.vue:244 #: src/components/map/contextbox/ImportSoundingresults.vue:275 @@ -67,7 +67,7 @@ msgid "Bottleneck Areas stroke-color" msgstr "" -#: src/components/Sidebar.vue:22 +#: src/components/Sidebar.vue:27 #: src/components/map/contextbox/Bottlenecks.vue:4 msgid "Bottlenecks" msgstr "" @@ -93,7 +93,7 @@ msgid "Compare with" msgstr "" -#: src/components/Sidebar.vue:58 +#: src/components/Sidebar.vue:76 msgid "Configuration" msgstr "" @@ -102,11 +102,11 @@ msgid "Confirm" msgstr "" -#: src/components/map/fairway/Profiles.vue:374 +#: src/components/map/fairway/Profiles.vue:378 msgid "Coordinates copied to clipboard!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:34 +#: src/components/admin/usermanagement/Userdetail.vue:33 msgid "Country" msgstr "" @@ -128,13 +128,18 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:37 +#: src/components/admin/importschedule/Importscheduledetail.vue:80 msgid "Email" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:60 +#: src/components/admin/usermanagement/Userdetail.vue:59 msgid "Email address" msgstr "" +#: src/components/admin/importschedule/Importscheduledetail.vue:61 +msgid "Email Notification" +msgstr "" + #: src/components/admin/Importqueue.vue:53 msgid "Enqueued" msgstr "" @@ -155,7 +160,7 @@ msgid "Enter username" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Userdetail.vue:386 msgid "Error while saving user" msgstr "" @@ -184,7 +189,7 @@ msgid "Import" msgstr "" -#: src/components/Sidebar.vue:31 +#: src/components/Sidebar.vue:40 msgid "Import soundingresults" msgstr "" @@ -196,20 +201,28 @@ msgid "Imported" msgstr "" -#: src/components/Sidebar.vue:66 src/components/admin/Importqueue.vue:9 +#: src/components/Sidebar.vue:92 src/components/admin/Importqueue.vue:9 msgid "Importqueue" msgstr "" -#: src/components/Sidebar.vue:70 +#: src/components/admin/importschedule/Importscheduledetail.vue:20 +msgid "Imports" +msgstr "" + +#: src/components/Sidebar.vue:100 #: src/components/admin/importschedule/Importschedule.vue:9 msgid "Importschedule" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:345 +#: src/components/admin/importschedule/Importscheduledetail.vue:34 +msgid "Importtype" +msgstr "" + +#: src/components/admin/usermanagement/Userdetail.vue:354 msgid "invalid email" msgstr "" -#: src/components/map/fairway/Profiles.vue:408 +#: src/components/map/fairway/Profiles.vue:412 msgid "Invalid input" msgstr "" @@ -241,19 +254,19 @@ msgid "Login failed" msgstr "" -#: src/components/Sidebar.vue:76 +#: src/components/Sidebar.vue:110 msgid "Logout" msgstr "" -#: src/components/Sidebar.vue:62 src/components/admin/Logs.vue:9 +#: src/components/Sidebar.vue:84 src/components/admin/Logs.vue:9 msgid "Logs" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:141 +#: src/components/admin/usermanagement/Userdetail.vue:140 msgid "Mail was sent" msgstr "" -#: src/components/Sidebar.vue:14 +#: src/components/Sidebar.vue:15 msgid "Map" msgstr "" @@ -266,7 +279,7 @@ msgid "Name" msgstr "" -#: src/components/admin/importschedule/Importscheduledetail.vue:6 +#: src/components/admin/importschedule/Importscheduledetail.vue:9 msgid "New import" msgstr "" @@ -274,7 +287,7 @@ msgid "New Import" msgstr "" -#: src/components/map/Identify.vue:50 +#: src/components/map/Identify.vue:47 msgid "No features identified." msgstr "" @@ -291,15 +304,15 @@ msgid "Open in new window" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:229 +#: src/components/admin/usermanagement/Userdetail.vue:238 msgid "password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:227 +#: src/components/admin/usermanagement/Userdetail.vue:236 msgid "Password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:230 +#: src/components/admin/usermanagement/Userdetail.vue:239 msgid "password again" msgstr "" @@ -307,12 +320,12 @@ msgid "Password reset requested!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:338 -#: src/components/admin/usermanagement/Userdetail.vue:339 +#: src/components/admin/usermanagement/Userdetail.vue:347 +#: src/components/admin/usermanagement/Userdetail.vue:348 msgid "Password should at least be 8 char long including 1 digit and 1 special char like $" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:334 +#: src/components/admin/usermanagement/Userdetail.vue:343 msgid "Passwords do not match!" msgstr "" @@ -320,11 +333,11 @@ msgid "Pending" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:323 +#: src/components/admin/usermanagement/Userdetail.vue:332 msgid "Please choose a country" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:328 +#: src/components/admin/usermanagement/Userdetail.vue:337 msgid "Please choose a role" msgstr "" @@ -340,8 +353,8 @@ msgid "Please enter a reference" msgstr "" -#: src/components/map/fairway/Profiles.vue:409 -#: src/components/map/fairway/Profiles.vue:410 +#: src/components/map/fairway/Profiles.vue:413 +#: src/components/map/fairway/Profiles.vue:414 msgid "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" msgstr "" @@ -349,8 +362,8 @@ msgid "Please select a bottleneck" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:41 -#: src/components/admin/usermanagement/Userdetail.vue:86 +#: src/components/admin/usermanagement/Userdetail.vue:40 +#: src/components/admin/usermanagement/Userdetail.vue:85 msgid "Please select one" msgstr "" @@ -358,11 +371,11 @@ msgid "portrait" msgstr "" -#: src/components/map/fairway/Profiles.vue:448 +#: src/components/map/fairway/Profiles.vue:452 msgid "Profile deleted!" msgstr "" -#: src/components/map/fairway/Profiles.vue:434 +#: src/components/map/fairway/Profiles.vue:438 msgid "Profile saved!" msgstr "" @@ -382,7 +395,7 @@ msgid "Rejected" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:228 +#: src/components/admin/usermanagement/Userdetail.vue:237 msgid "Repeat Password" msgstr "" @@ -390,7 +403,7 @@ msgid "Request password reset!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:79 +#: src/components/admin/usermanagement/Userdetail.vue:78 msgid "Role" msgstr "" @@ -399,6 +412,7 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:36 +#: src/components/admin/importschedule/Importscheduledetail.vue:48 msgid "Schedule" msgstr "" @@ -410,7 +424,7 @@ msgid "Send" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:139 +#: src/components/admin/usermanagement/Userdetail.vue:138 msgid "Send testmail" msgstr "" @@ -418,7 +432,7 @@ msgid "Signer" msgstr "" -#: src/components/map/Identify.vue:63 +#: src/components/map/Identify.vue:60 msgid "" "Some data ©\n" " <a href=\"https://www.openstreetmap.org/copyright\">%{ name }</a>\n" @@ -429,11 +443,11 @@ msgid "Sounding Result" msgstr "" -#: src/components/map/Identify.vue:60 +#: src/components/map/Identify.vue:57 msgid "source-code" msgstr "" -#: src/components/Sidebar.vue:44 +#: src/components/Sidebar.vue:54 msgid "Staging area" msgstr "" @@ -453,11 +467,12 @@ msgid "State" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:130 +#: src/components/admin/importschedule/Importscheduledetail.vue:85 +#: src/components/admin/usermanagement/Userdetail.vue:129 msgid "Submit" msgstr "" -#: src/components/map/fairway/Profiles.vue:373 +#: src/components/map/fairway/Profiles.vue:377 msgid "Success" msgstr "" @@ -465,11 +480,11 @@ msgid "Successful" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:89 +#: src/components/admin/usermanagement/Userdetail.vue:88 msgid "Sysadmin" msgstr "" -#: src/components/Sidebar.vue:47 +#: src/components/Sidebar.vue:57 msgid "Systemadministration" msgstr "" @@ -477,7 +492,7 @@ msgid "Systemconfiguration" msgstr "" -#: src/components/map/Identify.vue:54 +#: src/components/map/Identify.vue:51 msgid "" "This app uses <i>gemma</i>, which is Free Software under <br/>\n" " %{ license } without warranty, see docs for details." @@ -496,31 +511,40 @@ msgid "User" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:15 +#: src/components/admin/usermanagement/Userdetail.vue:14 #: src/components/map/contextbox/Staging.vue:16 msgid "Username" msgstr "" -#: src/components/Sidebar.vue:52 +#: src/components/Sidebar.vue:66 #: src/components/admin/usermanagement/Usermanagement.vue:14 msgid "Users" msgstr "" -#: src/components/map/Identify.vue:68 +#: src/components/map/Identify.vue:65 msgid "" "Uses\n" -" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a> under %{ geoLicense }." +" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a>\n" +" under %{ geoLicense }." msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:92 +#: src/components/admin/usermanagement/Userdetail.vue:91 msgid "Waterway Admin" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:95 +#: src/components/admin/usermanagement/Userdetail.vue:94 msgid "Waterway User" msgstr "" -#: src/components/map/fairway/Profiles.vue:435 -#: src/components/map/fairway/Profiles.vue:436 +#: src/components/map/fairway/Profiles.vue:439 +#: src/components/map/fairway/Profiles.vue:440 msgid "You can now select these coordinates from the \"Saved cross profiles\" menu to restore this cross profile." msgstr "" + +#: src/store/map.js:415 +msgid "Length" +msgstr "" + +#: src/store/map.js:436 +msgid "Area" +msgstr ""
--- a/client/src/locale/sk_SK/LC_MESSAGES/app.po Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/locale/sk_SK/LC_MESSAGES/app.po Sat Dec 29 16:07:40 2018 +0100 @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: wamosjs 0.1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-12-05 14:14+0100\n" +"POT-Creation-Date: 2018-12-10 15:30+0100\n" "PO-Revision-Date: 2018-07-03 17:18+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -25,7 +25,7 @@ msgid "Accesslog" msgstr "" -#: src/components/admin/usermanagement/Usermanagement.vue:106 +#: src/components/admin/usermanagement/Usermanagement.vue:103 msgid "Add User" msgstr "" @@ -38,17 +38,17 @@ msgstr "" #: src/components/admin/Importqueue.vue:136 -#: src/components/admin/Systemconfiguration.vue:133 -#: src/components/admin/Systemconfiguration.vue:148 -#: src/components/admin/Systemconfiguration.vue:167 -#: src/components/admin/Systemconfiguration.vue:184 -#: src/components/admin/usermanagement/Userdetail.vue:296 -#: src/components/admin/usermanagement/Userdetail.vue:368 -#: src/components/admin/usermanagement/Usermanagement.vue:300 -#: src/components/admin/usermanagement/Usermanagement.vue:308 -#: src/components/admin/usermanagement/Usermanagement.vue:334 +#: src/components/admin/Systemconfiguration.vue:134 +#: src/components/admin/Systemconfiguration.vue:149 +#: src/components/admin/Systemconfiguration.vue:168 +#: src/components/admin/Systemconfiguration.vue:185 +#: src/components/admin/usermanagement/Userdetail.vue:305 +#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Usermanagement.vue:313 +#: src/components/admin/usermanagement/Usermanagement.vue:321 +#: src/components/admin/usermanagement/Usermanagement.vue:347 #: src/components/map/Search.vue:257 -#: src/components/map/contextbox/Bottlenecks.vue:252 +#: src/components/map/contextbox/Bottlenecks.vue:275 #: src/components/map/contextbox/ImportSoundingresults.vue:205 #: src/components/map/contextbox/ImportSoundingresults.vue:244 #: src/components/map/contextbox/ImportSoundingresults.vue:275 @@ -67,7 +67,7 @@ msgid "Bottleneck Areas stroke-color" msgstr "" -#: src/components/Sidebar.vue:22 +#: src/components/Sidebar.vue:27 #: src/components/map/contextbox/Bottlenecks.vue:4 msgid "Bottlenecks" msgstr "" @@ -93,7 +93,7 @@ msgid "Compare with" msgstr "" -#: src/components/Sidebar.vue:58 +#: src/components/Sidebar.vue:76 msgid "Configuration" msgstr "" @@ -102,11 +102,11 @@ msgid "Confirm" msgstr "" -#: src/components/map/fairway/Profiles.vue:374 +#: src/components/map/fairway/Profiles.vue:378 msgid "Coordinates copied to clipboard!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:34 +#: src/components/admin/usermanagement/Userdetail.vue:33 msgid "Country" msgstr "" @@ -128,13 +128,18 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:37 +#: src/components/admin/importschedule/Importscheduledetail.vue:80 msgid "Email" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:60 +#: src/components/admin/usermanagement/Userdetail.vue:59 msgid "Email address" msgstr "" +#: src/components/admin/importschedule/Importscheduledetail.vue:61 +msgid "Email Notification" +msgstr "" + #: src/components/admin/Importqueue.vue:53 msgid "Enqueued" msgstr "" @@ -155,7 +160,7 @@ msgid "Enter username" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:377 +#: src/components/admin/usermanagement/Userdetail.vue:386 msgid "Error while saving user" msgstr "" @@ -184,7 +189,7 @@ msgid "Import" msgstr "" -#: src/components/Sidebar.vue:31 +#: src/components/Sidebar.vue:40 msgid "Import soundingresults" msgstr "" @@ -196,20 +201,28 @@ msgid "Imported" msgstr "" -#: src/components/Sidebar.vue:66 src/components/admin/Importqueue.vue:9 +#: src/components/Sidebar.vue:92 src/components/admin/Importqueue.vue:9 msgid "Importqueue" msgstr "" -#: src/components/Sidebar.vue:70 +#: src/components/admin/importschedule/Importscheduledetail.vue:20 +msgid "Imports" +msgstr "" + +#: src/components/Sidebar.vue:100 #: src/components/admin/importschedule/Importschedule.vue:9 msgid "Importschedule" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:345 +#: src/components/admin/importschedule/Importscheduledetail.vue:34 +msgid "Importtype" +msgstr "" + +#: src/components/admin/usermanagement/Userdetail.vue:354 msgid "invalid email" msgstr "" -#: src/components/map/fairway/Profiles.vue:408 +#: src/components/map/fairway/Profiles.vue:412 msgid "Invalid input" msgstr "" @@ -241,19 +254,19 @@ msgid "Login failed" msgstr "" -#: src/components/Sidebar.vue:76 +#: src/components/Sidebar.vue:110 msgid "Logout" msgstr "" -#: src/components/Sidebar.vue:62 src/components/admin/Logs.vue:9 +#: src/components/Sidebar.vue:84 src/components/admin/Logs.vue:9 msgid "Logs" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:141 +#: src/components/admin/usermanagement/Userdetail.vue:140 msgid "Mail was sent" msgstr "" -#: src/components/Sidebar.vue:14 +#: src/components/Sidebar.vue:15 msgid "Map" msgstr "" @@ -266,7 +279,7 @@ msgid "Name" msgstr "" -#: src/components/admin/importschedule/Importscheduledetail.vue:6 +#: src/components/admin/importschedule/Importscheduledetail.vue:9 msgid "New import" msgstr "" @@ -274,7 +287,7 @@ msgid "New Import" msgstr "" -#: src/components/map/Identify.vue:50 +#: src/components/map/Identify.vue:47 msgid "No features identified." msgstr "" @@ -291,15 +304,15 @@ msgid "Open in new window" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:229 +#: src/components/admin/usermanagement/Userdetail.vue:238 msgid "password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:227 +#: src/components/admin/usermanagement/Userdetail.vue:236 msgid "Password" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:230 +#: src/components/admin/usermanagement/Userdetail.vue:239 msgid "password again" msgstr "" @@ -307,12 +320,12 @@ msgid "Password reset requested!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:338 -#: src/components/admin/usermanagement/Userdetail.vue:339 +#: src/components/admin/usermanagement/Userdetail.vue:347 +#: src/components/admin/usermanagement/Userdetail.vue:348 msgid "Password should at least be 8 char long including 1 digit and 1 special char like $" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:334 +#: src/components/admin/usermanagement/Userdetail.vue:343 msgid "Passwords do not match!" msgstr "" @@ -320,11 +333,11 @@ msgid "Pending" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:323 +#: src/components/admin/usermanagement/Userdetail.vue:332 msgid "Please choose a country" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:328 +#: src/components/admin/usermanagement/Userdetail.vue:337 msgid "Please choose a role" msgstr "" @@ -340,8 +353,8 @@ msgid "Please enter a reference" msgstr "" -#: src/components/map/fairway/Profiles.vue:409 -#: src/components/map/fairway/Profiles.vue:410 +#: src/components/map/fairway/Profiles.vue:413 +#: src/components/map/fairway/Profiles.vue:414 msgid "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" msgstr "" @@ -349,8 +362,8 @@ msgid "Please select a bottleneck" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:41 -#: src/components/admin/usermanagement/Userdetail.vue:86 +#: src/components/admin/usermanagement/Userdetail.vue:40 +#: src/components/admin/usermanagement/Userdetail.vue:85 msgid "Please select one" msgstr "" @@ -358,11 +371,11 @@ msgid "portrait" msgstr "" -#: src/components/map/fairway/Profiles.vue:448 +#: src/components/map/fairway/Profiles.vue:452 msgid "Profile deleted!" msgstr "" -#: src/components/map/fairway/Profiles.vue:434 +#: src/components/map/fairway/Profiles.vue:438 msgid "Profile saved!" msgstr "" @@ -382,7 +395,7 @@ msgid "Rejected" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:228 +#: src/components/admin/usermanagement/Userdetail.vue:237 msgid "Repeat Password" msgstr "" @@ -390,7 +403,7 @@ msgid "Request password reset!" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:79 +#: src/components/admin/usermanagement/Userdetail.vue:78 msgid "Role" msgstr "" @@ -399,6 +412,7 @@ msgstr "" #: src/components/admin/importschedule/Importschedule.vue:36 +#: src/components/admin/importschedule/Importscheduledetail.vue:48 msgid "Schedule" msgstr "" @@ -410,7 +424,7 @@ msgid "Send" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:139 +#: src/components/admin/usermanagement/Userdetail.vue:138 msgid "Send testmail" msgstr "" @@ -418,7 +432,7 @@ msgid "Signer" msgstr "" -#: src/components/map/Identify.vue:63 +#: src/components/map/Identify.vue:60 msgid "" "Some data ©\n" " <a href=\"https://www.openstreetmap.org/copyright\">%{ name }</a>\n" @@ -429,11 +443,11 @@ msgid "Sounding Result" msgstr "" -#: src/components/map/Identify.vue:60 +#: src/components/map/Identify.vue:57 msgid "source-code" msgstr "" -#: src/components/Sidebar.vue:44 +#: src/components/Sidebar.vue:54 msgid "Staging area" msgstr "" @@ -453,11 +467,12 @@ msgid "State" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:130 +#: src/components/admin/importschedule/Importscheduledetail.vue:85 +#: src/components/admin/usermanagement/Userdetail.vue:129 msgid "Submit" msgstr "" -#: src/components/map/fairway/Profiles.vue:373 +#: src/components/map/fairway/Profiles.vue:377 msgid "Success" msgstr "" @@ -465,11 +480,11 @@ msgid "Successful" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:89 +#: src/components/admin/usermanagement/Userdetail.vue:88 msgid "Sysadmin" msgstr "" -#: src/components/Sidebar.vue:47 +#: src/components/Sidebar.vue:57 msgid "Systemadministration" msgstr "" @@ -477,7 +492,7 @@ msgid "Systemconfiguration" msgstr "" -#: src/components/map/Identify.vue:54 +#: src/components/map/Identify.vue:51 msgid "" "This app uses <i>gemma</i>, which is Free Software under <br/>\n" " %{ license } without warranty, see docs for details." @@ -496,31 +511,40 @@ msgid "User" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:15 +#: src/components/admin/usermanagement/Userdetail.vue:14 #: src/components/map/contextbox/Staging.vue:16 msgid "Username" msgstr "" -#: src/components/Sidebar.vue:52 +#: src/components/Sidebar.vue:66 #: src/components/admin/usermanagement/Usermanagement.vue:14 msgid "Users" msgstr "" -#: src/components/map/Identify.vue:68 +#: src/components/map/Identify.vue:65 msgid "" "Uses\n" -" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a> under %{ geoLicense }." +" <a href=\"https://download.geonames.org/export/dump/readme.txt\">GeoNames</a>\n" +" under %{ geoLicense }." msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:92 +#: src/components/admin/usermanagement/Userdetail.vue:91 msgid "Waterway Admin" msgstr "" -#: src/components/admin/usermanagement/Userdetail.vue:95 +#: src/components/admin/usermanagement/Userdetail.vue:94 msgid "Waterway User" msgstr "" -#: src/components/map/fairway/Profiles.vue:435 -#: src/components/map/fairway/Profiles.vue:436 +#: src/components/map/fairway/Profiles.vue:439 +#: src/components/map/fairway/Profiles.vue:440 msgid "You can now select these coordinates from the \"Saved cross profiles\" menu to restore this cross profile." msgstr "" + +#: src/store/map.js:415 +msgid "Length" +msgstr "" + +#: src/store/map.js:436 +msgid "Area" +msgstr ""
--- a/client/src/locale/translations.json Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/locale/translations.json Sat Dec 29 16:07:40 2018 +0100 @@ -1,1 +1,1 @@ -{"bg_BG":{},"de_AT":{"Accepted":"Akzeptiert","Accesslog":"Zugriffs-Protokoll","Add User":"Benutzer hinzufügen","Author":"Autor","back to login":"zurück zur Anmeldung","Backend Error":"Server-Fehler","Bottleneck":"Seichtstelle","Bottleneck Areas fill-color":"Flächenfüllfarbe Seichtstelle","Bottleneck Areas stroke-color":"Flächenumrandungsfarbe Seichtstelle","Bottlenecks":"Seichtstellen","Cancel Upload":"Hochladen abbrechen","Chainage":"Verkettung","choose .zip- file":"Wählen Sie eine .zip Datei","Chose format:":"Format wählen:","Compare with":"Vergleiche mit","Configuration":"Konfiguration","Confirm":"Bestätigen","Coordinates copied to clipboard!":"Koordinaten auf die Zwischenablage kopiert!","Country":"Land","Date":"Datum","Depthreference":"Tiefenreferenz","Download":"Herunterladen","Download Meta.json":"Meta.json Herunterladen","Email":"E-Mail","Email address":"E-Mail Adresse","Enqueued":"Hinzugefügt","Enter coordinates manually":"Manuelle Koordinateneingabe","Enter label for cross profile":"Namen für Profilschnitt eingeben","Enter passphrase":"Passphrase eingeben","Enter username":"Benutzername eingeben","Error while saving user":"Während des Speicherns der Nutzerdaten trat ein Fehler auf","Errorlog":"Fehlerprotokoll","Failed":"Fehlgeschlagen","Forgot password":"Passwort vergessen","Generate PDF":"PDF generieren","Identified":"Identifiziert","Import":"Daten-Import","Import soundingresults":"Seichtstellenmessungen importieren","Import Soundingresults":"Seichtstellenmessungen importieren","Imported":"Importiert","Importqueue":"Import-Warteschlange","Importschedule":"Import-Zeitplan","invalid email":"Ungültige E-Mail","Invalid input":"Ungültige Eingabe","Kind":"Art","landscape":"Querformat","Last refresh:":"Letzter Abgleich:","Latest":"Neustes","Layers":"Ebenen","Login":"Login","Login failed":"Login fehlgeschlagen","Logout":"Abmelden","Logs":"Protokolle","Mail was sent":"E-Mail wurde gesendet","Map":"Karte","Measurement":"Messung","Name":"Name","New import":"Neuer Import","New Import":"Neuer Import","No features identified.":"Keine Objekte identifiziert.","No results.":"Keine Ergebnisse.","No schedules":"Keine Pläne","Open in new window":"In neuem Fenster öffnen","password":"Passwort","Password":"Passwort","password again":"Noch einmal das Passwort","Password reset requested!":"Passwort Zurücksetzung angefragt!","Password should at least be 8 char long including 1 digit and 1 special char like $":"Das Passwort sollte mindestens 8 Zeichen lang sein, eine Zahlenziffer und ein Sonderzeichen wie etwa $ enthalten","Passwords do not match!":"Die Passwörter stimmen nicht überein!","Pending":"Ausstehend","Please choose a country":"Bitte wählen Sie ein Land aus","Please choose a role":"Bitte wählen Sie eine Rolle aus","Please enter a date":"Bitte ein Datum eingeben","Please enter a projection":"Bitte eine Projektion eingeben","Please enter a reference":"Bitte ein Höhenreferenzsystem eingeben","Please enter correct coordinates in the format: Lat,Lon,Lat,Lon":"Bitte geben Sie die Koordinaten in folgendem Format an: Lat,Lon,Lat,Lon","Please select a bottleneck":"Bitte eine Seichtstelle wählen","Please select one":"Bitte auswählen","portrait":"Hochformat","Profile deleted!":"Profil gelöscht!","Profile saved!":"Profil gespeichert!","Profiles":"Profile","Projection":"Projektion","Refresh":"Aktualisieren","Rejected":"Abgelehnt","Repeat Password":"Passwort erneut eingeben","Request password reset!":"Passwort-Zurücksetzung anfragen!","Role":"Rolle","Saved cross profiles":"Gespeicherte Profile","Schedule":"Zeitplan","Select Bottleneck":"Wähle Engstelle","Send":"Absenden","Send testmail":"Test-E-Mail versenden","Sounding Result":"Seichtstellenvermessungsergebnisse","source-code":"Quelltext","Staging area":"Import-Überprüfung","Staging Area":"Import-Überprüfung","Start":"Start","Starting import for ":"Import gestartet ","State":"Zustand","Submit":"Abschicken","Success":"Erfolg","Successful":"Erfolgreich","Sysadmin":"Sys-Admin","Systemadministration":"System-Administration","Systemconfiguration":"System-Konfiguation","Type":"Typ","Upload":"Hochladen","User":"Benutzer","Username":"Benutzername","Users":"Benutzer","Waterway Admin":"Waterway-Admin","Waterway User":"Waterway-Benutzer","You can now select these coordinates from the \"Saved cross profiles\" menu to restore this cross profile.":"Sie können diese Koordinaten aus dem \"Gespeicherte Profile\"-Menü auswählen, um diesen Profilschnitt wieder herzustellen."},"en_GB":{},"hr_HR":{},"hu_HU":{},"ro_RO":{},"sk_SK":{}} \ No newline at end of file +{"bg_BG":{},"de_AT":{"Accepted":"Akzeptiert","Accesslog":"Zugriffs-Protokoll","Add User":"Benutzer hinzufügen","Author":"Autor","back to login":"zurück zur Anmeldung","Backend Error":"Server-Fehler","Bottleneck":"Seichtstelle","Bottleneck Areas fill-color":"Flächenfüllfarbe Seichtstelle","Bottleneck Areas stroke-color":"Flächenumrandungsfarbe Seichtstelle","Bottlenecks":"Seichtstellen","Cancel Upload":"Hochladen abbrechen","Chainage":"Stationierung","choose .zip- file":"Wählen Sie eine .zip Datei","Chose format:":"Format wählen:","Compare with":"Vergleiche mit","Configuration":"Konfiguration","Confirm":"Bestätigen","Coordinates copied to clipboard!":"Koordinaten auf die Zwischenablage kopiert!","Country":"Land","Date":"Datum","Depthreference":"Tiefenreferenz","Download":"Herunterladen","Download Meta.json":"Meta.json Herunterladen","Email":"E-Mail","Email address":"E-Mail Adresse","Enqueued":"Hinzugefügt","Enter coordinates manually":"Manuelle Koordinateneingabe","Enter label for cross profile":"Namen für Profilschnitt eingeben","Enter passphrase":"Passphrase eingeben","Enter username":"Benutzername eingeben","Error while saving user":"Während des Speicherns der Nutzerdaten trat ein Fehler auf","Errorlog":"Fehlerprotokoll","Failed":"Fehlgeschlagen","Forgot password":"Passwort vergessen","Generate PDF":"PDF generieren","Identified":"Identifiziert","Import":"Daten-Import","Import soundingresults":"Seichtstellenmessungen importieren","Import Soundingresults":"Seichtstellenmessungen importieren","Imported":"Importiert","Importqueue":"Import-Warteschlange","Importschedule":"Import-Zeitplan","invalid email":"Ungültige E-Mail","Invalid input":"Ungültige Eingabe","Kind":"Art","landscape":"Querformat","Last refresh:":"Letzter Abgleich:","Latest":"Neuste","Layers":"Ebenen","Login":"Login","Login failed":"Login fehlgeschlagen","Logout":"Abmelden","Logs":"Protokolle","Mail was sent":"E-Mail wurde gesendet","Map":"Karte","Measurement":"Messung","Name":"Name","New import":"Neuer Import","New Import":"Neuer Import","No features identified.":"Keine Objekte identifiziert.","No results.":"Keine Ergebnisse.","No schedules":"Keine Pläne","Open in new window":"In neuem Fenster öffnen","password":"Passwort","Password":"Passwort","password again":"Noch einmal das Passwort","Password reset requested!":"Passwort Zurücksetzung angefragt!","Password should at least be 8 char long including 1 digit and 1 special char like $":"Das Passwort sollte mindestens 8 Zeichen lang sein, eine Zahlenziffer und ein Sonderzeichen wie etwa $ enthalten","Passwords do not match!":"Die Passwörter stimmen nicht überein!","Pending":"Ausstehend","Please choose a country":"Bitte wählen Sie ein Land aus","Please choose a role":"Bitte wählen Sie eine Rolle aus","Please enter a date":"Bitte ein Datum eingeben","Please enter a projection":"Bitte eine Projektion eingeben","Please enter a reference":"Bitte ein Höhenreferenzsystem eingeben","Please enter correct coordinates in the format: Lat,Lon,Lat,Lon":"Bitte geben Sie die Koordinaten in folgendem Format an: Lat,Lon,Lat,Lon","Please select a bottleneck":"Bitte eine Seichtstelle wählen","Please select one":"Bitte auswählen","portrait":"Hochformat","Profile deleted!":"Profil gelöscht!","Profile saved!":"Profil gespeichert!","Profiles":"Profile","Projection":"Projektion","Refresh":"Aktualisieren","Rejected":"Abgelehnt","Repeat Password":"Passwort erneut eingeben","Request password reset!":"Passwort-Zurücksetzung anfragen!","Role":"Rolle","Saved cross profiles":"Gespeicherte Profile","Schedule":"Zeitplan","Select Bottleneck":"Wähle Seichtstelle","Send":"Absenden","Send testmail":"Test-E-Mail versenden","Sounding Result":"Seichtstellenvermessungsergebnisse","source-code":"Quelltext","Staging area":"Import-Überprüfung","Staging Area":"Import-Überprüfung","Start":"Start","Starting import for ":"Import gestartet ","State":"Zustand","Submit":"Abschicken","Success":"Erfolg","Successful":"Erfolgreich","Sysadmin":"Sys-Admin","Systemadministration":"System-Administration","Systemconfiguration":"System-Konfiguation","Type":"Typ","Upload":"Hochladen","User":"Benutzer","Username":"Benutzername","Users":"Benutzer","Waterway Admin":"Waterway-Admin","Waterway User":"Waterway-Benutzer","You can now select these coordinates from the \"Saved cross profiles\" menu to restore this cross profile.":"Sie können diese Koordinaten aus dem \"Gespeicherte Profile\"-Menü auswählen, um diesen Profilschnitt wieder herzustellen."},"en_GB":{},"hr_HR":{},"hu_HU":{},"ro_RO":{},"sk_SK":{}} \ No newline at end of file
--- a/client/src/main.js Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/main.js Sat Dec 29 16:07:40 2018 +0100 @@ -26,102 +26,11 @@ import "../node_modules/ol/ol.css"; import "../node_modules/highlight.js/styles/paraiso-dark.css"; import "../node_modules/vue-snotify/styles/material.css"; -import VTooltip from "v-tooltip"; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { - faAngleDown, - faAngleLeft, - faAngleRight, - faAngleUp, - faBars, - faBook, - faChartArea, - faCheck, - faCity, - faClipboardCheck, - faClock, - faCopy, - faDrawPolygon, - faExclamationTriangle, - faEye, - faEyeSlash, - faFilePdf, - faFolderPlus, - faInfo, - faLayerGroup, - faMapMarkedAlt, - faMinus, - faPaperPlane, - faPencilAlt, - faPlus, - faPowerOff, - faRuler, - faSearch, - faShip, - faSortAmountDown, - faSortAmountUp, - faSpinner, - faStar, - faTasks, - faTimes, - faTrash, - faUpload, - faUser, - faUsersCog, - faWater, - faWrench -} from "@fortawesome/free-solid-svg-icons"; -import { faAdn } from "@fortawesome/free-brands-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import VueClipboard from "vue-clipboard2"; -library.add( - faAdn, - faAngleDown, - faAngleLeft, - faAngleRight, - faAngleUp, - faBars, - faBook, - faChartArea, - faCheck, - faCity, - faClipboardCheck, - faClock, - faCopy, - faDrawPolygon, - faExclamationTriangle, - faEye, - faEyeSlash, - faFilePdf, - faFolderPlus, - faInfo, - faLayerGroup, - faMapMarkedAlt, - faMinus, - faPaperPlane, - faPencilAlt, - faPlus, - faPowerOff, - faRuler, - faSearch, - faShip, - faSortAmountDown, - faSortAmountUp, - faSpinner, - faStar, - faTasks, - faTimes, - faTrash, - faUpload, - faUser, - faUsersCog, - faWater, - faWrench -); -Vue.component("font-awesome-icon", FontAwesomeIcon); +import ToggleButton from "vue-js-toggle-button"; -Vue.use(VTooltip); +Vue.use(ToggleButton); const options = { toast: { @@ -135,50 +44,29 @@ let browserLanguage = locale2; -// planned also SK, HU, HR, RS, BiH, BG, RO, UA +// planned also RS, BiH, UA const supportedLanguages = { en_GB: "British English", de_AT: "Deutsch", sk_SK: "slovenčina", - hu_HU: "Magyat", + hu_HU: "Magyar", hr_HR: "Hrvatska", bg_BG: "български", ro_RO: "Română" }; -if ( - browserLanguage === "de-DE" || - browserLanguage === "de-LI" || - browserLanguage === "de-LU" || - browserLanguage === "de-CH" || - browserLanguage === "de" -) { - browserLanguage = "de-AT"; // map german,liechtenstein,luxenburg and switzerland to austrian variant for now -} +let isAvailableLanguage = Object.keys(supportedLanguages).filter(language => { + return browserLanguage.replace("-", "_") === language; +}); -if (browserLanguage === "sk") { - browserLanguage = "sk_SK"; +if (isAvailableLanguage.length === 0) { + isAvailableLanguage = Object.keys(supportedLanguages).filter(language => { + return language.substr(0, 2) === browserLanguage.substr(0, 2); + }); } -if (browserLanguage === "hu") { - browserLanguage = "hu_HU"; -} - -if (browserLanguage === "hr") { - browserLanguage = "hr_HR"; -} - -if (browserLanguage === "bg") { - browserLanguage = "bg_BG"; -} - -if (browserLanguage === "ro") { - browserLanguage = "ro_RO"; -} - -const language = browserLanguage.replace("-", "_"); -const isLanguageAvailable = supportedLanguages[language]; -let defaultLanguage = isLanguageAvailable ? language : "en_GB"; +let defaultLanguage = + isAvailableLanguage.length > 0 ? isAvailableLanguage[0] : "en_GB"; Vue.use(GetTextPlugin, { translations: translations, @@ -187,11 +75,13 @@ }); Vue.config.productionTip = false; - -const app = new Vue({ - router, - store, - render: h => h(App) -}).$mount("#app"); +let app; +import("./fontawesome").then(() => { + app = new Vue({ + router, + store, + render: h => h(App) + }).$mount("#app"); +}); export default app;
--- a/client/src/router.js Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/router.js Sat Dec 29 16:07:40 2018 +0100 @@ -20,15 +20,6 @@ /* facilitate codesplitting */ const Login = () => import("./components/Login.vue"); -const Main = () => import("./components/map/Main.vue"); -const Usermanagement = () => - import("./components/admin/usermanagement/Usermanagement.vue"); -const Logs = () => import("./components/admin/Logs.vue"); -const Importqueue = () => import("./components/admin/Importqueue.vue"); -const Importschedule = () => - import("./components/admin/importschedule/Importschedule.vue"); -const Systemconfiguration = () => - import("./components/admin/Systemconfiguration.vue"); Vue.use(Router); @@ -42,7 +33,7 @@ { path: "/usermanagement", name: "usermanagement", - component: Usermanagement, + component: () => import("./components/usermanagement/Usermanagement.vue"), meta: { requiresAuth: true }, @@ -58,7 +49,7 @@ { path: "/logs", name: "logs", - component: Logs, + component: () => import("./components/Logs.vue"), meta: { requiresAuth: true }, @@ -74,7 +65,7 @@ { path: "/systemconfiguration", name: "systemconfiguration", - component: Systemconfiguration, + component: () => import("./components/Systemconfiguration.vue"), meta: { requiresAuth: true }, @@ -90,7 +81,39 @@ { path: "/importqueue", name: "importqueue", - component: Importqueue, + component: () => import("./components/importqueue/Importqueue.vue"), + meta: { + requiresAuth: true + }, + beforeEnter: (to, from, next) => { + const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; + if (!isWaterwayAdmin) { + next("/"); + } else { + next(); + } + } + }, + { + path: "/importsoundingresults", + name: "importsoundingresults", + component: () => import("./components/ImportSoundingresults.vue"), + meta: { + requiresAuth: true + }, + beforeEnter: (to, from, next) => { + const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; + if (!isWaterwayAdmin) { + next("/"); + } else { + next(); + } + } + }, + { + path: "/importstretches", + name: "importstretches", + component: () => import("./components/ImportStretches.vue"), meta: { requiresAuth: true }, @@ -106,7 +129,7 @@ { path: "/importschedule", name: "importschedule", - component: Importschedule, + component: () => import("./components/importschedule/Importschedule.vue"), meta: { requiresAuth: true }, @@ -122,7 +145,7 @@ { path: "/", name: "mainview", - component: Main, + component: () => import("./components/Main.vue"), meta: { requiresAuth: true },
--- a/client/src/store/application.js Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/store/application.js Sat Dec 29 16:07:40 2018 +0100 @@ -55,7 +55,10 @@ state.version.includes("beta") || state.version.includes("alpha")) ) - versionStr += " " + process.env.VUE_APP_HGREV; + // a '+' according to semver 2.0.0 starts a build meta info section + // which shall only have [0-9A-Za-z-] chars + // and is to be ignored when determining the order + versionStr += "+" + process.env.VUE_APP_HGREV; return versionStr; }
--- a/client/src/store/bottlenecks.js Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/store/bottlenecks.js Sat Dec 29 16:07:40 2018 +0100 @@ -12,9 +12,9 @@ * Markus Kottländer <markuks.kottlaender@intevation.de> * Thomas Junk <thomas.junk@intevation.de> */ -import { HTTP } from "../lib/http"; +import { HTTP } from "@/lib/http"; import { WFS } from "ol/format.js"; -import { displayError } from "../lib/errors.js"; +import { displayError } from "@/lib/errors.js"; // initial state const init = () => { @@ -41,6 +41,13 @@ setSurveys(state, surveys) { state.surveys = surveys; }, + setSelectedSurveyByDate(state, date) { + const survey = state.surveys.filter(x => x.date_info === date)[0]; + state.selectedSurvey = survey; + }, + setFirstSurveySelected(state) { + state.selectedSurvey = state.surveys[0]; + }, selectedSurvey(state, survey) { state.selectedSurvey = survey; }, @@ -49,83 +56,80 @@ } }, actions: { - setSelectedBottleneck( - { state, commit, dispatch, rootState, rootGetters }, - name, - date - ) { - if (name !== state.selectedBottleneck) { - commit("selectedSurvey", null); - commit("fairwayprofile/clearCurrentProfile", null, { root: true }); - commit("application/showSplitscreen", false, { root: true }); - rootState.map.cutTool.setActive(false); - rootGetters["map/getVSourceByName"]("Cut Tool").clear(); - } - if (name) { - commit("application/showProfiles", true, { root: true }); - } - commit("setSelectedBottleneck", name); - dispatch("querySurveys", name, date); + setSelectedBottleneck({ state, commit, rootState, rootGetters }, name) { + return new Promise((resolve, reject) => { + if (name !== state.selectedBottleneck) { + commit("selectedSurvey", null); + commit("fairwayprofile/clearCurrentProfile", null, { root: true }); + commit("application/showSplitscreen", false, { root: true }); + rootState.map.cutTool.setActive(false); + rootGetters["map/getVSourceByName"]("Cut Tool").clear(); + } + if (name) { + commit("application/showProfiles", true, { root: true }); + } + commit("setSelectedBottleneck", name); + if (name) { + commit("surveysLoading", true); + HTTP.get("/surveys/" + name, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + const surveys = response.data.surveys.sort((a, b) => + a.date_info < b.date_info ? 1 : -1 + ); + commit("setSurveys", surveys); + resolve(response); + }) + .catch(error => { + commit("setSurveys", []); + commit("selectedSurvey", null); + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + reject(error); + }) + .finally(() => commit("surveysLoading", false)); + } else { + commit("setSurveys", []); + resolve(); + } + }); }, loadBottlenecks({ commit }) { - var bottleneckFeatureCollectionRequest = new WFS().writeGetFeature({ - srsName: "EPSG:4326", - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["bottleneck_overview"], - outputFormat: "application/json" - }); - - HTTP.post( - "/internal/wfs", - new XMLSerializer().serializeToString( - bottleneckFeatureCollectionRequest - ), - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" + return new Promise((resolve, reject) => { + var bottleneckFeatureCollectionRequest = new WFS().writeGetFeature({ + srsName: "EPSG:4326", + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["bottleneck_overview"], + outputFormat: "application/json" + }); + HTTP.post( + "/internal/wfs", + new XMLSerializer().serializeToString( + bottleneckFeatureCollectionRequest + ), + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } } - } - ).then(response => { - commit("setBottlenecks", response.data.features); - }); - }, - querySurveys({ commit }, name, date) { - if (name) { - commit("surveysLoading", true); - HTTP.get("/surveys/" + name, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - }) + ) .then(response => { - const surveys = response.data.surveys.sort((a, b) => - a.date_info < b.date_info ? 1 : -1 - ); - if (date) { - const survey = surveys.filter(x => x.date_info === date)[0]; - commit("selectedSurvey", survey); - } else { - commit("selectedSurvey", surveys[0]); - } - - commit("setSurveys", surveys); + commit("setBottlenecks", response.data.features); + resolve(response); }) .catch(error => { - commit("setSurveys", []); - commit("selectedSurvey", null); - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }) - .finally(() => commit("surveysLoading", false)); - } else { - commit("setSurveys", []); - } + reject(error); + }); + }); } } };
--- a/client/src/store/imports.js Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/store/imports.js Sat Dec 29 16:07:40 2018 +0100 @@ -33,18 +33,8 @@ imports: [], staging: [], schedules: [], - importScheduleDetail: null, - importScheduleDetailVisible: false - }; -}; - -const newImportScheduleDetail = () => { - return { - import: "", - type: "", - author: "", - schedule: "", - emailNotification: null + importScheduleDetailVisible: false, + importToReview: null }; }; @@ -53,9 +43,6 @@ namespaced: true, state: init(), mutations: { - clearImportScheduleDetail: state => { - state.importScheduleDetail = newImportScheduleDetail(); - }, deleteSchedule: (state, index) => { state.schedules.splice(index, 1); }, @@ -77,7 +64,11 @@ }); state.staging = enriched; }, - + setImportToReview: (state, id) => { + if (!isNaN(parseFloat(id)) && isFinite(id)) { + state.importToReview = id; + } + }, toggleApproval: (state, change) => { const { id, newStatus } = change; const stagedResult = state.staging.find(e => {
--- a/client/src/store/map.js Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/store/map.js Sat Dec 29 16:07:40 2018 +0100 @@ -28,6 +28,7 @@ import { getLength, getArea } from "ol/sphere.js"; import { unByKey } from "ol/Observable"; import { getCenter } from "ol/extent"; +import app from "../main"; // initial state const init = () => { @@ -58,7 +59,7 @@ data: new TileLayer({ source: new TileWMS({ preload: 1, - url: "https://demo.d4d-portal.info/wms", + url: "https://service.d4d-portal.info/wms/", params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true } }) }), @@ -411,7 +412,7 @@ }); lineTool.on("drawend", event => { commit("setCurrentMeasurement", { - quantity: "Length", + quantity: app.$gettext("Length"), unitSymbol: "m", value: Math.round(getLength(event.feature.getGeometry()) * 10) / 10 }); @@ -432,7 +433,7 @@ polygonTool.on("drawend", event => { const areaSize = getArea(event.feature.getGeometry()); commit("setCurrentMeasurement", { - quantity: "Area", + quantity: app.$gettext("Area"), unitSymbol: areaSize > 100000 ? "km²" : "m²", value: areaSize > 100000 @@ -512,7 +513,9 @@ "bottlenecks/setSelectedBottleneck", feature.get("objnam"), { root: true } - ); + ).then(() => { + this.commit("bottlenecks/setFirstSurveySelected"); + }); commit("moveMap", { coordinates: getCenter( feature
--- a/client/src/store/user.js Sat Dec 29 16:06:54 2018 +0100 +++ b/client/src/store/user.js Sat Dec 29 16:07:40 2018 +0100 @@ -14,6 +14,7 @@ */ import { HTTP } from "../lib/http"; +import { toMillisFromString } from "../lib/session"; const init = () => { return { @@ -71,11 +72,21 @@ login({ commit }, user) { // using POST is a bit more secure than GET return new Promise((resolve, reject) => { + const handleResponse = response => { + const { expires } = response.data; + const renew = + (new Date(toMillisFromString(expires)) - new Date()) / 1010; + commit("authSuccess", response.data); + resolve(response); + setTimeout(() => { + HTTP.get("/renew", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }).then(handleResponse); + }, renew); + }; + HTTP.post("/login", user) - .then(response => { - commit("authSuccess", response.data); - resolve(response); - }) + .then(handleResponse) .catch(error => { commit("reset", null, { root: true }); commit("clearAuth");
--- a/client/vue.config.js Sat Dec 29 16:06:54 2018 +0100 +++ b/client/vue.config.js Sat Dec 29 16:07:40 2018 +0100 @@ -1,7 +1,9 @@ const CopyWebpackPlugin = require("copy-webpack-plugin"); - module.exports = { outputDir: "../web", + configureWebpack: { + devtool: "source-map" + }, chainWebpack: config => { let vendorImgPath = process.env.VUE_APP_VENDOR_IMG_PATH; if (!vendorImgPath) return; @@ -10,6 +12,11 @@ .use(CopyWebpackPlugin, [[{ from: vendorImgPath, to: "img" }]], { copyUnmodified: true }); + if (process.env.ANALYZE) { + var BundleAnalyzerPlugin = require("webpack-bundle-analyzer") + .BundleAnalyzerPlugin; + config.plugin("BundleAnalyzerPlugin").use(BundleAnalyzerPlugin, []); + } }, css: { loaderOptions: {
--- a/client/yarn.lock Sat Dec 29 16:06:54 2018 +0100 +++ b/client/yarn.lock Sat Dec 29 16:07:40 2018 +0100 @@ -710,7 +710,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== -"@turf/bbox@*": +"@turf/bbox@*", "@turf/bbox@6.x": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.0.1.tgz#b966075771475940ee1c16be2a12cf389e6e923a" integrity sha512-EGgaRLettBG25Iyx7VyUINsPpVj1x3nFQFiGS3ER8KCI1MximzNLsam3eXRabqQDjyAKyAE1bJ4EZEpGvspQxw== @@ -718,6 +718,14 @@ "@turf/helpers" "6.x" "@turf/meta" "6.x" +"@turf/center@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/center/-/center-6.0.1.tgz#40a17d0a170df5bba09ad93e133b904d8eb14601" + integrity sha512-bh/SLBwRC2QYcbVOxMFBtiARuMzMzfh4YuVtguYAjyBEIA4HXnnEZT+yZlzfcG3oikG7XgV8vg9eegcmwQe+MQ== + dependencies: + "@turf/bbox" "6.x" + "@turf/helpers" "6.x" + "@turf/distance@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-6.0.1.tgz#0761f28784286e7865a427c4e7e3593569c2dea8" @@ -8555,7 +8563,7 @@ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281" integrity sha512-KIU72UmYPGk4MujZGYMFwinB7lOf2LsDNGSOC8ufevsrPLISrZbNJlWstRi3m0AMuszbH+EFSQ/r6w56RSPK6w== -prettier@^1.13.0, prettier@^1.15.2: +prettier@^1.13.0, prettier@^1.15.2, prettier@^1.15.3: version "1.15.3" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.3.tgz#1feaac5bdd181237b54dbe65d874e02a1472786a" integrity sha512-gAU9AGAPMaKb3NNSUUuhhFAS7SCO4ALTN4nRIn6PJ075Qd28Yn2Ig2ahEJWdJwJmlEBTUfC7mMUSFy8MwsOCfg== @@ -8576,7 +8584,7 @@ ansi-regex "^3.0.0" ansi-styles "^3.2.0" -pretty-quick@^1.6.0: +pretty-quick@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-1.8.0.tgz#067ebe744ddb4e1ed4e1ee1af9648815121f78fc" integrity sha512-qV25sQF/ivJpdZ5efwemQYkQJa7sp3HqT/Vf/7z5vGYMcq1VrT2lDpFKAxJPf6219N1YAdR8mGkIhPAZ1odTmQ== @@ -10768,6 +10776,11 @@ tsconfig "^7.0.0" vue-template-es2015-compiler "^1.6.0" +vue-js-toggle-button@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/vue-js-toggle-button/-/vue-js-toggle-button-1.3.0.tgz#59240f215fd502f54f0c210c5fac878960b0131c" + integrity sha512-lnRy+D7gHlvEyv1WKnvYWkxx42obCmeST5eAUwCnyIS+dC1l9Cu4AuWfw9XrRdNRpY7UZgu2TblZcOOF1H661A== + vue-loader@^15.4.2: version "15.4.2" resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.4.2.tgz#812bb26e447dd3b84c485eb634190d914ce125e2"
--- a/cmd/bottlenecks/main.go Sat Dec 29 16:06:54 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,269 +0,0 @@ -// This is Free Software under GNU Affero General Public License v >= 3.0 -// without warranty, see README.md and license for details. -// -// SPDX-License-Identifier: AGPL-3.0-or-later -// License-Filename: LICENSES/AGPL-3.0.txt -// -// Copyright (C) 2018 by via donau -// – Österreichische Wasserstraßen-Gesellschaft mbH -// Software engineering by Intevation GmbH -// -// Author(s): -// * Sascha L. Teichmann <sascha.teichmann@intevation.de> - -package main - -import ( - "database/sql" - "flag" - "fmt" - "log" - "regexp" - "strconv" - "strings" - - "github.com/jackc/pgx" - "github.com/jackc/pgx/stdlib" - - "gemma.intevation.de/gemma/pkg/soap/ifbn" -) - -const insertSQL = `INSERT INTO waterway.bottlenecks ( - bottleneck_id, - fk_g_fid, - objnam, - nobjnm, - stretch, - area, - rb, - lb, - responsible_country, - revisiting_time, - limiting, - date_info, - source_organization -) VALUES( - $1, - isrs_fromText($2), - $3, - $4, - isrsrange(isrs_fromText($5), isrs_fromText($6)), - ST_MakePolygon(ST_ExteriorRing(ST_Buffer(ST_SetSRID(ST_Makepoint(13.05501, 47.80949), 4326), 0.01)))::Geography, - $7, - $8, - $9, - $10, - $11, - $12, - $13 -) ON CONFLICT (bottleneck_id) DO NOTHING` - -const insertDumpSQL = `INSERT INTO waterway.bottlenecks ( - bottleneck_id, - fk_g_fid, - objnam, - nobjnm, - stretch, - area, - rb, - lb, - responsible_country, - revisiting_time, - limiting, - date_info, - source_organization -) VALUES( - %s, - isrs_fromText(%s), - %s, - %s, - isrsrange(isrs_fromText(%s), isrs_fromText(%s)), - ST_MakePolygon(ST_ExteriorRing(ST_Buffer(ST_SetSRID(ST_Makepoint(13.05501, 47.80949), 4326), 0.01)))::Geography, - %s, - %s, - %s, - %d, - %s, - %s::timestamp with time zone, - %s -) ON CONFLICT (bottleneck_id) DO NOTHING; -` - -var ( - url = flag.String("url", "", "the IFBN service") - dump = flag.Bool("dump", false, "dump SQL insert statements") - - insecure = flag.Bool("insecure", false, "skip SSL verification") - dbhost = flag.String("dbhost", "localhost", "database host") - dbport = flag.Uint("dbport", 5432, "database port") - dbname = flag.String("dbname", "gemma", "database user") - dbuser = flag.String("dbuser", "scott", "database user") - dbpassword = flag.String("dbpw", "tiger", "database password") - dbssl = flag.String("dbssl", "prefer", "database SSL mode") -) - -func run(fn func(*sql.DB) error) error { - - // To ease SSL config ride a bit on parsing. - cc, err := pgx.ParseConnectionString("sslmode=" + *dbssl) - if err != nil { - return err - } - - // Do the rest manually to allow whitespace in user/password. - cc.Host = *dbhost - cc.Port = uint16(*dbport) - cc.User = *dbuser - cc.Password = *dbpassword - cc.Database = *dbname - - db := stdlib.OpenDB(cc) - defer db.Close() - - return fn(db) -} - -var rblbRe = regexp.MustCompile(`(..)_(..)`) - -func splitRBLB(s string) (string, string) { - m := rblbRe.FindStringSubmatch(s) - if len(m) == 0 { - return "", "" - } - return m[1], m[2] -} - -func revisitingTime(s string) int { - v, err := strconv.Atoi(s) - if err != nil { - v = 0 - } - return v -} - -func quote(s string) string { - return "'" + strings.Replace(s, "'", "'''", -1) + "'" -} - -func dumpSQLStatements(bns []*ifbn.BottleNeckType) error { - - fmt.Println("BEGIN;") - - for i := range bns { - bn := bns[i] - rb, lb := splitRBLB(bn.Rb_lb) - - var limiting, country string - - if bn.Limiting_factor != nil { - limiting = string(*bn.Limiting_factor) - } - - if bn.Responsible_country != nil { - country = string(*bn.Responsible_country) - } - - if _, err := fmt.Printf(insertDumpSQL, - quote(bn.Bottleneck_id), - quote(bn.Fk_g_fid), - quote(bn.OBJNAM), - quote(bn.NOBJNM), - quote(bn.From_ISRS), quote(bn.To_ISRS), - quote(rb), - quote(lb), - quote(country), - revisitingTime(bn.Revisiting_time), - quote(limiting), - quote(bn.Date_Info.Format("2006-01-02 15:04:05.999 MST")), - quote(bn.Source), - ); err != nil { - return err - } - } - _, err := fmt.Println("COMMIT;") - return err -} - -func storeInDatabase(bns []*ifbn.BottleNeckType) error { - return run(func(db *sql.DB) error { - - stmt, err := db.Prepare(insertSQL) - if err != nil { - return err - } - defer stmt.Close() - - tx, err := db.Begin() - if err != nil { - return err - } - - st := tx.Stmt(stmt) - - for i := range bns { - bn := bns[i] - rb, lb := splitRBLB(bn.Rb_lb) - - var limiting, country string - - if bn.Limiting_factor != nil { - limiting = string(*bn.Limiting_factor) - } - - if bn.Responsible_country != nil { - country = string(*bn.Responsible_country) - } - - fmt.Printf("%s '%s' %s %s\n", bn.Fk_g_fid, bn.OBJNAM, bn.From_ISRS, bn.To_ISRS) - - if _, err := st.Exec( - bn.Bottleneck_id, - bn.Fk_g_fid, - bn.OBJNAM, - bn.NOBJNM, - bn.From_ISRS, bn.To_ISRS, - rb, - lb, - country, - revisitingTime(bn.Revisiting_time), - limiting, - bn.Date_Info, - bn.Source, - ); err != nil { - tx.Rollback() - return err - } - } - - return tx.Commit() - }) -} - -func main() { - - flag.Parse() - - client := ifbn.NewIBottleneckService(*url, *insecure, nil) - - req := &ifbn.Export_bn_by_isrs{} - - resp, err := client.Export_bn_by_isrs(req) - if err != nil { - log.Fatalf("error: %v\n", err) - } - - if resp.Export_bn_by_isrsResult == nil { - return - } - bns := resp.Export_bn_by_isrsResult.BottleNeckType - - if *dump { - err = dumpSQLStatements(bns) - } else { - err = storeInDatabase(bns) - } - - if err != nil { - log.Fatalf("error: %v\n", err) - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd/wfs/dump.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,187 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "log" + "os" + + "gemma.intevation.de/gemma/pkg/wfs" +) + +func parseFeatures(urls []string, defaultCRS string) error { + + return wfs.DownloadURLs(urls, func(r io.Reader) error { + rfc, err := wfs.ParseRawFeatureCollection(r) + if err != nil { + return err + } + var crsName string + if rfc.Features == nil { + return errors.New("no features given") + } + if rfc.CRS != nil { + crsName = rfc.CRS.Properties.Name + } else { + crsName = defaultCRS + } + fmt.Printf("CRS: %s\n", crsName) + epsg, err := wfs.CRSToEPSG(crsName) + if err != nil { + log.Printf("error: %v\n", err) + } else { + fmt.Printf("EPSG: %d\n", epsg) + } + types := map[string]int{} + for _, feature := range rfc.Features { + types[feature.Geometry.Type]++ + } + fmt.Printf("found types in %d features:\n", len(rfc.Features)) + for typ, cnt := range types { + fmt.Printf("\t%s: %d\n", typ, cnt) + } + return nil + }) +} + +func dumpURLs(urls []string) error { + out := bufio.NewWriter(os.Stdout) + if err := wfs.DownloadURLs(urls, func(r io.Reader) error { + _, err := io.Copy(out, r) + return err + }); err != nil { + return err + } + return out.Flush() +} + +func dump(caps *wfs.Capabilities) { + fmt.Println("service identification") + fmt.Println("----------------------") + fmt.Printf("title: %s\n", caps.ServiceIdentification.Title) + var abstract string + if len(caps.ServiceIdentification.Abstract) > 40 { + abstract = fmt.Sprintf("%.40s...", caps.ServiceIdentification.Abstract) + } else { + abstract = caps.ServiceIdentification.Abstract + } + fmt.Printf("abstract: %s\n", abstract) + if len(caps.ServiceIdentification.Keywords.Keywords) > 0 { + fmt.Println("keywords:") + for _, kw := range caps.ServiceIdentification.Keywords.Keywords { + fmt.Printf("\t%s\n", kw.Value) + } + } + fmt.Printf("type: %s\n", caps.ServiceIdentification.ServiceType) + fmt.Printf("version: %s\n", caps.ServiceIdentification.ServiceTypeVersion) + fmt.Println() + fmt.Println("operations meta data") + fmt.Println("--------------------") + if len(caps.OperationsMetadata.Operations) > 0 { + fmt.Println("operations:") + for _, operation := range caps.OperationsMetadata.Operations { + fmt.Printf("\t%s\n", operation.Name) + if operation.DCP.HTTP.Get != nil { + fmt.Printf("\t\tGet: %s\n", operation.DCP.HTTP.Get.HRef) + } + if operation.DCP.HTTP.Post != nil { + fmt.Printf("\t\tPost: %s\n", operation.DCP.HTTP.Post.HRef) + } + + if len(operation.Parameters) > 0 { + fmt.Println("\t\tparameters:") + for _, p := range operation.Parameters { + fmt.Printf("\t\t\tparameter: %s\n", p.Name) + for _, av := range p.AllowedValues.Values { + fmt.Printf("\t\t\t\t%s\n", av.Value) + } + } + } + if len(operation.Constraints) > 0 { + fmt.Println("\t\tconstraints:") + for _, c := range operation.Constraints { + fmt.Printf("\t\t\tname: %s\n", c.Name) + if c.DefaultValue != nil { + fmt.Printf("\t\t\t\tdefault: %s\n", c.DefaultValue.Value) + } + if len(c.AllowedValues.Values) > 0 { + fmt.Println("\t\t\tallowed values:") + for _, av := range c.AllowedValues.Values { + fmt.Printf("\t\t\t\t%s", av.Value) + } + } + } + } + } + } + if len(caps.OperationsMetadata.Constraints) > 0 { + fmt.Println("constraints:") + for _, c := range caps.OperationsMetadata.Constraints { + fmt.Printf("\tname: %s\n", c.Name) + if c.DefaultValue != nil { + fmt.Printf("\t\tdefault: %s\n", c.DefaultValue.Value) + } + if len(c.AllowedValues.Values) > 0 { + fmt.Println("\tallowed values:") + for _, av := range c.AllowedValues.Values { + fmt.Printf("\t\t%s\n", av.Value) + } + } + } + } + fmt.Println() + fmt.Println("feature type list") + fmt.Println("------------------") + if len(caps.FeatureTypeList.FeatureTypes) > 0 { + fmt.Println("features:") + for _, ft := range caps.FeatureTypeList.FeatureTypes { + fmt.Printf("\tname: %s\n", ft.Name) + fmt.Printf("\ttitle: %s\n", ft.Title) + var abstract string + if len(ft.Abstract) > 40 { + abstract = fmt.Sprintf("%.40s...", ft.Abstract) + } else { + abstract = ft.Abstract + } + fmt.Printf("\tabstract: %s\n", abstract) + fmt.Printf("\tdefault CRS: %s\n", ft.DefaultCRS) + if len(ft.OtherCRSs) > 0 { + fmt.Println("\tother CRSs:") + for _, crs := range ft.OtherCRSs { + fmt.Printf("\t\t%s\n", crs) + } + } + if ft.WGS84BoundingBox != nil { + fmt.Printf("\tWGS84 bounding box: (%s) - (%s)\n", + ft.WGS84BoundingBox.LowerCorner, ft.WGS84BoundingBox.UpperCorner) + } + if len(ft.Keywords.Keywords) > 0 { + fmt.Println("\tkeywords:") + for _, kw := range ft.Keywords.Keywords { + fmt.Printf("\t\t%s\n", kw.Value) + } + } + if len(ft.Namespaces) > 0 { + fmt.Println("\tnamespaces:") + for _, ns := range ft.Namespaces { + fmt.Printf("\t\t%s:%s\n", ns.Space, ns.Local) + } + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd/wfs/main.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,65 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package main + +import ( + "flag" + "log" + + "gemma.intevation.de/gemma/pkg/wfs" +) + +func check(err error) { + if err != nil { + log.Fatalf("error: %v\n", err) + } +} + +func main() { + var ( + dumpCaps = flag.Bool("dump-caps", false, "Dump capabilities document") + dumpFeatures = flag.Bool("dump-features", false, "Dump features") + featureType = flag.String("features", "", "feature to get") + sortBy = flag.String("sortby", "", "Sort features by this property") + ) + flag.Parse() + + for _, arg := range flag.Args() { + caps, err := wfs.GetCapabilities(arg) + check(err) + if *dumpCaps { + dump(caps) + } + + if *featureType == "" { + continue + } + + feature := caps.FindFeatureType(*featureType) + if feature == nil { + log.Fatalf("Unknown feature type '%s'\n", *featureType) + } + + urls, err := wfs.GetFeaturesGET( + caps, *featureType, "application/json", *sortBy) + check(err) + + log.Printf("urls: %v\n", urls) + if *dumpFeatures { + check(dumpURLs(urls)) + } + + parseFeatures(urls, feature.DefaultCRS) + } +}
--- a/docker/Dockerfile.backend Sat Dec 29 16:06:54 2018 +0100 +++ b/docker/Dockerfile.backend Sat Dec 29 16:07:40 2018 +0100 @@ -7,7 +7,8 @@ RUN apt-get update &&\ apt-get -y install --no-install-recommends \ - make git golang-go golang-github-gorilla-context-dev + make git golang-go \ + ca-certificates WORKDIR /opt/gemma
--- a/docker/Dockerfile.db Sat Dec 29 16:06:54 2018 +0100 +++ b/docker/Dockerfile.db Sat Dec 29 16:07:40 2018 +0100 @@ -3,14 +3,16 @@ LABEL description="Contains software from gemma, for right holders and\ licensing infos, see https://hg.intevation.de/gemma ." +ENV DEBIAN_FRONTEND noninteractive + RUN apt-get update &&\ - apt-get -y install --no-install-recommends curl gnupg + apt-get -y install --no-install-recommends curl ca-certificates gnupg # Add PostgreSQL's repository for current PostgreSQL release and extensions: RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main' \ >> /etc/apt/sources.list &&\ curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | \ - sudo apt-key add - &&\ + apt-key add - &&\ apt-get update &&\ apt-get -y install postgresql-11-postgis-2.5 postgresql-11-pgtap
--- a/docker/Dockerfile.spa Sat Dec 29 16:06:54 2018 +0100 +++ b/docker/Dockerfile.spa Sat Dec 29 16:07:40 2018 +0100 @@ -6,10 +6,11 @@ RUN sed -i 's/\(deb.*\)$/\1 universe/' /etc/apt/sources.list RUN apt-get update &&\ - apt-get -y install --no-install-recommends curl gnupg nodejs make mercurial + apt-get -y install --no-install-recommends \ + curl ca-certificates gnupg nodejs make mercurial # Install yarn -RUN curl https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - &&\ +RUN curl https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\ echo 'deb https://dl.yarnpkg.com/debian/ stable main' >> \ /etc/apt/sources.list &&\ apt-get update &&\
--- a/example_conf.toml Sat Dec 29 16:06:54 2018 +0100 +++ b/example_conf.toml Sat Dec 29 16:07:40 2018 +0100 @@ -56,6 +56,10 @@ # Proxy settings for external OGC services #proxy-key = "SECRET" #proxy-prefix = "http://localhost:8000" +# + +# Server is known on the outside as: +# external-url = "http://localhost:8000" # ---------------------------------------------------------------------- # CORS setup:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/common/json.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,34 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package common + +import ( + "encoding/json" + "strings" +) + +// FromJSONString revives data from a JSON string. +func FromJSONString(data string, dst interface{}) error { + return json.NewDecoder(strings.NewReader(data)).Decode(dst) +} + +// ToJSONString serializes src into a string to +// be revived by FromJSONString. +func ToJSONString(src interface{}) (string, error) { + var b strings.Builder + if err := json.NewEncoder(&b).Encode(src); err != nil { + return "", err + } + return b.String(), nil +}
--- a/pkg/config/config.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/config/config.go Sat Dec 29 16:07:40 2018 +0100 @@ -106,11 +106,12 @@ func TmpDir() string { return viper.GetString("tmp-dir") } var ( - proxyKeyOnce sync.Once - proxyKey []byte - + proxyKeyOnce sync.Once + proxyKey []byte proxyPrefixOnce sync.Once proxyPrefix string + externalURLOnce sync.Once + externalURL string ) // ProxyKey is a crytographic key to sign the URLs generated by the proxy. @@ -142,13 +143,29 @@ func ProxyPrefix() string { fetchPrefix := func() { if proxyPrefix == "" { - proxyPrefix = fmt.Sprintf("http://%s:%d", WebHost(), WebPort()) + if proxyPrefix = viper.GetString("proxy-prefix"); proxyPrefix == "" { + proxyPrefix = fmt.Sprintf("http://%s:%d", WebHost(), WebPort()) + } } } proxyPrefixOnce.Do(fetchPrefix) return proxyPrefix } +// ExternalURL is the URL to find this server from the outside. +// It defauls to http://${WebHost}:${WebPort}". +func ExternalURL() string { + fetchExternal := func() { + if externalURL == "" { + if externalURL = viper.GetString("external-url"); externalURL == "" { + externalURL = fmt.Sprintf("http://%s:%d", WebHost(), WebPort()) + } + } + } + externalURLOnce.Do(fetchExternal) + return externalURL +} + // RootCmd is cobra command to be bound th the cobra/viper infrastructure. var RootCmd = &cobra.Command{ Use: "gemma", @@ -219,10 +236,16 @@ str("geoserver-password", "geoserver", "GeoServer password") bl("geoserver-clean", false, "Clean GeoServer setup") - str("proxy-key", "", `signing key for proxy URLs. Defaults to random key.`) - str("proxy-prefix", "", `URL prefix of proxy. Defaults to "http://${web-host}:${web-port}"`) + str("proxy-key", "", "signing key for proxy URLs.\n"+ + "Defaults to random key.") + str("proxy-prefix", "", "URL prefix of proxy.\n"+ + "Defaults to 'http://${web-host}:${web-port}'") - str("tmp-dir", "", "Temp directory of gemma server. Defaults to system temp directory.") + str("external-url", "", "URL to find the server from the outside.\n"+ + "Defaults to 'http://${web-host}:${web-port}'") + + str("tmp-dir", "", "Temp directory of gemma server.\n"+ + "Defaults to system temp directory.") } var (
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/controllers/importconfig.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,397 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package controllers + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/gorilla/mux" + + "gemma.intevation.de/gemma/pkg/auth" + "gemma.intevation.de/gemma/pkg/imports" + "gemma.intevation.de/gemma/pkg/scheduler" +) + +const ( + selectImportConfigurationPrefix = ` +SELECT + id, + username, + kind, + send_email, + auto_accept, + cron, + url +FROM waterway.import_configuration` + + selectImportConfigurationSQL = selectImportConfigurationPrefix + ` +ORDER by id` + + selectImportConfigurationIDSQL = selectImportConfigurationPrefix + ` +WHERE id = $1` + + insertImportConfigurationSQL = ` +INSERT INTO waterway.import_configuration +(username, kind, cron, send_email, auto_accept, url) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id` + + hasImportConfigurationSQL = ` +SELECT true FROM waterway.import_configuration +WHERE id = $1` + + deleteImportConfiguationSQL = ` +DELETE FROM waterway.import_configuration +WHERE id = $1` + + updateImportConfigurationSQL = ` +UPDATE waterway.import_configuration SET + username = $2 + kind = $3, + cron = $4, + url = $5, + send_email = $6, + auto_accept = $7 +WHERE id = $1 +` +) + +func modifyImportConfig( + input interface{}, + req *http.Request, + conn *sql.Conn, +) (jr JSONResult, err error) { + + ctx := req.Context() + + importConfig := input.(*imports.Config) + + id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64) + + var tx *sql.Tx + + if tx, err = conn.BeginTx(ctx, nil); err != nil { + return + } + defer tx.Rollback() + + var ( + entry imports.IDConfig + kind string + cron sql.NullString + url sql.NullString + ) + + err = conn.QueryRowContext(ctx, selectImportConfigurationIDSQL, id).Scan( + &entry.ID, + &entry.User, + &kind, + &entry.SendEMail, + &entry.AutoAccept, + &cron, + &url, + ) + + switch { + case err == sql.ErrNoRows: + err = JSONError{ + Code: http.StatusNotFound, + Message: fmt.Sprintf("No schedule %d found", id), + } + return + case err != nil: + return + } + + session, _ := auth.GetSession(req) + + entry.SendEMail = importConfig.SendEMail + entry.AutoAccept = importConfig.AutoAccept + + if importConfig.Cron != nil { + cron = sql.NullString{String: string(*importConfig.Cron), Valid: true} + } + + if importConfig.URL != nil { + url = sql.NullString{String: *importConfig.URL, Valid: true} + } + + if _, err = tx.ExecContext(ctx, updateImportConfigurationSQL, + id, + session.User, + string(importConfig.Kind), + cron, + url, + importConfig.SendEMail, + importConfig.AutoAccept, + ); err != nil { + return + } + + scheduler.UnbindByID(id) + + if cron.Valid { + if err = scheduler.BindAction( + string(importConfig.Kind), + cron.String, + id, + ); err != nil { + return + } + } + + if err = tx.Commit(); err != nil { + return + } + + var result = struct { + ID int64 `json:"id"` + }{ + ID: id, + } + + jr = JSONResult{Result: &result} + return +} + +func infoImportConfig( + _ interface{}, + req *http.Request, + conn *sql.Conn, +) (jr JSONResult, err error) { + + ctx := req.Context() + + id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64) + + var ( + entry imports.IDConfig + kind string + cron sql.NullString + url sql.NullString + ) + + err = conn.QueryRowContext(ctx, selectImportConfigurationIDSQL, id).Scan( + &entry.ID, + &entry.User, + &kind, + &entry.SendEMail, + &entry.AutoAccept, + &cron, + &url, + ) + + switch { + case err == sql.ErrNoRows: + err = JSONError{ + Code: http.StatusNotFound, + Message: fmt.Sprintf("No schedule %d found", id), + } + return + case err != nil: + return + } + + entry.Kind = imports.ImportKind(kind) + if cron.Valid { + cs := imports.CronSpec(cron.String) + entry.Cron = &cs + } + if url.Valid { + entry.URL = &url.String + } + + jr = JSONResult{Result: &entry} + return +} + +func deleteImportConfig( + _ interface{}, + req *http.Request, + conn *sql.Conn, +) (jr JSONResult, err error) { + + ctx := req.Context() + + id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64) + + var tx *sql.Tx + if tx, err = conn.BeginTx(ctx, nil); err != nil { + return + } + defer tx.Rollback() + + var found bool + err = tx.QueryRowContext(ctx, hasImportConfigurationSQL, id).Scan(&found) + switch { + case err == sql.ErrNoRows: + err = JSONError{ + Code: http.StatusNotFound, + Message: fmt.Sprintf("No schedule %d found", id), + } + return + case err != nil: + return + case !found: + err = errors.New("Unexpected result") + return + } + + if _, err = tx.ExecContext(ctx, deleteImportConfiguationSQL, id); err != nil { + return + } + + // Remove from running scheduler. + scheduler.UnbindByID(id) + + if err = tx.Commit(); err != nil { + return + } + + var result = struct { + ID int64 `json:"id"` + }{ + ID: id, + } + + jr = JSONResult{Result: &result} + return +} + +func addImportConfig( + input interface{}, + req *http.Request, + conn *sql.Conn, +) (jr JSONResult, err error) { + + importConfig := input.(*imports.Config) + + session, _ := auth.GetSession(req) + + var cron, url sql.NullString + + if importConfig.Cron != nil { + cron = sql.NullString{String: string(*importConfig.Cron), Valid: true} + } + if importConfig.URL != nil { + url = sql.NullString{String: *importConfig.URL, Valid: true} + } + + ctx := req.Context() + + var tx *sql.Tx + + if tx, err = conn.BeginTx(ctx, nil); err != nil { + return + } + defer tx.Rollback() + + var id int64 + if err = tx.QueryRowContext( + ctx, + insertImportConfigurationSQL, + session.User, + string(importConfig.Kind), + cron, + importConfig.SendEMail, + importConfig.AutoAccept, + url, + ).Scan(&id); err != nil { + return + } + + // Need to start a scheduler job right away? + if importConfig.Cron != nil { + if err = scheduler.BindAction( + string(importConfig.Kind), + string(*importConfig.Cron), + id, + ); err != nil { + return + } + } + + if err = tx.Commit(); err != nil { + scheduler.UnbindByID(id) + } + + var result = struct { + ID int64 `json:"id"` + }{ + ID: id, + } + + jr = JSONResult{ + Code: http.StatusCreated, + Result: &result, + } + return +} + +func listImportConfigs( + _ interface{}, + req *http.Request, + conn *sql.Conn, +) (jr JSONResult, err error) { + + ctx := req.Context() + var rows *sql.Rows + + if rows, err = conn.QueryContext(ctx, selectImportConfigurationSQL); err != nil { + return + } + defer rows.Close() + + list := []*imports.IDConfig{} + + for rows.Next() { + var ( + entry imports.IDConfig + kind string + cron sql.NullString + url sql.NullString + ) + if err = rows.Scan( + &entry.ID, + &entry.User, + &kind, + &entry.SendEMail, + &entry.AutoAccept, + &cron, + &url, + ); err != nil { + return + } + entry.Kind = imports.ImportKind(kind) + if cron.Valid { + cs := imports.CronSpec(cron.String) + entry.Cron = &cs + } + if url.Valid { + entry.URL = &url.String + } + list = append(list, &entry) + } + + if err = rows.Err(); err != nil { + return + } + + jr = JSONResult{Result: list} + return +}
--- a/pkg/controllers/importqueue.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/controllers/importqueue.go Sat Dec 29 16:07:40 2018 +0100 @@ -66,6 +66,25 @@ DELETE FROM waterway.imports WHERE id = $1` ) +func toInt8Array(txt string) *pgtype.Int8Array { + parts := strings.Split(txt, ",") + var ints []int64 + for _, part := range parts { + part = strings.TrimSpace(part) + v, err := strconv.ParseInt(part, 10, 64) + if err != nil { + continue + } + ints = append(ints, v) + } + var ia pgtype.Int8Array + if err := ia.Set(ints); err != nil { + log.Printf("warn: %v\n", err) + return nil + } + return &ia +} + func toTextArray(txt string, allowed []string) *pgtype.TextArray { parts := strings.Split(txt, ",") var accepted []string @@ -101,6 +120,7 @@ args []interface{} states *pgtype.TextArray kinds *pgtype.TextArray + ids *pgtype.Int8Array ) arg := func(format string, v interface{}) { @@ -116,8 +136,12 @@ kinds = toTextArray(ks, imports.ImportKindNames()) } + if idss := req.FormValue("ids"); idss != "" { + ids = toInt8Array(idss) + } + stmt.WriteString(selectImportsSQL) - if states != nil || kinds != nil { + if states != nil || kinds != nil || ids != nil { stmt.WriteString(" WHERE ") } @@ -125,7 +149,7 @@ arg(" state = ANY($%d) ", states) } - if states != nil && kinds != nil { + if states != nil && (kinds != nil || ids != nil) { stmt.WriteString("AND") } @@ -133,6 +157,14 @@ arg(" kind = ANY($%d) ", kinds) } + if (states != nil || kinds != nil) && ids != nil { + stmt.WriteString("AND") + } + + if ids != nil { + arg(" id = ANY($%d) ", ids) + } + stmt.WriteString(" ORDER BY enqueued DESC ") if lim := req.FormValue("limit"); lim != "" {
--- a/pkg/controllers/json.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/controllers/json.go Sat Dec 29 16:07:40 2018 +0100 @@ -31,10 +31,13 @@ Result interface{} } +const JSONDefaultLimit = 2048 + type JSONHandler struct { Input func() interface{} Handle func(interface{}, *http.Request, *sql.Conn) (JSONResult, error) NoConn bool + Limit int64 } type JSONError struct { @@ -52,7 +55,16 @@ if j.Input != nil { input = j.Input() defer req.Body.Close() - if err := json.NewDecoder(req.Body).Decode(input); err != nil { + var r io.Reader + switch { + case j.Limit == 0: + r = io.LimitReader(req.Body, JSONDefaultLimit) + case j.Limit > 0: + r = io.LimitReader(req.Body, j.Limit) + default: + r = req.Body + } + if err := json.NewDecoder(r).Decode(input); err != nil { http.Error(rw, "error: "+err.Error(), http.StatusBadRequest) return }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/controllers/manualimports.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,106 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> +// * Raimund Renkert <raimund.renkert@intevation.de> + +package controllers + +import ( + "database/sql" + "log" + "net/http" + + "gemma.intevation.de/gemma/pkg/auth" + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/imports" + "gemma.intevation.de/gemma/pkg/models" +) + +func importBottleneck(input interface{}) (interface{}, bool, bool) { + bi := input.(*models.BottleneckImport) + bn := &imports.Bottleneck{ + URL: bi.URL, + Insecure: bi.Insecure, + } + return bn, bi.SendEmail, false +} + +func importGaugeMeasurement(input interface{}) (interface{}, bool, bool) { + gi := input.(*models.GaugeMeasurementImport) + gm := &imports.GaugeMeasurement{ + URL: gi.URL, + Insecure: gi.Insecure, + } + return gm, gi.SendEmail, true +} + +func importFairwayAvailability(input interface{}) (interface{}, bool, bool) { + fai := input.(*models.FairwayAvailabilityImport) + fa := &imports.FairwayAvailability{ + URL: fai.URL, + Insecure: fai.Insecure, + } + return fa, fai.SendEmail, true +} + +func importWaterwayAxis(input interface{}) (interface{}, bool, bool) { + wxi := input.(*models.WaterwayAxisImport) + wx := &imports.WaterwayAxis{ + URL: wxi.URL, + FeatureType: wxi.FeatureType, + SortBy: wxi.SortBy, + } + return wx, wxi.SendEmail, true +} + +func manualImport( + kind imports.JobKind, + setup func(interface{}) (interface{}, bool, bool), +) func(interface{}, *http.Request, *sql.Conn) (JSONResult, error) { + + return func(input interface{}, req *http.Request, _ *sql.Conn) ( + jr JSONResult, err error) { + + what, sendEmail, autoAccept := setup(input) + + var serialized string + if serialized, err = common.ToJSONString(what); err != nil { + return + } + + session, _ := auth.GetSession(req) + + var jobID int64 + if jobID, err = imports.AddJob( + kind, + session.User, + sendEmail, autoAccept, + serialized, + ); err != nil { + return + } + + log.Printf("info: added import #%d to queue\n", jobID) + + result := struct { + ID int64 `json:"id"` + }{ + ID: jobID, + } + + jr = JSONResult{ + Code: http.StatusCreated, + Result: &result, + } + return + } +}
--- a/pkg/controllers/routes.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/controllers/routes.go Sat Dec 29 16:07:40 2018 +0100 @@ -21,6 +21,7 @@ "github.com/gorilla/mux" "gemma.intevation.de/gemma/pkg/auth" + "gemma.intevation.de/gemma/pkg/imports" "gemma.intevation.de/gemma/pkg/middleware" "gemma.intevation.de/gemma/pkg/models" ) @@ -170,6 +171,57 @@ api.Handle("/imports/soundingresult", waterwayAdmin( http.HandlerFunc(importSoundingResult))).Methods(http.MethodPost) + api.Handle("/imports/bottleneck", waterwayAdmin(&JSONHandler{ + Input: func() interface{} { return new(models.BottleneckImport) }, + Handle: manualImport(imports.BNJobKind, importBottleneck), + NoConn: true, + })).Methods(http.MethodPost) + + api.Handle("/imports/gaugemeasurement", waterwayAdmin(&JSONHandler{ + Input: func() interface{} { return new(models.GaugeMeasurementImport) }, + Handle: manualImport(imports.GMJobKind, importGaugeMeasurement), + NoConn: true, + })).Methods(http.MethodPost) + + api.Handle("/imports/fairwayavailability", waterwayAdmin(&JSONHandler{ + Input: func() interface{} { return new(models.FairwayAvailabilityImport) }, + Handle: manualImport(imports.FAJobKind, importFairwayAvailability), + NoConn: true, + })).Methods(http.MethodPost) + + api.Handle("/imports/waterwayaxis", waterwayAdmin(&JSONHandler{ + Input: func() interface{} { return new(models.WaterwayAxisImport) }, + Handle: manualImport(imports.WXJobKind, importWaterwayAxis), + NoConn: true, + })).Methods(http.MethodPost) + + // Import scheduler configuration + api.Handle("/imports/config/{id:[0-9]+}", + waterwayAdmin(&JSONHandler{ + Handle: modifyImportConfig, + })).Methods(http.MethodPatch) + + api.Handle("/imports/config/{id:[0-9]+}", + waterwayAdmin(&JSONHandler{ + Handle: deleteImportConfig, + })).Methods(http.MethodDelete) + + api.Handle("/imports/config/{id:[0-9]+}", + waterwayAdmin(&JSONHandler{ + Handle: infoImportConfig, + })).Methods(http.MethodGet) + + api.Handle("/imports/config", + waterwayAdmin(&JSONHandler{ + Input: func() interface{} { return new(imports.Config) }, + Handle: addImportConfig, + })).Methods(http.MethodPost) + + api.Handle("/imports/config", + waterwayAdmin(&JSONHandler{ + Handle: listImportConfigs, + })).Methods(http.MethodGet) + // Import queue lsImports := waterwayAdmin(&JSONHandler{ Handle: listImports,
--- a/pkg/controllers/srimports.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/controllers/srimports.go Sat Dec 29 16:07:40 2018 +0100 @@ -152,7 +152,7 @@ } sr.Dir = dir - serialized, err := sr.ToString() + serialized, err := common.ToJSONString(sr) if err != nil { log.Printf("error: %v\n", err) http.Error(rw, "error: "+err.Error(), http.StatusInternalServerError) @@ -161,7 +161,14 @@ session, _ := auth.GetSession(req) - jobID, err := imports.AddJob(imports.SRJobKind, session.User, serialized) + sendEmail := req.FormValue("bottleneck") != "" + + jobID, err := imports.AddJob( + imports.SRJobKind, + session.User, + sendEmail, false, + serialized) + if err != nil { log.Printf("error: %v\n", err) http.Error(rw, "error: "+err.Error(), http.StatusInternalServerError) @@ -217,12 +224,14 @@ Messages []string `json:"messages,omitempty"` } - var noXYZ bool - if noXYZ = common.FindInZIP(zr, ".xyz") == nil; noXYZ { - messages = append(messages, "no .xyz file found.") + find := func(ext string) *zip.File { return common.FindInZIP(zr, ext) } + + noXYZ := find(".xyz") == nil && find(".txt") == nil + if noXYZ { + messages = append(messages, "no .xyz or .txt file found.") } - if mj := common.FindInZIP(zr, "meta.json"); mj == nil { + if mj := find("meta.json"); mj == nil { messages = append(messages, "no 'meta.json' file found.") } else { if meta, err := loadMeta(mj); err != nil {
--- a/pkg/controllers/surveys.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/controllers/surveys.go Sat Dec 29 16:07:40 2018 +0100 @@ -23,10 +23,13 @@ ) const ( - listSurveysSQL = `SELECT s.bottleneck_id, - s.date_info::text -FROM waterway.bottlenecks b, waterway.sounding_results s -WHERE b.objnam=$1 AND s.bottleneck_id = b.bottleneck_id;` + listSurveysSQL = ` +SELECT + s.bottleneck_id, + s.date_info::text +FROM waterway.bottlenecks b JOIN waterway.sounding_results s +ON b.id = s.bottleneck_id +WHERE b.objnam=$1` ) func listSurveys(
--- a/pkg/controllers/user.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/controllers/user.go Sat Dec 29 16:07:40 2018 +0100 @@ -29,6 +29,7 @@ "gemma.intevation.de/gemma/pkg/auth" "gemma.intevation.de/gemma/pkg/misc" "gemma.intevation.de/gemma/pkg/models" + "gemma.intevation.de/gemma/pkg/scheduler" ) const ( @@ -112,9 +113,21 @@ return } + ctx := req.Context() + + // Remove scheduled tasks. + ids, err2 := scheduler.ScheduledUserIDs(ctx, db, user) + if err2 == nil { + if len(ids) > 0 { + go func() { scheduler.UnbindByIDs(ids) }() + } + } else { + log.Printf("error: %v\n", err2) + } + var res sql.Result - if res, err = db.ExecContext(req.Context(), deleteUserSQL, user); err != nil { + if res, err = db.ExecContext(ctx, deleteUserSQL, user); err != nil { return }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/imports/bn.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,265 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package imports + +import ( + "context" + "database/sql" + "errors" + "regexp" + "strconv" + "time" + + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/soap/ifbn" +) + +type Bottleneck struct { + URL string `json:"url"` + Insecure bool `json:"insecure"` +} + +const BNJobKind JobKind = "bn" + +const ( + hasBottleneckSQL = ` +SELECT true FROM waterway.bottlenecks WHERE bottleneck_id = $1` + + insertBottleneckSQL = ` +INSERT INTO waterway.bottlenecks ( + bottleneck_id, + fk_g_fid, + objnam, + nobjnm, + stretch, + area, + rb, + lb, + responsible_country, + revisiting_time, + limiting, + date_info, + source_organization +) VALUES( + $1, + isrs_fromText($2), + $3, + $4, + isrsrange(isrs_fromText($5), isrs_fromText($6)), + ISRSrange_area( + isrsrange(isrs_fromText($5), isrs_fromText($6)), + (SELECT ST_Union(CAST(area AS geometry)) + FROM waterway.fairway_dimensions + WHERE level_of_service = 3)), + $7, + $8, + $9, + $10, + $11, + $12, + $13 +) +RETURNING id` +) + +type bnJobCreator struct{} + +func init() { + RegisterJobCreator(BNJobKind, bnJobCreator{}) +} + +func (bnJobCreator) Description() string { + return "bottlenecks" +} + +func (bnJobCreator) Create(_ JobKind, data string) (Job, error) { + bn := new(Bottleneck) + if err := common.FromJSONString(data, bn); err != nil { + return nil, err + } + return bn, nil +} + +func (bnJobCreator) Depends() []string { + return []string{ + "gauges", + "bottlenecks", + } +} + +const ( + bnStageDoneSQL = ` +UPDATE waterway.bottlenecks SET staging_done = true +WHERE id IN ( + SELECT key from waterway.track_imports + WHERE import_id = $1 AND + relation = 'waterway.bottlenecks'::regclass)` +) + +// StageDone moves the imported bottleneck out of the staging area. +func (bnJobCreator) StageDone( + ctx context.Context, + tx *sql.Tx, + id int64, +) error { + _, err := tx.ExecContext(ctx, bnStageDoneSQL, id) + return err +} + +// CleanUp of a bottleneck import is a NOP. +func (*Bottleneck) CleanUp() error { return nil } + +var rblbRe = regexp.MustCompile(`(..)_(..)`) + +func splitRBLB(s string) (string, string) { + m := rblbRe.FindStringSubmatch(s) + if len(m) == 0 { + return "", "" + } + return m[1], m[2] +} + +func revisitingTime(s string) int { + v, err := strconv.Atoi(s) + if err != nil { + v = 0 + } + return v +} + +// Do executes the actual bottleneck import. +func (bn *Bottleneck) Do( + ctx context.Context, + importID int64, + conn *sql.Conn, + feedback Feedback, +) (interface{}, error) { + client := ifbn.NewIBottleneckService(bn.URL, bn.Insecure, nil) + + req := &ifbn.Export_bn_by_isrs{} + + resp, err := client.Export_bn_by_isrs(req) + if err != nil { + feedback.Error("%v", err) + return nil, err + } + + if resp.Export_bn_by_isrsResult == nil { + err := errors.New("no Bottlenecks found") + feedback.Error("%v", err) + return nil, err + } + + bns := resp.Export_bn_by_isrsResult.BottleNeckType + feedback.Info("Found %d bottlenecks for import", len(bns)) + + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + var hasStmt, insertStmt, trackStmt *sql.Stmt + + for _, x := range []struct { + sql string + stmt **sql.Stmt + }{ + {hasBottleneckSQL, &hasStmt}, + {insertBottleneckSQL, &insertStmt}, + {trackImportSQL, &trackStmt}, + } { + var err error + if *x.stmt, err = tx.PrepareContext(ctx, x.sql); err != nil { + return nil, err + } + defer (*x.stmt).Close() + } + + var nids []string + + start := time.Now() + +nextBN: + for _, bn := range bns { + + var found bool + err := hasStmt.QueryRowContext(ctx, bn.Bottleneck_id).Scan(&found) + switch { + case err == sql.ErrNoRows: + // This is good. + case err != nil: + return nil, err + case found: + // TODO: Deep comparison database vs. SOAP. + continue nextBN + } + + rb, lb := splitRBLB(bn.Rb_lb) + + var limiting, country string + + if bn.Limiting_factor != nil { + limiting = string(*bn.Limiting_factor) + } + + if bn.Responsible_country != nil { + country = string(*bn.Responsible_country) + } + + var nid int64 + + err = insertStmt.QueryRowContext( + ctx, + bn.Bottleneck_id, + bn.Fk_g_fid, + bn.OBJNAM, + bn.NOBJNM, + bn.From_ISRS, bn.To_ISRS, + rb, + lb, + country, + revisitingTime(bn.Revisiting_time), + limiting, + bn.Date_Info, + bn.Source, + ).Scan(&nid) + if err != nil { + return nil, err + } + nids = append(nids, bn.Bottleneck_id) + if _, err := trackStmt.ExecContext( + ctx, importID, "waterway.bottlenecks", nid, + ); err != nil { + return nil, err + } + feedback.Info("Inserted '%s' into database", bn.OBJNAM) + } + if len(nids) == 0 { + feedback.Error("No new bottlenecks found") + return nil, errors.New("No new bottlenecks found") + } + + feedback.Info("Storing %d bottlenecks took %s", len(nids), time.Since(start)) + if err = tx.Commit(); err == nil { + feedback.Info("Import of bottlenecks was successful") + } + + summary := struct { + Bottlenecks []string `json:"bottlenecks"` + }{ + Bottlenecks: nids, + } + return &summary, err +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/imports/config.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,129 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package imports + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + + "gemma.intevation.de/gemma/pkg/auth" + "github.com/robfig/cron" +) + +type ( + CronSpec string + ImportKind string + + Config struct { + Kind ImportKind `json:"kind"` + SendEMail bool `json:"send-email"` + AutoAccept bool `json:"auto-accept"` + Cron *CronSpec `json:"cron"` + URL *string `json:"url"` + } + + IDConfig struct { + ID int64 `json:"id"` + User string `json:"user"` + Kind ImportKind `json:"kind"` + SendEMail bool `json:"send-email"` + AutoAccept bool `json:"auto-accept"` + Cron *CronSpec `json:"cron,omitempty"` + URL *string `json:"url,omitempty"` + } +) + +func (ik *ImportKind) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + if !HasImportKindName(s) { + return fmt.Errorf("Unknown kind '%s'", s) + } + + *ik = ImportKind(s) + + return nil +} + +func (cs *CronSpec) UnmarshalJSON(data []byte) error { + var spec string + if err := json.Unmarshal(data, &spec); err != nil { + return err + } + if _, err := cron.Parse(spec); err != nil { + return err + } + *cs = CronSpec(spec) + + return nil +} + +const ( + configUser = "sys_admin" + + loadConfigSQL = ` +SELECT + username, + kind, + send_email, + auto_accept, + cron, + url +FROM waterway.import_configuration +WHERE id = $1` +) + +func loadIDConfig(id int64) (*IDConfig, error) { + + ctx := context.Background() + cfg := &IDConfig{ID: id} + + err := auth.RunAs(ctx, configUser, func(conn *sql.Conn) error { + var kind ImportKind + var cron, url sql.NullString + if err := conn.QueryRowContext(ctx, loadConfigSQL, id).Scan( + &cfg.User, + &kind, + &cfg.SendEMail, + &cfg.AutoAccept, + &cron, + &url, + ); err != nil { + return err + } + cfg.Kind = ImportKind(kind) + if cron.Valid { + c := CronSpec(cron.String) + cfg.Cron = &c + } + if url.Valid { + cfg.URL = &url.String + } + return nil + }) + + switch { + case err == sql.ErrNoRows: + return nil, nil + case err != nil: + return nil, err + } + + return cfg, nil +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/imports/email.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,91 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package imports + +import ( + "context" + "database/sql" + "log" + "strings" + "text/template" + + "gemma.intevation.de/gemma/pkg/auth" + "gemma.intevation.de/gemma/pkg/config" + "gemma.intevation.de/gemma/pkg/misc" +) + +const ( + selectEmailSQL = `SELECT email_address FROM users.list_users WHERE username = $1` + + importNotificationMailSubject = `import notification mail` +) + +var ( + importNotificationMailTmpl = template.Must( + template.New("notification").Parse(`Dear {{ .User }}, + +a {{ .Description }} import on server {{ .Server }} triggered +this email notification. + +{{ if eq .State "accepted" }}The imported data were successfully integrated into the database.{{ end -}} +{{ if eq .State "failed" }}The import failed for some reasons.{{ end -}} +{{ if eq .State "pending" }}The imported data could be integrated into the database +but your final decision is needed.{{ end }} + +Please follow this link to have a closer look at the details: + +{{ .Server }}/#/?{{ if eq .State "pending" }}review{{ else }}importlog{{ end }}={{ .ID }} + +Best regards + Your service team`)) +) + +func sendNotificationMail(user, description, state string, id int64) { + config.WaitReady() + + ctx := context.Background() + var email string + if err := auth.RunAs(ctx, user, + func(conn *sql.Conn) error { + return conn.QueryRowContext(ctx, selectEmailSQL, user).Scan(&email) + }, + ); err != nil { + log.Printf("error: %v\n", err) + return + } + + data := struct { + User string + Description string + Server string + State string + ID int64 + }{ + User: user, + Description: description, + Server: config.ExternalURL(), + State: state, + ID: id, + } + + var body strings.Builder + if err := importNotificationMailTmpl.Execute(&body, &data); err != nil { + log.Printf("error: %v\n", err) + return + } + + if err := misc.SendMail(email, importNotificationMailSubject, body.String()); err != nil { + log.Printf("error: %v\n", err) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/imports/fa.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,363 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Raimund Renkert <raimund.renkert@intevation.de> +package imports + +import ( + "context" + "database/sql" + "errors" + "time" + + "github.com/jackc/pgx/pgtype" + + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/models" + "gemma.intevation.de/gemma/pkg/soap/ifaf" +) + +type FairwayAvailability struct { + URL string `json:"url"` + Insecure bool `json:"insecure"` +} + +const FAJobKind JobKind = "fa" + +const ( + listBottlenecksSQL = ` +SELECT + bottleneck_id, + responsible_country +FROM waterway.bottlenecks +WHERE responsible_country = users.current_user_country() + AND staging_done = true +` + insertFASQL = ` +INSERT INTO waterway.fairway_availability ( + position_code, + bottleneck_id, + surdat, + critical, + date_info, + source_organization +) VALUES ( + $1, + (SELECT id FROM waterway.bottlenecks WHERE bottleneck_id = $2), + $3, + $4, + $5, + $6 +) +RETURNING id` + + insertBnPdfsSQL = ` +INSERT INTO waterway.bottleneck_pdfs ( + fairway_availability_id, + profile_pdf_filename, + profile_pdf_url, + pdf_generation_date, + source_organization +) VALUES ( + $1, + $2, + $3, + $4, + $5 +)` + insertEFASQL = ` +INSERT INTO waterway.effective_fairway_availability ( + fairway_availability_id, + measure_date, + level_of_service, + available_depth_value, + available_width_value, + water_level_value, + measure_type, + source_organization, + forecast_generation_time, + value_lifetime +) VALUES ( + $1, + $2, + (SELECT + level_of_service + FROM levels_of_service + WHERE name = $3), + $4, + $5, + $6, + $7, + $8, + $9, + $10 +)` + insertFAVSQL = ` +INSERT INTO waterway.fa_reference_values ( + fairway_availability_id, + level_of_service, + fairway_depth, + fairway_width, + fairway_radius, + shallowest_spot +) VALUES ( + $1, + (SELECT + level_of_service + FROM levels_of_service + WHERE name = $2), + $3, + $4, + $5, + ST_MakePoint($6, $7)::geography +)` +) + +type faJobCreator struct{} + +func init() { + RegisterJobCreator(FAJobKind, faJobCreator{}) +} + +func (faJobCreator) Description() string { + return "fairway availability" +} + +func (faJobCreator) Create(_ JobKind, data string) (Job, error) { + fa := new(FairwayAvailability) + if err := common.FromJSONString(data, fa); err != nil { + return nil, err + } + return fa, nil +} + +func (faJobCreator) Depends() []string { + return []string{ + "bottlenecks", + "fairway_availability", + "bottleneck_pdfs", + "effective_fairway_availability", + "fa_reference_values", + "levels_of_service", + } +} + +// StageDone moves the imported fairway availablities out of the staging area. +// Currently doing nothing. +func (faJobCreator) StageDone(context.Context, *sql.Tx, int64) error { + return nil +} + +// CleanUp of a fairway availablities import is a NOP. +func (*FairwayAvailability) CleanUp() error { return nil } + +// Do executes the actual fairway availability import. +func (fa *FairwayAvailability) Do( + ctx context.Context, + importID int64, + conn *sql.Conn, + feedback Feedback, +) (interface{}, error) { + + // Get available bottlenecks from database for use as filter in SOAP request + var rows *sql.Rows + + rows, err := conn.QueryContext(ctx, listBottlenecksSQL) + if err != nil { + return nil, err + } + defer rows.Close() + + bottlenecks := []models.Bottleneck{} + + for rows.Next() { + var bn models.Bottleneck + if err = rows.Scan( + &bn.ID, + &bn.ResponsibleCountry, + ); err != nil { + return nil, err + } + bottlenecks = append(bottlenecks, bn) + } + + if err = rows.Err(); err != nil { + return nil, err + } + + faids, err := fa.doForFAs(ctx, bottlenecks, conn, feedback) + if err != nil { + feedback.Error("Error processing data: %s", err) + } + if len(faids) == 0 { + feedback.Info("No new fairway availablity data found") + return nil, nil + } + feedback.Info("Processed %d of %d bottlenecks", len(faids), len(bottlenecks)) + // TODO: needs to be filled more useful. + summary := struct { + FairwayAvailabilities []string `json:"fairwayAvailabilities"` + }{ + FairwayAvailabilities: faids, + } + return &summary, err +} + +func (fa *FairwayAvailability) doForFAs( + ctx context.Context, + bottlenecks []models.Bottleneck, + conn *sql.Conn, + feedback Feedback, +) ([]string, error) { + start := time.Now() + + client := ifaf.NewFairwayAvailabilityService(fa.URL, fa.Insecure, nil) + + var bnIds []string + for _, bn := range bottlenecks { + bnIds = append(bnIds, bn.ID) + } + + ids := ifaf.ArrayOfString{ + String: bnIds, + } + + // TODO: Filter by period. Period should start after latest measurement date. + req := &ifaf.Get_bottleneck_fa{ + Bottleneck_id: &ids, + } + resp, err := client.Get_bottleneck_fa(req) + if err != nil { + feedback.Error("%v", err) + return nil, err + } + + if resp.Get_bottleneck_faResult == nil { + err := errors.New("no fairway availabilities found") + return nil, err + } + + result := resp.Get_bottleneck_faResult + + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + insertFAStmt, err := tx.PrepareContext(ctx, insertFASQL) + if err != nil { + return nil, err + } + defer insertFAStmt.Close() + insertBnPdfsStmt, err := tx.PrepareContext(ctx, insertBnPdfsSQL) + if err != nil { + return nil, err + } + defer insertBnPdfsStmt.Close() + insertEFAStmt, err := tx.PrepareContext(ctx, insertEFASQL) + if err != nil { + return nil, err + } + defer insertEFAStmt.Close() + insertFAVStmt, err := tx.PrepareContext(ctx, insertFAVSQL) + if err != nil { + return nil, err + } + defer insertFAVStmt.Close() + + var faids []string + var faId int64 + feedback.Info("Found %d fairway availabilities", len(result.FairwayAvailability)) + for _, faRes := range result.FairwayAvailability { + // TODO: high frequent requests lead to "duplicate key value violates unique constraint "fairway_availability_bottleneck_id_surdat_key" + // in the database. This has to be resolved. + // All data subsets can also ocure as duplicates! + err = insertFAStmt.QueryRowContext( + ctx, + faRes.POSITION, + faRes.Bottleneck_id, + faRes.SURDAT, + faRes.Critical, + faRes.Date_Info, + faRes.Source, + ).Scan(&faId) + if err != nil { + return nil, err + } + feedback.Info("Processing for Bottleneck %s", faRes.Bottleneck_id) + faids = append(faids, faRes.Bottleneck_id) + for _, bnPdfs := range faRes.Bottleneck_PDFs.PdfInfo { + _, err = insertBnPdfsStmt.ExecContext( + ctx, + faId, + bnPdfs.ProfilePdfFilename, + bnPdfs.ProfilePdfURL, + bnPdfs.PDF_Generation_Date, + bnPdfs.Source, + ) + if err != nil { + return nil, err + } + feedback.Info("Add %d Pdfs", len(faRes.Bottleneck_PDFs.PdfInfo)) + } + for _, efa := range faRes.Effective_fairway_availability.EffectiveFairwayAvailability { + los := efa.Level_of_Service + fgt := efa.Forecast_generation_time + if efa.Forecast_generation_time.Status == pgtype.Undefined { + fgt = pgtype.Timestamp{ + Status: pgtype.Null, + } + } + _, err = insertEFAStmt.ExecContext( + ctx, + faId, + efa.Measure_date, + string(*los), + efa.Available_depth_value, + efa.Available_width_value, + efa.Water_level_value, + efa.Measure_type, + efa.Source, + fgt, + efa.Value_lifetime, + ) + if err != nil { + return nil, err + } + feedback.Info("Add %d Effective Fairway Availability", len( + faRes.Effective_fairway_availability.EffectiveFairwayAvailability)) + } + for _, fav := range faRes.Reference_values.ReferenceValue { + _, err = insertFAVStmt.ExecContext( + ctx, + faId, + fav.Level_of_Service, + fav.Fairway_depth, + fav.Fairway_width, + fav.Fairway_radius, + fav.Shallowest_spot_Lat, + fav.Shallowest_spot_Lon, + ) + if err != nil { + return nil, err + } + feedback.Info("Add %d Reference Values", + len(faRes.Reference_values.ReferenceValue)) + } + } + feedback.Info("Storing fairway availabilities took %s", time.Since(start)) + if err = tx.Commit(); err == nil { + feedback.Info("Import of fairway availabilities was successful") + } + + return faids, nil +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/imports/gm.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,264 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Raimund Renkert <raimund.renkert@intevation.de> +package imports + +import ( + "context" + "database/sql" + "errors" + "time" + + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/models" + "gemma.intevation.de/gemma/pkg/soap/nts" +) + +type GaugeMeasurement struct { + URL string `json:"url"` + Insecure bool `json:"insecure"` +} + +const GMJobKind JobKind = "gm" + +const ( + listGaugesSQL = ` +SELECT + (location).country_code, + (location).locode, + (location).fairway_section, + (location).orc, + (location).hectometre +FROM waterway.gauges +WHERE (location).country_code = users.current_user_country()` + + hasGaugeMeasurementSQL = ` +SELECT true FROM waterway.gauge_measurements WHERE fk_gauge_id = $1` + + insertGMSQL = ` +INSERT INTO waterway.gauge_measurements ( + fk_gauge_id, + measure_date, + sender, + language_code, + date_issue, + water_level, + predicted, + is_waterlevel, + value_min, + value_max, + date_info, + source_organization +) VALUES( + ($1, $2, $3, $4, $5), + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16 +) +RETURNING id` +) + +type gmJobCreator struct{} + +func init() { + RegisterJobCreator(GMJobKind, gmJobCreator{}) +} + +func (gmJobCreator) Description() string { + return "gauge measurements" +} + +func (gmJobCreator) Create(_ JobKind, data string) (Job, error) { + gm := new(GaugeMeasurement) + if err := common.FromJSONString(data, gm); err != nil { + return nil, err + } + return gm, nil +} + +func (gmJobCreator) Depends() []string { + return []string{ + "gauges", + "gauge_measurements", + } +} + +// StageDone moves the imported gauge measurement out of the staging area. +// Currently doing nothing. +func (gmJobCreator) StageDone(context.Context, *sql.Tx, int64) error { + return nil +} + +// CleanUp of a gauge measurement import is a NOP. +func (*GaugeMeasurement) CleanUp() error { return nil } + +// Do executes the actual bottleneck import. +func (gm *GaugeMeasurement) Do( + ctx context.Context, + importID int64, + conn *sql.Conn, + feedback Feedback, +) (interface{}, error) { + + // Get available gauges from database for use as filter in SOAP request + var rows *sql.Rows + + rows, err := conn.QueryContext(ctx, listGaugesSQL) + if err != nil { + return nil, err + } + defer rows.Close() + + gauges := []models.GaugeMeasurement{} + + for rows.Next() { + var g models.GaugeMeasurement + if err = rows.Scan( + &g.Gauge.CountryCode, + &g.Gauge.LoCode, + &g.Gauge.FairwaySection, + &g.Gauge.Orc, + &g.Gauge.Hectometre, + ); err != nil { + return nil, err + } + gauges = append(gauges, g) + } + + if err = rows.Err(); err != nil { + return nil, err + } + + // TODO get date_issue for selected gauges + gids, err := gm.doForGM(ctx, gauges, conn, feedback) + if err != nil { + feedback.Error("Error processing %d gauges: %s", len(gauges), err) + } + if len(gids) == 0 { + feedback.Info("No new gauge measurements found") + return nil, nil + } + // TODO: needs to be filled more useful. + summary := struct { + GaugeMeasuremets []string `json:"gaugeMeasurements"` + }{ + GaugeMeasuremets: gids, + } + return &summary, err +} + +func (gm *GaugeMeasurement) doForGM( + ctx context.Context, + gauges []models.GaugeMeasurement, + conn *sql.Conn, + feedback Feedback, +) ([]string, error) { + start := time.Now() + + client := nts.NewINtSMessageService(gm.URL, gm.Insecure, nil) + + var idPairs []*nts.Id_pair + for _, g := range gauges { + isrs := g.Gauge.String() + isrsID := nts.Isrs_code_type(isrs) + idPairs = append(idPairs, &nts.Id_pair{ + Id: &isrsID, + }) + } + mt := nts.Message_type_typeWRM + req := &nts.Get_messages_query{ + Message_type: &mt, + Ids: idPairs, + } + resp, err := client.Get_messages(req) + if err != nil { + feedback.Error("%v", err) + return nil, err + } + + if resp.Result_message == nil { + err := errors.New("no gauge measurements found") + for i, e := range resp.Result_error { + feedback.Error("%d: %v", i, e) + } + return nil, err + } + + result := resp.Result_message + + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + insertStmt, err := tx.PrepareContext(ctx, insertGMSQL) + if err != nil { + return nil, err + } + defer insertStmt.Close() + + var gid int64 + var gids []string + for _, msg := range result { + feedback.Info("Found %d gauges with measurements", len(msg.Wrm)) + for _, wrm := range msg.Wrm { + currIsrs, err := models.IsrsFromString(string(*wrm.Geo_object.Id)) + if err != nil { + feedback.Warn("Invalid ISRS code %v", err) + continue + } + for _, measure := range wrm.Measure { + isWaterlevel := *measure.Measure_code == nts.Measure_code_enumWAL + err = insertStmt.QueryRowContext( + ctx, + currIsrs.CountryCode, + currIsrs.LoCode, + currIsrs.FairwaySection, + currIsrs.Orc, + currIsrs.Hectometre, + measure.Measuredate, + msg.Identification.From, + msg.Identification.Language_code, + msg.Identification.Date_issue, + measure.Value, + measure.Predicted, + isWaterlevel, + measure.Value_min, + measure.Value_max, + msg.Identification.Date_issue, + msg.Identification.Originator, + ).Scan(&gid) + if err != nil { + return nil, err + } + } + feedback.Info("Inserted %d measurements for %s", + len(wrm.Measure), currIsrs) + gids = append(gids, currIsrs.String()) + } + } + feedback.Info("Storing gauge measurements took %s", time.Since(start)) + if err = tx.Commit(); err == nil { + feedback.Info("Import of gauge measurements was successful") + } + + return gids, nil +}
--- a/pkg/imports/polygon.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/imports/polygon.go Sat Dec 29 16:07:40 2018 +0100 @@ -50,11 +50,18 @@ func toPolygon(numParts int32, parts []int32, points []shp.Point) polygon { out := make(polygon, numParts) - pos := 0 + var pos int32 + for i := range out { - ps := parts[i] - line := make(lineString, ps) - for j := int32(0); j < ps; j, pos = j+1, pos+1 { + var howMany int32 + if i+1 >= len(parts) { + howMany = int32(len(points)) - pos + } else { + howMany = parts[i+1] - parts[i] + } + + line := make(lineString, howMany) + for j := int32(0); j < howMany; j, pos = j+1, pos+1 { p := &points[pos] line[j] = point{p.X, p.Y} }
--- a/pkg/imports/queue.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/imports/queue.go Sat Dec 29 16:07:40 2018 +0100 @@ -66,6 +66,8 @@ // JobCreator is used to bring a job to life as it is stored // in pure meta-data form to the database. JobCreator interface { + // Description is the long name of the import. + Description() string // Create build the actual job. // kind is the name of the import type. // data is a free form string to pass arguments to the creation @@ -83,10 +85,12 @@ } idJob struct { - id int64 - kind JobKind - user string - data string + id int64 + kind JobKind + user string + sendEmail bool + autoAccept bool + data string } ) @@ -128,11 +132,15 @@ INSERT INTO waterway.imports ( kind, username, + send_email, + auto_accept, data ) VALUES ( $1, $2, - $3 + $3, + $4, + $5 ) RETURNING id` selectJobSQL = ` @@ -140,6 +148,8 @@ id, kind, username, + send_email, + auto_accept, data FROM waterway.imports WHERE state = 'queued'::waterway.import_state AND enqueued IN ( @@ -193,6 +203,18 @@ return iqueue.importKindNames() } +// HasImportKind checks if the import queue supports a given kind. +func HasImportKindName(kind string) bool { + return iqueue.hasImportKindName(kind) +} + +// +func (q *importQueue) hasImportKindName(kind string) bool { + q.creatorsMu.Lock() + defer q.creatorsMu.Unlock() + return q.creators[JobKind(kind)] != nil +} + // RegisterJobCreator adds a JobCreator to the global import queue. // This a good candidate to be called in a init function for // a particular JobCreator. @@ -220,11 +242,23 @@ return q.creators[kind] } -func (q *importQueue) addJob(kind JobKind, user, data string) (int64, error) { +func (q *importQueue) addJob( + kind JobKind, + user string, + sendEmail, autoAccept bool, + data string, +) (int64, error) { ctx := context.Background() var id int64 err := auth.RunAs(ctx, queueUser, func(conn *sql.Conn) error { - return conn.QueryRowContext(ctx, insertJobSQL, string(kind), user, data).Scan(&id) + return conn.QueryRowContext( + ctx, + insertJobSQL, + string(kind), + user, + sendEmail, + autoAccept, + data).Scan(&id) }) if err == nil { select { @@ -238,8 +272,8 @@ // AddJob adds a job to the global import queue to be executed // as soon as possible. This is gone in a separate Go routine // so this will not block. -func AddJob(kind JobKind, user, data string) (int64, error) { - return iqueue.addJob(kind, user, data) +func AddJob(kind JobKind, user string, sendEmail, autoAccept bool, data string) (int64, error) { + return iqueue.addJob(kind, user, sendEmail, autoAccept, data) } type logFeedback int64 @@ -321,7 +355,13 @@ } defer tx.Rollback() if err = tx.QueryRowContext(ctx, selectJobSQL, &kinds).Scan( - &ji.id, &ji.kind, &ji.user, &ji.data); err != nil { + &ji.id, + &ji.kind, + &ji.user, + &ji.sendEmail, + &ji.autoAccept, + &ji.data, + ); err != nil { return err } _, err = tx.ExecContext(ctx, updateStateSQL, "running", ji.id) @@ -468,15 +508,22 @@ } var state string - if errDo != nil || errCleanup != nil { + switch { + case errDo != nil || errCleanup != nil: state = "failed" - } else { + case idj.autoAccept: + state = "accepted" + default: state = "pending" } if err := updateStateSummary(ctx, idj.id, state, summary); err != nil { log.Printf("setting state of job %d failed: %v\n", idj.id, err) } + // TODO: Send email if sendEmail is set. log.Printf("import #%d finished: %s\n", idj.id, state) + if idj.sendEmail { + go sendNotificationMail(idj.user, jc.Description(), state, idj.id) + } }(jc, idj) } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/imports/scheduled.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,100 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package imports + +import ( + "log" + + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/scheduler" +) + +func init() { + registerAction(GMJobKind, func(cfg *IDConfig) interface{} { + log.Println("info: schedule 'gm' import") + return &GaugeMeasurement{ + URL: *cfg.URL, + Insecure: false, + } + }) + registerAction(FAJobKind, func(cfg *IDConfig) interface{} { + log.Println("info: schedule 'fa' import") + return &FairwayAvailability{ + URL: *cfg.URL, + Insecure: false, + } + }) + registerAction(BNJobKind, func(cfg *IDConfig) interface{} { + log.Println("info: schedule 'bn' import") + return &Bottleneck{ + URL: *cfg.URL, + Insecure: false, + } + }) + registerAction(WXJobKind, func(cfg *IDConfig) interface{} { + log.Println("info: schedule 'wx' import") + // TODO: Take this from configuration. + var ( + featureType = "ws-wamos:ienc_wtwaxs" + sortBy = "hydro_scamin" + ) + return &WaterwayAxis{ + URL: *cfg.URL, + FeatureType: featureType, + SortBy: sortBy, + } + }) +} + +func registerAction(kind JobKind, setup func(cfg *IDConfig) interface{}) { + + action := func(id int64) { + cfg, err := loadIDConfig(id) + if err != nil { + log.Printf("error: %v\n", err) + return + } + if cfg == nil { + log.Printf("error: No config found for id %d.\n", id) + return + } + if cfg.URL == nil { + log.Println("error: No URL specified") + return + } + + what := setup(cfg) + + var serialized string + if serialized, err = common.ToJSONString(what); err != nil { + log.Printf("error: %v\n", err) + return + } + + var jobID int64 + if jobID, err = AddJob( + kind, + cfg.User, + cfg.SendEMail, cfg.AutoAccept, + serialized, + ); err != nil { + log.Printf("error: %v\n", err) + return + } + + log.Printf("info: added import #%d to queue\n", jobID) + } + + scheduler.RegisterAction(string(kind), action) +}
--- a/pkg/imports/sr.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/imports/sr.go Sat Dec 29 16:07:40 2018 +0100 @@ -21,7 +21,6 @@ "crypto/sha1" "database/sql" "encoding/hex" - "encoding/json" "errors" "fmt" "io" @@ -74,9 +73,13 @@ RegisterJobCreator(SRJobKind, srJobCreator{}) } +func (srJobCreator) Description() string { + return "sounding results" +} + func (srJobCreator) Create(_ JobKind, data string) (Job, error) { sr := new(SoundingResult) - if err := sr.FromString(data); err != nil { + if err := common.FromJSONString(data, sr); err != nil { return nil, err } return sr, nil @@ -84,9 +87,9 @@ func (srJobCreator) Depends() []string { return []string{ - "waterway.sounding_results", - "waterway.sounding_results_contour_lines", - "waterway.bottlenecks", + "sounding_results", + "sounding_results_contour_lines", + "bottlenecks", } } @@ -115,7 +118,7 @@ point_cloud, area ) VALUES ( - (SELECT bottleneck_id from waterway.bottlenecks where objnam = $1), + (SELECT id from waterway.bottlenecks where objnam = $1), $2::date, $3, ST_Transform(ST_GeomFromWKB($4, $6::integer), 4326)::geography, @@ -169,21 +172,6 @@ ` ) -// FromString revives a SoundingResult import from a string. -func (sr *SoundingResult) FromString(data string) error { - return json.NewDecoder(strings.NewReader(data)).Decode(sr) -} - -// ToString serializes a SoundingResult import into a string to -// be revived by FromString. -func (sr *SoundingResult) ToString() (string, error) { - var b strings.Builder - if err := json.NewEncoder(&b).Encode(sr); err != nil { - return "", err - } - return b.String(), nil -} - // Do executes the actual sounding result import. func (sr *SoundingResult) Do( ctx context.Context, @@ -213,10 +201,15 @@ return nil, common.ToError(err) } - feedback.Info("Looking for '*.xyz'") - xyzf := common.FindInZIP(z, ".xyz") + var xyzf *zip.File + for _, ext := range []string{".xyz", ".txt"} { + feedback.Info("Looking for '*%s'", ext) + if xyzf = common.FindInZIP(z, ext); xyzf != nil { + break + } + } if xyzf == nil { - return nil, errors.New("Cannot find any *.xyz file") + return nil, errors.New("Cannot find any *.xyz or *.txt file") } xyz, err := loadXYZ(xyzf, feedback)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/imports/wx.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,291 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package imports + +import ( + "bytes" + "context" + "database/sql" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "strings" + "time" + + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/wfs" +) + +type WaterwayAxis struct { + URL string `json:"url"` + FeatureType string `json:"feature-type"` + SortBy string `json:"sort-by"` +} + +const WXJobKind JobKind = "wx" + +type wxJobCreator struct{} + +func init() { + RegisterJobCreator(WXJobKind, wxJobCreator{}) +} + +func (wxJobCreator) Description() string { + return "waterway axis" +} + +func (wxJobCreator) Create(_ JobKind, data string) (Job, error) { + wx := new(WaterwayAxis) + if err := common.FromJSONString(data, wx); err != nil { + return nil, err + } + return wx, nil +} + +func (wxJobCreator) Depends() []string { + return []string{ + "waterway_axis", + } +} + +// StageDone is a NOP for waterway axis imports. +func (wxJobCreator) StageDone(context.Context, *sql.Tx, int64) error { + return nil +} + +// CleanUp for waterway imports is a NOP. +func (*WaterwayAxis) CleanUp() error { return nil } + +type waterwayAxisProperties struct { + ObjNam string `json:"hydro_objnam"` + NObjNnm *string `json:"hydro_nobjnm"` +} + +type line [][]float64 + +const wkbLineString uint32 = 2 + +func (l line) asWKB() []byte { + + size := 1 + 4 + 4 + len(l)*(2*8) + + buf := bytes.NewBuffer(make([]byte, 0, size)) + + binary.Write(buf, binary.LittleEndian, wkbNDR) + binary.Write(buf, binary.LittleEndian, wkbLineString) + binary.Write(buf, binary.LittleEndian, uint32(len(l))) + + for _, c := range l { + var lat, lon float64 + if len(c) > 0 { + lat = c[0] + } + if len(c) > 1 { + lon = c[1] + } + binary.Write(buf, binary.LittleEndian, math.Float64bits(lat)) + binary.Write(buf, binary.LittleEndian, math.Float64bits(lon)) + } + + return buf.Bytes() +} + +const ( + deleteWaterwayAxisSQL = `DELETE FROM waterway.waterway_axis` + insertWaterwayAxisSQL = ` +INSERT INTO waterway.waterway_axis (wtwaxs, objnam, nobjnam) +VALUES ( + ST_Transform(ST_GeomFromWKB($1, $2::integer), 4326)::geography, + $3, + $4 +)` +) + +// Do executes the actual waterway exis import. +func (wx *WaterwayAxis) Do( + ctx context.Context, + importID int64, + conn *sql.Conn, + feedback Feedback, +) (interface{}, error) { + + start := time.Now() + + feedback.Info("Import waterway axis") + + feedback.Info("Loading capabilities from %s", wx.URL) + caps, err := wfs.GetCapabilities(wx.URL) + if err != nil { + feedback.Error("Loading capabilities failed: %v", err) + return nil, err + } + + ft := caps.FindFeatureType(wx.FeatureType) + if ft == nil { + return nil, fmt.Errorf("Unknown feature type '%s'", wx.FeatureType) + } + + epsg, err := wfs.CRSToEPSG(ft.DefaultCRS) + if err != nil { + feedback.Error("Unsupported CRS name '%s'", ft.DefaultCRS) + return nil, err + } + + urls, err := wfs.GetFeaturesGET( + caps, wx.FeatureType, "application/json", wx.SortBy) + if err != nil { + feedback.Error("Cannot create GetFeature URLs. %v", err) + return nil, err + } + + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + insertStmt, err := tx.PrepareContext(ctx, insertWaterwayAxisSQL) + if err != nil { + return nil, err + } + defer insertStmt.Close() + + // Delete the old features. + if _, err := tx.ExecContext(ctx, deleteWaterwayAxisSQL); err != nil { + return nil, err + } + + var ( + unsupportedTypes = map[string]int{} + missingProperties int + badProperties int + features int + ) + + if err := wfs.DownloadURLs(urls, func(r io.Reader) error { + rfc, err := wfs.ParseRawFeatureCollection(r) + if err != nil { + return err + } + if rfc.CRS != nil { + crsName := rfc.CRS.Properties.Name + if epsg, err = wfs.CRSToEPSG(crsName); err != nil { + feedback.Error("Unsupported CRS: %d", crsName) + return err + } + } + + // No features -> ignore. + if rfc.Features == nil { + return nil + } + + feedback.Info("Using EPSG: %d", epsg) + + for _, feature := range rfc.Features { + if feature.Properties == nil || feature.Geometry.Coordinates == nil { + missingProperties++ + continue + } + + var props waterwayAxisProperties + + if err := json.Unmarshal(*feature.Properties, &props); err != nil { + badProperties++ + continue + } + + var nobjnam sql.NullString + if props.NObjNnm != nil { + nobjnam = sql.NullString{String: *props.NObjNnm, Valid: true} + } + + switch feature.Geometry.Type { + case "LineString": + var l line + if err := json.Unmarshal(*feature.Geometry.Coordinates, &l); err != nil { + return err + } + if _, err := insertStmt.ExecContext( + ctx, + l.asWKB(), + epsg, + props.ObjNam, + nobjnam, + ); err != nil { + return err + } + features++ + case "MultiLineString": + var ls []line + if err := json.Unmarshal(*feature.Geometry.Coordinates, &ls); err != nil { + return err + } + for _, l := range ls { + if _, err := insertStmt.ExecContext( + ctx, + l.asWKB(), + epsg, + props.ObjNam, + nobjnam, + ); err != nil { + return err + } + features++ + } + default: + unsupportedTypes[feature.Geometry.Type]++ + } + } + return nil + }); err != nil { + feedback.Error("Downloading features failed: %v", err) + return nil, err + } + + if features == 0 { + err := errors.New("No features found") + feedback.Error("%v", err) + return nil, err + } + + if badProperties > 0 { + feedback.Warn("Bad properties: %d", badProperties) + } + + if missingProperties > 0 { + feedback.Warn("Missing properties: %d", missingProperties) + } + + if len(unsupportedTypes) != 0 { + var b strings.Builder + for t, c := range unsupportedTypes { + if b.Len() > 0 { + b.WriteString(", ") + } + b.WriteString(fmt.Sprintf("%s: %d", t, c)) + } + feedback.Warn("Unsupported types found: %s", b.String()) + } + + if err = tx.Commit(); err == nil { + feedback.Info("Storing %d features took %s", + features, time.Since(start)) + } + + return nil, err +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/models/bn.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,25 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package models + +type BottleneckImport struct { + URL string `json:"url"` + Insecure bool `json:"insecure"` + SendEmail bool `json:"send-email"` +} + +type Bottleneck struct { + ID string + ResponsibleCountry string +}
--- a/pkg/models/cross.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/models/cross.go Sat Dec 29 16:07:40 2018 +0100 @@ -155,7 +155,7 @@ func (lc GeoJSONLineCoordinates) AsWKB() []byte { - size := 1 + 4 + 4 + len(lc)*(1+4+2*8) + size := 1 + 4 + 4 + len(lc)*(2*8) buf := bytes.NewBuffer(make([]byte, 0, size))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/models/fa.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,21 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Raimund Renkert <raimund.renkert@intevation.de> + +package models + +// FairwayAvailabilityImport contains data used to define the endpoint +type FairwayAvailabilityImport struct { + URL string `json:"url"` + Insecure bool `json:"insecure"` + SendEmail bool `json:"send-email"` +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/models/gauge.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,72 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Raimund Renkert <raimund.renkert@intevation.de> + +package models + +import ( + "errors" + "fmt" + "strconv" + "time" +) + +// GaugeMeasurementImport contains data used to define the endpoint +type GaugeMeasurementImport struct { + URL string `json:"url"` + Insecure bool `json:"insecure"` + SendEmail bool `json:"send-email"` +} + +// GaugeMeasurement holds information about a gauge and the latest measurement +type GaugeMeasurement struct { + Gauge Isrs + LatestDateIssue time.Time +} + +// Isrs represents the gauge identification data structure +type Isrs struct { + CountryCode string + LoCode string + FairwaySection string + Orc string + Hectometre int +} + +// IsrsFromString converts string representation of isrs code to type Isrs +func IsrsFromString(isrsCode string) (*Isrs, error) { + if len(isrsCode) < 20 { + return nil, errors.New("ISRS code too short") + } + hm, err := strconv.Atoi(isrsCode[15:20]) + if err != nil { + return nil, err + } + isrs := Isrs{ + CountryCode: isrsCode[0:2], + LoCode: isrsCode[2:5], + FairwaySection: isrsCode[5:10], + Orc: isrsCode[10:15], + Hectometre: hm, + } + return &isrs, nil +} + +// String creates a isrs code string from Isrs +func (isrs *Isrs) String() string { + return fmt.Sprintf("%s%s%s%s%05d", + isrs.CountryCode, + isrs.LoCode, + isrs.FairwaySection, + isrs.Orc, + isrs.Hectometre) +}
--- a/pkg/models/import.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/models/import.go Sat Dec 29 16:07:40 2018 +0100 @@ -61,7 +61,7 @@ } func (it ImportTime) MarshalJSON() ([]byte, error) { - return json.Marshal(it.Format("2006-01-02T15:04:05")) + return json.Marshal(it.Format("2006-01-02T15:04:05.000")) } func (it *ImportTime) Scan(x interface{}) error {
--- a/pkg/models/sr.go Sat Dec 29 16:06:54 2018 +0100 +++ b/pkg/models/sr.go Sat Dec 29 16:07:40 2018 +0100 @@ -46,7 +46,7 @@ checkBottleneckDateUniqueSQL = ` SELECT true FROM waterway.sounding_results sr JOIN - waterway.bottlenecks bn ON sr.bottleneck_id = bn.bottleneck_id + waterway.bottlenecks bn ON sr.bottleneck_id = bn.id WHERE bn.objnam = $1 AND sr.date_info = $2` )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/models/waterway.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,21 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package models + +type WaterwayAxisImport struct { + URL string `json:"url"` + FeatureType string `json:"feature-type"` + SortBy string `json:"sort-by"` + SendEmail bool `json:"send-email"` +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/scheduler/boot.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,105 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package scheduler + +import ( + "context" + "database/sql" + "log" + + "gemma.intevation.de/gemma/pkg/auth" + "gemma.intevation.de/gemma/pkg/config" +) + +const ( + bootRole = "sys_admin" + + selectImportConfSQL = ` +SELECT id, username, kind, cron +FROM waterway.import_configuration +WHERE cron IS NOT NULL` + + scheduledIDsSQL = ` +SELECT id from waterway.import_configuration +WHERE username = $1 AND cron IS NOT NULL` +) + +func init() { go boot() } + +// boot starts the scheduler with the configurations from +// the database which have a schedule. +func boot() { + config.WaitReady() + log.Println("info: booting scheduler from database.") + ctx := context.Background() + err := auth.RunAs( + ctx, bootRole, + func(conn *sql.Conn) error { + rows, err := conn.QueryContext(ctx, selectImportConfSQL) + if err != nil { + return err + } + defer rows.Close() + err = BootActions(func(ba *BoundAction) (bool, error) { + if err != nil { + return false, err + } + if !rows.Next() { + return false, nil + } + var id int64 + if err = rows.Scan( + &id, + &ba.Name, + &ba.Spec, + ); err != nil { + return false, err + } + ba.CfgID = id + return true, nil + }) + if err != nil { + return err + } + return rows.Err() + }) + if err != nil { + log.Printf("error: %v\n", err) + } +} + +// ScheduledUserIDs returns the IDs with a schedule for a given user. +func ScheduledUserIDs( + ctx context.Context, + conn *sql.Conn, + user string, +) (map[int64]struct{}, error) { + ids := map[int64]struct{}{} + rows, err := conn.QueryContext(ctx, scheduledIDsSQL, user) + if err != nil { + return nil, nil + } + defer rows.Close() + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + ids[id] = struct{}{} + } + if err := rows.Err(); err != nil { + return nil, err + } + return ids, nil +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/scheduler/scheduler.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,305 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package scheduler + +import ( + "errors" + "log" + "sync" + + "github.com/robfig/cron" +) + +// ErrNoSuchAction if no fitting action was found. +var ErrNoSuchAction = errors.New("No such action") + +// Action is called with a configuration id. +type Action func(cfgID int64) + +type userAction struct { + scheduler *scheduler + name string + cfgID int64 +} + +type scheduler struct { + cr *cron.Cron + actions map[string]Action + mu sync.Mutex +} + +// Run implements cron.Job. +func (ua *userAction) Run() { + if a := ua.scheduler.action(ua.name); a != nil { + a(ua.cfgID) + } else { + log.Printf("warn: scheduled action '%s' not found.", ua.name) + } +} + +var global = scheduler{ + cr: cron.New(), + actions: make(map[string]Action), +} + +// RegisterAction registers a named action to the global scheduler. +func RegisterAction(name string, action Action) { + global.registerAction(name, action) +} + +// UnregisterAction ungesiters a named action from the global scheduler. +func UnregisterAction(name string) { + global.unregisterAction(name) +} + +// BoundAction is a complete set of infos for +// an action to be bound to a schedule and +// configuration id. +type BoundAction struct { + Name string + Spec string + CfgID int64 +} + +// BootActions setup the global scheduler with a set +// of bound actions delivered by the next function. +func BootActions(next func(*BoundAction) (bool, error)) error { + return global.bootActions(next) +} + +func (s *scheduler) bootActions(next func(*BoundAction) (bool, error)) error { + + cr := cron.New() + + for { + var ba BoundAction + ok, err := next(&ba) + if err != nil { + return err + } + if !ok { + break + } + schedule, err := cron.Parse(ba.Spec) + if err != nil { + return err + } + job := &userAction{ + scheduler: s, + cfgID: ba.CfgID, + } + cr.Schedule(schedule, job) + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.cr.Stop() + s.cr = cr + cr.Start() + + return nil +} + +// BindAction binds a named action to a cron spec and +// a configuration id. +func BindAction(name, spec string, cfgID int64) error { + return global.bindAction(name, spec, cfgID) +} + +// UnbindAction unbinds a named action from a user and +// a configuration id. +func UnbindAction(name string, cfgID int64) { + global.unbindAction(name, cfgID) +} + +// UnbindByID unbinds all schedules with a given id. +func UnbindByID(cfgID int64) { + global.unbindByID(cfgID) +} + +// UnbindUser unbinds all schedules for a given user. +func UnbindByIDs(ids map[int64]struct{}) { + global.unbindByIDs(ids) +} + +// HasAction asks if there is an action with a given name. +func HasAction(name string) bool { + return global.hasAction(name) +} + +func (s *scheduler) hasAction(name string) bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.actions[name] != nil +} + +func (s *scheduler) unbindByIDs(ids map[int64]struct{}) { + s.mu.Lock() + defer s.mu.Unlock() + + entries := s.cr.Entries() + + if len(entries) == 0 { + return + } + + var found bool + for _, entry := range entries { + ua := entry.Job.(*userAction) + if _, found = ids[ua.cfgID]; found { + break + } + } + if !found { + return + } + + s.cr.Stop() + s.cr = cron.New() + for _, entry := range entries { + ua := entry.Job.(*userAction) + if _, found := ids[ua.cfgID]; !found { + s.cr.Schedule(entry.Schedule, entry.Job) + } + } + s.cr.Start() +} + +func (s *scheduler) unbindByID(cfgID int64) { + s.mu.Lock() + defer s.mu.Unlock() + + entries := s.cr.Entries() + + var found bool + for _, entry := range entries { + ua := entry.Job.(*userAction) + if ua.cfgID == cfgID { + found = true + break + } + } + + if !found { + return + } + + s.cr.Stop() + s.cr = cron.New() + for _, entry := range entries { + ua := entry.Job.(*userAction) + if ua.cfgID != cfgID { + s.cr.Schedule(entry.Schedule, entry.Job) + } + } + s.cr.Start() +} + +func (s *scheduler) unbindAction(name string, cfgID int64) { + s.mu.Lock() + defer s.mu.Unlock() + + entries := s.cr.Entries() + + var found *userAction + for _, entry := range entries { + ua := entry.Job.(*userAction) + if ua.name == name && cfgID == ua.cfgID { + // Already have such a action/cfg tuple -> re-schedule. + found = ua + break + } + } + + if found == nil { + return + } + + s.cr.Stop() + s.cr = cron.New() + for _, entry := range entries { + ua := entry.Job.(*userAction) + if ua != found { + s.cr.Schedule(entry.Schedule, entry.Job) + } + } + s.cr.Start() +} + +func (s *scheduler) bindAction(name, spec string, cfgID int64) error { + + schedule, err := cron.Parse(spec) + if err != nil { + return err + } + + s.mu.Lock() + defer s.mu.Unlock() + + entries := s.cr.Entries() + + var found *userAction + for _, entry := range entries { + ua := entry.Job.(*userAction) + if ua.name == name && cfgID == ua.cfgID { + // Already have such a user/action/cfg tuple -> re-schedule. + found = ua + break + } + } + + if found == nil { + // Add to current plan. + job := &userAction{scheduler: s, name: name, cfgID: cfgID} + s.cr.Schedule(schedule, job) + } else { + // If found re-build all. + s.cr.Stop() + s.cr = cron.New() + for _, entry := range entries { + ua := entry.Job.(*userAction) + var sch cron.Schedule + if found == ua { + // replace with new schedule. + sch = schedule + } else { + sch = entry.Schedule + } + s.cr.Schedule(sch, entry.Job) + } + } + s.cr.Start() + + return nil +} + +func (s *scheduler) action(name string) Action { + s.mu.Lock() + defer s.mu.Unlock() + return s.actions[name] +} + +func (s *scheduler) registerAction(name string, action Action) { + log.Printf("info: register action '%s' in scheduler.", name) + s.mu.Lock() + defer s.mu.Unlock() + s.actions[name] = action +} + +func (s *scheduler) unregisterAction(name string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.actions, name) +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/soap/ifaf/service.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,935 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Raimund Renkert <raimund.renkert@intevation.de> + +package ifaf + +import ( + "crypto/tls" + "encoding/xml" + "time" + + "github.com/jackc/pgx/pgtype" + + "gemma.intevation.de/gemma/pkg/soap" +) + +// against "unused imports" +var _ time.Time +var _ xml.Name + +type Get_bottleneck_fa struct { + XMLName xml.Name `xml:"http://www.ris.eu/fairwayavailability/3.0 get_bottleneck_fa"` + + Bottleneck_id *ArrayOfString `xml:"bottleneck_id,omitempty"` + + Period *RequestedPeriod `xml:"period,omitempty"` +} + +type Get_bottleneck_faResponse struct { + XMLName xml.Name `xml:"http://www.ris.eu/fairwayavailability/3.0 get_bottleneck_faResponse"` + + Get_bottleneck_faResult *ArrayOfFairwayAvailability `xml:"get_bottleneck_faResult,omitempty"` +} + +type Get_stretch_fa struct { + XMLName xml.Name `xml:"http://www.ris.eu/fairwayavailability/3.0 get_stretch_fa"` + + ISRS *ArrayOfISRSPair `xml:"ISRS,omitempty"` + + Period *RequestedPeriod `xml:"period,omitempty"` +} + +type Get_stretch_faResponse struct { + XMLName xml.Name `xml:"http://www.ris.eu/fairwayavailability/3.0 get_stretch_faResponse"` + + Get_stretch_faResult *ArrayOfFairwayAvailability `xml:"get_stretch_faResult,omitempty"` +} + +type ArrayOfFairwayAvailability struct { + FairwayAvailability []*FairwayAvailability `xml:"FairwayAvailability,omitempty"` +} + +type FairwayAvailability struct { + Bottleneck_id string `xml:"bottleneck_id,omitempty"` + + SURDAT time.Time `xml:"SURDAT,omitempty"` + + POSITION *PositionEnum `xml:"POSITION,omitempty"` + + Reference_values *ArrayOfReferenceValue `xml:"Reference_values,omitempty"` + + AdditionalData *ArrayOfKeyValuePair `xml:"AdditionalData,omitempty"` + + Critical bool `xml:"Critical,omitempty"` + + Bottleneck_PDFs *ArrayOfPdfInfo `xml:"Bottleneck_PDFs,omitempty"` + + Effective_fairway_availability *ArrayOfEffectiveFairwayAvailability `xml:"Effective_fairway_availability,omitempty"` + + Date_Info time.Time `xml:"Date_Info,omitempty"` + + Source string `xml:"Source,omitempty"` +} + +type ArrayOfPdfInfo struct { + PdfInfo []*PdfInfo `xml:"PdfInfo,omitempty"` +} + +type PdfInfo struct { + ProfilePdfFilename string `xml:"ProfilePdfFilename,omitempty"` + + ProfilePdfURL string `xml:"ProfilePdfURL,omitempty"` + + PDF_Generation_Date time.Time `xml:"PDF_Generation_Date,omitempty"` + + Source string `xml:"Source,omitempty"` +} + +type ArrayOfEffectiveFairwayAvailability struct { + EffectiveFairwayAvailability []*EffectiveFairwayAvailability `xml:"EffectiveFairwayAvailability,omitempty"` +} + +type EffectiveFairwayAvailability struct { + Available_depth_value int32 `xml:"Available_depth_value,omitempty"` + + Available_width_value int32 `xml:"Available_width_value,omitempty"` + + Water_level_value int32 `xml:"Water_level_value,omitempty"` + + Measure_date time.Time `xml:"Measure_date,omitempty"` + + Measure_type *MeasureType `xml:"Measure_type,omitempty"` + + Source string `xml:"Source,omitempty"` + + Level_of_Service *LosEnum `xml:"Level_of_Service,omitempty"` + + Forecast_generation_time pgtype.Timestamp `xml:"Forecast_generation_time,omitempty"` + + Value_lifetime time.Time `xml:"Value_lifetime,omitempty"` +} + +type ArrayOfReferenceValue struct { + ReferenceValue []*ReferenceValue `xml:"ReferenceValue,omitempty"` +} + +type ReferenceValue struct { + Fairway_depth int32 `xml:"fairway_depth,omitempty"` + + Fairway_width int32 `xml:"fairway_width,omitempty"` + + Fairway_radius int32 `xml:"fairway_radius,omitempty"` + + Shallowest_spot_Lat float64 `xml:"Shallowest_spot_Lat,omitempty"` + + Shallowest_spot_Lon float64 `xml:"Shallowest_spot_Lon,omitempty"` + + Level_of_Service *LosEnum `xml:"Level_of_Service,omitempty"` +} + +type ErrorCode string + +const ( + + // <summary> Description: message type not supported, Explanation: + // web service does not support the requested message type + // </summary> + ErrorCodeE010 ErrorCode = "e010" + + // <summary> Description: syntax error in request, Explanation: + // request violates the schema for requests </summary> + ErrorCodeE100 ErrorCode = "e100" + + // <summary> Description: incorrect message type, Explanation: given + // message type is not known </summary> + ErrorCodeE110 ErrorCode = "e110" + + // <summary> Description: incorrect type-specific parameters, + // Explanation: type-specific parameters are erroneous </summary> + ErrorCodeE120 ErrorCode = "e120" + + // <summary> Description: operation not known, Explanation: the + // requested operation is unknown </summary> + ErrorCodeE200 ErrorCode = "e200" + + // <summary> Description: requested method or operation is not + // implemented </summary> + ErrorCodeE210 ErrorCode = "e210" + + // <summary> Description: data source unavailable, Explanation: data + // source of the web service for is temporarily unavailable + // </summary> + ErrorCodeE300 ErrorCode = "e300" + + // <summary> Description: too many results for request, Explanation: + // server is unable to handle number of results </summary> + ErrorCodeE310 ErrorCode = "e310" + + // <summary> Description: unexpected or other error + // </summary> + ErrorCodeE999 ErrorCode = "e999" +) + +type CountryCode string + +const ( + CountryCodeAF CountryCode = "AF" + + CountryCodeAX CountryCode = "AX" + + CountryCodeAL CountryCode = "AL" + + CountryCodeDZ CountryCode = "DZ" + + CountryCodeAS CountryCode = "AS" + + CountryCodeAD CountryCode = "AD" + + CountryCodeAO CountryCode = "AO" + + CountryCodeAI CountryCode = "AI" + + CountryCodeAQ CountryCode = "AQ" + + CountryCodeAG CountryCode = "AG" + + CountryCodeAR CountryCode = "AR" + + CountryCodeAM CountryCode = "AM" + + CountryCodeAW CountryCode = "AW" + + CountryCodeAU CountryCode = "AU" + + CountryCodeAT CountryCode = "AT" + + CountryCodeAZ CountryCode = "AZ" + + CountryCodeBS CountryCode = "BS" + + CountryCodeBH CountryCode = "BH" + + CountryCodeBD CountryCode = "BD" + + CountryCodeBB CountryCode = "BB" + + CountryCodeBY CountryCode = "BY" + + CountryCodeBE CountryCode = "BE" + + CountryCodeBZ CountryCode = "BZ" + + CountryCodeBJ CountryCode = "BJ" + + CountryCodeBM CountryCode = "BM" + + CountryCodeBT CountryCode = "BT" + + CountryCodeBO CountryCode = "BO" + + CountryCodeBQ CountryCode = "BQ" + + CountryCodeBA CountryCode = "BA" + + CountryCodeBW CountryCode = "BW" + + CountryCodeBV CountryCode = "BV" + + CountryCodeBR CountryCode = "BR" + + CountryCodeIO CountryCode = "IO" + + CountryCodeBN CountryCode = "BN" + + CountryCodeBG CountryCode = "BG" + + CountryCodeBF CountryCode = "BF" + + CountryCodeBI CountryCode = "BI" + + CountryCodeCV CountryCode = "CV" + + CountryCodeKH CountryCode = "KH" + + CountryCodeCM CountryCode = "CM" + + CountryCodeCA CountryCode = "CA" + + CountryCodeKY CountryCode = "KY" + + CountryCodeCF CountryCode = "CF" + + CountryCodeTD CountryCode = "TD" + + CountryCodeCL CountryCode = "CL" + + CountryCodeCN CountryCode = "CN" + + CountryCodeCX CountryCode = "CX" + + CountryCodeCC CountryCode = "CC" + + CountryCodeCO CountryCode = "CO" + + CountryCodeKM CountryCode = "KM" + + CountryCodeCG CountryCode = "CG" + + CountryCodeCD CountryCode = "CD" + + CountryCodeCK CountryCode = "CK" + + CountryCodeCR CountryCode = "CR" + + CountryCodeCI CountryCode = "CI" + + CountryCodeHR CountryCode = "HR" + + CountryCodeCU CountryCode = "CU" + + CountryCodeCW CountryCode = "CW" + + CountryCodeCY CountryCode = "CY" + + CountryCodeCZ CountryCode = "CZ" + + CountryCodeDK CountryCode = "DK" + + CountryCodeDJ CountryCode = "DJ" + + CountryCodeDM CountryCode = "DM" + + CountryCodeDO CountryCode = "DO" + + CountryCodeEC CountryCode = "EC" + + CountryCodeEG CountryCode = "EG" + + CountryCodeSV CountryCode = "SV" + + CountryCodeGQ CountryCode = "GQ" + + CountryCodeER CountryCode = "ER" + + CountryCodeEE CountryCode = "EE" + + CountryCodeET CountryCode = "ET" + + CountryCodeFK CountryCode = "FK" + + CountryCodeFO CountryCode = "FO" + + CountryCodeFJ CountryCode = "FJ" + + CountryCodeFI CountryCode = "FI" + + CountryCodeFR CountryCode = "FR" + + CountryCodeGF CountryCode = "GF" + + CountryCodePF CountryCode = "PF" + + CountryCodeTF CountryCode = "TF" + + CountryCodeGA CountryCode = "GA" + + CountryCodeGM CountryCode = "GM" + + CountryCodeGE CountryCode = "GE" + + CountryCodeDE CountryCode = "DE" + + CountryCodeGH CountryCode = "GH" + + CountryCodeGI CountryCode = "GI" + + CountryCodeGR CountryCode = "GR" + + CountryCodeGL CountryCode = "GL" + + CountryCodeGD CountryCode = "GD" + + CountryCodeGP CountryCode = "GP" + + CountryCodeGU CountryCode = "GU" + + CountryCodeGT CountryCode = "GT" + + CountryCodeGG CountryCode = "GG" + + CountryCodeGN CountryCode = "GN" + + CountryCodeGW CountryCode = "GW" + + CountryCodeGY CountryCode = "GY" + + CountryCodeHT CountryCode = "HT" + + CountryCodeHM CountryCode = "HM" + + CountryCodeVA CountryCode = "VA" + + CountryCodeHN CountryCode = "HN" + + CountryCodeHK CountryCode = "HK" + + CountryCodeHU CountryCode = "HU" + + CountryCodeIS CountryCode = "IS" + + CountryCodeIN CountryCode = "IN" + + CountryCodeID CountryCode = "ID" + + CountryCodeIR CountryCode = "IR" + + CountryCodeIQ CountryCode = "IQ" + + CountryCodeIE CountryCode = "IE" + + CountryCodeIM CountryCode = "IM" + + CountryCodeIL CountryCode = "IL" + + CountryCodeIT CountryCode = "IT" + + CountryCodeJM CountryCode = "JM" + + CountryCodeJP CountryCode = "JP" + + CountryCodeJE CountryCode = "JE" + + CountryCodeJO CountryCode = "JO" + + CountryCodeKZ CountryCode = "KZ" + + CountryCodeKE CountryCode = "KE" + + CountryCodeKI CountryCode = "KI" + + CountryCodeKP CountryCode = "KP" + + CountryCodeKR CountryCode = "KR" + + CountryCodeKW CountryCode = "KW" + + CountryCodeKG CountryCode = "KG" + + CountryCodeLA CountryCode = "LA" + + CountryCodeLV CountryCode = "LV" + + CountryCodeLB CountryCode = "LB" + + CountryCodeLS CountryCode = "LS" + + CountryCodeLR CountryCode = "LR" + + CountryCodeLY CountryCode = "LY" + + CountryCodeLI CountryCode = "LI" + + CountryCodeLT CountryCode = "LT" + + CountryCodeLU CountryCode = "LU" + + CountryCodeMO CountryCode = "MO" + + CountryCodeMK CountryCode = "MK" + + CountryCodeMG CountryCode = "MG" + + CountryCodeMW CountryCode = "MW" + + CountryCodeMY CountryCode = "MY" + + CountryCodeMV CountryCode = "MV" + + CountryCodeML CountryCode = "ML" + + CountryCodeMT CountryCode = "MT" + + CountryCodeMH CountryCode = "MH" + + CountryCodeMQ CountryCode = "MQ" + + CountryCodeMR CountryCode = "MR" + + CountryCodeMU CountryCode = "MU" + + CountryCodeYT CountryCode = "YT" + + CountryCodeMX CountryCode = "MX" + + CountryCodeFM CountryCode = "FM" + + CountryCodeMD CountryCode = "MD" + + CountryCodeMC CountryCode = "MC" + + CountryCodeMN CountryCode = "MN" + + CountryCodeME CountryCode = "ME" + + CountryCodeMS CountryCode = "MS" + + CountryCodeMA CountryCode = "MA" + + CountryCodeMZ CountryCode = "MZ" + + CountryCodeMM CountryCode = "MM" + + CountryCodeNA CountryCode = "NA" + + CountryCodeNR CountryCode = "NR" + + CountryCodeNP CountryCode = "NP" + + CountryCodeNL CountryCode = "NL" + + CountryCodeNC CountryCode = "NC" + + CountryCodeNZ CountryCode = "NZ" + + CountryCodeNI CountryCode = "NI" + + CountryCodeNE CountryCode = "NE" + + CountryCodeNG CountryCode = "NG" + + CountryCodeNU CountryCode = "NU" + + CountryCodeNF CountryCode = "NF" + + CountryCodeMP CountryCode = "MP" + + CountryCodeNO CountryCode = "NO" + + CountryCodeOM CountryCode = "OM" + + CountryCodePK CountryCode = "PK" + + CountryCodePW CountryCode = "PW" + + CountryCodePS CountryCode = "PS" + + CountryCodePA CountryCode = "PA" + + CountryCodePG CountryCode = "PG" + + CountryCodePY CountryCode = "PY" + + CountryCodePE CountryCode = "PE" + + CountryCodePH CountryCode = "PH" + + CountryCodePN CountryCode = "PN" + + CountryCodePL CountryCode = "PL" + + CountryCodePT CountryCode = "PT" + + CountryCodePR CountryCode = "PR" + + CountryCodeQA CountryCode = "QA" + + CountryCodeRE CountryCode = "RE" + + CountryCodeRO CountryCode = "RO" + + CountryCodeRU CountryCode = "RU" + + CountryCodeRW CountryCode = "RW" + + CountryCodeBL CountryCode = "BL" + + CountryCodeSH CountryCode = "SH" + + CountryCodeKN CountryCode = "KN" + + CountryCodeLC CountryCode = "LC" + + CountryCodeMF CountryCode = "MF" + + CountryCodePM CountryCode = "PM" + + CountryCodeVC CountryCode = "VC" + + CountryCodeWS CountryCode = "WS" + + CountryCodeSM CountryCode = "SM" + + CountryCodeST CountryCode = "ST" + + CountryCodeSA CountryCode = "SA" + + CountryCodeSN CountryCode = "SN" + + CountryCodeRS CountryCode = "RS" + + CountryCodeSC CountryCode = "SC" + + CountryCodeSL CountryCode = "SL" + + CountryCodeSG CountryCode = "SG" + + CountryCodeSX CountryCode = "SX" + + CountryCodeSK CountryCode = "SK" + + CountryCodeSI CountryCode = "SI" + + CountryCodeSB CountryCode = "SB" + + CountryCodeSO CountryCode = "SO" + + CountryCodeZA CountryCode = "ZA" + + CountryCodeGS CountryCode = "GS" + + CountryCodeSS CountryCode = "SS" + + CountryCodeES CountryCode = "ES" + + CountryCodeLK CountryCode = "LK" + + CountryCodeSD CountryCode = "SD" + + CountryCodeSR CountryCode = "SR" + + CountryCodeSJ CountryCode = "SJ" + + CountryCodeSZ CountryCode = "SZ" + + CountryCodeSE CountryCode = "SE" + + CountryCodeCH CountryCode = "CH" + + CountryCodeSY CountryCode = "SY" + + CountryCodeTW CountryCode = "TW" + + CountryCodeTJ CountryCode = "TJ" + + CountryCodeTZ CountryCode = "TZ" + + CountryCodeTH CountryCode = "TH" + + CountryCodeTL CountryCode = "TL" + + CountryCodeTG CountryCode = "TG" + + CountryCodeTK CountryCode = "TK" + + CountryCodeTO CountryCode = "TO" + + CountryCodeTT CountryCode = "TT" + + CountryCodeTN CountryCode = "TN" + + CountryCodeTR CountryCode = "TR" + + CountryCodeTM CountryCode = "TM" + + CountryCodeTC CountryCode = "TC" + + CountryCodeTV CountryCode = "TV" + + CountryCodeUG CountryCode = "UG" + + CountryCodeUA CountryCode = "UA" + + CountryCodeAE CountryCode = "AE" + + CountryCodeGB CountryCode = "GB" + + CountryCodeUS CountryCode = "US" + + CountryCodeUM CountryCode = "UM" + + CountryCodeUY CountryCode = "UY" + + CountryCodeUZ CountryCode = "UZ" + + CountryCodeVU CountryCode = "VU" + + CountryCodeVE CountryCode = "VE" + + CountryCodeVN CountryCode = "VN" + + CountryCodeVG CountryCode = "VG" + + CountryCodeVI CountryCode = "VI" + + CountryCodeWF CountryCode = "WF" + + CountryCodeEH CountryCode = "EH" + + CountryCodeYE CountryCode = "YE" + + CountryCodeZM CountryCode = "ZM" + + CountryCodeZW CountryCode = "ZW" +) + +type CoverageEnum string + +const ( + CoverageEnumCrossProfiles CoverageEnum = "CrossProfiles" + + CoverageEnumLongitudinalProfiles CoverageEnum = "LongitudinalProfiles" + + CoverageEnumFairway CoverageEnum = "Fairway" + + CoverageEnumRiver CoverageEnum = "River" + + CoverageEnumRiverBanks CoverageEnum = "RiverBanks" +) + +type DepthReferenceEnum string + +const ( + DepthReferenceEnumNAP DepthReferenceEnum = "NAP" + + DepthReferenceEnumKP DepthReferenceEnum = "KP" + + DepthReferenceEnumFZP DepthReferenceEnum = "FZP" + + DepthReferenceEnumADR DepthReferenceEnum = "ADR" + + DepthReferenceEnumTAW DepthReferenceEnum = "TAW" + + DepthReferenceEnumPUL DepthReferenceEnum = "PUL" + + DepthReferenceEnumNGM DepthReferenceEnum = "NGM" + + DepthReferenceEnumETRS DepthReferenceEnum = "ETRS" + + DepthReferenceEnumPOT DepthReferenceEnum = "POT" + + DepthReferenceEnumLDC DepthReferenceEnum = "LDC" + + DepthReferenceEnumHDC DepthReferenceEnum = "HDC" + + DepthReferenceEnumZPG DepthReferenceEnum = "ZPG" + + DepthReferenceEnumGLW DepthReferenceEnum = "GLW" + + DepthReferenceEnumHSW DepthReferenceEnum = "HSW" + + DepthReferenceEnumLNW DepthReferenceEnum = "LNW" + + DepthReferenceEnumHNW DepthReferenceEnum = "HNW" + + DepthReferenceEnumIGN DepthReferenceEnum = "IGN" + + DepthReferenceEnumWGS DepthReferenceEnum = "WGS" + + DepthReferenceEnumRN DepthReferenceEnum = "RN" + + DepthReferenceEnumHBO DepthReferenceEnum = "HBO" +) + +type LimitingFactorEnum string + +const ( + LimitingFactorEnumDepth LimitingFactorEnum = "depth" + + LimitingFactorEnumWidth LimitingFactorEnum = "width" + + LimitingFactorEnumCurveRadius LimitingFactorEnum = "curveRadius" +) + +type LosEnum string + +const ( + LosEnumNotAvailable LosEnum = "NotAvailable" + + LosEnumLOS1 LosEnum = "LOS1" + + LosEnumLOS2 LosEnum = "LOS2" + + LosEnumLOS3 LosEnum = "LOS3" +) + +type MaterialEnum string + +const ( + MaterialEnumGravel MaterialEnum = "Gravel" + + MaterialEnumRocky MaterialEnum = "Rocky" + + MaterialEnumStone MaterialEnum = "Stone" + + MaterialEnumAndesite MaterialEnum = "Andesite" + + MaterialEnumSleazyAndesite MaterialEnum = "SleazyAndesite" + + MaterialEnumSandyGravel MaterialEnum = "SandyGravel" + + MaterialEnumMarl MaterialEnum = "Marl" + + MaterialEnumSand MaterialEnum = "Sand" + + MaterialEnumSarmatianLimestone MaterialEnum = "SarmatianLimestone" + + MaterialEnumSandstonePeaks MaterialEnum = "SandstonePeaks" + + MaterialEnumRoughSandyGravel MaterialEnum = "RoughSandyGravel" +) + +type MeasureType string + +const ( + MeasureTypeMeasured MeasureType = "Measured" + + MeasureTypeForecasted MeasureType = "Forecasted" + + MeasureTypeMinimumGuaranteed MeasureType = "MinimumGuaranteed" +) + +type PositionEnum string + +const ( + PositionEnumRedBuoy PositionEnum = "RedBuoy" + + PositionEnumGreenBuoy PositionEnum = "GreenBuoy" + + PositionEnumRightBank PositionEnum = "RightBank" + + PositionEnumLeftBank PositionEnum = "LeftBank" + + PositionEnumMiddle PositionEnum = "Middle" + + PositionEnumAll PositionEnum = "All" +) + +type SurtypEnum string + +const ( + SurtypEnumMultibeam SurtypEnum = "Multibeam" + + SurtypEnumSinglebeam SurtypEnum = "Singlebeam" + + SurtypEnumADCP SurtypEnum = "ADCP" + + SurtypEnumInspectionTour SurtypEnum = "InspectionTour" +) + +type Error struct { + Detail string `xml:"detail,omitempty"` + + Error_code *ErrorCode `xml:"error_code,omitempty"` +} + +type ArrayOfMaterial struct { + Material []*MaterialEnum `xml:"Material,omitempty"` +} + +type ArrayOfKeyValuePair struct { + KeyValuePair []*KeyValuePair `xml:"KeyValuePair,omitempty"` +} + +type KeyValuePair struct { + Key string `xml:"Key,omitempty"` + + Value string `xml:"Value,omitempty"` +} + +type ArrayOfISRSPair struct { + ISRSPair []*ISRSPair `xml:"ISRSPair,omitempty"` +} + +type ISRSPair struct { + FromISRS string `xml:"fromISRS,omitempty"` + + ToISRS string `xml:"toISRS,omitempty"` +} + +type RequestedPeriod struct { + Date_start time.Time `xml:"Date_start,omitempty"` + + Date_end time.Time `xml:"Date_end,omitempty"` + + Value_interval int32 `xml:"Value_interval,omitempty"` +} + +type ArrayOfString struct { + String []string `xml:"http://www.ris.eu/wamos/common/3.0 string,omitempty"` +} + +type Char int32 + +type Duration *Duration + +type Guid string + +type IFairwayAvailabilityService interface { + + // Error can be either of the following types: + // + // - ErrorFault + + Get_bottleneck_fa(request *Get_bottleneck_fa) (*Get_bottleneck_faResponse, error) + + // Error can be either of the following types: + // + // - ErrorFault + + Get_stretch_fa(request *Get_stretch_fa) (*Get_stretch_faResponse, error) +} + +type FairwayAvailabilityService struct { + client *soap.SOAPClient +} + +func NewFairwayAvailabilityService(url string, tls bool, auth *soap.BasicAuth) *FairwayAvailabilityService { + if url == "" { + url = "" + } + client := soap.NewSOAPClient(url, tls, auth) + return &FairwayAvailabilityService{ + client: client, + } +} + +func NewFairwayAvailabilityServiceWithTLS(url string, tlsCfg *tls.Config, auth *soap.BasicAuth) *FairwayAvailabilityService { + if url == "" { + url = "" + } + client := soap.NewSOAPClientWithTLSConfig(url, tlsCfg, auth) + + return &FairwayAvailabilityService{ + client: client, + } +} + +func (service *FairwayAvailabilityService) Get_bottleneck_fa(request *Get_bottleneck_fa) (*Get_bottleneck_faResponse, error) { + response := new(Get_bottleneck_faResponse) + err := service.client.Call("http://www.ris.eu/fairwayavailability/3.0/IFairwayAvailabilityService/get_bottleneck_fa", request, response) + if err != nil { + return nil, err + } + + return response, nil +} + +func (service *FairwayAvailabilityService) Get_stretch_fa(request *Get_stretch_fa) (*Get_stretch_faResponse, error) { + response := new(Get_stretch_faResponse) + err := service.client.Call("http://www.ris.eu/fairwayavailability/3.0/IFairwayAvailabilityService/get_stretch_fa", request, response) + if err != nil { + return nil, err + } + + return response, nil +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/soap/nts/service.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,1578 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Raimund Renkert <raimund.renkert@intevation.de> + +package nts + +import ( + "crypto/tls" + "encoding/xml" + "time" + + "gemma.intevation.de/gemma/pkg/soap" +) + +// against "unused imports" +var _ time.Time +var _ xml.Name + +type Message_type_type string + +const ( + Message_type_typeFTM Message_type_type = "FTM" + + Message_type_typeWRM Message_type_type = "WRM" + + Message_type_typeICEM Message_type_type = "ICEM" + + Message_type_typeWERM Message_type_type = "WERM" +) + +type Error_code_type string + +type NonNegativeInteger uint + +type GYear uint + +type Duration string + +const ( + + // Description: message type not supported, Explanation: web service does not support the requested message type + Error_code_typeE010 Error_code_type = "e010" + + // Description: paging parameters inconsistent with messages, Explanation: parameters for paging mechanism do not fit the available messages, e.g. Offset >= Total Count + Error_code_typeE030 Error_code_type = "e030" + + // Description: syntax error in request, Explanation: request violates the schema for requests + Error_code_typeE100 Error_code_type = "e100" + + // Description: incorrect message type, Explanation: given message type is not known + Error_code_typeE110 Error_code_type = "e110" + + // Description: incorrect type-specific parameters, Explanation: type-specific parameters are erroneous + Error_code_typeE120 Error_code_type = "e120" + + // Description: incorrect paging parameters, Explanation: given parameters for the paging mechanism are erroneous + Error_code_typeE130 Error_code_type = "e130" + + // Description: operation not known, Explanation: the requested operation is unknown + Error_code_typeE200 Error_code_type = "e200" + + // Description: data source unavailable, Explanation: data source of the web service for the NtS data is temporarily unavailable + Error_code_typeE300 Error_code_type = "e300" + + // Description: too many results for request, Explanation: server is unable to handle number of results + Error_code_typeE310 Error_code_type = "e310" +) + +type Get_messages_query struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts.ms/2.0.4.0 get_messages_query"` + + Message_type *Message_type_type `xml:"http://www.ris.eu/nts.ms/2.0.4.0 message_type,omitempty"` + + Ids []*Id_pair `xml:"ids,omitempty"` + + Validity_period *Validity_period_type `xml:"validity_period,omitempty"` + + Dates_issue []*Date_pair `xml:"dates_issue,omitempty"` + + Paging_request *Paging_request_type `xml:"paging_request,omitempty"` +} + +type Get_messages_result struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts.ms/2.0.4.0 get_messages_result"` + + Result_message []*RIS_Message_Type `xml:"result_message,omitempty"` + + Result_error []*Error_code_type `xml:"result_error,omitempty"` + + Paging_result *Paging_result_type `xml:"paging_result,omitempty"` +} + +type Id_pair struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts.ms/2.0.4.0 ids"` + + Id *Isrs_code_type `xml:"http://www.ris.eu/nts.ms/2.0.4.0 id,omitempty"` +} + +type Date_pair struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 date_pair"` + + Date_start time.Time `xml:"date_start,omitempty"` + + Date_end time.Time `xml:"date_end,omitempty"` +} + +type Paging_request_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 paging_request"` + + Offset *NonNegativeInteger `xml:"offset,omitempty"` + + Limit *NonNegativeInteger `xml:"limit,omitempty"` + + Total_count bool `xml:"total_count,omitempty"` +} + +type Paging_result_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 paging_result"` + + Offset *NonNegativeInteger `xml:"offset,omitempty"` + + Count *NonNegativeInteger `xml:"count,omitempty"` + + Total_count *NonNegativeInteger `xml:"total_count,omitempty"` +} + +type Country_code_enum string + +const ( + Country_code_enumAT Country_code_enum = "AT" + + Country_code_enumBE Country_code_enum = "BE" + + Country_code_enumBG Country_code_enum = "BG" + + Country_code_enumCH Country_code_enum = "CH" + + Country_code_enumCY Country_code_enum = "CY" + + Country_code_enumCZ Country_code_enum = "CZ" + + Country_code_enumDE Country_code_enum = "DE" + + Country_code_enumDK Country_code_enum = "DK" + + Country_code_enumEE Country_code_enum = "EE" + + Country_code_enumES Country_code_enum = "ES" + + Country_code_enumFI Country_code_enum = "FI" + + Country_code_enumFR Country_code_enum = "FR" + + Country_code_enumGB Country_code_enum = "GB" + + Country_code_enumGR Country_code_enum = "GR" + + Country_code_enumHR Country_code_enum = "HR" + + Country_code_enumHU Country_code_enum = "HU" + + Country_code_enumIE Country_code_enum = "IE" + + Country_code_enumIT Country_code_enum = "IT" + + Country_code_enumLT Country_code_enum = "LT" + + Country_code_enumLU Country_code_enum = "LU" + + Country_code_enumLV Country_code_enum = "LV" + + Country_code_enumMD Country_code_enum = "MD" + + Country_code_enumME Country_code_enum = "ME" + + Country_code_enumMT Country_code_enum = "MT" + + Country_code_enumNL Country_code_enum = "NL" + + Country_code_enumPL Country_code_enum = "PL" + + Country_code_enumPT Country_code_enum = "PT" + + Country_code_enumRO Country_code_enum = "RO" + + Country_code_enumRS Country_code_enum = "RS" + + Country_code_enumSE Country_code_enum = "SE" + + Country_code_enumSI Country_code_enum = "SI" + + Country_code_enumSK Country_code_enum = "SK" + + Country_code_enumRU Country_code_enum = "RU" + + Country_code_enumUA Country_code_enum = "UA" +) + +type Language_code_enum string + +const ( + Language_code_enumDE Language_code_enum = "DE" + + Language_code_enumEN Language_code_enum = "EN" + + Language_code_enumFR Language_code_enum = "FR" + + Language_code_enumNL Language_code_enum = "NL" + + Language_code_enumSK Language_code_enum = "SK" + + Language_code_enumHU Language_code_enum = "HU" + + Language_code_enumHR Language_code_enum = "HR" + + Language_code_enumSR Language_code_enum = "SR" + + Language_code_enumBG Language_code_enum = "BG" + + Language_code_enumRO Language_code_enum = "RO" + + Language_code_enumRU Language_code_enum = "RU" + + Language_code_enumCS Language_code_enum = "CS" + + Language_code_enumPL Language_code_enum = "PL" + + Language_code_enumPT Language_code_enum = "PT" + + Language_code_enumES Language_code_enum = "ES" + + Language_code_enumSV Language_code_enum = "SV" + + Language_code_enumFI Language_code_enum = "FI" + + Language_code_enumDA Language_code_enum = "DA" + + Language_code_enumET Language_code_enum = "ET" + + Language_code_enumLV Language_code_enum = "LV" + + Language_code_enumLT Language_code_enum = "LT" + + Language_code_enumIT Language_code_enum = "IT" + + Language_code_enumMT Language_code_enum = "MT" + + Language_code_enumEL Language_code_enum = "EL" + + Language_code_enumSL Language_code_enum = "SL" +) + +type Subject_code_enum string + +const ( + Subject_code_enumANNOUN Subject_code_enum = "ANNOUN" + + Subject_code_enumWARNIN Subject_code_enum = "WARNIN" + + Subject_code_enumCANCEL Subject_code_enum = "CANCEL" + + Subject_code_enumINFSER Subject_code_enum = "INFSER" + + Subject_code_enumOBSTRU Subject_code_enum = "OBSTRU" + + Subject_code_enumPAROBS Subject_code_enum = "PAROBS" + + Subject_code_enumDELAY Subject_code_enum = "DELAY" + + Subject_code_enumVESLEN Subject_code_enum = "VESLEN" + + Subject_code_enumVESHEI Subject_code_enum = "VESHEI" + + Subject_code_enumVESBRE Subject_code_enum = "VESBRE" + + Subject_code_enumVESDRA Subject_code_enum = "VESDRA" + + Subject_code_enumAVALEN Subject_code_enum = "AVALEN" + + Subject_code_enumCLEHEI Subject_code_enum = "CLEHEI" + + Subject_code_enumCLEWID Subject_code_enum = "CLEWID" + + Subject_code_enumAVADEP Subject_code_enum = "AVADEP" + + Subject_code_enumNOMOOR Subject_code_enum = "NOMOOR" + + Subject_code_enumSERVIC Subject_code_enum = "SERVIC" + + Subject_code_enumNOSERV Subject_code_enum = "NOSERV" + + Subject_code_enumSPEED Subject_code_enum = "SPEED" + + Subject_code_enumWAVWAS Subject_code_enum = "WAVWAS" + + Subject_code_enumPASSIN Subject_code_enum = "PASSIN" + + Subject_code_enumANCHOR Subject_code_enum = "ANCHOR" + + Subject_code_enumOVRTAK Subject_code_enum = "OVRTAK" + + Subject_code_enumMINPWR Subject_code_enum = "MINPWR" + + Subject_code_enumDREDGE Subject_code_enum = "DREDGE" + + Subject_code_enumWORK Subject_code_enum = "WORK" + + Subject_code_enumEVENT Subject_code_enum = "EVENT" + + Subject_code_enumCHGMAR Subject_code_enum = "CHGMAR" + + Subject_code_enumCHGSER Subject_code_enum = "CHGSER" + + Subject_code_enumSPCMAR Subject_code_enum = "SPCMAR" + + Subject_code_enumEXERC Subject_code_enum = "EXERC" + + Subject_code_enumLEADEP Subject_code_enum = "LEADEP" + + Subject_code_enumLEVDEC Subject_code_enum = "LEVDEC" + + Subject_code_enumLEVRIS Subject_code_enum = "LEVRIS" + + Subject_code_enumLIMITA Subject_code_enum = "LIMITA" + + Subject_code_enumMISECH Subject_code_enum = "MISECH" + + Subject_code_enumECDISU Subject_code_enum = "ECDISU" + + Subject_code_enumNEWOBJ Subject_code_enum = "NEWOBJ" + + Subject_code_enumCHWWY Subject_code_enum = "CHWWY" + + Subject_code_enumCONWWY Subject_code_enum = "CONWWY" + + Subject_code_enumDIVER Subject_code_enum = "DIVER" + + Subject_code_enumSPECTR Subject_code_enum = "SPECTR" + + Subject_code_enumLOCRUL Subject_code_enum = "LOCRUL" + + Subject_code_enumVHFCOV Subject_code_enum = "VHFCOV" + + Subject_code_enumHIGVOL Subject_code_enum = "HIGVOL" + + Subject_code_enumTURNIN Subject_code_enum = "TURNIN" + + Subject_code_enumCONBRE Subject_code_enum = "CONBRE" + + Subject_code_enumCONLEN Subject_code_enum = "CONLEN" + + Subject_code_enumREMOBJ Subject_code_enum = "REMOBJ" +) + +type Reason_code_enum string + +const ( + Reason_code_enumEVENT Reason_code_enum = "EVENT" + + Reason_code_enumWORK Reason_code_enum = "WORK" + + Reason_code_enumDREDGE Reason_code_enum = "DREDGE" + + Reason_code_enumEXERC Reason_code_enum = "EXERC" + + Reason_code_enumHIGWAT Reason_code_enum = "HIGWAT" + + Reason_code_enumHIWAI Reason_code_enum = "HIWAI" + + Reason_code_enumHIWAII Reason_code_enum = "HIWAII" + + Reason_code_enumLOWWAT Reason_code_enum = "LOWWAT" + + Reason_code_enumSHALLO Reason_code_enum = "SHALLO" + + Reason_code_enumCALAMI Reason_code_enum = "CALAMI" + + Reason_code_enumLAUNCH Reason_code_enum = "LAUNCH" + + Reason_code_enumDECLEV Reason_code_enum = "DECLEV" + + Reason_code_enumFLOMEA Reason_code_enum = "FLOMEA" + + Reason_code_enumBLDWRK Reason_code_enum = "BLDWRK" + + Reason_code_enumREPAIR Reason_code_enum = "REPAIR" + + Reason_code_enumINSPEC Reason_code_enum = "INSPEC" + + Reason_code_enumFIRWRK Reason_code_enum = "FIRWRK" + + Reason_code_enumLIMITA Reason_code_enum = "LIMITA" + + Reason_code_enumCHGFWY Reason_code_enum = "CHGFWY" + + Reason_code_enumCONSTR Reason_code_enum = "CONSTR" + + Reason_code_enumDIVING Reason_code_enum = "DIVING" + + Reason_code_enumSPECTR Reason_code_enum = "SPECTR" + + Reason_code_enumEXT Reason_code_enum = "EXT" + + Reason_code_enumMIN Reason_code_enum = "MIN" + + Reason_code_enumSOUND Reason_code_enum = "SOUND" + + Reason_code_enumOTHER Reason_code_enum = "OTHER" + + Reason_code_enumSTRIKE Reason_code_enum = "STRIKE" + + Reason_code_enumFLOMAT Reason_code_enum = "FLOMAT" + + Reason_code_enumEXPLOS Reason_code_enum = "EXPLOS" + + Reason_code_enumICE Reason_code_enum = "ICE" + + Reason_code_enumOBSTAC Reason_code_enum = "OBSTAC" + + Reason_code_enumCHGMAR Reason_code_enum = "CHGMAR" + + Reason_code_enumDAMMAR Reason_code_enum = "DAMMAR" + + Reason_code_enumFALMAT Reason_code_enum = "FALMAT" + + Reason_code_enumMISECH Reason_code_enum = "MISECH" + + Reason_code_enumHEARIS Reason_code_enum = "HEARIS" + + Reason_code_enumHIGVOL Reason_code_enum = "HIGVOL" + + Reason_code_enumECDISU Reason_code_enum = "ECDISU" + + Reason_code_enumLOCRUL Reason_code_enum = "LOCRUL" + + Reason_code_enumNEWOBJ Reason_code_enum = "NEWOBJ" + + Reason_code_enumOBUNWA Reason_code_enum = "OBUNWA" + + Reason_code_enumVHFCOV Reason_code_enum = "VHFCOV" + + Reason_code_enumREMOBJ Reason_code_enum = "REMOBJ" + + Reason_code_enumLEVRIS Reason_code_enum = "LEVRIS" + + Reason_code_enumSPCMAR Reason_code_enum = "SPCMAR" + + Reason_code_enumWERMCO Reason_code_enum = "WERMCO" + + Reason_code_enumINFSER Reason_code_enum = "INFSER" +) + +type Reporting_code_enum string + +const ( + Reporting_code_enumINF Reporting_code_enum = "INF" + + Reporting_code_enumADD Reporting_code_enum = "ADD" + + Reporting_code_enumREG Reporting_code_enum = "REG" +) + +type Communication_code_enum string + +const ( + Communication_code_enumTE Communication_code_enum = "TE" + + Communication_code_enumAP Communication_code_enum = "AP" + + Communication_code_enumEM Communication_code_enum = "EM" + + Communication_code_enumAH Communication_code_enum = "AH" + + Communication_code_enumTT Communication_code_enum = "TT" + + Communication_code_enumFX Communication_code_enum = "FX" + + Communication_code_enumLS Communication_code_enum = "LS" + + Communication_code_enumFS Communication_code_enum = "FS" + + Communication_code_enumSO Communication_code_enum = "SO" + + Communication_code_enumEI Communication_code_enum = "EI" +) + +type Measure_code_enum string + +const ( + Measure_code_enumDIS Measure_code_enum = "DIS" + + Measure_code_enumREG Measure_code_enum = "REG" + + Measure_code_enumBAR Measure_code_enum = "BAR" + + Measure_code_enumVER Measure_code_enum = "VER" + + Measure_code_enumLSD Measure_code_enum = "LSD" + + Measure_code_enumWAL Measure_code_enum = "WAL" + + Measure_code_enumNOM Measure_code_enum = "NOM" +) + +type Barrage_code_enum string + +const ( + Barrage_code_enumCLD Barrage_code_enum = "CLD" + + Barrage_code_enumOPG Barrage_code_enum = "OPG" + + Barrage_code_enumCLG Barrage_code_enum = "CLG" + + Barrage_code_enumOPD Barrage_code_enum = "OPD" + + Barrage_code_enumOPN Barrage_code_enum = "OPN" +) + +type Regime_code_enum string + +const ( + Regime_code_enumNO Regime_code_enum = "NO" + + Regime_code_enumHI Regime_code_enum = "HI" + + Regime_code_enumII Regime_code_enum = "II" + + Regime_code_enumI Regime_code_enum = "I" + + Regime_code_enumNN Regime_code_enum = "NN" + + Regime_code_enumLO Regime_code_enum = "LO" +) + +type Ice_condition_code_enum string + +const ( + Ice_condition_code_enumA Ice_condition_code_enum = "A" + + Ice_condition_code_enumB Ice_condition_code_enum = "B" + + Ice_condition_code_enumC Ice_condition_code_enum = "C" + + Ice_condition_code_enumD Ice_condition_code_enum = "D" + + Ice_condition_code_enumE Ice_condition_code_enum = "E" + + Ice_condition_code_enumF Ice_condition_code_enum = "F" + + Ice_condition_code_enumG Ice_condition_code_enum = "G" + + Ice_condition_code_enumH Ice_condition_code_enum = "H" + + Ice_condition_code_enumK Ice_condition_code_enum = "K" + + Ice_condition_code_enumL Ice_condition_code_enum = "L" + + Ice_condition_code_enumM Ice_condition_code_enum = "M" + + Ice_condition_code_enumP Ice_condition_code_enum = "P" + + Ice_condition_code_enumR Ice_condition_code_enum = "R" + + Ice_condition_code_enumS Ice_condition_code_enum = "S" + + Ice_condition_code_enumU Ice_condition_code_enum = "U" + + Ice_condition_code_enumO Ice_condition_code_enum = "O" + + Ice_condition_code_enumV Ice_condition_code_enum = "V" +) + +type Ice_accessibility_code_enum string + +const ( + Ice_accessibility_code_enumA Ice_accessibility_code_enum = "A" + + Ice_accessibility_code_enumB Ice_accessibility_code_enum = "B" + + Ice_accessibility_code_enumF Ice_accessibility_code_enum = "F" + + Ice_accessibility_code_enumL Ice_accessibility_code_enum = "L" + + Ice_accessibility_code_enumC Ice_accessibility_code_enum = "C" + + Ice_accessibility_code_enumD Ice_accessibility_code_enum = "D" + + Ice_accessibility_code_enumE Ice_accessibility_code_enum = "E" + + Ice_accessibility_code_enumG Ice_accessibility_code_enum = "G" + + Ice_accessibility_code_enumH Ice_accessibility_code_enum = "H" + + Ice_accessibility_code_enumM Ice_accessibility_code_enum = "M" + + Ice_accessibility_code_enumK Ice_accessibility_code_enum = "K" + + Ice_accessibility_code_enumT Ice_accessibility_code_enum = "T" + + Ice_accessibility_code_enumP Ice_accessibility_code_enum = "P" + + Ice_accessibility_code_enumV Ice_accessibility_code_enum = "V" + + Ice_accessibility_code_enumX Ice_accessibility_code_enum = "X" +) + +type Ice_classification_code_enum string + +const ( + Ice_classification_code_enumA Ice_classification_code_enum = "A" + + Ice_classification_code_enumB Ice_classification_code_enum = "B" + + Ice_classification_code_enumC Ice_classification_code_enum = "C" + + Ice_classification_code_enumD Ice_classification_code_enum = "D" + + Ice_classification_code_enumE Ice_classification_code_enum = "E" +) + +type Ice_situation_code_enum string + +const ( + Ice_situation_code_enumNOL Ice_situation_code_enum = "NOL" + + Ice_situation_code_enumLIM Ice_situation_code_enum = "LIM" + + Ice_situation_code_enumNON Ice_situation_code_enum = "NON" +) + +type Weather_class_code_enum string + +const ( + Weather_class_code_enumCLR Weather_class_code_enum = "CLR" + + Weather_class_code_enumCLDY Weather_class_code_enum = "CLDY" + + Weather_class_code_enumOCST Weather_class_code_enum = "OCST" + + Weather_class_code_enumDZZL Weather_class_code_enum = "DZZL" + + Weather_class_code_enumRAIN Weather_class_code_enum = "RAIN" + + Weather_class_code_enumLRAIN Weather_class_code_enum = "LRAIN" + + Weather_class_code_enumORAIN Weather_class_code_enum = "ORAIN" + + Weather_class_code_enumHRAIN Weather_class_code_enum = "HRAIN" + + Weather_class_code_enumSLEET Weather_class_code_enum = "SLEET" + + Weather_class_code_enumSNOW Weather_class_code_enum = "SNOW" + + Weather_class_code_enumSNFALL Weather_class_code_enum = "SNFALL" + + Weather_class_code_enumHAIL Weather_class_code_enum = "HAIL" + + Weather_class_code_enumSHWRS Weather_class_code_enum = "SHWRS" + + Weather_class_code_enumTHSTRM Weather_class_code_enum = "THSTRM" + + Weather_class_code_enumHAZY Weather_class_code_enum = "HAZY" + + Weather_class_code_enumFOG Weather_class_code_enum = "FOG" + + Weather_class_code_enumFOGPAT Weather_class_code_enum = "FOGPAT" + + Weather_class_code_enumGALE Weather_class_code_enum = "GALE" + + Weather_class_code_enumSTRM Weather_class_code_enum = "STRM" + + Weather_class_code_enumHURRC Weather_class_code_enum = "HURRC" + + Weather_class_code_enumFZRA Weather_class_code_enum = "FZRA" +) + +type Weather_item_code_enum string + +const ( + Weather_item_code_enumWI Weather_item_code_enum = "WI" + + Weather_item_code_enumWA Weather_item_code_enum = "WA" + + Weather_item_code_enumFG Weather_item_code_enum = "FG" + + Weather_item_code_enumRN Weather_item_code_enum = "RN" + + Weather_item_code_enumSN Weather_item_code_enum = "SN" + + Weather_item_code_enumAT Weather_item_code_enum = "AT" + + Weather_item_code_enumWT Weather_item_code_enum = "WT" +) + +type Weather_category_code_enum string + +const ( + Weather_category_code_enum0 Weather_category_code_enum = "0" + + Weather_category_code_enum1 Weather_category_code_enum = "1" + + Weather_category_code_enum2 Weather_category_code_enum = "2" + + Weather_category_code_enum3 Weather_category_code_enum = "3" + + Weather_category_code_enum4 Weather_category_code_enum = "4" + + Weather_category_code_enum5 Weather_category_code_enum = "5" + + Weather_category_code_enum6 Weather_category_code_enum = "6" + + Weather_category_code_enum7 Weather_category_code_enum = "7" + + Weather_category_code_enum8 Weather_category_code_enum = "8" + + Weather_category_code_enum9 Weather_category_code_enum = "9" + + Weather_category_code_enum10 Weather_category_code_enum = "10" + + Weather_category_code_enum11 Weather_category_code_enum = "11" + + Weather_category_code_enum12 Weather_category_code_enum = "12" + + Weather_category_code_enum13 Weather_category_code_enum = "13" + + Weather_category_code_enum14 Weather_category_code_enum = "14" + + Weather_category_code_enum15 Weather_category_code_enum = "15" + + Weather_category_code_enum16 Weather_category_code_enum = "16" + + Weather_category_code_enum17 Weather_category_code_enum = "17" + + Weather_category_code_enum18 Weather_category_code_enum = "18" + + Weather_category_code_enum19 Weather_category_code_enum = "19" + + Weather_category_code_enum20 Weather_category_code_enum = "20" + + Weather_category_code_enum21 Weather_category_code_enum = "21" + + Weather_category_code_enum22 Weather_category_code_enum = "22" +) + +type Weather_direction_code_enum string + +const ( + Weather_direction_code_enumN Weather_direction_code_enum = "N" + + Weather_direction_code_enumNE Weather_direction_code_enum = "NE" + + Weather_direction_code_enumE Weather_direction_code_enum = "E" + + Weather_direction_code_enumSE Weather_direction_code_enum = "SE" + + Weather_direction_code_enumS Weather_direction_code_enum = "S" + + Weather_direction_code_enumSW Weather_direction_code_enum = "SW" + + Weather_direction_code_enumW Weather_direction_code_enum = "W" + + Weather_direction_code_enumNW Weather_direction_code_enum = "NW" + + Weather_direction_code_enumWRB Weather_direction_code_enum = "WRB" +) + +// Internal ID - best practice: global unique identifier +type Internal_id_type string + +// ISRS location code, unique identification of the geo object as defined in RIS Index encoding guide +type Isrs_code_type string + +type Type_code_enum string + +const ( + Type_code_enumRIV Type_code_enum = "RIV" + + Type_code_enumCAN Type_code_enum = "CAN" + + Type_code_enumLAK Type_code_enum = "LAK" + + Type_code_enumFWY Type_code_enum = "FWY" + + Type_code_enumLCK Type_code_enum = "LCK" + + Type_code_enumBRI Type_code_enum = "BRI" + + Type_code_enumRMP Type_code_enum = "RMP" + + Type_code_enumBAR Type_code_enum = "BAR" + + Type_code_enumBNK Type_code_enum = "BNK" + + Type_code_enumGAU Type_code_enum = "GAU" + + Type_code_enumBUO Type_code_enum = "BUO" + + Type_code_enumBEA Type_code_enum = "BEA" + + Type_code_enumANC Type_code_enum = "ANC" + + Type_code_enumBER Type_code_enum = "BER" + + Type_code_enumMOO Type_code_enum = "MOO" + + Type_code_enumTER Type_code_enum = "TER" + + Type_code_enumHAR Type_code_enum = "HAR" + + Type_code_enumFDO Type_code_enum = "FDO" + + Type_code_enumCAB Type_code_enum = "CAB" + + Type_code_enumFER Type_code_enum = "FER" + + Type_code_enumPIP Type_code_enum = "PIP" + + Type_code_enumPPO Type_code_enum = "PPO" + + Type_code_enumHFA Type_code_enum = "HFA" + + Type_code_enumHMO Type_code_enum = "HMO" + + Type_code_enumSHY Type_code_enum = "SHY" + + Type_code_enumREF Type_code_enum = "REF" + + Type_code_enumMAR Type_code_enum = "MAR" + + Type_code_enumLIG Type_code_enum = "LIG" + + Type_code_enumSIG Type_code_enum = "SIG" + + Type_code_enumTUR Type_code_enum = "TUR" + + Type_code_enumCBR Type_code_enum = "CBR" + + Type_code_enumTUN Type_code_enum = "TUN" + + Type_code_enumBCO Type_code_enum = "BCO" + + Type_code_enumREP Type_code_enum = "REP" + + Type_code_enumFLO Type_code_enum = "FLO" + + Type_code_enumSLI Type_code_enum = "SLI" + + Type_code_enumDUK Type_code_enum = "DUK" + + Type_code_enumVTC Type_code_enum = "VTC" + + Type_code_enumRES Type_code_enum = "RES" + + Type_code_enumLKB Type_code_enum = "LKB" + + Type_code_enumBRO Type_code_enum = "BRO" + + Type_code_enumBNS Type_code_enum = "BNS" +) + +type Interval_code_enum string + +const ( + Interval_code_enumCON Interval_code_enum = "CON" + + Interval_code_enumDAY Interval_code_enum = "DAY" + + Interval_code_enumWRK Interval_code_enum = "WRK" + + Interval_code_enumWKN Interval_code_enum = "WKN" + + Interval_code_enumSUN Interval_code_enum = "SUN" + + Interval_code_enumMON Interval_code_enum = "MON" + + Interval_code_enumTUE Interval_code_enum = "TUE" + + Interval_code_enumWED Interval_code_enum = "WED" + + Interval_code_enumTHU Interval_code_enum = "THU" + + Interval_code_enumFRI Interval_code_enum = "FRI" + + Interval_code_enumSAT Interval_code_enum = "SAT" + + Interval_code_enumDTI Interval_code_enum = "DTI" + + Interval_code_enumNTI Interval_code_enum = "NTI" + + Interval_code_enumRVI Interval_code_enum = "RVI" + + Interval_code_enumEXC Interval_code_enum = "EXC" + + Interval_code_enumWRD Interval_code_enum = "WRD" +) + +type Limitation_code_enum string + +const ( + Limitation_code_enumOBSTRU Limitation_code_enum = "OBSTRU" + + Limitation_code_enumPAROBS Limitation_code_enum = "PAROBS" + + Limitation_code_enumDELAY Limitation_code_enum = "DELAY" + + Limitation_code_enumVESLEN Limitation_code_enum = "VESLEN" + + Limitation_code_enumVESHEI Limitation_code_enum = "VESHEI" + + Limitation_code_enumVESBRE Limitation_code_enum = "VESBRE" + + Limitation_code_enumVESDRA Limitation_code_enum = "VESDRA" + + Limitation_code_enumAVALEN Limitation_code_enum = "AVALEN" + + Limitation_code_enumCLEHEI Limitation_code_enum = "CLEHEI" + + Limitation_code_enumCLEWID Limitation_code_enum = "CLEWID" + + Limitation_code_enumAVADEP Limitation_code_enum = "AVADEP" + + Limitation_code_enumNOMOOR Limitation_code_enum = "NOMOOR" + + Limitation_code_enumSERVIC Limitation_code_enum = "SERVIC" + + Limitation_code_enumNOSERV Limitation_code_enum = "NOSERV" + + Limitation_code_enumSPEED Limitation_code_enum = "SPEED" + + Limitation_code_enumWAVWAS Limitation_code_enum = "WAVWAS" + + Limitation_code_enumPASSIN Limitation_code_enum = "PASSIN" + + Limitation_code_enumANCHOR Limitation_code_enum = "ANCHOR" + + Limitation_code_enumOVRTAK Limitation_code_enum = "OVRTAK" + + Limitation_code_enumMINPWR Limitation_code_enum = "MINPWR" + + Limitation_code_enumALTER Limitation_code_enum = "ALTER" + + Limitation_code_enumCAUTIO Limitation_code_enum = "CAUTIO" + + Limitation_code_enumNOLIM Limitation_code_enum = "NOLIM" + + Limitation_code_enumTURNIN Limitation_code_enum = "TURNIN" + + Limitation_code_enumNOSHORE Limitation_code_enum = "NOSHORE" + + Limitation_code_enumCONBRE Limitation_code_enum = "CONBRE" + + Limitation_code_enumCONLEN Limitation_code_enum = "CONLEN" + + Limitation_code_enumLEADEP Limitation_code_enum = "LEADEP" + + Limitation_code_enumNOBERT Limitation_code_enum = "NOBERT" +) + +type Position_code_enum string + +const ( + Position_code_enumAL Position_code_enum = "AL" + + Position_code_enumLE Position_code_enum = "LE" + + Position_code_enumMI Position_code_enum = "MI" + + Position_code_enumRI Position_code_enum = "RI" + + Position_code_enumLB Position_code_enum = "LB" + + Position_code_enumRB Position_code_enum = "RB" + + Position_code_enumN Position_code_enum = "N" + + Position_code_enumNE Position_code_enum = "NE" + + Position_code_enumE Position_code_enum = "E" + + Position_code_enumSE Position_code_enum = "SE" + + Position_code_enumS Position_code_enum = "S" + + Position_code_enumSW Position_code_enum = "SW" + + Position_code_enumW Position_code_enum = "W" + + Position_code_enumNW Position_code_enum = "NW" + + Position_code_enumBI Position_code_enum = "BI" + + Position_code_enumSM Position_code_enum = "SM" + + Position_code_enumOL Position_code_enum = "OL" + + Position_code_enumEW Position_code_enum = "EW" + + Position_code_enumMP Position_code_enum = "MP" + + Position_code_enumFP Position_code_enum = "FP" + + Position_code_enumVA Position_code_enum = "VA" + + Position_code_enumRY Position_code_enum = "RY" + + Position_code_enumGY Position_code_enum = "GY" +) + +type Reference_code_enum string + +const ( + Reference_code_enumNAP Reference_code_enum = "NAP" + + Reference_code_enumKP Reference_code_enum = "KP" + + Reference_code_enumFZP Reference_code_enum = "FZP" + + Reference_code_enumADR Reference_code_enum = "ADR" + + Reference_code_enumTAW Reference_code_enum = "TAW" + + Reference_code_enumPUL Reference_code_enum = "PUL" + + Reference_code_enumNGM Reference_code_enum = "NGM" + + Reference_code_enumETRS Reference_code_enum = "ETRS" + + Reference_code_enumPOT Reference_code_enum = "POT" + + Reference_code_enumLDC Reference_code_enum = "LDC" + + Reference_code_enumHDC Reference_code_enum = "HDC" + + Reference_code_enumZPG Reference_code_enum = "ZPG" + + Reference_code_enumGLW Reference_code_enum = "GLW" + + Reference_code_enumHSW Reference_code_enum = "HSW" + + Reference_code_enumLNW Reference_code_enum = "LNW" + + Reference_code_enumHNW Reference_code_enum = "HNW" + + Reference_code_enumIGN Reference_code_enum = "IGN" + + Reference_code_enumWGS Reference_code_enum = "WGS" + + Reference_code_enumRN Reference_code_enum = "RN" + + Reference_code_enumHBO Reference_code_enum = "HBO" +) + +type Indication_code_enum string + +const ( + Indication_code_enumMAX Indication_code_enum = "MAX" + + Indication_code_enumMIN Indication_code_enum = "MIN" + + Indication_code_enumRED Indication_code_enum = "RED" +) + +type Target_group_code_enum string + +const ( + Target_group_code_enumALL Target_group_code_enum = "ALL" + + Target_group_code_enumCDG Target_group_code_enum = "CDG" + + Target_group_code_enumCOM Target_group_code_enum = "COM" + + Target_group_code_enumPAX Target_group_code_enum = "PAX" + + Target_group_code_enumPLE Target_group_code_enum = "PLE" + + Target_group_code_enumCNV Target_group_code_enum = "CNV" + + Target_group_code_enumPUS Target_group_code_enum = "PUS" + + Target_group_code_enumNNU Target_group_code_enum = "NNU" + + Target_group_code_enumLOA Target_group_code_enum = "LOA" + + Target_group_code_enumSMA Target_group_code_enum = "SMA" + + Target_group_code_enumCND Target_group_code_enum = "CND" + + Target_group_code_enumWOC Target_group_code_enum = "WOC" + + Target_group_code_enumMOV Target_group_code_enum = "MOV" + + Target_group_code_enumNMV Target_group_code_enum = "NMV" +) + +type Direction_code_enum string + +const ( + Direction_code_enumALL Direction_code_enum = "ALL" + + Direction_code_enumUPS Direction_code_enum = "UPS" + + Direction_code_enumDWN Direction_code_enum = "DWN" +) + +type Unit_enum string + +const ( + Unit_enumCm Unit_enum = "cm" + + Unit_enumM3s Unit_enum = "m³/s" + + Unit_enumH Unit_enum = "h" + + Unit_enumKmh Unit_enum = "km/h" + + Unit_enumKW Unit_enum = "kW" + + Unit_enumMs Unit_enum = "m/s" + + Unit_enumMmh Unit_enum = "mm/h" + + Unit_enumC Unit_enum = "°C" +) + +type RIS_Message_Type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts.ms/2.0.4.0 result_message"` + + // Identification section + Identification *Identification_type `xml:"identification,omitempty"` + + // Fairway and traffic related section + Ftm []*Ftm_type `xml:"ftm,omitempty"` + + // Water related section + Wrm []*Wrm_type `xml:"wrm,omitempty"` + + // Ice related section + Icem []*Icem_type `xml:"icem,omitempty"` + + // Weather related section + Werm []*Werm_type `xml:"werm,omitempty"` +} + +type Identification_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 identification"` + + // Internal ID + Internal_id *Internal_id_type `xml:"internal_id,omitempty"` + + // Sender (System) of the message + From string `xml:"from,omitempty"` + + // Originator (initiator) of the information in this message + Originator string `xml:"originator,omitempty"` + + // Country where message is valid + Country_code *Country_code_enum `xml:"country_code,omitempty"` + + // Original language used in the textual info. (contents) + Language_code *Language_code_enum `xml:"language_code,omitempty"` + + // District / Region within the specified country, where the message is applicable + District string `xml:"district,omitempty"` + + // Date and time of publication including time zone + Date_issue time.Time `xml:"date_issue,omitempty"` +} + +type Ftm_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 ftm_type"` + + // Internal ID + Internal_id *Internal_id_type `xml:"internal_id,omitempty"` + + // NtS Number + Nts_number *Nts_number_type `xml:"nts_number,omitempty"` + + // Target group information + Target_group []*Target_group_type `xml:"target_group,omitempty"` + + // Subject code must contain one of the following: Announcement (ANNOUN), Warning (WARNIN), Notice withdrawn (CANCEL) or Information service (INFSER). More information on the use of codes can be found in the NtS Encoding Guide. + Subject_code *Subject_code_enum `xml:"subject_code,omitempty"` + + // Overall period of validity + Validity_period *Validity_period_type `xml:"validity_period,omitempty"` + + // Additional information in local language + Contents string `xml:"contents,omitempty"` + + // Notice source (name of authority) + Source string `xml:"source,omitempty"` + + // Reason / justification of the notice + Reason_code *Reason_code_enum `xml:"reason_code,omitempty"` + + // Communication channel information + Communication []*Communication_type `xml:"communication,omitempty"` + + // Fairway section + Fairway_section *Fairway_section_type `xml:"fairway_section,omitempty"` + + // Object section + Object *Object_type `xml:"object,omitempty"` +} + +type Communication_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 communication_type"` + + // Reporting regime (information, or duty to report) + Reporting_code *Reporting_code_enum `xml:"reporting_code,omitempty"` + + // Communication code (telephone, VHF etc.) + Communication_code *Communication_code_enum `xml:"communication_code,omitempty"` + + // Telephone, VHF number (including callsign), e-mail address, URL or teletext + Number string `xml:"number,omitempty"` + + // Name of the attachment or additional information + Label string `xml:"label,omitempty"` + + // Additional remarks concerning the communication + Remark string `xml:"remark,omitempty"` +} + +type Object_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 object_type"` + + // Geo Information of object + Geo_object *Geo_object_type `xml:"geo_object,omitempty"` + + // Object limitation section + Limitation []*Limitation_type `xml:"limitation,omitempty"` +} + +type Wrm_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 wrm"` + + // Internal ID + Internal_id *Internal_id_type `xml:"internal_id,omitempty"` + + // NtS Number + Nts_number *Nts_number_type `xml:"nts_number,omitempty"` + + // Overall period of validity + Validity_period *Validity_period_type `xml:"validity_period,omitempty"` + + // Object section + Geo_object *Geo_object_type `xml:"geo_object,omitempty"` + + // Value reference (measurement reference) + Reference_code *Reference_code_enum `xml:"reference_code,omitempty"` + + // Measurements (normal or predicted values) + Measure []*Measure_type `xml:"measure,omitempty"` +} + +type Measure_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 measure"` + + // Predicted measurement (1 or true) or real measurement (0 or false) + Predicted bool `xml:"predicted,omitempty"` + + // Kind of water related information + Measure_code *Measure_code_enum `xml:"measure_code,omitempty"` + + // Measured or predicted value + Value float32 `xml:"value,omitempty"` + + // Lowest value of confidence interval + Value_min float32 `xml:"value_min,omitempty"` + + // Highest value of confidence interval + Value_max float32 `xml:"value_max,omitempty"` + + // Unit of the water related value + Unit *Unit_enum `xml:"unit,omitempty"` + + // Barrage status + Barrage_code *Barrage_code_enum `xml:"barrage_code,omitempty"` + + // Regime applicable + Regime_code *Regime_code_enum `xml:"regime_code,omitempty"` + + // Date and Time of measurement or predicted value including time zone + Measuredate time.Time `xml:"measuredate,omitempty"` + + // Difference with comparative value + Difference *Difference_type `xml:"difference,omitempty"` +} + +type Difference_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 difference"` + + // Difference with comparative value + Value_difference float32 `xml:"value_difference,omitempty"` + + // Time difference with measuredata of comparative measurement + Time_difference *Duration `xml:"time_difference,omitempty"` +} + +type Icem_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 icem_type"` + + // Internal ID + Internal_id *Internal_id_type `xml:"internal_id,omitempty"` + + // NtS Number + Nts_number *Nts_number_type `xml:"nts_number,omitempty"` + + // Overall period of validity + Validity_period *Validity_period_type `xml:"validity_period,omitempty"` + + // Fairway section - the limitation inside the fairway section cannot be used in the ICEM + Fairway_section *Fairway_section_type `xml:"fairway_section,omitempty"` + + // Ice conditions + Ice_condition []*Ice_condition_type `xml:"ice_condition,omitempty"` +} + +type Ice_condition_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 ice_condition_type"` + + // Date and Time of measurement or prediction including time zone + Measuredate time.Time `xml:"measuredate,omitempty"` + + // Condition code + Ice_condition_code *Ice_condition_code_enum `xml:"ice_condition_code,omitempty"` + + // Accessibility code + Ice_accessibility_code *Ice_accessibility_code_enum `xml:"ice_accessibility_code,omitempty"` + + // Classification code + Ice_classification_code *Ice_classification_code_enum `xml:"ice_classification_code,omitempty"` + + // Situation code + Ice_situation_code *Ice_situation_code_enum `xml:"ice_situation_code,omitempty"` +} + +type Werm_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 werm_type"` + + // Internal ID + Internal_id *Internal_id_type `xml:"internal_id,omitempty"` + + // NtS Number + Nts_number *Nts_number_type `xml:"nts_number,omitempty"` + + // Overall period of validity + Validity_period *Validity_period_type `xml:"validity_period,omitempty"` + + // Fairway section + Fairway_section *Fairway_section_werm_type `xml:"fairway_section,omitempty"` + + // Actual or Forecast report sections + Weather_report *Weather_report_type `xml:"weather_report,omitempty"` +} + +type Fairway_section_werm_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 fairway_section_werm_type"` + + // Geo Information of fairway + Geo_object *Geo_object_type `xml:"geo_object,omitempty"` +} + +type Weather_report_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 weather_report_type"` + + // Date and time of measurement or predicted value including timezone + Measuredate time.Time `xml:"measuredate,omitempty"` + + // Forecast (true or 1) OR Actual report (false or 0) + Forecast bool `xml:"forecast,omitempty"` + + // Classification of weather report + Weather_class_code []*Weather_class_code_enum `xml:"weather_class_code,omitempty"` + + // Weather items + Weather_item []*Weather_item_type `xml:"weather_item,omitempty"` +} + +type Weather_item_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 weather_item_type"` + + // Weather item type (Wind, Wave etc) + Weather_item_code *Weather_item_code_enum `xml:"weather_item_code,omitempty"` + + // Actual or Minimum value + Value_min float32 `xml:"value_min,omitempty"` + + // Maximum value + Value_max float32 `xml:"value_max,omitempty"` + + // Gusts value (Wind) + Value_gusts float32 `xml:"value_gusts,omitempty"` + + // Unit of the value + Unit *Unit_enum `xml:"unit,omitempty"` + + // Classification of wind report + Weather_category_code *Weather_category_code_enum `xml:"weather_category_code,omitempty"` + + // Direction of wind or wave + Direction_code_min *Weather_direction_code_enum `xml:"direction_code_min,omitempty"` + + // Direction of wind or wave + Direction_code_max *Weather_direction_code_enum `xml:"direction_code_max,omitempty"` +} + +type Nts_number_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 nts_number"` + + // Name of the publishing organisation (NtS Provider) + Organisation string `xml:"organisation,omitempty"` + + // Year of first issuing of the notice + Year *GYear `xml:"year,omitempty"` + + // Number of the notice (per year, starting with: 1, 0 shall not be used for published notices) + Number int32 `xml:"number,omitempty"` + + // Serial number of notice (replacements and withdrawals), original notice: 0 + Serial_number int32 `xml:"serial_number,omitempty"` +} + +type Validity_period_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 validity_period"` + + // Start date of validity period including time zone + Date_start string `xml:"date_start,omitempty"` + + // End date of validity period including time zone + Date_end string `xml:"date_end,omitempty"` +} + +type Fairway_section_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 fairway_section_type"` + + // Geo information of fairway + Geo_object *Geo_object_type `xml:"geo_object,omitempty"` + + // Fairway section limitations + Limitation []*Limitation_type `xml:"limitation,omitempty"` +} + +type Geo_object_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 geo_object"` + + // ISRS Location Code of the fairway/object + Id *Isrs_code_type `xml:"id,omitempty"` + + // Local name of the fairway section + Name string `xml:"name,omitempty"` + + // Type of geographical object + Type_code *Type_code_enum `xml:"type_code,omitempty"` + + // Describes the position related to the fairway + Position_code *Position_code_enum `xml:"position_code,omitempty"` + + // Fairway section begin and end coordinates + Coordinate *Coordinate_type `xml:"coordinate,omitempty"` + + // Waterway name (usefull if no RIS Index is available) + Fairway_name string `xml:"fairway_name,omitempty"` +} + +type Coordinate_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 coordinate"` + + Lat string `xml:"lat,omitempty"` + + Long string `xml:"long,omitempty"` +} + +type Limitation_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 limitation_type"` + + // Limitation periods / intervals + Limitation_period []*Limitation_period_type `xml:"limitation_period,omitempty"` + + // Kind of limitation + Limitation_code *Limitation_code_enum `xml:"limitation_code,omitempty"` + + // Describes the position of the limitation related to the fairway + Position_code *Position_code_enum `xml:"position_code,omitempty"` + + // Value of limitation (i.e. max draught) + Value float32 `xml:"value,omitempty"` + + // Unit of the value of the limitation + Unit *Unit_enum `xml:"unit,omitempty"` + + // Value reference + Reference_code *Reference_code_enum `xml:"reference_code,omitempty"` + + // Minimum or maximum or reduced by + Indication_code *Indication_code_enum `xml:"indication_code,omitempty"` + + // Target group information + Target_group []*Target_group_type `xml:"target_group,omitempty"` +} + +type Limitation_period_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 limitation_period_type"` + + // Start date of limitation period including time zone + Date_start time.Time `xml:"date_start,omitempty"` + + // End date of limitation period including time zone + Date_end time.Time `xml:"date_end,omitempty"` + + // Start time of limitation period without time zone + Time_start time.Time `xml:"time_start,omitempty"` + + // End time of limitation period without time zone + Time_end time.Time `xml:"time_end,omitempty"` + + // Interval for limitation if applicable + Interval_code *Interval_code_enum `xml:"interval_code,omitempty"` +} + +type Target_group_type struct { + XMLName xml.Name `xml:"http://www.ris.eu/nts/4.0.4.0 target_group_type"` + + // Target group (vessel type) + Target_group_code *Target_group_code_enum `xml:"target_group_code,omitempty"` + + // Upstream or downstream traffic, or both + Direction_code *Direction_code_enum `xml:"direction_code,omitempty"` +} + +type NtS_message_service interface { + Get_messages(request *Get_messages_query) (*Get_messages_result, error) +} + +type INtSMessageService struct { + client *soap.SOAPClient +} + +func NewINtSMessageService(url string, tls bool, auth *soap.BasicAuth) *INtSMessageService { + if url == "" { + url = "" + } + client := soap.NewSOAPClient(url, tls, auth) + return &INtSMessageService{ + client: client, + } +} + +func NewINtSMessageServiceWithTLSConfig(url string, tlsCfg *tls.Config, auth *soap.BasicAuth) *INtSMessageService { + if url == "" { + url = "" + } + client := soap.NewSOAPClientWithTLSConfig(url, tlsCfg, auth) + + return &INtSMessageService{ + client: client, + } +} + +func (service *INtSMessageService) Get_messages(request *Get_messages_query) (*Get_messages_result, error) { + response := new(Get_messages_result) + err := service.client.Call("http://www.ris.eu/nts.ms/get_messages", request, response) + if err != nil { + return nil, err + } + + return response, nil +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/wfs/capabilities.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,344 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package wfs + +import ( + "encoding/xml" + "errors" + "io" + "regexp" + "strconv" + + "golang.org/x/net/html/charset" +) + +type Keyword struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Keyword"` + Value string `xml:",cdata"` +} +type Keywords struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Keywords"` + Keywords []Keyword `xml:"Keyword"` +} + +type ServiceIdentification struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 ServiceIdentification"` + Title string + Abstract string + Keywords Keywords `xml:"Keywords"` + ServiceType string + ServiceTypeVersion string +} + +type Get struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Get"` + HRef string `xml:"http://www.w3.org/1999/xlink href,attr"` +} + +type Post struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Post"` + HRef string `xml:"http://www.w3.org/1999/xlink href,attr"` +} + +type HTTP struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 HTTP"` + Get *Get `xml:"Get"` + Post *Post `xml:"Post"` +} + +type DCP struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 DCP"` + HTTP HTTP `xml:"HTTP"` +} + +type Value struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Value"` + Value string `xml:",cdata"` +} + +type AllowedValues struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 AllowedValues"` + Values []Value `xml:"Value"` +} + +type Parameter struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Parameter"` + Name string `xml:"name,attr"` + AllowedValues AllowedValues `xml:"AllowedValues"` +} + +type DefaultValue struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 DefaultValue"` + Value string `xml:",cdata"` +} + +type Constraint struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Constraint"` + Name string `xml:"name,attr"` + AllowedValues AllowedValues `xml:"AllowedValues"` + DefaultValue *DefaultValue `xml:"DefaultValue"` +} + +type Operation struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Operation"` + Name string `xml:"name,attr"` + DCP DCP `xml:"DCP"` + Parameters []*Parameter `xml:"Parameter"` + Constraints []*Constraint `xml:"Constraint"` +} + +type OperationsMetadata struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 OperationsMetadata"` + Operations []*Operation `xml:"Operation"` + Constraints []*Constraint `xml:"Constraint"` +} + +type WGS84BoundingBox struct { + XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 WGS84BoundingBox"` + LowerCorner string `xml:"LowerCorner"` + UpperCorner string `xml:"UpperCorner"` +} + +type FeatureType struct { + XMLName xml.Name `xml:"http://www.opengis.net/wfs/2.0 FeatureType"` + Name string `xml:"Name"` + Title string `xml:"Title"` + Abstract string `xml:"Abstract"` + Keywords Keywords `xml:"Keywords"` + DefaultCRS string `xml:"DefaultCRS"` + OtherCRSs []string `xml:"OtherCRS"` + WGS84BoundingBox *WGS84BoundingBox `xml:"WGS84BoundingBox"` + Namespaces []xml.Name `xml:"-"` +} + +// shadowFeatureType is used to prevent recursive UnmarshalXML for FeatureType. +type shadowFeatureType struct { + XMLName xml.Name `xml:"http://www.opengis.net/wfs/2.0 FeatureType"` + Name string `xml:"Name"` + Title string `xml:"Title"` + Abstract string `xml:"Abstract"` + Keywords Keywords `xml:"Keywords"` + DefaultCRS string `xml:"DefaultCRS"` + OtherCRSs []string `xml:"OtherCRS"` + WGS84BoundingBox *WGS84BoundingBox `xml:"WGS84BoundingBox"` + Namespaces []xml.Name `xml:"-"` +} + +func (ft *FeatureType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + // Filter out the namespaces for this feature type. + var ns []xml.Name + for _, attr := range start.Attr { + if attr.Name.Space == "xmlns" { + ns = append(ns, xml.Name{Space: attr.Name.Local, Local: attr.Value}) + } + } + var sft shadowFeatureType + if err := d.DecodeElement(&sft, &start); err != nil { + return err + } + *ft = FeatureType(sft) + ft.Namespaces = ns + return nil +} + +type FeatureTypeList struct { + XMLName xml.Name `xml:"http://www.opengis.net/wfs/2.0 FeatureTypeList"` + FeatureTypes []*FeatureType `xml:"FeatureType"` +} + +type Capabilities struct { + XMLName xml.Name `xml:"http://www.opengis.net/wfs/2.0 WFS_Capabilities"` + + BaseURL string `xml:"-"` + + ServiceIdentification ServiceIdentification + OperationsMetadata OperationsMetadata + FeatureTypeList FeatureTypeList +} + +func (c *Capabilities) FindOperation(name string) *Operation { + for _, op := range c.OperationsMetadata.Operations { + if op.Name == name { + return op + } + } + return nil +} + +func (o *Operation) SupportsHits() bool { + for _, p := range o.Parameters { + if p.Name == "resultType" { + for _, av := range p.AllowedValues.Values { + if av.Value == "hits" { + return true + } + } + } + } + return false +} + +func (o *Operation) SupportsOutputFormat(formats ...string) bool { + for _, p := range o.Parameters { + if p.Name == "outputFormat" { + for _, av := range p.AllowedValues.Values { + for _, f := range formats { + if av.Value == f { + return true + } + } + } + } + } + return false +} + +func (o *Operation) FeaturesPerPage() (int, bool) { + for _, c := range o.Constraints { + if c.Name == "CountDefault" { + if c.DefaultValue != nil { + if v, err := strconv.Atoi(c.DefaultValue.Value); err == nil { + return v, true + } + } + for _, av := range c.AllowedValues.Values { + if v, err := strconv.Atoi(av.Value); err == nil { + return v, true + } + + } + } + } + return 0, false +} + +func (c *Capabilities) FindFeatureType(name string) *FeatureType { + for _, ft := range c.FeatureTypeList.FeatureTypes { + if ft.Name == name { + return ft + } + } + return nil +} + +func (op *Operation) FindParameter(name string) *Parameter { + for _, p := range op.Parameters { + if p.Name == name { + return p + } + } + return nil +} + +const WFS2_0_0 = "2.0.0" + +var versionRe = regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`) + +func versionIsLess(a, b string) bool { + am := versionRe.FindStringSubmatch(a) + bm := versionRe.FindStringSubmatch(b) + + var n int + if len(am) < len(bm) { + n = len(am) + } else { + n = len(bm) + } + n-- + + for i := 0; i < n; i++ { + ai, _ := strconv.Atoi(am[i+1]) + bi, _ := strconv.Atoi(bm[i+1]) + switch { + case ai < bi: + return true + case ai > bi: + return false + } + } + return false +} + +func maxVersion(a, b string) string { + am := versionRe.FindStringSubmatch(a) + bm := versionRe.FindStringSubmatch(b) + + var n int + if len(am) < len(bm) { + n = len(am) + } else { + n = len(bm) + } + n-- + + for i := 0; i < n; i++ { + ai, _ := strconv.Atoi(am[i+1]) + bi, _ := strconv.Atoi(bm[i+1]) + switch { + case ai > bi: + return a + case bi > ai: + return b + } + } + return a +} + +func (c *Capabilities) HighestWFSVersion(def string) string { + op := c.FindOperation("GetCapabilities") + if op == nil { + return def + } + p := op.FindParameter("AcceptVersions") + if p == nil { + return def + } + if len(p.AllowedValues.Values) == 0 { + return def + } + + max := p.AllowedValues.Values[0].Value + for _, v := range p.AllowedValues.Values[1:] { + max = maxVersion(max, v.Value) + } + + return max +} + +var ( + ErrInvalidCRS = errors.New("Invalid CRS string") + crsRe = regexp.MustCompile(`urn:ogc:def:crs:EPSG:[^:]*:(\d+)`) +) + +func CRSToEPSG(s string) (int, error) { + m := crsRe.FindStringSubmatch(s) + if m == nil { + return 0, ErrInvalidCRS + } + return strconv.Atoi(m[1]) +} + +func ParseCapabilities(r io.Reader) (*Capabilities, error) { + + decoder := xml.NewDecoder(r) + decoder.CharsetReader = charset.NewReaderLabel + + var capabilities Capabilities + + if err := decoder.Decode(&capabilities); err != nil { + return nil, err + } + + return &capabilities, nil +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/wfs/download.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,260 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package wfs + +import ( + "bufio" + "encoding/xml" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strconv" + + "golang.org/x/net/html/charset" +) + +var ( + ErrNoSuchFeatureType = errors.New("No such feature type") + ErrGetFeatureNotSupported = errors.New("GetFeature not supported") + ErrMethodGetNotSupported = errors.New("GET not supported") + ErrNoNumberMatchedFound = errors.New("No numberMatched attribute found") + ErrOutputFormatNotSupported = errors.New("Output format not supported") +) + +func GetCapabilities(capURL string) (*Capabilities, error) { + + base, err := url.Parse(capURL) + if err != nil { + return nil, err + } + v := url.Values{} + v.Set("SERVICE", "WFS") + v.Set("REQUEST", "GetCapabilities") + v.Set("ACCEPTVERSIONS", "2.0.0,1.1.0,1.0.0") + base.RawQuery = v.Encode() + + baseURL := base.String() + resp, err := http.Get(baseURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + caps, err := ParseCapabilities(bufio.NewReader(resp.Body)) + if err == nil { + caps.BaseURL = baseURL + } + return caps, err +} + +func numberFeaturesGET(u *url.URL, featureType, version string) (int, error) { + + v := url.Values{} + v.Set("SERVICE", "WFS") + v.Set("REQUEST", "GetFeature") + v.Set("resultType", "hits") + v.Set("VERSION", version) + v.Set("TYPENAMES", featureType) + + q := *u + q.RawQuery = v.Encode() + + resp, err := http.Get(q.String()) + if err != nil { + return 0, err + } + defer resp.Body.Close() + dec := xml.NewDecoder(resp.Body) + dec.CharsetReader = charset.NewReaderLabel + + var result struct { + NumberMatched *int `xml:"numberMatched,attr"` + } + + if err := dec.Decode(&result); err != nil { + return 0, err + } + + if result.NumberMatched == nil { + return 0, ErrNoNumberMatchedFound + } + + return *result.NumberMatched, nil +} + +func GetFeaturesGET( + caps *Capabilities, + featureTypeName, + outputFormat string, + sortBy string, +) ([]string, error) { + + feature := caps.FindFeatureType(featureTypeName) + if feature == nil { + return nil, ErrNoSuchFeatureType + } + op := caps.FindOperation("GetFeature") + if op == nil { + return nil, ErrGetFeatureNotSupported + } + + if op.DCP.HTTP.Get == nil { + return nil, ErrMethodGetNotSupported + } + + getRaw := op.DCP.HTTP.Get.HRef + getU, err := url.Parse(getRaw) + if err != nil { + return nil, err + } + // The URL could be relative so resolve against Capabilities URL. + if !getU.IsAbs() { + base, err := url.Parse(caps.BaseURL) + if err != nil { + return nil, err + } + getU = getU.ResolveReference(base) + } + + if !op.SupportsOutputFormat(outputFormat) { + return nil, ErrOutputFormatNotSupported + } + + wfsVersion := caps.HighestWFSVersion(WFS2_0_0) + + featuresPerPage, supportsPaging := op.FeaturesPerPage() + + var numFeatures int + + if supportsPaging { + log.Printf("Paging supported with %d feature per page.\n", + featuresPerPage) + + if !op.SupportsHits() { + supportsPaging = false + } else { + numFeatures, err = numberFeaturesGET(getU, featureTypeName, wfsVersion) + if err != nil { + log.Printf("error: %v\n", err) + supportsPaging = false + } else { + log.Printf("Number of features: %d\n", numFeatures) + } + } + } + + var downloadURLs []string + wfs2 := !versionIsLess(wfsVersion, WFS2_0_0) + + addNS := func(v url.Values) { + if len(feature.Namespaces) == 0 { + return + } + // Only use first namespace + ns := feature.Namespaces[0] + if wfs2 { + v.Set("NAMESPACES", fmt.Sprintf("(%s,%s)", ns.Space, ns.Local)) + } else { + v.Set("NAMESPACE", fmt.Sprintf("(%s:%s)", ns.Space, ns.Local)) + } + } + + addOutputFormat := func(v url.Values) { + if outputFormat != "" { + v.Set("outputFormat", outputFormat) + } + } + + addSortBy := func(v url.Values) { + if sortBy != "" { + v.Set("sortBy", sortBy) + } + } + + if supportsPaging { + pagedURL := func(ofs, count int) string { + v := url.Values{} + v.Set("SERVICE", "WFS") + v.Set("REQUEST", "GetFeature") + v.Set("VERSION", wfsVersion) + v.Set("startIndex", strconv.Itoa(ofs)) + if wfs2 { + v.Set("count", strconv.Itoa(count)) + } else { + v.Set("maxFeatures", strconv.Itoa(count)) + } + v.Set("TYPENAMES", featureTypeName) + addNS(v) + addOutputFormat(v) + addSortBy(v) + q := *getU + q.RawQuery = v.Encode() + return q.String() + } + if numFeatures <= featuresPerPage { + log.Println("All features can be fetched in one page") + downloadURLs = []string{pagedURL(0, numFeatures)} + } else { + log.Println("Features need to be downloaded in pages.") + for pos := 0; pos < numFeatures; { + var count int + if rest := numFeatures - pos; rest >= numFeatures { + count = numFeatures + } else { + count = rest + } + downloadURLs = append(downloadURLs, pagedURL(pos, count)) + pos += count + } + } + } else { // No paging support. + v := url.Values{} + v.Set("SERVICE", "WFS") + v.Set("REQUEST", "GetFeature") + v.Set("VERSION", wfsVersion) + v.Set("TYPENAMES", featureTypeName) + addNS(v) + addOutputFormat(v) + addSortBy(v) + q := *getU + q.RawQuery = v.Encode() + downloadURLs = []string{q.String()} + } + + return downloadURLs, nil +} + +func downloadURL(url string, handler func(io.Reader) error) error { + resp, err := http.Get(url) + if err != nil { + return err + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return fmt.Errorf("Invalid HTTP status code: %d (%s)", + resp.StatusCode, resp.Status) + } + defer resp.Body.Close() + return handler(resp.Body) +} + +func DownloadURLs(urls []string, handler func(io.Reader) error) error { + for _, url := range urls { + if err := downloadURL(url, handler); err != nil { + return nil + } + } + return nil +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/wfs/rawfeaturecollection.go Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,42 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package wfs + +import ( + "encoding/json" + "io" +) + +type RawFeatureCollection struct { + CRS *struct { + Properties struct { + Name string `json:"name"` + } `json:"properties"` + } `json:"crs"` + Features []*struct { + Geometry struct { + Coordinates *json.RawMessage `json:"coordinates"` + Type string `json:"type"` + } `json:"geometry"` + Properties *json.RawMessage `json:"properties"` + } `json:"features"` +} + +func ParseRawFeatureCollection(r io.Reader) (*RawFeatureCollection, error) { + rfc := new(RawFeatureCollection) + if err := json.NewDecoder(r).Decode(rfc); err != nil { + return nil, err + } + return rfc, nil +}
--- a/schema/auth.sql Sat Dec 29 16:06:54 2018 +0100 +++ b/schema/auth.sql Sat Dec 29 16:07:40 2018 +0100 @@ -34,14 +34,13 @@ -- -- Extended privileges for waterway_admin -- -GRANT INSERT, UPDATE ON ALL TABLES IN SCHEMA waterway TO waterway_admin; --- TODO: will there ever be UPDATEs or can we drop that due to historicisation? +GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA waterway + TO waterway_admin; +-- TODO: will there ever be UPDATEs and DELETEs or can we drop that for +-- imported data due to historicisation? Special tables like +-- import_configuration will further need UPDATE and DELETE privileges. GRANT INSERT, UPDATE, DELETE ON users.templates, users.user_templates TO waterway_admin; -GRANT INSERT, UPDATE, DELETE ON - waterway.imports, waterway.import_logs, waterway.track_imports, - waterway.sounding_results - TO waterway_admin; -- -- Extended privileges for sys_admin @@ -78,7 +77,8 @@ DECLARE the_table varchar; BEGIN FOREACH the_table IN ARRAY ARRAY[ - 'gauge_measurements', + -- 'gauge_measurements', XXX Removed since this table has currently no + -- staging 'sections_stretches', 'waterway_profiles', 'fairway_dimensions', @@ -151,4 +151,13 @@ FOR ALL TO waterway_admin USING (utm_covers(area)); +CREATE POLICY import_configuration_policy ON waterway.import_configuration + FOR ALL TO waterway_admin + USING ( + users.current_user_country() = ( + SELECT country FROM users.list_users lu + WHERE lu.username = waterway.import_configuration.username)); + +ALTER table waterway.import_configuration ENABLE ROW LEVEL SECURITY; + COMMIT;
--- a/schema/gemma.sql Sat Dec 29 16:06:54 2018 +0100 +++ b/schema/gemma.sql Sat Dec 29 16:07:40 2018 +0100 @@ -139,9 +139,13 @@ ); CREATE TABLE levels_of_service ( - level_of_service smallint PRIMARY KEY + level_of_service smallint PRIMARY KEY, + name varchar(4) ); -INSERT INTO levels_of_service VALUES (1), (2), (3); +INSERT INTO levels_of_service ( + level_of_service, + name +) VALUES (1, 'LOS1'), (2, 'LOS2'), (3, 'LOS3'); CREATE TABLE riverbed_materials ( material varchar PRIMARY KEY @@ -261,11 +265,11 @@ ) CREATE TABLE gauge_measurements ( + id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, fk_gauge_id isrs NOT NULL REFERENCES gauges, measure_date timestamp with time zone NOT NULL, - PRIMARY KEY (fk_gauge_id, measure_date), - -- XXX: Is country_code really relevant for GEMMA or just NtS? - -- country_code char(2) NOT NULL REFERENCES countries, + -- PRIMARY KEY (fk_gauge_id, measure_date), + country_code char(2) NOT NULL REFERENCES countries, -- TODO: add relations to stuff provided as enumerations sender varchar NOT NULL, -- "from" attribute from DRC language_code varchar NOT NULL REFERENCES language_codes, @@ -281,8 +285,11 @@ value_max double precision, -- XXX: NOT NULL if predicted? --- TODO: Add a double range type for checking? date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - source_organization varchar NOT NULL, -- "originator" - staging_done boolean NOT NULL DEFAULT false + source_organization varchar NOT NULL -- "originator" + -- XXX removed staging done temporarily. Currently imported raw data is + -- not staged. When importing approved gauge measurements uncomment this + -- and add policy to allow select on this table for waterway_admin + -- staging_done boolean NOT NULL DEFAULT false ) CREATE TRIGGER gauge_measurements_date_info BEFORE UPDATE ON gauge_measurements @@ -295,7 +302,8 @@ objnam varchar NOT NULL, nobjnam varchar ) - CREATE UNIQUE INDEX ON waterway_axis ((ST_GeoHash(wtwaxs, 23))) + -- TODO: @tom: Why did you choose this index kind? + -- CREATE UNIQUE INDEX ON waterway_axis ((ST_GeoHash(wtwaxs, 23))) -- This table allows linkage between 1D ISRS location codes and 2D space -- e.g. for cutting bottleneck area out of waterway area based on virtual @@ -402,7 +410,8 @@ -- (minOccurs=0; nillable seems to be set arbitrarily as even bottleneck_id and -- fk_g_fid (both mandatory, i.e. marked "M" in DRC) have nillable="true" in WSDL) CREATE TABLE bottlenecks ( - bottleneck_id varchar PRIMARY KEY, + id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + bottleneck_id varchar UNIQUE NOT NULL, fk_g_fid isrs NOT NULL REFERENCES gauges, -- XXX: DRC references "ch. 3.1.1", which does not exist in document. objnam varchar, @@ -431,7 +440,7 @@ FOR EACH ROW EXECUTE PROCEDURE update_date_info() CREATE TABLE bottlenecks_riverbed_materials ( - bottleneck_id varchar REFERENCES bottlenecks, + bottleneck_id int REFERENCES bottlenecks(id), riverbed varchar REFERENCES riverbed_materials, -- XXX: should be 'natsur' according to IENC Encoding Guide M.4.3 PRIMARY KEY (bottleneck_id, riverbed) @@ -439,7 +448,7 @@ CREATE TABLE sounding_results ( id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, - bottleneck_id varchar NOT NULL REFERENCES bottlenecks, + bottleneck_id int NOT NULL REFERENCES bottlenecks(id), date_info date NOT NULL, UNIQUE (bottleneck_id, date_info), area geography(POLYGON, 4326) NOT NULL, @@ -476,7 +485,7 @@ CREATE TABLE fairway_availability ( id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, position_code char(2) REFERENCES position_codes, - bottleneck_id varchar NOT NULL REFERENCES bottlenecks, + bottleneck_id int NOT NULL REFERENCES bottlenecks(id), surdat date NOT NULL, UNIQUE (bottleneck_id, surdat), -- additional_data xml -- Currently not relevant for GEMMA @@ -535,12 +544,25 @@ ST_Centroid(area)::Geometry AS point, (lower(stretch)).hectometre AS from, (upper(stretch)).hectometre AS to, - sr.current + sr.current::text FROM waterway.bottlenecks bn LEFT JOIN ( SELECT bottleneck_id, max(date_info) AS current FROM waterway.sounding_results - GROUP BY bottleneck_id) sr ON sr.bottleneck_id = bn.bottleneck_id + GROUP BY bottleneck_id) sr ON sr.bottleneck_id = bn.id ORDER BY objnam + + CREATE TABLE import_configuration ( + id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + username varchar NOT NULL + REFERENCES internal.user_profiles(username) + ON DELETE CASCADE + ON UPDATE CASCADE, + kind varchar NOT NULL, + send_email boolean NOT NULL DEFAULT false, + auto_accept boolean NOT NULL DEFAULT false, + cron varchar, + url varchar + ) ; -- Configure primary keys for geoserver views @@ -570,6 +592,8 @@ REFERENCES internal.user_profiles(username) ON DELETE SET NULL ON UPDATE CASCADE, + send_email boolean NOT NULL DEFAULT false, + auto_accept boolean NOT NULL DEFAULT false, data TEXT, summary TEXT );
--- a/schema/install-db.sh Sat Dec 29 16:06:54 2018 +0100 +++ b/schema/install-db.sh Sat Dec 29 16:07:40 2018 +0100 @@ -125,6 +125,7 @@ -f "$BASEDIR/geonames.sql" \ -f "$BASEDIR/manage_users.sql" \ -f "$BASEDIR/auth.sql" \ + -f "$BASEDIR/isrs_functions.sql" \ -f "$BASEDIR/default_sysconfig.sql"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/isrs_functions.sql Sat Dec 29 16:07:40 2018 +0100 @@ -0,0 +1,82 @@ +-- Clip an area to a stretch given by a pair of ISRS location codes. +-- Uses the table waterway.distance_marks_virtual to map ISRS location codes +-- to their geo-location and the table waterway.waterway_axis to retrieve +-- perpendicular direction at these geo-locations. +-- Distance marks are assumed to be near the axis and the area passed as +-- argument is assumed to intersect with the axis +-- (use e.g. waterway area or fairway dimensions). +CREATE OR REPLACE FUNCTION ISRSrange_area( + stretch isrsrange, + area geometry +) RETURNS geometry +AS $$ + WITH + -- Get coordinates of location codes + from_geog AS ( + SELECT geom FROM waterway.distance_marks_virtual + WHERE location_code = lower(stretch)), + to_geog AS ( + SELECT geom FROM waterway.distance_marks_virtual + WHERE location_code = upper(stretch)), + utm_zone AS ( + -- Find best matchting UTM zone + SELECT best_utm(ST_Collect( + from_geog.geom::geometry, + to_geog.geom::geometry)) AS z + FROM from_geog, to_geog), + axis AS ( + -- Transform and sew together contiguous axis chunks + SELECT ST_LineMerge(ST_Collect(ST_Transform( + wtwaxs::geometry, z))) AS wtwaxs + FROM waterway.waterway_axis, utm_zone), + -- In order to guarantee the following ST_Covers to work, + -- snap distance mark coordinates to axis + from_point AS ( + SELECT ST_ClosestPoint( + wtwaxs, + ST_Transform(from_geog.geom::geometry, z)) AS geom + FROM axis, from_geog, utm_zone), + to_point AS ( + SELECT ST_ClosestPoint( + wtwaxs, + ST_Transform(to_geog.geom::geometry, z)) AS geom + FROM axis, to_geog, utm_zone), + axis_segment AS ( + -- select the contiguous axis on which distance marks lie + SELECT line + FROM ( + SELECT (ST_Dump(wtwaxs)).geom AS line + FROM axis) AS lines, + (SELECT ST_Collect(from_point.geom, to_point.geom) AS pts + FROM from_point, to_point) AS points + WHERE ST_Covers(lines.line, points.pts)), + axis_substring AS ( + -- Use linear referencing to clip axis between distance marks + SELECT ST_LineSubstring( + axis_segment.line, + ST_LineLocatePoint(axis_segment.line, from_point.geom), + ST_LineLocatePoint(axis_segment.line, to_point.geom) + ) AS line + FROM axis_segment, from_point, to_point), + range_area AS ( + -- Create a buffer around the clipped axis, as large as it could + -- potentially be intersecting with the area polygon that + -- intersects with the clipped axis. Get the intersection of that + -- buffer with the area polygon, which can potentially + -- be a multipolygon. + SELECT (ST_Dump(ST_Intersection( + ST_Buffer( + axis_substring.line, + ST_MaxDistance( + axis_substring.line, + ST_Transform(area, z)), + 'endcap=flat'), + ST_Transform(area, z)))).geom + FROM axis_substring, utm_zone) + -- From the polygons returned by the last CTE, select only the one + -- around the clipped axis + SELECT ST_Transform(range_area.geom, ST_SRID(area)) + FROM axis_substring, range_area + WHERE ST_Intersects(range_area.geom, axis_substring.line) + $$ + LANGUAGE sql;
--- a/schema/isrs_tests.sql Sat Dec 29 16:06:54 2018 +0100 +++ b/schema/isrs_tests.sql Sat Dec 29 16:07:40 2018 +0100 @@ -28,3 +28,33 @@ $$, 22023, NULL, 'ISRS text input needs to have correct length'); + +SELECT ok( + ISRSrange_area(isrsrange( + ('AT', 'XXX', '00001', '00000', 0)::isrs, + ('AT', 'XXX', '00001', '00000', 1)::isrs), + ST_SetSRID('POLYGON((0 1, 0 2, 1 2, 1 1, 0 1))'::geometry, 4326) + ) IS NULL, + 'ISRSrange_area returns NULL, if given area does not intersect with axis'); + +SELECT ok( + ST_DWithin( + (SELECT geom FROM waterway.distance_marks_virtual + WHERE location_code = ('AT', 'XXX', '00001', '00000', 0)::isrs), + ST_Boundary(ISRSrange_area(isrsrange( + ('AT', 'XXX', '00001', '00000', 0)::isrs, + ('AT', 'XXX', '00001', '00000', 1)::isrs), + ST_SetSRID('POLYGON((-1 1, 2 1, 2 -1, -1 -1, -1 1))'::geometry, + 4326)))::geography, + 1) + AND + ST_DWithin( + (SELECT geom FROM waterway.distance_marks_virtual + WHERE location_code = ('AT', 'XXX', '00001', '00000', 1)::isrs), + ST_Boundary(ISRSrange_area(isrsrange( + ('AT', 'XXX', '00001', '00000', 0)::isrs, + ('AT', 'XXX', '00001', '00000', 1)::isrs), + ST_SetSRID('POLYGON((-1 1, 2 1, 2 -1, -1 -1, -1 1))'::geometry, + 4326)))::geography, + 1), + 'Resulting polygon almost ST_Touches points corresponding to stretch');
--- a/schema/run_tests.sh Sat Dec 29 16:06:54 2018 +0100 +++ b/schema/run_tests.sh Sat Dec 29 16:07:40 2018 +0100 @@ -28,7 +28,7 @@ -c 'SET client_min_messages TO WARNING' \ -c "DROP ROLE IF EXISTS $TEST_ROLES" \ -f tap_tests_data.sql \ - -c 'SELECT plan(45)' \ + -c 'SELECT plan(47)' \ -f isrs_tests.sql \ -f auth_tests.sql \ -f manage_users_tests.sql \
--- a/schema/tap_tests_data.sql Sat Dec 29 16:06:54 2018 +0100 +++ b/schema/tap_tests_data.sql Sat Dec 29 16:07:40 2018 +0100 @@ -64,6 +64,26 @@ 1, 'depth', 'testorganization', true ); +INSERT INTO waterway.distance_marks_virtual VALUES ( + ('AT', 'XXX', '00001', '00000', 0)::isrs, + ST_SetSRID('POINT(0 0)'::geometry, 4326), + 'someENC' +), ( + ('AT', 'XXX', '00001', '00000', 1)::isrs, + ST_SetSRID('POINT(1 0)'::geometry, 4326), + 'someENC' +); + +INSERT INTO waterway.waterway_axis (wtwaxs, objnam) VALUES ( + ST_SetSRID(ST_CurveToLine( + 'CIRCULARSTRING(0 0, 0.5 0.5, 1 0, 1.5 -0.2, 2 0)'::geometry), + 4326), + 'testriver' +), ( + ST_SetSRID('LINESTRING(0.5 0.5, 1 1)'::geometry, 4326), + 'testriver' +); + INSERT INTO users.templates (template_name, template_data) VALUES ('AT', '\x'), ('RO', '\x'); INSERT INTO users.user_templates