Mercurial > gemma
changeset 2481:3cf5d27a6c8b octree-diff
Merged defualt into octree-diff branch.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Fri, 01 Mar 2019 11:06:27 +0100 |
parents | 242104c338ff (current diff) 9de710bdb535 (diff) |
children | 620038ade708 |
files | client/src/components/importqueue/Importqueue.vue client/src/components/importqueue/Importqueuedetail.vue client/src/components/staging/Staging.vue client/src/components/staging/StagingDetail.vue client/src/components/ui/box/Header.vue |
diffstat | 36 files changed, 1905 insertions(+), 1853 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/assets/application.scss Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/assets/application.scss Fri Mar 01 11:06:27 2019 +0100 @@ -57,6 +57,14 @@ border: 1px solid red; } +.debug2 { + border: 1px solid magenta; +} + +.debug3 { + border: 1px solid greenyellow; +} + %fully-centered { position: absolute; top: 50%; @@ -120,10 +128,12 @@ font-weight: bold; } -.fade-enter-active, .fade-leave-active { - transition: opacity .3s; +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.3s; } -.fade-enter, .fade-leave-to { +.fade-enter, +.fade-leave-to { opacity: 0; }
--- a/client/src/components/App.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/App.vue Fri Mar 01 11:06:27 2019 +0100 @@ -37,9 +37,6 @@ .small { width: $icon-width; } -.wide { - width: 600px; -} .userinterface { position: absolute; @@ -90,7 +87,9 @@ ...mapState("user", ["isAuthenticated"]), ...mapState("application", ["contextBoxContent", "showSearchbar"]), isMapVisible() { - return /stretches|review|bottlenecks|mainview/.test(this.routeName); + return /importoverview|stretches|review|bottlenecks|mainview/.test( + this.routeName + ); }, routeName() { const routeName = this.$route.name;
--- a/client/src/components/Bottlenecks.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/Bottlenecks.vue Fri Mar 01 11:06:27 2019 +0100 @@ -158,7 +158,7 @@ "showSearchbarLastState", "showSplitscreen" ]), - ...mapState("bottlenecks", ["bottlenecks"]), + ...mapState("bottlenecks", ["bottlenecksList"]), sortIcon() { return this.sortDirection === "ASC" ? "sort-amount-down" @@ -170,7 +170,7 @@ return formatSurveyDate(date); }, filteredAndSortedBottlenecks() { - return this.bottlenecks + return this.bottlenecksList .filter(bn => { return bn.properties.name .toLowerCase() @@ -287,7 +287,7 @@ } }, mounted() { - this.$store.dispatch("bottlenecks/loadBottlenecks"); + this.$store.dispatch("bottlenecks/loadBottlenecksList"); } }; </script>
--- a/client/src/components/Contextbox.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/Contextbox.vue Fri Mar 01 11:06:27 2019 +0100 @@ -3,6 +3,9 @@ <Bottlenecks v-if="contextBoxContent === 'bottlenecks'"></Bottlenecks> <Staging v-if="contextBoxContent === 'staging'"></Staging> <Stretches v-if="contextBoxContent === 'stretches'"></Stretches> + <ImportOverview + v-if="contextBoxContent === 'importoverview'" + ></ImportOverview> </div> </template> @@ -25,9 +28,10 @@ export default { name: "contextbox", components: { - Bottlenecks: () => import("./Bottlenecks"), - Staging: () => import("./staging/Staging.vue"), - Stretches: () => import("./ImportStretches.vue") + Bottlenecks: () => import("@/components/Bottlenecks"), + Stretches: () => import("@/components/ImportStretches.vue"), + ImportOverview: () => + import("@/components/importoverview/ImportOverview.vue") }, computed: { ...mapState("application", [ @@ -71,7 +75,7 @@ background: #fff; } .contextbox > div:last-child { - width: 600px; + width: 660px; } .contextboxcollapsed { @@ -80,8 +84,7 @@ } .contextboxextended { - max-width: 700px; - max-height: 640px; + max-width: 660px; } .close-contextbox {
--- a/client/src/components/ImportSoundingresults.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/ImportSoundingresults.vue Fri Mar 01 11:06:27 2019 +0100 @@ -2,21 +2,30 @@ <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"> + <div class="card shadow-xs mt-3 mr-3 w-100 h-100"> <UIBoxHeader icon="upload" title="Import Soundingresults" /> - <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"> + <div v-if="editState"> + <div + v-for="(message, index) in messages" + :key="index" + class="alert alert-warning small rounded-0" + > + {{ message }} + </div> + <div class="container"> + <div class="row"> + <div class="col-5"> <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 + :value="bottleneck" + :key="bottleneck.properties.objnam" > + {{ bottleneck.properties.objnam }} + </option> </select> <span class="text-danger"> <small v-if="!bottleneck"> @@ -24,7 +33,7 @@ </small> </span> </div> - <div class="d-flex flex-column mt-1 text-left w-50 mr-2"> + <div class="col-2"> <small class="text-muted"> <translate>Projection</translate> (EPSG) </small> @@ -41,9 +50,7 @@ </small> </span> </div> - </div> - <div class="d-flex flex-row"> - <div class="mt-1 text-left w-50 ml-2 mr-4"> + <div class="col-2"> <small class="text-muted"> <translate>Depthreference</translate> </small> @@ -53,7 +60,7 @@ id="depthreference" > <option - v-for="option in this.$options.depthReferenceOptions" + v-for="option in this.depthReferenceOptions" :key="option" >{{ option }}</option > @@ -64,7 +71,7 @@ </small> </span> </div> - <div class="mt-1 text-left w-50 mr-2"> + <div class="col-3"> <small class="text-muted"> <translate>Date</translate> </small> <input id="importdate" @@ -82,15 +89,11 @@ </span> </div> </div> - </div> - <div class="ml-2 mt-2 text-left"> - <small v-for="(message, index) in messages" :key="index"> - {{ message }} - </small> + <div class="row"></div> </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="container py-5"> + <div v-if="uploadState" class="input-group"> <div class="custom-file"> <input accept=".zip" @@ -104,29 +107,31 @@ </label> </div> </div> - <div class="buttons text-right" v-if="editState"> + <div class="d-flex justify-content-between" v-if="editState"> <a download="meta.json" :href="dataLink" - class="btn btn-outline-info pull-left mt-4" + class="btn btn-outline-info" > <translate>Download Meta.json</translate> </a> - <button - @click="deleteTempData" - class="btn btn-danger mt-4" - type="button" - > - <translate>Cancel Upload</translate> - </button> - <button - :disabled="disableUploadButton" - @click="confirm" - class="btn btn-info mt-4" - type="button" - > - <translate>Confirm</translate> - </button> + <span> + <button + @click="deleteTempData" + class="btn btn-danger" + type="button" + > + <translate>Cancel Upload</translate> + </button> + <button + :disabled="disableUploadButton" + @click="confirm" + class="btn btn-info ml-2" + type="button" + > + <translate>Confirm</translate> + </button> + </span> </div> </div> </div> @@ -179,7 +184,7 @@ initialState() { this.importState = IMPORTSTATE.UPLOAD; this.depthReference = ""; - this.bottleneck = ""; + this.bottleneck = null; this.projection = ""; this.importDate = ""; this.uploadLabel = this.$gettext("choose .zip- file"); @@ -225,7 +230,9 @@ if (response.data.meta) { const { bottleneck, date, epsg } = response.data.meta; const depthReference = response.data.meta["depth-reference"]; - this.bottleneck = bottleneck; + this.bottleneck = this.bottlenecks.find( + bn => bn.properties.objnam === bottleneck + ); this.depthReference = depthReference; this.importDate = new Date(date).toISOString().split("T")[0]; this.projection = epsg; @@ -246,7 +253,8 @@ confirm() { let formData = new FormData(); formData.append("token", this.token); - if (this.bottleneck) formData.append("bottleneck", this.bottleneck); + if (this.bottleneck) + formData.append("bottleneck", this.bottleneck.properties.objnam); if (this.importDate) formData.append("date", this.importDate.split("T")[0]); if (this.depthReference) @@ -262,7 +270,9 @@ .then(() => { displayInfo({ title: this.$gettext("Import"), - message: this.$gettext("Starting import for ") + this.bottleneck + message: + this.$gettext("Starting import for ") + + this.bottleneck.properties.objnam }); this.initialState(); }) @@ -298,7 +308,7 @@ return this.disableUpload; }, availableBottlenecks() { - return this.bottlenecks.map(x => x.properties.name); + return this.bottlenecks; }, editState() { return this.importState === IMPORTSTATE.EDIT; @@ -318,63 +328,23 @@ encodeURIComponent( JSON.stringify({ depthReference: this.depthReference, - bottleneck: this.bottleneck, + bottleneck: this.bottleneck.properties.objnam, date: this.importDate }) ) ); + }, + depthReferenceOptions() { + if ( + this.bottleneck && + this.bottleneck.properties.reference_water_levels + ) { + return Object.keys( + JSON.parse(this.bottleneck.properties.reference_water_levels) + ); + } + return []; } - }, - 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>
--- a/client/src/components/ImportStretches.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/ImportStretches.vue Fri Mar 01 11:06:27 2019 +0100 @@ -346,8 +346,8 @@ this.countryCode = properties.countries; this.source = properties["source_organization"]; this.edit = true; - this.startrhm = this.sanitizeRHM(properties.lower); - this.endrhm = this.sanitizeRHM(properties.upper); + this.startrhm = properties.lower; + this.endrhm = properties.upper; this.idEditable = false; }, deleteStretch(stretch) { @@ -535,8 +535,8 @@ }, pointsValid() { if (!this.startrhm || !this.endrhm) return true; - const start = this.startrhm.replace(/\D+/, "") * 1; - const end = this.endrhm.replace(/\D+/, "") * 1; + const start = this.startrhm.replace(/\D+/g, "") * 1; + const end = this.endrhm.replace(/\D+/g, "") * 1; const result = start < end; return result; }
--- a/client/src/components/Maplayer.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/Maplayer.vue Fri Mar 01 11:06:27 2019 +0100 @@ -399,7 +399,6 @@ this.updateBottleneckFilter("does_not_exist", "1999-10-01"); this.$store.dispatch("map/disableIdentifyTool"); this.$store.dispatch("map/enableIdentifyTool"); - this.$store.dispatch("bottlenecks/loadBottlenecks"); } }; </script>
--- a/client/src/components/Search.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/Search.vue Fri Mar 01 11:06:27 2019 +0100 @@ -77,7 +77,7 @@ .searchgroup { margin-left: -3px; - width: 571px; + width: 630px; overflow: hidden; }
--- a/client/src/components/Sidebar.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/Sidebar.vue Fri Mar 01 11:06:27 2019 +0100 @@ -28,7 +28,7 @@ <span class="fix-trans-space" v-translate>Bottlenecks</span> </router-link> <div v-if="isWaterwayAdmin"> - <router-link to="/review" class="position-relative"> + <router-link to="/imports/overview" class="position-relative"> <font-awesome-icon class="fa-fw mr-2" fixed-width @@ -126,16 +126,6 @@ <span class="fix-trans-space" v-translate>Logs</span> </router-link> </div> - <div v-if="isWaterwayAdmin"> - <router-link to="/importqueue"> - <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> - </div> <hr class="m-0" /> <a @click="logoff" href="#" class="logout"> <font-awesome-icon @@ -212,13 +202,15 @@ } }, mounted() { - this.$store.dispatch("imports/getStaging").catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` + setTimeout(() => { + this.$store.dispatch("imports/getStaging").catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); }); - }); + }, 15000); } }; </script> @@ -255,9 +247,13 @@ .indicator { left: auto; right: 10px; - top: 10px; + top: 12px; border-radius: 0.25rem; } + &.router-link-exact-active .indicator { + background: #fff; + color: #333; + } } .menu a svg path {
--- a/client/src/components/fairway/Fairwayprofile.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/fairway/Fairwayprofile.vue Fri Mar 01 11:06:27 2019 +0100 @@ -142,22 +142,26 @@ ...mapGetters("fairwayprofile", ["totalLength"]), ...mapState("application", ["showSplitscreen"]), ...mapState("fairwayprofile", [ - "startPoint", + "additionalSurvey", + "currentProfile", "endPoint", - "currentProfile", - "additionalSurvey", + "fairwayData", "minAlt", "maxAlt", - "fairwayData", - "waterLevels", + "profileLoading", + "referenceWaterLevel", "selectedWaterLevel", - "profileLoading" + "startPoint", + "waterLevels" ]), ...mapState("bottlenecks", [ "selectedBottleneck", "selectedSurvey", "surveysLoading" ]), + relativeWaterLevelDelta() { + return this.selectedWaterLevel.value - this.referenceWaterLevel; + }, currentLevel: { get() { return this.selectedWaterLevel.date; @@ -192,6 +196,9 @@ return [0, this.totalLength]; }, yScaleRight() { + //ToDO calcReleativeDepth(this.maxAlt) to get the + // maximum depth according to the actual waterlevel + // additionally: take the one which is higher reference or current waterlevel const DELTA = this.maxAlt * 1.1 - this.maxAlt; return [this.maxAlt * 1 + DELTA, -DELTA]; } @@ -223,6 +230,25 @@ formatSurveyDate(value) { return formatSurveyDate(value); }, + calcRelativeDepth(depth) { + /* takes a depth value and substracts the delta of the relative waterlevel + * say the reference level is above the current level, the ground is nearer, + * thus, the depth is lower. + * + * E.g.: + * + * Reference waterlevel 5m, current 4m => delta = -1m + * If the distance to the ground was 3m from the 5m mark + * it is now only 2m from the current waterlevel. + * + * Vice versa: + * + * If the reference level is 5m and the current 6m => delta = +1m + * The ground is one meter farer away from the current waterlevel + * + */ + return depth - this.relativeWaterLevelDelta; + }, drawDiagram() { this.coordinatesSelect = null; const chartDiv = document.querySelector(".fairwayprofile");
--- a/client/src/components/fairway/Profiles.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/fairway/Profiles.vue Fri Mar 01 11:06:27 2019 +0100 @@ -25,7 +25,7 @@ <translate>Select Bottleneck</translate> </option> <option - v-for="bn in bottlenecks" + v-for="bn in bottlenecksList" :key="bn.properties.name" :value="bn.properties.name" >{{ bn.properties.name }}</option @@ -259,7 +259,11 @@ ...mapGetters("map", ["getVSourceByName"]), ...mapState("application", ["showProfiles"]), ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), - ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]), + ...mapState("bottlenecks", [ + "bottlenecksList", + "surveys", + "surveysLoading" + ]), ...mapState("fairwayprofile", [ "previousCuts", "startPoint", @@ -452,7 +456,7 @@ displayInfo({ title: this.$gettext("Profile deleted!") }); }, moveToBottleneck() { - const bottleneck = this.bottlenecks.find( + const bottleneck = this.bottlenecksList.find( bn => bn.properties.name === this.selectedBottleneck ); if (!bottleneck) return; @@ -462,6 +466,9 @@ preventZoomOut: true }); } + }, + mounted() { + this.$store.dispatch("bottlenecks/loadBottlenecksList"); } }; </script>
--- a/client/src/components/identify/Identify.vue Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/identify/Identify.vue Fri Mar 01 11:06:27 2019 +0100 @@ -135,20 +135,40 @@ return this.featureId(feature); }, featureProps(feature) { + let featureId = this.featureId(feature); + // create array with {key, val} objects let propsArray = []; Object.keys(feature.getProperties()).forEach(key => { - // avoid cyclic object value - if (key !== feature.getGeometryName()) - propsArray.push({ key, val: feature.getProperties()[key] }); + // skip geometry (would lead to cyclic object error) + if (key !== feature.getGeometryName()) { + let val = feature.getProperties()[key]; + + // if val is a valid json object string, spread its values into the array + let jsonObj = this.getObjectFromString(val); + if (jsonObj) { + Object.keys(jsonObj).forEach(key => { + propsArray.push({ key, val: jsonObj[key] }); + }); + } else { + // otherwise just put the key value pair into the array + propsArray.push({ key, val }); + } + } }); // change labels and remove unneeded properties - if (formatter.hasOwnProperty(this.featureId(feature))) { - propsArray = propsArray - .map(formatter[this.featureId(feature)].props) - .filter(p => p); // remove empty entries + // for all features + propsArray = propsArray.map(formatter.all); + // feature specific + if ( + formatter.hasOwnProperty(featureId) && + formatter[featureId].hasOwnProperty("props") + ) { + propsArray = propsArray.map(formatter[featureId].props); } + // remove empty entries + propsArray = propsArray.filter(p => p); // remove underscores in labels that where not previously changed already propsArray = propsArray.map(prop => { @@ -156,6 +176,22 @@ }); return propsArray; + }, + getObjectFromString(val) { + // JSON.parse() accepts integers and null as valid json. So to be sure to + // get an object, we cannot just try JSON.parse() but we need to check if + // the given value is a string and starts with a {. + if ( + Object.prototype.toString.call(val) === "[object String]" && + val[0] === "{" + ) { + try { + return JSON.parse(val); + } catch (e) { + return null; + } + } + return null; } } };
--- a/client/src/components/identify/formatter.js Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/components/identify/formatter.js Fri Mar 01 11:06:27 2019 +0100 @@ -1,66 +1,42 @@ const formatter = { + all(p) { + if (p.key === "objnam") p.key = "Name"; + if (p.key === "staging_done") p.val = p.val ? "yes" : "no"; + if (p.key === "date_info") { + p.val = new Date(p.val).toLocaleString(); + } + return p; + }, bottlenecks_geoserver: { label: "Bottleneck", props: p => { if (p.key === "bottleneck_id") p.key = "ID"; - if (p.key === "objnam") p.key = "Name"; if (p.key === "responsible_country") p.key = "Responsible Country"; - if (p.key === "date_info") { - p.val = new Date(p.val).toLocaleString(); - } // remove certain props - let propsToRemove = ["nobjnm"]; + let propsToRemove = ["nobjnm", "reference_water_levels"]; if (propsToRemove.indexOf(p.key) !== -1) return null; return p; } }, fairway_dimensions: { - label: "Fairway Dimensions", - props: p => { - if (p.key === "staging_done") p.val = p.val ? "yes" : "no"; - if (p.key === "date_info") { - p.val = new Date(p.val).toLocaleString(); - } - - // remove certain props - let propsToRemove = []; - if (propsToRemove.indexOf(p.key) !== -1) return null; - - return p; - } + label: "Fairway Dimensions" }, waterway_area: { - label: "Waterway Area", - props: p => p + label: "Waterway Area" }, distance_marks_geoserver: { - label: "Distance Mark", - props: p => p + label: "Distance Mark" }, waterway_axis: { - label: "Waterway Axis", - props: p => { - if (p.key === "objnam") p.key = "Name"; - return p; - } + label: "Waterway Axis" }, waterway_profiles: { - label: "Waterway Profile", - props: p => { - if (p.key === "staging_done") p.val = p.val ? "yes" : "no"; - if (p.key === "date_info") { - p.key = "Date info"; - p.val = new Date(p.val).toLocaleString(); - } - - // remove certain props - let propsToRemove = []; - if (propsToRemove.indexOf(p.key) !== -1) return null; - - return p; - } + label: "Waterway Profile" + }, + stretches_geoserver: { + label: "Stretch" } };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importoverview/ImportOverview.vue Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,134 @@ +<template> + <div> + <UIBoxHeader + icon="clipboard-check" + title="Staging Area" + :closeCallback="$parent.close" + /> + <div class="d-flex flex-row w-100 justify-content-end"> + <button + class="btn btn-sm btn-dark align-self-start mt-3 mr-3" + @click="refresh" + > + <translate>Refresh</translate> + </button> + </div> + <div class="d-flex flex-row w-100 border-bottom"> + <font-awesome-icon + class="pointer" + @click="toggleStaging()" + v-if="stagingVisible && staging.length > 0" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + @click="toggleStaging()" + v-if="!stagingVisible && staging.length > 0" + icon="angle-down" + fixed-width + ></font-awesome-icon> + <span style="width:1.25em;" v-if="!(staging.length > 0)"></span> + <Staging v-if="stagingVisible && staging.length > 0"></Staging> + <div v-else class="d-flex flex-row"> + <h6> + <small><translate>Review</translate></small> + </h6> + <small class="ml-3" v-if="!(staging.length > 0)" + ><translate>Nothing to review</translate></small + > + </div> + </div> + <div class="mt-2"> + <div class="d-flex flex-row"> + <font-awesome-icon + class="pointer" + @click="toggleLogs()" + v-if="logsVisible" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + @click="toggleLogs()" + v-if="!logsVisible" + icon="angle-down" + fixed-width + ></font-awesome-icon> + <Logs v-if="logsVisible"></Logs> + <div v-else> + <h6> + <small><translate>Logs</translate></small> + </h6> + </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 { displayError } from "@/lib/errors.js"; +import { mapState } from "vuex"; + +export default { + name: "importoverview", + components: { + Staging: () => import("./staging/Staging.vue"), + Logs: () => import("./importlogs/Logs.vue") + }, + computed: { + ...mapState("imports", ["stagingVisible", "logsVisible", "staging"]) + }, + methods: { + toggleStaging() { + this.$store.commit("imports/setStagingVisibility", !this.stagingVisible); + }, + toggleLogs() { + this.$store.commit("imports/setLogsVisibility", !this.logsVisible); + }, + refresh() { + this.loadImportQueue(); + this.loadLogs(); + }, + loadImportQueue() { + this.$store.dispatch("imports/getStaging").catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }, + loadLogs() { + this.$store + .dispatch("imports/getImports") + .then(() => {}) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + mounted() { + this.refresh(); + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importoverview/importlogs/LogDetail.vue Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,356 @@ +<template> + <div class="entry d-flex flex-column py-1 border-bottom"> + <div class="d-flex flex-row position-relative"> + <small @click="showDetails(job.id)" class="jobid ml-2 mt-1 mr-2"> + {{ job.id }} + </small> + <small @click="showDetails(job.id)" class="enqueued mt-1 mr-2"> + {{ formatDateTime(job.enqueued) }} + </small> + <small @click="showDetails(job.id)" class="kind mt-1 mr-2"> + {{ job.kind.toUpperCase() }} + </small> + <small @click="showDetails(job.id)" class="user mt-1 mr-2"> + {{ job.user }} + </small> + <small @click="showDetails(job.id)" class="signer mt-1 mr-2"> + {{ job.signer }} + </small> + <small @click="showDetails(job.id)" class="state mt-1 mr-2"> + <span :class="{ 'text-danger': job.state.toUpperCase() == 'FAILED' }" + >{{ job.state + }}<font-awesome-icon + v-if="job.warnings" + class="ml-1 text-warning" + icon="exclamation-triangle" + fixed-width + ></font-awesome-icon> + </span> + <span v-if="!job.warnings" style="margin-right: 1.6em;"></span> + </small> + <div @click="showDetails(job.id)" class="mt-1 text-info detailsbutton"> + <font-awesome-icon + class="pointer" + v-if="show" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + v-if="loading" + icon="spinner" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + 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', + { + 'text-danger': entry.kind.toUpperCase() == 'ERROR', + 'text-warning': entry.kind.toUpperCase() == 'WARN' + } + ]" + >{{ entry.kind.toUpperCase() }}</span + > + </td> + <td class="datetime"> + <span + :class="[ + 'condensed', + { + 'text-danger': entry.kind.toUpperCase() == 'ERROR', + 'text-warning': entry.kind.toUpperCase() == 'WARN' + } + ]" + >{{ formatDateTime(entry.time) }}</span + > + </td> + <td class="message"> + <span + :class="[ + 'condensed', + { + 'text-danger': entry.kind.toUpperCase() == 'ERROR', + 'text-warning': entry.kind.toUpperCase() == 'WARN' + } + ]" + >{{ 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 + }; + }, + mounted() { + this.openSpecificDetail(); + }, + watch: { + $route() { + this.openSpecificDetail(); + }, + reload() { + if (this.reload) { + this.entries = []; + this.show = false; + } + } + }, + methods: { + openSpecificDetail() { + const { id } = this.$route.params; + if (id == this.job.id) { + this.showDetails(id); + } else { + this.show = false; + } + }, + 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: 5%; +} + +.user { + width: 15%; +} + +.signer { + width: 15%; +} + +.kind { + width: 10%; +} + +.state { + width: 15%; +} + +.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/importoverview/importlogs/Logs.vue Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,132 @@ +<template> + <div class="w-95"> + <div class="text-left"> + <h6><translate>Logs</translate></h6> + </div> + <div class="d-flex justify-content-between flex-row"> + <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> + <button @click="setFilter('warning')" :class="warningStyle"> + <translate>Warning</translate> + </button> + </div> + <div class="mt-3 logdetails"> + <div v-for="job in imports" :key="job.id" class="d-flex flex-row"> + <LogDetail :job="job"></LogDetail> + </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 { mapState } from "vuex"; + +export default { + name: "logsection", + components: { + LogDetail: () => import("./LogDetail.vue") + }, + data() { + return { + failed: false, + pending: false, + rejected: false, + accepted: false, + warning: false + }; + }, + computed: { + ...mapState("imports", ["imports"]), + pendingStyle() { + return { + btn: true, + "btn-sm": true, + "btn-light": !this.pending, + "btn-info": this.pending + }; + }, + failedStyle() { + return { + btn: true, + "btn-sm": true, + "btn-light": !this.failed, + "btn-info": this.failed + }; + }, + rejectedStyle() { + return { + btn: true, + "btn-sm": true, + "btn-light": !this.rejected, + "btn-info": this.rejected + }; + }, + acceptedStyle() { + return { + btn: true, + "btn-sm": true, + "btn-light": !this.accepted, + "btn-info": this.accepted + }; + }, + warningStyle() { + return { + btn: true, + "btn-sm": true, + "btn-light": !this.warning, + "btn-info": this.warning + }; + } + }, + methods: { + setFilter(name) { + this[name] = !this[name]; + const allSet = + this.failed && + this.pending && + this.accepted && + this.rejected && + this.warning; + if (allSet) { + this.warning = false; + this.successful = false; + this.failed = false; + this.pending = false; + this.accepted = false; + this.rejected = false; + } + } + } +}; +</script> + +<style lang="scss" scoped> +.logdetails { + overflow-y: auto; + height: 650px; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importoverview/staging/Staging.vue Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,91 @@ +<template> + <div class="w-100"> + <div class="d-flex justify-content-between flex-row w-100 border-bottom"> + <h6><translate>Review</translate></h6> + <button class="btn btn-sm btn-info align-self-end" @click="save"> + <translate>Confirm</translate> + </button> + </div> + <StagingDetail + class="mb-3 border-bottom" + :key="data.id" + v-for="data in filteredData" + :data="data" + ></StagingDetail> + </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, mapGetters } from "vuex"; +import { displayError, displayInfo } from "@/lib/errors.js"; + +export default { + name: "stagingsection", + computed: { + ...mapState("imports", ["staging"]), + ...mapGetters("imports", ["processedReviews"]), + filteredData() { + return this.staging; + } + }, + methods: { + loadImportQueue() { + this.$store.dispatch("imports/getStaging").catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }, + save() { + if (!this.processedReviews.length) return; + this.$store + .dispatch("imports/confirmReview", this.processedReviews) + .then(response => { + this.loadImportQueue(); + 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 }] + } + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } + }, + components: { + StagingDetail: () => import("./StagingDetail.vue") + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importoverview/staging/StagingDetail.vue Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,518 @@ +<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="isSoundingResult(data.kind.toUpperCase())" + class="text-left" + @click="zoomTo()" + href="#" + >{{ data.summary.bottleneck }}</a + > + <span v-if="isBottleneck(data.kind.toUpperCase())" class="text-left" + ><translate>Bottlenecks</translate> ({{ + data.summary.bottlenecks.length + }})</span + > + <a + v-if="isApprovedGaugeMeasurement(data.kind.toUpperCase())" + class="text-left" + ><translate>Approved Gauge Measurements</translate> ({{ + data.summary.length + }})</a + > + <span + class="text-left" + v-if="isFairwayDimension(data.kind.toUpperCase())" + >{{ data.summary["source-organization"] }} (LOS: + {{ data.summary.los }})</span + > + <a + href="#" + class="text-left" + @click="zoomToStretch(data.summary.stretch)" + v-if="isStretch(data.kind.toUpperCase())" + >{{ data.summary.stretch }}</a + > + </div> + <div class="mt-auto mb-auto small text-left type"> + {{ data.kind.toUpperCase() }} + </div> + <div v-if="data.summary" class="mt-auto mb-auto small text-left date"> + {{ formatSurveyDate(data.summary.date) }} + </div> + <div v-else class="mt-auto mb-auto small text-left 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" class="pointer"></font-awesome-icon> + </button> + </div> + <div + v-if=" + !isBottleneck(data.kind.toUpperCase()) || + isApprovedGaugeMeasurement(data.kind.toUpperCase()) + " + class="expander" + ></div> + <div v-if="isBottleneck(data.kind.toUpperCase())"> + <div class="mt-auto mb-auto text-info text-left"> + <font-awesome-icon + class="pointer" + @click="showDetails()" + v-if="show" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + @click="showDetails()" + v-if="loading" + icon="spinner" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + @click="showDetails()" + class="pointer" + v-if="!show && !loading" + icon="angle-down" + fixed-width + ></font-awesome-icon> + </div> + </div> + <div v-if="isApprovedGaugeMeasurement(data.kind.toUpperCase())"> + <div + @click="showAGMDetails = !showAGMDetails" + class="mt-auto mb-auto text-info text-left" + > + <font-awesome-icon + class="pointer" + v-if="showAGMDetails" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + v-if="!showAGMDetails" + icon="angle-down" + fixed-width + ></font-awesome-icon> + </div> + </div> + <div v-else class="empty"></div> + </div> + </div> + <div v-if="show && bottlenecks.length > 0" class="bottlenecksdetails"> + <div + v-for="(bottleneck, index) in bottlenecks" + :key="index" + class="d-flex flex-row" + > + <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="showBottleneckDetails(index)" + class="small mt-auto mb-auto text-info text-left" + > + <font-awesome-icon + class="pointer" + v-if="showBottleneckDetail === index" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + v-if="!(showBottleneckDetail === index)" + icon="angle-down" + fixed-width + ></font-awesome-icon> + </div> + </div> + + <div class="d-flex flex-row" v-if="showBottleneckDetail === index"> + <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> + <div v-if="showAGMDetails"> + <div class="pl-3 d-flex flex-row"> + <span class="condensed agmcode text-left" + ><small><translate>ISRS Code</translate></small></span + > + <span class="condensed agmdetail text-left" + ><small><translate>Date of measurement</translate></small></span + > + </div> + <div class="diffs"> + <div v-for="(result, index) in data.summary" :key="index"> + <div class="pl-3 d-flex flex-row"> + <span + v-if="result.versions.length == 1" + class="condensed agmcode text-left" + ><small + >{{ result["fk-gauge-id"] }} + <translate>( New )</translate></small + ></span + > + <span + v-if="result.versions.length == 2" + class="condensed agmcode text-left" + ><small>{{ result["fk-gauge-id"] }}</small></span + > + <span class="condensed agmdetail text-left" + ><small>{{ formatDateTime(result["measure-date"]) }}</small></span + > + <div + @click="toggleDiff(index)" + class="small ml-auto mt-auto mb-auto text-info text-left" + > + <font-awesome-icon + class="pointer" + v-if="showDiff == index" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + v-if="showDiff != index" + icon="angle-down" + fixed-width + ></font-awesome-icon> + </div> + </div> + <div v-if="showDiff == index" class="pl-3 d-flex flex-row"> + <div> + <div class="d-flex flex-row condensed pl-3 text-left"> + <div class="header border-bottom agmdetailskeys"> + <small><translate>Value</translate></small> + </div> + <div + v-if="result.versions.length == 2" + class="header border-bottom agmdetailsvalues" + > + <small><translate>Old</translate></small> + </div> + <div class="header border-bottom agmdetailsvalues"> + <small><translate>New</translate></small> + </div> + </div> + <div + class="d-flex flex-row condensed pl-3 text-left" + v-for="(entry, index) in Object.keys(result.versions[0])" + :key="index" + > + <div + v-if=" + result.versions.length == 1 || + result.versions[0][entry] != result.versions[1][entry] + " + class="agmdetailskeys" + > + <small>{{ entry }}</small> + </div> + <div + v-if=" + result.versions.length == 1 || + result.versions[0][entry] != result.versions[1][entry] + " + class="agmdetailsvalues" + > + <small>{{ result.versions[0][entry] }}</small> + </div> + <div + v-if=" + result.versions.length == 2 && + result.versions[0][entry] != result.versions[1][entry] + " + class="agmdetailsvalues" + > + <small>{{ result.versions[1][entry] }}</small> + </div> + </div> + </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): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { formatSurveyDate, formatDateTime } 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 { LAYERS } from "@/store/map.js"; + +const NO_DIFF = -1; +const NO_BOTTLENECK = -1; + +export default { + name: "stagingdetail", + props: ["data"], + data() { + return { + showDiff: NO_DIFF, + showAGMDetails: false, + showBottleneckDetail: NO_BOTTLENECK, + show: false, + loading: false, + bottlenecks: [] + }; + }, + mounted() { + this.bottlenecks = []; + const { id } = this.$route.params; + this.$store.commit("imports/setImportToReview", id); + 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: { + showAGMDetails() { + if (!this.showAGMDetails) this.showDiff = NO_DIFF; + }, + open() { + this.show = this.open; + }, + $route() { + const { id } = this.$route.params; + this.$store.commit("imports/setImportToReview", id); + if (this.open) this.showDetails(); + } + }, + methods: { + showBottleneckDetails(index) { + if (index == this.showBottleneckDetail) { + this.showBottleneckDetail = NO_BOTTLENECK; + return; + } + this.showBottleneckDetail = index; + }, + toggleDiff(number) { + if (this.showDiff !== number || this.showDiff == -1) { + this.showDiff = number; + } else { + this.showDiff = -1; + } + }, + zoomToStretch(name) { + this.$store.commit("map/setLayerVisible", LAYERS.STRETCHES); + this.$store + .dispatch("imports/loadStretch", name) + .then(response => { + if (response.data.features.length < 1) + throw new Error("no feaures found for: " + name); + this.moveToExtent(response.data.features[0]); + }) + .catch(error => { + console.log(error); + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + showDetails() { + if (!this.isBottleneck(this.data.kind.toUpperCase())) 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 => { + return equalToFilter("bottleneck_id", x); + }); + return orFilter(...orExpressions); + }; + const filterExpression = generateFilter(); + const bottleneckFeatureCollectionRequest = new WFS().writeGetFeature({ + srsName: "EPSG:4326", + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["bottlenecks_geoserver"], + 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}` + }); + }); + }, + isFairwayDimension(kind) { + return kind === "FD"; + }, + isApprovedGaugeMeasurement(kind) { + return kind === "AGM"; + }, + isBottleneck(kind) { + return kind === "BN" || kind === "UBN"; + }, + isStretch(kind) { + return kind === "ST"; + }, + isSoundingResult(kind) { + return kind === "SR"; + }, + formatSurveyDate(date) { + return formatSurveyDate(date); + }, + formatDateTime(date) { + return formatDateTime(date); + }, + needsApproval(item) { + return item.status === STATES.NEEDSAPPROVAL; + }, + isRejected(item) { + return item.status === STATES.REJECTED; + }, + isApproved(item) { + return item.status === STATES.APPROVED; + }, + moveToBottleneck(index) { + this.$store.commit("map/setLayerVisible", LAYERS.BOTTLENECKS); + this.moveToExtent(this.bottlenecks[index]); + }, + moveToExtent(feature) { + this.$store.commit("map/moveToExtent", { + feature: feature, + zoom: 17, + preventZoomOut: true + }); + }, + 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></style>
--- a/client/src/components/importqueue/Importqueue.vue Thu Feb 28 17:28:54 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,351 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <Spacer></Spacer> - <div class="mt-3 importqueuecard flex-grow-1"> - <div class="card shadow-xs"> - <UIBoxHeader icon="tasks" title="Importqueue" /> - <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('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> - <button @click="setFilter('warning')" :class="warningStyle"> - <translate>Warning</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, - warning: false - }; - }, - mounted() { - this.loadQueue(); - }, - methods: { - setFilter(name) { - this[name] = !this[name]; - const allSet = - this.failed && - this.pending && - this.accepted && - this.rejected && - this.warning; - if (allSet) { - this.warning = false; - 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.failed && - !this.pending && - !this.accepted && - !this.rejected && - !this.warning - ) - return true; - let filterCriteria = []; - if (this.failed) filterCriteria.push("failed"); - if (this.pending) filterCriteria.push("pending"); - if (this.accepted) filterCriteria.push("accepted"); - if (this.rejected) filterCriteria.push("declined"); - const result = filterCriteria.map(selectedState => { - return y.state === selectedState; - }); - if (this.warning) return result.some(x => x) || y.warnings; - return result.some(x => x); - }); - return filtered; - }, - 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 - }; - }, - warningStyle() { - return { - btn: true, - "btn-light": !this.warning, - "btn-dark": this.warning - }; - } - } -}; -</script> - -<style lang="scss" scoped> -.importqueuedetail { - margin-bottom: 3rem; -} -.entries { - width: 100%; -} - -.jobid { - width: 15%; -} - -.enqueued { - width: 15%; -} - -.user { - width: 15%; -} - -.signer { - width: 15%; -} - -.kind { - width: 10%; -} - -.state { - width: 15%; -} - -.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>
--- a/client/src/components/importqueue/Importqueuedetail.vue Thu Feb 28 17:28:54 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,359 +0,0 @@ -<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"> - {{ formatDateTime(job.enqueued) }} - </div> - <div @click="showDetails(job.id)" class="kind mt-1 mr-2"> - {{ job.kind.toUpperCase() }} - </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"> - <span :class="{ 'text-danger': job.state.toUpperCase() == 'FAILED' }" - >{{ job.state - }}<font-awesome-icon - v-if="job.warnings" - class="ml-1 text-warning" - icon="exclamation-triangle" - fixed-width - ></font-awesome-icon - ></span> - </div> - <div @click="showDetails(job.id)" class="mt-1 text-info detailsbutton"> - <font-awesome-icon - class="pointer" - v-if="show" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="loading" - icon="spinner" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - 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', - { - 'text-danger': entry.kind.toUpperCase() == 'ERROR', - 'text-warning': entry.kind.toUpperCase() == 'WARN' - } - ]" - >{{ entry.kind.toUpperCase() }}</span - > - </td> - <td class="datetime"> - <span - :class="[ - 'condensed', - { - 'text-danger': entry.kind.toUpperCase() == 'ERROR', - 'text-warning': entry.kind.toUpperCase() == 'WARN' - } - ]" - >{{ formatDateTime(entry.time) }}</span - > - </td> - <td class="message"> - <span - :class="[ - 'condensed', - { - 'text-danger': entry.kind.toUpperCase() == 'ERROR', - 'text-warning': entry.kind.toUpperCase() == 'WARN' - } - ]" - >{{ 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 - }; - }, - mounted() { - this.openSpecificDetail(); - }, - watch: { - $route() { - this.openSpecificDetail(); - }, - reload() { - if (this.reload) { - this.entries = []; - this.show = false; - } - } - }, - methods: { - openSpecificDetail() { - const { id } = this.$route.params; - if (id == this.job.id) { - this.showDetails(id); - } else { - this.show = false; - } - }, - 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: 15%; -} - -.user { - width: 15%; -} - -.signer { - width: 15%; -} - -.kind { - width: 10%; -} - -.state { - width: 15%; -} - -.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>
--- a/client/src/components/staging/Staging.vue Thu Feb 28 17:28:54 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,193 +0,0 @@ -<template> - <div class="w-90 stagingcard"> - <UIBoxHeader - icon="clipboard-check" - title="Staging Area" - :closeCallback="$parent.close" - /> - <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 stagingdetails details" 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> -.stagingdetails { - overflow-y: auto; - max-height: 250px; -} -.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>
--- a/client/src/components/staging/StagingDetail.vue Thu Feb 28 17:28:54 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,583 +0,0 @@ -<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="isSoundingResult(data.kind.toUpperCase())" - class="text-left" - @click="zoomTo()" - href="#" - >{{ data.summary.bottleneck }}</a - > - <span v-if="isBottleneck(data.kind.toUpperCase())" class="text-left" - ><translate>Bottlenecks</translate> ({{ - data.summary.bottlenecks.length - }})</span - > - <a - v-if="isApprovedGaugeMeasurement(data.kind.toUpperCase())" - class="text-left" - ><translate>Approved Gauge Measurements</translate> ({{ - data.summary.length - }})</a - > - <span - class="text-left" - v-if="isFairwayDimension(data.kind.toUpperCase())" - >{{ data.summary["source-organization"] }} (LOS: - {{ data.summary.los }})</span - > - <a - href="#" - class="text-left" - @click="zoomToStretch(data.summary.stretch)" - v-if="isStretch(data.kind.toUpperCase())" - >{{ data.summary.stretch }}</a - > - </div> - <div class="mt-auto mb-auto small text-left type"> - {{ data.kind.toUpperCase() }} - </div> - <div v-if="data.summary" class="mt-auto mb-auto small text-left date"> - {{ formatSurveyDate(data.summary.date) }} - </div> - <div v-else class="mt-auto mb-auto small text-left 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" class="pointer"></font-awesome-icon> - </button> - </div> - <div - v-if=" - !isBottleneck(data.kind.toUpperCase()) || - isApprovedGaugeMeasurement(data.kind.toUpperCase()) - " - class="expander" - ></div> - <div v-if="isBottleneck(data.kind.toUpperCase())"> - <div class="mt-auto mb-auto text-info text-left"> - <font-awesome-icon - class="pointer" - @click="showDetails()" - v-if="show" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - @click="showDetails()" - v-if="loading" - icon="spinner" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - @click="showDetails()" - class="pointer" - v-if="!show && !loading" - icon="angle-down" - fixed-width - ></font-awesome-icon> - </div> - </div> - <div v-if="isApprovedGaugeMeasurement(data.kind.toUpperCase())"> - <div - @click="showAGMDetails = !showAGMDetails" - class="mt-auto mb-auto text-info text-left" - > - <font-awesome-icon - class="pointer" - v-if="showAGMDetails" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="!showAGMDetails" - icon="angle-down" - fixed-width - ></font-awesome-icon> - </div> - </div> - <div v-else class="empty"></div> - </div> - </div> - <div v-if="show && bottlenecks.length > 0" class="bottlenecksdetails"> - <div - v-for="(bottleneck, index) in bottlenecks" - :key="index" - class="d-flex flex-row" - > - <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="showBottleneckDetails(index)" - class="small mt-auto mb-auto text-info text-left" - > - <font-awesome-icon - class="pointer" - v-if="showBottleneckDetail === index" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="!(showBottleneckDetail === index)" - icon="angle-down" - fixed-width - ></font-awesome-icon> - </div> - </div> - - <div class="d-flex flex-row" v-if="showBottleneckDetail === index"> - <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> - <div v-if="showAGMDetails"> - <div class="pl-3 d-flex flex-row"> - <span class="condensed agmcode text-left" - ><small><translate>ISRS Code</translate></small></span - > - <span class="condensed agmdetail text-left" - ><small><translate>Date of measurement</translate></small></span - > - </div> - <div class="diffs"> - <div v-for="(result, index) in data.summary" :key="index"> - <div class="pl-3 d-flex flex-row"> - <span - v-if="result.versions.length == 1" - class="condensed agmcode text-left" - ><small - >{{ result["fk-gauge-id"] }} - <translate>( New )</translate></small - ></span - > - <span - v-if="result.versions.length == 2" - class="condensed agmcode text-left" - ><small>{{ result["fk-gauge-id"] }}</small></span - > - <span class="condensed agmdetail text-left" - ><small>{{ formatDateTime(result["measure-date"]) }}</small></span - > - <div - @click="toggleDiff(index)" - class="small ml-auto mt-auto mb-auto text-info text-left" - > - <font-awesome-icon - class="pointer" - v-if="showDiff == index" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="showDiff != index" - icon="angle-down" - fixed-width - ></font-awesome-icon> - </div> - </div> - <div v-if="showDiff == index" class="pl-3 d-flex flex-row"> - <div> - <div class="d-flex flex-row condensed pl-3 text-left"> - <div class="header border-bottom agmdetailskeys"> - <small><translate>Value</translate></small> - </div> - <div - v-if="result.versions.length == 2" - class="header border-bottom agmdetailsvalues" - > - <small><translate>Old</translate></small> - </div> - <div class="header border-bottom agmdetailsvalues"> - <small><translate>New</translate></small> - </div> - </div> - <div - class="d-flex flex-row condensed pl-3 text-left" - v-for="(entry, index) in Object.keys(result.versions[0])" - :key="index" - > - <div - v-if=" - result.versions.length == 1 || - result.versions[0][entry] != result.versions[1][entry] - " - class="agmdetailskeys" - > - <small>{{ entry }}</small> - </div> - <div - v-if=" - result.versions.length == 1 || - result.versions[0][entry] != result.versions[1][entry] - " - class="agmdetailsvalues" - > - <small>{{ result.versions[0][entry] }}</small> - </div> - <div - v-if=" - result.versions.length == 2 && - result.versions[0][entry] != result.versions[1][entry] - " - class="agmdetailsvalues" - > - <small>{{ result.versions[1][entry] }}</small> - </div> - </div> - </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): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import { formatSurveyDate, formatDateTime } 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 { LAYERS } from "@/store/map.js"; - -const NO_DIFF = -1; -const NO_BOTTLENECK = -1; - -export default { - name: "stagingdetail", - props: ["data"], - data() { - return { - showDiff: NO_DIFF, - showAGMDetails: false, - showBottleneckDetail: NO_BOTTLENECK, - show: false, - loading: false, - bottlenecks: [] - }; - }, - mounted() { - this.bottlenecks = []; - const { id } = this.$route.params; - this.$store.commit("imports/setImportToReview", id); - 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: { - showAGMDetails() { - if (!this.showAGMDetails) this.showDiff = NO_DIFF; - }, - open() { - this.show = this.open; - }, - $route() { - const { id } = this.$route.params; - this.$store.commit("imports/setImportToReview", id); - if (this.open) this.showDetails(); - } - }, - methods: { - showBottleneckDetails(index) { - if (index == this.showBottleneckDetail) { - this.showBottleneckDetail = NO_BOTTLENECK; - return; - } - this.showBottleneckDetail = index; - }, - toggleDiff(number) { - if (this.showDiff !== number || this.showDiff == -1) { - this.showDiff = number; - } else { - this.showDiff = -1; - } - }, - zoomToStretch(name) { - this.$store.commit("map/setLayerVisible", LAYERS.STRETCHES); - this.$store - .dispatch("imports/loadStretch", name) - .then(response => { - if (response.data.features.length < 1) - throw new Error("no feaures found for: " + name); - this.moveToExtent(response.data.features[0]); - }) - .catch(error => { - console.log(error); - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - showDetails() { - if (!this.isBottleneck(this.data.kind.toUpperCase())) 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 => { - return equalToFilter("bottleneck_id", x); - }); - return orFilter(...orExpressions); - }; - const filterExpression = generateFilter(); - const bottleneckFeatureCollectionRequest = new WFS().writeGetFeature({ - srsName: "EPSG:4326", - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["bottlenecks_geoserver"], - 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}` - }); - }); - }, - isFairwayDimension(kind) { - return kind === "FD"; - }, - isApprovedGaugeMeasurement(kind) { - return kind === "AGM"; - }, - isBottleneck(kind) { - return kind === "BN" || kind === "UBN"; - }, - isStretch(kind) { - return kind === "ST"; - }, - isSoundingResult(kind) { - return kind === "SR"; - }, - formatSurveyDate(date) { - return formatSurveyDate(date); - }, - formatDateTime(date) { - return formatDateTime(date); - }, - needsApproval(item) { - return item.status === STATES.NEEDSAPPROVAL; - }, - isRejected(item) { - return item.status === STATES.REJECTED; - }, - isApproved(item) { - return item.status === STATES.APPROVED; - }, - moveToBottleneck(index) { - this.$store.commit("map/setLayerVisible", LAYERS.BOTTLENECKS); - this.moveToExtent(this.bottlenecks[index]); - }, - moveToExtent(feature) { - this.$store.commit("map/moveToExtent", { - feature: feature, - zoom: 17, - preventZoomOut: true - }); - }, - 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> -.expander { - margin-right: 1.25rem; -} - -.bottlenecksdetails { - overflow-y: auto; - max-height: 250px; -} -.diffs { - overflow-y: auto; - max-height: 250px; -} - -.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; -} - -.agmcode { - width: 200px; -} - -.agmdate { - width: 100px; -} - -.agmdetailskeys { - width: 130px; -} - -.agmdetailsvalues { - width: 200px; -} -</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/ui/UIBoxHeader.vue Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,63 @@ +<template> + <h6 class="box-header"> + <span class="box-title"> + <font-awesome-icon + :icon="icon" + class="box-icon" + v-if="icon" + fixed-width + /> + {{ $gettext(title) }} + </span> + <span class="box-close" @click="closeCallback" v-if="closeCallback"> + <font-awesome-icon icon="times" /> + </span> + </h6> +</template> + +<style lang="sass"> +.box-header + display: flex + justify-content: space-between + align-items: center + min-height: 35px + padding-left: .5rem + border-bottom: 1px solid #dee2e6 + color: $color-info + margin-bottom: 0 + padding: 0.25rem + font-weight: bold + .box-title + padding-left: 0.25rem + .box-icon + margin-right: 0.25rem + .box-close + color: #888 + padding: 3px 7px + border-radius: 0.25rem + cursor: pointer + transition: background-color 0.3s, color 0.3s + &:hover + color: #666 + background-color: #eee +</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> + */ + +export default { + props: ["icon", "title", "closeCallback"] +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/ui/UITableBody.vue Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,51 @@ +<template> + <transition-group + name="fade" + tag="div" + class="table-body text-left small" + :style="'overflow-y: auto; max-height: ' + maxHeight" + v-if="data.length" + > + <div + v-for="(item, index) in data" + :key="index" + :class="['border-top row mx-0', { active: active === item }]" + > + <slot :item="item" :index="index"></slot> + </div> + </transition-group> + <div v-else class="small text-center py-3 border-top"> + <translate>No results.</translate> + </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> + */ + +export default { + props: { + data: { + type: Array + }, + maxHeight: { + type: String, + default: "18rem" + }, + active: { + type: [Object, Array] + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/ui/UITableHeader.vue Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,94 @@ +<template> + <div + :class="['table-header row no-gutters bg-light', { sortable: sortable }]" + > + <a + v-for="column in columns" + @click.prevent="sortBy(column.id)" + :key="column.id" + :class="[ + 'd-flex py-2 align-items-center justify-content-center small col ' + + column.class, + { active: sortColumn === column.id } + ]" + > + <span + :style="'opacity: ' + (sortColumn === column.id ? '1' : '0.3')" + v-if="sortable" + > + <font-awesome-icon + :icon="sortIcon(column.id)" + class="ml-1" + fixed-width + /> + </span> + {{ $gettext(column.title) }} + </a> + <div v-if="extraColumnForButtons" class="col"></div> + </div> +</template> + +<style lang="sass"> +.table-header + > a + border-right: solid 1px #e7e8e9 + background-color: #f8f9fa + a + outline: none + &:hover + text-decoration: none + background-color: #f8f9fa + &.sortable + a + cursor: pointer + &:hover, + &.active + background: #e7e8e9 +</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> + */ +export default { + props: { + columns: { type: Array }, + sortable: { type: Boolean, default: true }, + extraColumnForButtons: { type: Boolean, default: true } + }, + data() { + return { + sortColumn: null, + sortDirection: "ASC" + }; + }, + methods: { + sortIcon(id) { + if (this.sortColumn === id) { + return "sort-" + (this.sortDirection === "ASC" ? "down" : "up"); + } + return "sort"; + }, + sortBy(id) { + if (this.sortable) { + this.sortColumn = id; + this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC"; + this.$emit("sortingChanged", { + sortColumn: this.sortColumn, + sortDirection: this.sortDirection + }); + } + } + } +}; +</script>
--- a/client/src/components/ui/box/Header.vue Thu Feb 28 17:28:54 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -<template> - <h6 class="box-header"> - <span class="box-title"> - <font-awesome-icon - :icon="icon" - class="box-icon" - v-if="icon" - fixed-width - /> - {{ $gettext(title) }} - </span> - <span class="box-close" @click="closeCallback" v-if="closeCallback"> - <font-awesome-icon icon="times" /> - </span> - </h6> -</template> - -<style lang="sass"> -.box-header - display: flex - justify-content: space-between - align-items: center - min-height: 35px - padding-left: .5rem - border-bottom: 1px solid #dee2e6 - color: $color-info - margin-bottom: 0 - padding: 0.25rem - font-weight: bold - .box-title - padding-left: 0.25rem - .box-icon - margin-right: 0.25rem - .box-close - color: #888 - padding: 3px 7px - border-radius: 0.25rem - cursor: pointer - transition: background-color 0.3s, color 0.3s - &:hover - color: #666 - background-color: #eee -</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> - */ - -export default { - props: ["icon", "title", "closeCallback"] -}; -</script>
--- a/client/src/main.js Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/main.js Fri Mar 01 11:06:27 2019 +0100 @@ -29,7 +29,9 @@ import store from "./store"; import translations from "./locale/translations.json"; import { supportedLanguages, defaultLanguage } from "./locale/languages.js"; -import Header from "@/components/ui/box/Header"; +import UIBoxHeader from "@/components/ui/UIBoxHeader"; +import UITableHeader from "@/components/ui/UITableHeader"; +import UITableBody from "@/components/ui/UITableBody"; // styles import "../node_modules/bootstrap/dist/css/bootstrap.min.css"; @@ -74,8 +76,11 @@ faRuler, faSearch, faShip, + faSort, faSortAmountDown, faSortAmountUp, + faSortDown, + faSortUp, faSpinner, faStar, faTasks, @@ -126,6 +131,9 @@ faRuler, faSearch, faShip, + faSort, + faSortDown, + faSortUp, faSortAmountDown, faSortAmountUp, faSpinner, @@ -154,7 +162,9 @@ // register global components Vue.component("font-awesome-icon", FontAwesomeIcon); -Vue.component("UIBoxHeader", Header); +Vue.component("UIBoxHeader", UIBoxHeader); +Vue.component("UITableHeader", UITableHeader); +Vue.component("UITableBody", UITableBody); // global vue config Vue.config.productionTip = false;
--- a/client/src/router.js Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/router.js Fri Mar 01 11:06:27 2019 +0100 @@ -81,22 +81,6 @@ } }, { - path: "/importqueue/:id?", - name: "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"), @@ -168,6 +152,7 @@ requiresAuth: true }, beforeEnter: (to, from, next) => { + store.commit("application/searchQuery", ""); store.commit("application/showContextBox", false); store.commit("application/contextBoxContent", ""); store.commit("application/showSearchbar", false); @@ -182,6 +167,7 @@ requiresAuth: true }, beforeEnter: (to, from, next) => { + store.commit("application/searchQuery", ""); store.commit("application/showContextBox", true); store.commit("application/contextBoxContent", "bottlenecks"); store.commit("application/showSearchbar", true); @@ -189,8 +175,8 @@ } }, { - path: "/review/:id?", - name: "review", + path: "/imports/overview/:id?", + name: "importoverview", component: Main, meta: { requiresAuth: true @@ -201,7 +187,7 @@ next("/"); } else { store.commit("application/showContextBox", true); - store.commit("application/contextBoxContent", "staging"); + store.commit("application/contextBoxContent", "importoverview"); store.commit("application/showSearchbar", true); next(); } @@ -219,6 +205,7 @@ if (!isSysadmin) { next("/"); } else { + store.commit("application/searchQuery", ""); store.commit("application/showContextBox", true); store.commit("application/contextBoxContent", "stretches"); store.commit("application/showSearchbar", true);
--- a/client/src/store/bottlenecks.js Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/store/bottlenecks.js Fri Mar 01 11:06:27 2019 +0100 @@ -21,6 +21,7 @@ const init = () => { return { bottlenecks: [], + bottlenecksList: [], selectedBottleneck: null, surveys: [], selectedSurvey: null, @@ -36,6 +37,9 @@ setBottlenecks: (state, bottlenecks) => { state.bottlenecks = bottlenecks; }, + setBottlenecksList: (state, bottlenecksList) => { + state.bottlenecksList = bottlenecksList; + }, setSelectedBottleneck: (state, name) => { state.selectedBottleneck = name; }, @@ -102,13 +106,43 @@ } }); }, + loadBottlenecksList({ commit }) { + 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("setBottlenecksList", response.data.features); + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + }, loadBottlenecks({ commit }) { return new Promise((resolve, reject) => { var bottleneckFeatureCollectionRequest = new WFS().writeGetFeature({ srsName: "EPSG:4326", featureNS: "gemma", featurePrefix: "gemma", - featureTypes: ["bottleneck_overview"], + featureTypes: ["bottlenecks_geoserver"], outputFormat: "application/json" }); HTTP.post(
--- a/client/src/store/imports.js Thu Feb 28 17:28:54 2019 +0100 +++ b/client/src/store/imports.js Fri Mar 01 11:06:27 2019 +0100 @@ -30,7 +30,9 @@ stretches: [], imports: [], staging: [], - importToReview: null + importToReview: null, + stagingVisible: true, + logsVisible: true }; }; @@ -67,6 +69,18 @@ init, namespaced: true, state: init(), + getters: { + processedReviews: state => { + return state.staging + .filter(x => x.status !== STATES.NEEDSAPPROVAL) + .map(r => { + return { + id: r.id, + state: r.status + }; + }); + } + }, mutations: { setStretches: (state, stretches) => { state.stretches = stretches; @@ -74,6 +88,12 @@ setImports: (state, imports) => { state.imports = imports; }, + setStagingVisibility: (state, visibility) => { + state.stagingVisible = visibility; + }, + setLogsVisibility: (state, visibility) => { + state.logsVisible = visibility; + }, setStaging: (state, staging) => { const enriched = staging.map(x => { return { ...x, status: STATES.NEEDSAPPROVAL }; @@ -138,9 +158,11 @@ }); }); }, - getImports({ commit }) { + getImports({ commit }, filter) { + let queryParams = ""; + if (filter) queryParams = "?states=" + filter.join(","); return new Promise((resolve, reject) => { - HTTP.get("/imports", { + HTTP.get("/imports" + queryParams, { headers: { "X-Gemma-Auth": localStorage.getItem("token") } }) .then(response => { @@ -165,6 +187,22 @@ reject(error); }); }); + }, + confirmReview({ state }, reviewResults) { + return new Promise((resolve, reject) => { + HTTP.patch("/imports", reviewResults, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); } } };
--- a/schema/README.md Thu Feb 28 17:28:54 2019 +0100 +++ b/schema/README.md Fri Mar 01 11:06:27 2019 +0100 @@ -1,3 +1,5 @@ +The gemma database schema requires PostgreSQL >= 11. + `dot.tmpl` is a template file for `postgresql_autodoc` to be more similiar to UML and leave out some less useful labels.
--- a/schema/gemma.sql Thu Feb 28 17:28:54 2019 +0100 +++ b/schema/gemma.sql Fri Mar 01 11:06:27 2019 +0100 @@ -473,56 +473,6 @@ PRIMARY KEY (bottleneck_id, riverbed) ) - -- Published view for GeoServer - CREATE VIEW bottlenecks_geoserver AS - WITH fairway_availability_latest AS ( - SELECT DISTINCT ON (bottleneck_id) bottleneck_id,date_info,critical - FROM fairway_availability - ORDER BY bottleneck_id, date_info DESC NULLS LAST), - gauge_measurements_waterlevel AS ( - SELECT DISTINCT ON (fk_gauge_id) fk_gauge_id,measure_date,predicted,water_level - FROM gauge_measurements WHERE predicted ='false' - ORDER BY fk_gauge_id, measure_date DESC NULLS LAST) - SELECT - b.id, - b.bottleneck_id, - b.objnam, - b.nobjnm, - b.stretch, - b.area, - b.rb, - b.lb, - b.responsible_country, - b.revisiting_time, - b.limiting, - b.date_info, - b.source_organization, - g.location AS gauge_isrs_code, - g.objname AS gauge_objname, - rwl_ldc.value AS ldc, - rwl_mw.value AS mw, - rwl_hdc.value AS hdc, - fal.date_info AS fa_date_info, - fal.critical AS fa_critical, - gmw.water_level as gm_waterlevel - FROM bottlenecks b, gauges g, - (SELECT gauge_id, value FROM gauges_reference_water_levels - WHERE depth_reference = 'LDC') rwl_ldc, - (SELECT gauge_id, value FROM gauges_reference_water_levels - WHERE depth_reference = 'MW') rwl_mw, - (SELECT gauge_id, value FROM gauges_reference_water_levels - WHERE depth_reference = 'HDC') rwl_hdc - LEFT JOIN LATERAL ( - SELECT bottleneck_id,date_info,critical - FROM fairway_availability_latest - WHERE b.id=bottleneck_id) fal ON TRUE - LEFT JOIN LATERAL ( - SELECT water_level - FROM gauge_measurements_waterlevel - WHERE b.fk_g_fid=fk_gauge_id) gmw ON TRUE - WHERE b.fk_g_fid = g.location AND g.location = rwl_ldc.gauge_id - AND g.location = rwl_mw.gauge_id AND g.location = rwl_hdc.gauge_id - CREATE TABLE sounding_results ( id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, bottleneck_id int NOT NULL REFERENCES bottlenecks(id), @@ -630,6 +580,52 @@ SELECT bottleneck_id, max(date_info) AS current FROM sounding_results GROUP BY bottleneck_id) sr ON sr.bottleneck_id = bn.id ORDER BY objnam + + -- Published view for GeoServer + CREATE VIEW bottlenecks_geoserver AS + WITH fairway_availability_latest AS ( + SELECT DISTINCT ON (bottleneck_id) bottleneck_id,date_info,critical + FROM fairway_availability + ORDER BY bottleneck_id, date_info DESC NULLS LAST), + gauge_measurements_waterlevel AS ( + SELECT DISTINCT ON (fk_gauge_id) + fk_gauge_id, measure_date, predicted, water_level + FROM gauge_measurements WHERE predicted ='false' + ORDER BY fk_gauge_id, measure_date DESC NULLS LAST) + SELECT + b.id, + b.bottleneck_id, + b.objnam, + b.nobjnm, + b.stretch, + b.area, + b.rb, + b.lb, + b.responsible_country, + b.revisiting_time, + b.limiting, + b.date_info, + b.source_organization, + g.location AS gauge_isrs_code, + g.objname AS gauge_objname, + json_object_agg(r.depth_reference, r.value) AS reference_water_levels, + fal.date_info AS fa_date_info, + fal.critical AS fa_critical, + gmw.water_level as gm_waterlevel + FROM bottlenecks b LEFT JOIN gauges g ON b.fk_g_fid = g.location + LEFT JOIN LATERAL ( + SELECT gauge_id,depth_reference,value + FROM gauges_reference_water_levels + ) r ON r.gauge_id = b.fk_g_fid + LEFT JOIN LATERAL ( + SELECT bottleneck_id,date_info,critical + FROM fairway_availability_latest + WHERE b.id=bottleneck_id) fal ON TRUE + LEFT JOIN LATERAL ( + SELECT water_level + FROM gauge_measurements_waterlevel + WHERE b.fk_g_fid=fk_gauge_id) gmw ON TRUE + GROUP BY b.id, g.location, fal.date_info, fal.critical, gmw.water_level; ; -- Configure primary keys for geoserver views
--- a/schema/isrs_functions.sql Thu Feb 28 17:28:54 2019 +0100 +++ b/schema/isrs_functions.sql Fri Mar 01 11:06:27 2019 +0100 @@ -26,7 +26,7 @@ area geometry ) RETURNS geometry AS $$ - WITH + WITH RECURSIVE -- Get coordinates of location codes points_geog AS ( SELECT geom FROM waterway.distance_marks_virtual @@ -37,9 +37,7 @@ SELECT best_utm(ST_Collect(geom::geometry)) AS z FROM points_geog), axis AS ( - -- Transform and sew together contiguous axis chunks - SELECT ST_LineMerge(ST_Collect(ST_Transform( - wtwaxs::geometry, z))) AS wtwaxs + SELECT id, 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 @@ -47,16 +45,49 @@ SELECT ST_ClosestPoint( wtwaxs, ST_Transform(points_geog.geom::geometry, z)) AS geom - FROM axis, points_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, + FROM points_geog, utm_zone, ( + SELECT ST_Collect(wtwaxs) AS wtwaxs + FROM axis) AS ax), + axis_snapped AS ( + -- Iteratively connect non-contiguous axis chunks + -- to find the contiguous axis on which given distance marks lie + (SELECT ARRAY[id] AS ids, wtwaxs + FROM axis, points + WHERE ST_Intersects( + ST_Buffer(axis.wtwaxs, 0.0001), points.geom) + FETCH FIRST ROW ONLY) + UNION + -- Connect endpoint of next linestring with closest + -- endpoint of merged linestring until a contiguous + -- linestring connecting both distance marks is build up + (SELECT refids || id, + ST_LineMerge(ST_Collect(ARRAY( + -- Linestring build up so far + SELECT refgeom + UNION + -- Fill eventual gap + SELECT ST_MakeLine( + ST_ClosestPoint( + ST_Boundary(refgeom), ST_Boundary(geom)), + ST_ClosestPoint( + ST_Boundary(geom), ST_Boundary(refgeom))) + UNION + -- Linestring to be added + SELECT geom))) + FROM axis_snapped AS axis_snapped (refids, refgeom), + axis AS axis (id, geom), (SELECT ST_Collect(points.geom) AS pts FROM points) AS points - WHERE ST_Covers(ST_Buffer(lines.line, 0.0001), points.pts)), + WHERE id <> ALL(refids) + AND NOT ST_Covers(ST_Buffer(refgeom, 0.0001), points.pts) + ORDER BY ST_Distance(ST_Boundary(refgeom), ST_Boundary(geom)) + FETCH FIRST ROW ONLY)), + axis_segment AS ( + -- Fetch end result from snapping + SELECT wtwaxs AS line + FROM axis_snapped + WHERE array_length(ids, 1) = ( + SELECT max(array_length(ids, 1)) FROM axis_snapped)), axis_substring AS ( -- Use linear referencing to clip axis between distance marks. -- Simplification is used to work-around the problem, that @@ -78,29 +109,34 @@ -- polygons, which intersect with the axis. The union is to avoid -- problems with invalid/self-intersecting multipolygons SELECT ST_Union(a_dmp.geom) AS area - FROM axis_substring, utm_zone, - ST_Dump(ST_Transform(area, z)) AS a_dmp + FROM axis_substring, utm_zone, LATERAL ( + SELECT ST_MakeValid(ST_Transform(geom, z)) AS geom + FROM ST_Dump(area)) AS a_dmp WHERE ST_Intersects(a_dmp.geom, axis_substring.line) ), + rotated_ends AS ( + SELECT ST_Collect(ST_Scale( + ST_Translate(e, + (ST_X(p1) - ST_X(p2)) / 2, + (ST_Y(p1) - ST_Y(p2)) / 2), + ST_Point(d, d), p1)) AS blade + FROM axis_substring, area_subset, + LATERAL (SELECT i, ST_PointN(line, i) AS p1 + FROM (VALUES (1), (-1)) AS idx (i)) AS ep, + ST_Rotate(ST_PointN(line, i*2), pi()/2, p1) AS ep2 (p2), + ST_Makeline(p1, p2) AS e (e), + LATERAL (SELECT (ST_MaxDistance(p1, area) / ST_Length(e)) + * 2) AS d (d)), 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, - area_subset.area), - 'endcap=flat'), - area_subset.area))).geom - FROM axis_substring, area_subset) + -- Split area by orthogonal lines at the ends of the clipped axis + SELECT (ST_Dump(ST_CollectionExtract( + ST_Split(area, blade), 3))).geom + FROM area_subset, rotated_ends) -- From the polygons returned by the last CTE, select only those -- around the clipped axis - SELECT ST_Collect(ST_Transform(range_area.geom, ST_SRID(area))) + SELECT ST_Multi(ST_Transform(ST_Union(range_area.geom), ST_SRID(area))) FROM axis_substring, range_area - WHERE ST_Intersects(range_area.geom, axis_substring.line) + WHERE ST_Intersects(ST_Buffer(range_area.geom, -0.0001), + axis_substring.line) $$ LANGUAGE sql;
--- a/schema/isrs_tests.sql Thu Feb 28 17:28:54 2019 +0100 +++ b/schema/isrs_tests.sql Fri Mar 01 11:06:27 2019 +0100 @@ -42,7 +42,6 @@ ) IS NULL, 'ISRSrange_area returns NULL, if given area does not intersect with axis'); -\set test_area 'POLYGON((-1 1, 2 1, 2 -1, -1 -1, -1 1))' SELECT ok( ST_DWithin( (SELECT geom FROM waterway.distance_marks_virtual @@ -50,8 +49,8 @@ ST_Boundary(ISRSrange_area(isrsrange( ('AT', 'XXX', '00001', '00000', 0)::isrs, ('AT', 'XXX', '00001', '00000', 1)::isrs), - ST_SetSRID(:'test_area'::geometry, - 4326)))::geography, + (SELECT ST_Collect(CAST(area AS geometry)) + FROM waterway.waterway_area))), 1) AND ST_DWithin( @@ -60,11 +59,12 @@ ST_Boundary(ISRSrange_area(isrsrange( ('AT', 'XXX', '00001', '00000', 0)::isrs, ('AT', 'XXX', '00001', '00000', 1)::isrs), - ST_SetSRID(:'test_area'::geometry, - 4326)))::geography, + (SELECT ST_Collect(CAST(area AS geometry)) + FROM waterway.waterway_area))), 1), 'Resulting polygon almost ST_Touches points corresponding to stretch'); +\set test_area 'POLYGON((-1 1, 2 1, 2 -1, -1 -1, -1 1))' SELECT ok( 2 = ST_NumGeometries( ISRSrange_area( @@ -100,3 +100,12 @@ 0)), 4326))), 'Self-intersecting multipolygon leads to one polygon in result'); + +SELECT ok( + ISRSrange_area( + isrsrange( + ('AT', 'XXX', '00001', '00000', 0)::isrs, + ('AT', 'XXX', '00001', '00000', 2)::isrs), + (SELECT ST_Collect(CAST(area AS geometry)) + FROM waterway.waterway_area)) IS NOT NULL, + 'Area generated from non-matching distance mark and non-contiguous axis');
--- a/schema/run_tests.sh Thu Feb 28 17:28:54 2019 +0100 +++ b/schema/run_tests.sh Fri Mar 01 11:06:27 2019 +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(57)' \ + -c 'SELECT plan(58)' \ -f isrs_tests.sql \ -f auth_tests.sql \ -f manage_users_tests.sql \
--- a/schema/tap_tests_data.sql Thu Feb 28 17:28:54 2019 +0100 +++ b/schema/tap_tests_data.sql Fri Mar 01 11:06:27 2019 +0100 @@ -76,18 +76,46 @@ ('AT', 'XXX', '00001', '00000', 1)::isrs, ST_SetSRID('POINT(1 0)'::geometry, 4326), 'someENC' +), ( + ('AT', 'XXX', '00001', '00000', 2)::isrs, + ST_SetSRID('POINT(1.6 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), + 'CIRCULARSTRING(0 0, 0.5 0.5, 0.6 0.4)'), 4326), 'testriver' ), ( + ST_SetSRID(ST_CurveToLine('CIRCULARSTRING(0.6 0.4, 1 0, 1.5 0)'), 4326), + 'testriver' +), ( ST_SetSRID('LINESTRING(0.5 0.5, 1 1)'::geometry, 4326), 'testriver' +), ( + ST_SetSRID('LINESTRING(1.5 0.1, 2 0)'::geometry, 4326), + 'testriver' ); +-- Simulate waterway area as non-intersecting buffers around axis +WITH RECURSIVE +buffer AS ( + SELECT id, ST_Buffer(wtwaxs, 10000, 'endcap=flat')::geometry AS buf + FROM waterway.waterway_axis), +cleaned AS ( + (SELECT ARRAY[id] AS ids, buf AS cbuf, buf AS others + FROM buffer ORDER BY id FETCH FIRST ROW ONLY) + UNION + (SELECT ids || id, + ST_Difference(buf, others), + ST_Union(buf, others) + FROM cleaned, buffer + WHERE id <> ALL(ids) + ORDER BY id ASC, ids DESC + FETCH FIRST ROW ONLY)) +INSERT INTO waterway.waterway_area (area) SELECT cbuf FROM cleaned; + INSERT INTO users.templates (template_name, country, template_data) VALUES ('AT', 'AT', '\x'), ('RO', 'RO', '\x');