Mercurial > gemma
changeset 1558:0ded4c56978e
refac: component filestructure. remove admin/map hierarchy
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Wed, 12 Dec 2018 09:22:20 +0100 |
parents | 62171cd9a42b |
children | 5d84dcb79a54 |
files | client/src/components/App.vue client/src/components/Bottlenecks.vue client/src/components/Contextbox.vue client/src/components/Identify.vue client/src/components/ImportSoundingresults.vue client/src/components/Importqueue.vue client/src/components/Importqueuedetail.vue client/src/components/Logs.vue client/src/components/Main.vue client/src/components/Maplayer.vue client/src/components/Pdftool.vue client/src/components/Search.vue client/src/components/Staging.vue client/src/components/Systemconfiguration.vue client/src/components/Zoom.vue client/src/components/admin/Importqueue.vue client/src/components/admin/Importqueuedetail.vue client/src/components/admin/Logs.vue client/src/components/admin/Systemconfiguration.vue client/src/components/admin/importschedule/Importschedule.vue client/src/components/admin/importschedule/Importscheduledetail.vue client/src/components/admin/usermanagement/Passwordfield.vue client/src/components/admin/usermanagement/Userdetail.vue client/src/components/admin/usermanagement/Usermanagement.vue client/src/components/fairway/Fairwayprofile.vue client/src/components/fairway/Infobar.vue client/src/components/fairway/Profiles.vue client/src/components/importschedule/Importschedule.vue client/src/components/importschedule/Importscheduledetail.vue client/src/components/layers/Layers.vue client/src/components/layers/Layerselect.vue client/src/components/layers/LegendElement.vue client/src/components/map/Identify.vue client/src/components/map/Main.vue client/src/components/map/Maplayer.vue client/src/components/map/Pdftool.vue client/src/components/map/Search.vue client/src/components/map/Zoom.vue client/src/components/map/contextbox/Bottlenecks.vue client/src/components/map/contextbox/Contextbox.vue client/src/components/map/contextbox/ImportSoundingresults.vue client/src/components/map/contextbox/Staging.vue client/src/components/map/fairway/Fairwayprofile.vue client/src/components/map/fairway/Infobar.vue client/src/components/map/fairway/Profiles.vue client/src/components/map/layers/Layers.vue client/src/components/map/layers/Layerselect.vue client/src/components/map/layers/LegendElement.vue client/src/components/map/toolbar/Identify.vue client/src/components/map/toolbar/Layers.vue client/src/components/map/toolbar/Linetool.vue client/src/components/map/toolbar/Pdftool.vue client/src/components/map/toolbar/Polygontool.vue client/src/components/map/toolbar/Profiles.vue client/src/components/map/toolbar/Toolbar.vue client/src/components/toolbar/Identify.vue client/src/components/toolbar/Layers.vue client/src/components/toolbar/Linetool.vue client/src/components/toolbar/Pdftool.vue client/src/components/toolbar/Polygontool.vue client/src/components/toolbar/Profiles.vue client/src/components/toolbar/Toolbar.vue client/src/components/usermanagement/Passwordfield.vue client/src/components/usermanagement/Userdetail.vue client/src/components/usermanagement/Usermanagement.vue client/src/router.js |
diffstat | 66 files changed, 5829 insertions(+), 5829 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/App.vue Tue Dec 11 22:59:10 2018 +0100 +++ b/client/src/components/App.vue Wed Dec 12 09:22:20 2018 +0100 @@ -87,16 +87,16 @@ } }, components: { - Profiles: () => import("./map/fairway/Profiles"), - Infobar: () => import("./map/fairway/Infobar"), - Pdftool: () => import("./map/Pdftool"), - Zoom: () => import("./map/Zoom"), - Identify: () => import("./map/Identify"), - Layers: () => import("./map/layers/Layers"), + Profiles: () => import("./fairway/Profiles"), + Infobar: () => import("./fairway/Infobar"), + Pdftool: () => import("./Pdftool"), + Zoom: () => import("./Zoom"), + Identify: () => import("./Identify"), + Layers: () => import("./layers/Layers"), Sidebar: () => import("./Sidebar"), - Search: () => import("./map/Search"), - Contextbox: () => import("./map/contextbox/Contextbox"), - Toolbar: () => import("./map/toolbar/Toolbar") + Search: () => import("./Search"), + Contextbox: () => import("./Contextbox"), + Toolbar: () => import("./toolbar/Toolbar") } }; </script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Bottlenecks.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,324 @@ +<template> + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="ship" class="mr-2"></font-awesome-icon> + <translate>Bottlenecks</translate> + </h6> + <div class="row p-2 text-left small"> + <div class="col-5"> + <a href="#" @click="sortBy('name')" class="sort-link"> + <translate>Name</translate> + </a> + <font-awesome-icon + :icon="sortIcon" + class="ml-1" + v-if="sortColumn === 'name'" + ></font-awesome-icon> + </div> + <div class="col-2"> + <a href="#" @click="sortBy('latestMeasurement')" class="sort-link"> + <translate>Latest</translate> <br /> + <translate>Measurement</translate> + </a> + <font-awesome-icon + :icon="sortIcon" + class="ml-1" + v-if="sortColumn === 'latestMeasurement'" + ></font-awesome-icon> + </div> + <div class="col-3"> + <a href="#" @click="sortBy('chainage')" class="sort-link"> + <translate>Chainage</translate> + </a> + <font-awesome-icon + :icon="sortIcon" + class="ml-1" + v-if="sortColumn === 'chainage'" + ></font-awesome-icon> + </div> + <div class="col-2"></div> + </div> + <div + class="bottleneck-list small text-left" + :style="'max-height: ' + (showSplitscreen ? 18 : 35) + 'rem'" + v-if="filteredAndSortedBottlenecks().length" + > + <div + v-for="bottleneck in filteredAndSortedBottlenecks()" + :key="bottleneck.properties.name" + class="border-top row bottleneck-row mx-0" + > + <div class="col-5 py-2 text-left"> + <a href="#" @click="selectBottleneck(bottleneck)">{{ + bottleneck.properties.name + }}</a> + </div> + <div class="col-2 py-2"> + {{ formatSurveyDate(bottleneck.properties.current) }} + </div> + <div class="col-3 py-2"> + {{ + displayCurrentChainage( + bottleneck.properties.from, + bottleneck.properties.to + ) + }} + </div> + <div class="col-2 pr-0 text-right"> + <button + type="button" + class="btn btn-sm btn-info rounded-0 h-100" + @click="loadSurveys(bottleneck.properties.name)" + v-if="bottleneck.properties.current" + > + <font-awesome-icon + icon="spinner" + fixed-width + spin + v-if="loading === bottleneck.properties.name" + ></font-awesome-icon> + <font-awesome-icon + icon="angle-down" + fixed-width + v-if=" + loading !== bottleneck.properties.name && + openBottleneck !== bottleneck.properties.name + " + ></font-awesome-icon> + <font-awesome-icon + icon="angle-up" + fixed-width + v-if=" + loading !== bottleneck.properties.name && + openBottleneck === bottleneck.properties.name + " + ></font-awesome-icon> + </button> + </div> + <div + :class="[ + 'col-12 p-0', + 'surveys', + { open: openBottleneck === bottleneck.properties.name } + ]" + > + <a + href="#" + class="d-block px-3 py-2" + v-for="(survey, index) in openBottleneckSurveys" + :key="index" + @click="selectSurvey(survey, bottleneck)" + >{{ formatSurveyDate(survey.date_info) }}</a + > + </div> + </div> + </div> + <div v-else class="small text-center py-3 border-top"> + <translate>No results.</translate> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; +import { HTTP } from "../lib/http"; +import { displayError } from "../lib/errors.js"; +import { formatSurveyDate } from "../lib/date.js"; + +export default { + name: "bottlenecks", + data() { + return { + sortColumn: "name", + sortDirection: "ASC", + openBottleneck: null, + openBottleneckSurveys: null, + loading: null + }; + }, + computed: { + ...mapState("application", [ + "searchQuery", + "showSearchbarLastState", + "showSplitscreen" + ]), + ...mapState("bottlenecks", ["bottlenecks"]), + sortIcon() { + return this.sortDirection === "ASC" + ? "sort-amount-down" + : "sort-amount-up"; + } + }, + methods: { + formatSurveyDate(date) { + return formatSurveyDate(date); + }, + filteredAndSortedBottlenecks() { + return this.bottlenecks + .filter(bn => { + return bn.properties.name + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + }) + .sort((bnA, bnB) => { + switch (this.sortColumn) { + case "name": + if ( + bnA.properties.name.toLowerCase() < + bnB.properties.name.toLowerCase() + ) + return this.sortDirection === "ASC" ? -1 : 1; + if ( + bnA.properties.name.toLowerCase() > + bnB.properties.name.toLowerCase() + ) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + + case "latestMeasurement": { + if ( + (bnA.properties.current || "") < (bnB.properties.current || "") + ) + return this.sortDirection === "ASC" ? -1 : 1; + if ( + (bnA.properties.current || "") > (bnB.properties.current || "") + ) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + } + + case "chainage": + if (bnA.properties.from < bnB.properties.from) + return this.sortDirection === "ASC" ? -1 : 1; + if (bnA.properties.from > bnB.properties.from) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + + default: + return 0; + } + }); + }, + selectSurvey(survey, bottleneck) { + this.$store + .dispatch( + "bottlenecks/setSelectedBottleneck", + bottleneck.properties.name + ) + .then(() => { + this.$store.commit("bottlenecks/selectedSurvey", survey); + }) + .then(() => { + this.$store.commit("map/moveMap", { + coordinates: bottleneck.geometry.coordinates, + zoom: 17, + preventZoomOut: true + }); + }); + }, + selectBottleneck(bottleneck) { + this.$store + .dispatch( + "bottlenecks/setSelectedBottleneck", + bottleneck.properties.name + ) + .then(() => { + this.$store.commit("bottlenecks/setFirstSurveySelected"); + }) + .then(() => { + this.$store.commit("map/moveMap", { + coordinates: bottleneck.geometry.coordinates, + zoom: 17, + preventZoomOut: true + }); + }); + }, + sortBy(column) { + this.sortColumn = column; + this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC"; + }, + loadSurveys(name) { + this.openBottleneckSurveys = null; + if (name === this.openBottleneck) { + this.openBottleneck = null; + } else { + this.openBottleneck = name; + this.loading = name; + + HTTP.get("/surveys/" + name, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + this.openBottleneckSurveys = response.data.surveys.sort((a, b) => { + return a.date_info < b.date_info ? 1 : -1; + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => (this.loading = null)); + } + }, + displayCurrentChainage(from, to) { + return from / 10 + " - " + to / 10; + } + }, + mounted() { + this.$store.dispatch("bottlenecks/loadBottlenecks"); + } +}; +</script> + +<style lang="scss" scoped> +.bottleneck-list { + overflow-y: auto; +} + +.bottleneck-list .bottleneck-row a { + text-decoration: none; +} + +.bottleneck-list .bottleneck-row:hover { + background: #fbfbfb; +} + +.surveys { + max-height: 0; + min-height: 0; + overflow: hidden; +} + +.surveys a:hover { + background: #f3f3f3; +} + +.surveys.open { + max-height: 250px; + overflow: auto; +} + +.sort-link { + color: #444; + font-weight: bold; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Contextbox.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,96 @@ +<template> + <div :class="style"> + <div @click="close" class="ui-element close-contextbox text-muted"> + <font-awesome-icon icon="times"></font-awesome-icon> + </div> + <Bottlenecks v-if="contextBoxContent === 'bottlenecks'"></Bottlenecks> + <Importsounding v-if="contextBoxContent === 'imports'"></Importsounding> + <Staging v-if="contextBoxContent === 'staging'"></Staging> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "contextbox", + components: { + Bottlenecks: () => import("./Bottlenecks"), + Importsounding: () => import("./ImportSoundingresults.vue"), + Staging: () => import("./Staging.vue") + }, + computed: { + ...mapState("application", [ + "showSearchbarLastState", + "contextBoxContent", + "showContextBox" + ]), + style() { + return [ + "ui-element shadow-xs contextbox", + { + contextboxcollapsed: !this.showContextBox, + contextboxextended: this.showContextBox, + "rounded-bottom": this.contextBoxContent !== "imports", + rounded: this.contextBoxContent === "imports" + } + ]; + } + }, + methods: { + close() { + this.$store.commit("application/showContextBox", false); + this.$store.commit( + "application/showSearchbar", + this.showSearchbarLastState + ); + } + } +}; +</script> + +<style lang="scss" scoped> +.contextbox { + position: relative; + background-color: #ffffff; + opacity: $slight-transparent; + transition: max-width 0.3s, max-height 0.3s; + overflow: hidden; + background: #fff; +} +.contextbox > div:last-child { + width: 600px; +} + +.contextboxcollapsed { + max-width: 0; + max-height: 0; +} + +.contextboxextended { + max-width: 600px; + max-height: 640px; +} + +.close-contextbox { + position: absolute; + z-index: 2; + right: 0; + top: 7px; + height: $icon-width; + width: $icon-height; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Identify.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,124 @@ +<template> + <div + :class="[ + 'box ui-element rounded bg-white text-nowrap', + { expanded: showIdentify } + ]" + > + <div style="width: 20rem"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="info" class="mr-2"></font-awesome-icon> + <translate>Identified</translate> + <font-awesome-icon + icon="times" + class="ml-auto text-muted" + @click="$store.commit('application/showIdentify', false)" + ></font-awesome-icon> + </h6> + <div class="d-flex flex-column features p-3 flex-grow-1 text-left"> + <div v-if="currentMeasurement"> + <b> + {{ currentMeasurement.quantity }} ({{ + currentMeasurement.unitSymbol + }}): + </b> + <br /> + <small>{{ currentMeasurement.value }}</small> + </div> + <div v-for="(feature, i) of identifiedFeatures" :key="feature.getId()"> + <div v-if="feature.getId()" :class="{ 'mt-2': i }"> + <strong> + {{ + feature.getId().replace(/[.][^.]*$/, "") + /* cut away everything from the last . to the end */ + }}: + </strong> + <small + v-for="(value, key) in prepareProperties(feature)" + :key="key" + > + <div v-if="value">{{ key }}:{{ value }}</div> + </small> + </div> + </div> + <div + v-if="!currentMeasurement && !identifiedFeatures.length" + class="text-muted small text-center my-auto" + > + <translate>No features identified.</translate> + </div> + </div> + <div class="versioninfo border-top p-3 text-left"> + <span v-translate="{ license: 'AGPL-3.0-or-later' }"> + This app uses <i>gemma</i>, which is Free Software under <br /> + %{ license } without warranty, see docs for details. + </span> + <br /> + <a href="https://hg.intevation.de/gemma/file/tip"> + <translate>source-code</translate> + </a> + {{ versionStr }} <br />© via donau. ⓔ Intevation. <br /> + <span v-translate="{ name: 'OpenSteetMap' }" + >Some data © + <a href="https://www.openstreetmap.org/copyright">%{ name }</a> + contributors. + </span> + <p v-translate="{ geoLicense: 'CC-BY-4.0' }"> + Uses + <a href="https://download.geonames.org/export/dump/readme.txt" + >GeoNames</a + > + under %{ geoLicense }. + </p> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.features { + max-height: 19rem; + overflow-y: auto; +} + +.versioninfo { + font-size: 60%; + white-space: normal; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Bernhard E. Reiter <bernhard.reiter@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; + +export default { + name: "identify", + computed: { + ...mapGetters("application", ["versionStr"]), + ...mapState("application", ["showIdentify"]), + ...mapState("map", ["identifiedFeatures", "currentMeasurement"]) + }, + methods: { + prepareProperties(feature) { + // return dict object with propertyname:plainvalue prepared for display + var properties = feature.getProperties(); + delete properties[feature.getGeometryName()]; + return properties; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/ImportSoundingresults.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,379 @@ +<template> + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="upload" class="mr-2"></font-awesome-icon> + <translate>Import Soundingresults</translate> + </h6> + <div v-if="editState" class="ml-auto mr-auto mt-4 w-95"> + <div class="d-flex flex-column"> + <div class="d-flex flex-row"> + <div class="mt-1 text-left w-50 ml-2 mr-4"> + <small class="text-muted"> + <translate>Bottleneck</translate> + </small> + <select v-model="bottleneck" class="custom-select"> + <option + v-for="bottleneck in availableBottlenecks" + :key="bottleneck" + >{{ bottleneck }}</option + > + </select> + <span class="text-danger"> + <small v-if="!bottleneck"> + <translate>Please select a bottleneck</translate> + </small> + </span> + </div> + <div class="d-flex flex-column mt-1 text-left w-50 mr-2"> + <small class="text-muted"> + <translate>Projection</translate> (EPSG) + </small> + <input + class="form-control" + v-model="projection" + value="4326" + placeholder="e.g. 4326" + type="number" + /> + <span class="text-left text-danger"> + <small v-if="!projection"> + <translate>Please enter a projection</translate> + </small> + </span> + </div> + </div> + <div class="d-flex flex-row"> + <div class="mt-1 text-left w-50 ml-2 mr-4"> + <small class="text-muted"> + <translate>Depthreference</translate> + </small> + <select + v-model="depthReference" + class="custom-select" + id="depthreference" + > + <option + v-for="option in this.$options.depthReferenceOptions" + :key="option" + >{{ option }}</option + > + </select> + <span class="text-left text-danger"> + <small v-if="!depthReference"> + <translate>Please enter a reference</translate> + </small> + </span> + </div> + <div class="mt-1 text-left w-50 mr-2"> + <small class="text-muted"> <translate>Date</translate> </small> + <input + id="importdate" + type="date" + class="form-control" + placeholder="Date of import" + aria-label="bottleneck" + aria-describedby="bottlenecklabel" + v-model="importDate" + /> + <span class="text-left text-danger"> + <small v-if="!importDate"> + <translate>Please enter a date</translate> + </small> + </span> + </div> + </div> + </div> + <div class="ml-2 mt-2 text-left"> + <small v-for="(message, index) in messages" :key="index"> + {{ message }} + </small> + </div> + </div> + <div class="w-95 ml-auto mr-auto mt-4 mb-4"> + <div v-if="uploadState" class="d-flex flex-row input-group mb-4"> + <div class="custom-file"> + <input + accept=".zip" + type="file" + @change="fileSelected" + class="custom-file-input" + id="uploadFile" + /> + <label class="custom-file-label" for="uploadFile"> + {{ uploadLabel }} + </label> + </div> + </div> + <div class="buttons text-right"> + <a + v-if="editState" + download="meta.json" + :href="dataLink" + class="btn btn-outline-info pull-left" + > + <translate>Download Meta.json</translate> + </a> + <button + v-if="editState" + @click="deleteTempData" + class="btn btn-danger" + type="button" + > + <translate>Cancel Upload</translate> + </button> + <button + :disabled="disableUploadButton" + @click="submit" + class="btn btn-info" + type="button" + > + {{ uploadState ? Upload : Confirm }} + </button> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { HTTP } from "../lib/http"; +import { displayError, displayInfo } from "../lib/errors.js"; +import { mapState } from "vuex"; + +const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; + +export default { + name: "imports", + data() { + return { + importState: IMPORTSTATE.UPLOAD, + depthReference: "", + bottleneck: "", + projection: "", + importDate: "", + uploadLabel: this.$gettext("choose .zip- file"), + uploadFile: null, + disableUpload: false, + token: null, + messages: [] + }; + }, + methods: { + initialState() { + this.importState = IMPORTSTATE.UPLOAD; + this.depthReference = ""; + this.bottleneck = ""; + this.projection = ""; + this.importDate = ""; + this.uploadLabel = this.$gettext("choose .zip- file"); + this.uploadFile = null; + this.disableUpload = false; + this.token = null; + this.messages = []; + }, + fileSelected(e) { + const files = e.target.files || e.dataTransfer.files; + if (!files) return; + this.uploadLabel = files[0].name; + this.uploadFile = files[0]; + }, + deleteTempData() { + HTTP.delete("/imports/soundingresult-upload/" + this.token, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + } + }) + .then(() => { + this.initialState(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + submit() { + if (!this.uploadFile || this.disableUpload) return; + if (this.importState === IMPORTSTATE.UPLOAD) { + this.upload(); + } else { + this.confirm(); + } + }, + upload() { + let formData = new FormData(); + formData.append("soundingresult", this.uploadFile); + HTTP.post("/imports/soundingresult-upload", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(response => { + if (response.data.meta) { + const { bottleneck, date, epsg } = response.data.meta; + const depthReference = response.data.meta["depth-reference"]; + this.bottleneck = bottleneck; + this.depthReference = depthReference; + this.importDate = new Date(date).toISOString().split("T")[0]; + this.projection = epsg; + } + this.importState = IMPORTSTATE.EDIT; + this.token = response.data.token; + this.messages = response.data.messages; + }) + .catch(error => { + const { status, data } = error.response; + const messages = data.messages ? data.messages.join(", ") : ""; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${messages}` + }); + }); + }, + confirm() { + let formData = new FormData(); + formData.append("token", this.token); + if (this.bottleneck) formData.append("bottleneck", this.bottleneck); + if (this.importDate) + formData.append("date", this.importDate.split("T")[0]); + if (this.depthReference) + formData.append("depth-reference", this.depthReference); + if (this.projection) formData.append("", this.projection); + + HTTP.post("/imports/soundingresult", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(() => { + displayInfo({ + title: this.$gettext("Import"), + message: this.$gettext("Starting import for ") + this.bottleneck + }); + this.initialState(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + mounted() { + this.$store.dispatch("bottlenecks/loadBottlenecks"); + }, + watch: { + showContextBox() { + if (!this.showContextBox && this.token) this.deleteTempData(); + } + }, + computed: { + ...mapState("application", ["showContextBox"]), + ...mapState("bottlenecks", ["bottlenecks"]), + disableUploadButton() { + if (this.importState === IMPORTSTATE.UPLOAD) return this.disableUpload; + if ( + !this.bottleneck || + !this.importDate || + !this.depthReference || + !this.projection + ) + return true; + return this.disableUpload; + }, + availableBottlenecks() { + return this.bottlenecks.map(x => x.properties.name); + }, + editState() { + return this.importState === IMPORTSTATE.EDIT; + }, + uploadState() { + return this.importState === IMPORTSTATE.UPLOAD; + }, + Upload() { + return this.$gettext("Upload"); + }, + Confirm() { + return this.$gettext("Confirm"); + }, + dataLink() { + return ( + "data:text/json;charset=utf-8," + + encodeURIComponent( + JSON.stringify({ + depthReference: this.depthReference, + bottleneck: this.bottleneck, + date: this.importDate + }) + ) + ); + } + }, + depthReferenceOptions: [ + "", + // "NAP", + // "KP", + // "FZP", + // "ADR", + // "TAW", + // "PUL", + // "NGM", + // "ETRS", + // "POT", + // "LDC", + // "HDC", + // "ZPG", + // "GLW", + // "HSW", + // "LNW", + // "HNW", + // "IGN", + // "WGS", + "RN" //, + // "HBO" + ] +}; +</script> + +<style lang="scss" scoped> +.projectionLabel { + margin-left: $small-offset; +} + +.depthreferencelabel { + margin-left: $small-offset; +} + +.offset-r { + margin-right: $small-offset; +} + +.buttons button { + margin-left: $offset !important; +} + +.label-text { + width: 5rem; + text-align: left; + line-height: 2.25rem; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Importqueue.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,362 @@ +<template> + <div class="d-flex flex-row"> + <div :class="spacerStyle"></div> + <div class="mt-3 importqueuecard flex-grow-1"> + <div class="card shadow-xs"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon icon="tasks" class="mr-2"></font-awesome-icon> + <translate class="headline">Importqueue</translate> + </h6> + <div class="card-body importcardbody"> + <div class="card-body importcardbody"> + <div class="searchandfilter d-flex flex-row"> + <div class="searchgroup input-group"> + <div class="input-group-prepend"> + <span class="input-group-text" id="search"> + <font-awesome-icon icon="search"></font-awesome-icon> + </span> + </div> + <input + v-model="searchQuery" + type="text" + class="form-control" + placeholder + aria-label="Search" + aria-describedby="search" + /> + </div> + <div class="filters"> + <button + @click="setFilter('successful')" + :class="successfulStyle" + > + <translate>Successful</translate> + </button> + <button @click="setFilter('failed')" :class="failedStyle"> + <translate>Failed</translate> + </button> + <button @click="setFilter('pending')" :class="pendingStyle"> + <translate>Pending</translate> + </button> + <button @click="setFilter('rejected')" :class="rejectedStyle"> + <translate>Rejected</translate> + </button> + <button @click="setFilter('accepted')" :class="acceptedStyle"> + <translate>Accepted</translate> + </button> + </div> + </div> + <div class="text-left d-flex flex-row w-50 border-bottom"> + <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="text-left" v-for="job in filteredImports" :key="job.id"> + <Importqueuedetail :job="job"></Importqueuedetail> + </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"; +import Importqueuedetail from "./Importqueuedetail"; + +export default { + name: "importqueue", + components: { + Importqueuedetail + }, + data() { + return { + searchQuery: "", + successful: false, + failed: false, + pending: false, + rejected: false, + accepted: false + }; + }, + mounted() { + this.loadQueue(); + }, + methods: { + setFilter(name) { + this[name] = !this[name]; + const allSet = + this.successful && + this.failed && + this.pending && + this.accepted && + this.rejected; + if (allSet) { + this.successful = false; + this.failed = false; + this.pending = false; + this.accepted = false; + this.rejected = false; + } + }, + loadQueue() { + this.$store.dispatch("imports/getImports").catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + refresh() { + this.loadQueue(); + }, + showDetails(id) { + HTTP.get("/imports/" + id, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + const { entries } = response.data; + this.entries = entries; + this.$modal.show("details"); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + close() { + this.$modal.hide("details"); + } + }, + computed: { + ...mapState("imports", ["imports"]), + ...mapState("application", ["showSidebar"]), + sortIcon() { + return this.sortAsc ? "sort-amount-down" : "sort-amount-up"; + }, + filteredImports() { + const filtered = this.imports + .filter(element => { + if (!this.searchQuery) return true; + return [(element.kind, element.user, element.enqueued)].some(x => { + return x.toLowerCase().includes(this.searchQuery.toLowerCase()); + }); + }) + .filter(y => { + if ( + !this.successful && + !this.failed && + !this.pending && + !this.accepted && + !this.rejected + ) + return true; + let filterCriteria = []; + if (this.successful) filterCriteria.push("successful"); + if (this.failed) filterCriteria.push("failed"); + if (this.pending) filterCriteria.push("pending"); + if (this.accepted) filterCriteria.push("accepted"); + if (this.rejected) filterCriteria.push("rejected"); + const result = filterCriteria.map(selectedState => { + return y.state === selectedState; + }); + return result.some(x => x); + }); + return filtered; + }, + spacerStyle() { + return [ + "spacer ml-3", + { + "spacer-expanded": this.showSidebar, + "spacer-collapsed": !this.showSidebar + } + ]; + }, + successfulStyle() { + return { + btn: true, + "btn-light": !this.successful, + "btn-dark": this.successful + }; + }, + pendingStyle() { + return { + btn: true, + "btn-light": !this.pending, + "btn-dark": this.pending + }; + }, + failedStyle() { + return { + btn: true, + "btn-light": !this.failed, + "btn-dark": this.failed + }; + }, + rejectedStyle() { + return { + btn: true, + "btn-light": !this.rejected, + "btn-dark": this.rejected + }; + }, + acceptedStyle() { + return { + btn: true, + "btn-light": !this.accepted, + "btn-dark": this.accepted + }; + } + } +}; +</script> + +<style lang="scss" scoped> +.jobid { + width: 80px; +} + +.enqueued { + width: 120px; +} + +.user { + width: 80px; +} + +.signer { + width: 80px; +} + +.kind { + width: 80px; +} + +.state { + width: 80px; +} + +.header { + font-weight: bold; + font-size: 0.9em; +} + +.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; +} + +.spacer { + height: 100vh; +} + +.spacer-collapsed { + min-width: $icon-width + $offset; + transition: $transition-fast; +} + +.spacer-expanded { + min-width: $sidebar-width; +} + +.importqueuecard { + width: 97%; + margin-left: $offset; + margin-right: $offset; + min-height: 20rem; +} + +.card-body { + width: 100%; + margin-left: auto; + margin-right: auto; +} + +.searchandfilter { + position: relative; + margin-bottom: $xx-large-offset; +} + +.filters { + position: absolute; + right: 0; +} + +.filters button { + margin-right: $small-offset; +} + +.table td, +.table th { + border-top: 0 !important; + text-align: left; + padding: $small-offset !important; +} + +.searchgroup { + position: absolute; + left: 0; + width: 45%; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Importqueuedetail.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,275 @@ +<template> + <div class="entry d-flex flex-column py-1 border-bottom w-50"> + <div class="d-flex flex-row position-relative"> + <div @click="showDetails(job.id)" class="jobid ml-2 mt-2 mr-2"> + {{ job.id }} + </div> + <div @click="showDetails(job.id)" class="enqueued mt-2 mr-2"> + {{ formatDate(job.enqueued) }} + </div> + <div @click="showDetails(job.id)" class="kind mt-2 mr-2"> + {{ job.kind }} + </div> + <div @click="showDetails(job.id)" class="user mt-2 mr-2"> + {{ job.user }} + </div> + <div @click="showDetails(job.id)" class="signer mt-2 mr-2"> + {{ job.signer }} + </div> + <div @click="showDetails(job.id)" class="state mt-2 mr-2"> + {{ job.state }} + </div> + <div + @click="showDetails(job.id)" + class="btn btn-sm h-100 rounded-0 btn-info detailsbutton" + > + <font-awesome-icon + v-if="show" + icon="angle-up" + fixed-width + ></font-awesome-icon> + <font-awesome-icon + v-else + 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="first pb-0"> + <small class="condensed"><translate>Kind</translate></small> + </th> + <th class="second 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="third 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="first"> + <span class="condensed">{{ entry.kind }}</span> + </td> + <td class="second"> + <span class="condensed">{{ formatDate(entry.time) }}</span> + </td> + <td class="third"> + <span class="condensed">{{ entry.message }}</span> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { HTTP } from "../lib/http.js"; +import { displayError } from "../lib/errors.js"; +import locale2 from "locale2"; + +export default { + name: "importqueuedetail", + props: ["job"], + data() { + return { + show: false, + entries: [], + sortAsc: true + }; + }, + methods: { + formatDate(date) { + return date + ? new Date(date).toLocaleDateString(locale2, { + day: "2-digit", + month: "2-digit", + year: "numeric" + }) + : ""; + }, + showDetails(id) { + if (this.show) { + this.show = false; + return; + } + if (this.entries.length === 0) { + HTTP.get("/imports/" + id, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + const { entries } = response.data; + this.entries = entries; + this.show = true; + }) + .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; +} + +.entry:hover { + background-color: #f0f0f0; + transition: 1s; +} + +.detailstable { + margin-left: $offset; + margin-right: $large-offset; +} + +.detailsbutton { + position: absolute; + top: 0; + right: 0; + height: 100%; +} +.jobid { + width: 80px; +} + +.enqueued { + width: 120px; +} + +.user { + width: 80px; +} + +.signer { + width: 80px; +} + +.kind { + width: 80px; +} + +.state { + width: 80px; +} + +.details { + width: 50%; +} + +.detailsrow { + line-height: 0.1em; +} + +.first { + width: 65px; + padding-left: 0px; + border-top: 0px; + padding-bottom: $small-offset; +} + +.second { + width: 100px; + padding-left: 0px; + border-top: 0px; + padding-bottom: $small-offset; +} + +.third { + width: 600px; + padding-left: 0px; + border-top: 0px; + padding-bottom: $small-offset; +} + +thead, +tbody { + display: block; +} + +tbody { + height: 150px; + overflow-y: auto; + overflow-x: hidden; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Logs.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,198 @@ +<template> + <div class="main d-flex flex-column"> + <div class="d-flex flex-row"> + <div :class="spacer"></div> + <div class="card logs shadow-xs mt-3 mr-3"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon class="mr-2 fa-fw" icon="book"></font-awesome-icon> + <translate class="headline">Logs</translate> + </h6> + <div class="logoutput text-left bg-white"> + <pre id="code" v-highlightjs="logs"> + <code class="bash hljs hljs-string"></code> + </pre> + </div> + <div class="logmenu"> + <div class="d-flex align-self-center"> + <ul class="nav nav-pills"> + <li class="nav-item"> + <a + :class="accesslogStyle" + @click="fetch('system/log/apache2/access.log', 'accesslog')" + href="#" + > + <translate>Accesslog</translate> + </a> + </li> + <li class="nav-item"> + <a + :class="errorlogStyle" + @click="fetch('system/log/apache2/error.log', 'errorlog')" + href="#" + > + <translate>Errorlog</translate> + </a> + </li> + </ul> + </div> + <div class="statuscontainer d-flex flex-row mb-3"> + <div class="statusline align-self-center"> + <h3><translate>Last refresh:</translate> {{ refreshed }}</h3> + </div> + <div class="refresh"> + <button + @click="fetch(currentFile, currentLog)" + class="btn btn-dark" + > + <translate>Refresh</translate> + </button> + </div> + </div> + </div> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.statuscontainer { + width: 87%; + position: relative; +} + +.logmenu { + position: relative; + margin-left: $offset; + margin-top: $offset; +} + +.logs { + height: 85vh; +} + +#code { + overflow: auto; +} + +.refresh { + position: absolute; + right: $offset; + bottom: 0; +} + +.logoutput { + margin-left: $offset; + margin-right: $offset; + margin-top: $offset; + height: 90%; + overflow: auto; + transition: $transition-fast; +} + +.spacer { + height: 90vh; +} + +.spacer-collapsed { + min-width: $icon-width + $offset; + transition: $transition-fast; +} + +.spacer-expanded { + min-width: $sidebar-width + $offset; +} + +.statusline { + position: absolute; + right: 0; + margin-right: 9rem; + bottom: -0.5rem; +} + +.statuscontainer { + width: 100%; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { mapState } from "vuex"; +import { HTTP } from "../lib/http.js"; +import "../../node_modules/highlight.js/styles/paraiso-dark.css"; +import Vue from "vue"; +import VueHighlightJS from "vue-highlightjs"; +Vue.use(VueHighlightJS); + +const ACCESSLOG = "accesslog"; +const ERRORLOG = "errorlog"; + +export default { + name: "logs", + mounted() { + this.fetch("system/log/apache2/access.log", ACCESSLOG); + }, + data() { + return { + logs: null, + currentLog: null, + currentFile: null, + refreshed: null + }; + }, + methods: { + fetch(file, type) { + HTTP.get(file, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.logs = response.data.content; + this.currentLog = type; + this.refreshed = new Date().toLocaleString(); + this.currentFile = file; + }) + .catch(); + }, + disallow(e) { + e.target.blur(); + } + }, + computed: { + ...mapState("application", ["showSidebar"]), + accesslogStyle() { + return { + active: this.currentLog == ACCESSLOG, + "nav-link": true + }; + }, + errorlogStyle() { + return { + active: this.currentLog == ERRORLOG, + "nav-link": true + }; + }, + spacer() { + return [ + "spacer ml-3", + { + "spacer-expanded": this.showSidebar, + "spacer-collapsed": !this.showSidebar + } + ]; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Main.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,33 @@ +<template> + <div class="main d-flex flex-column"> + <Maplayer></Maplayer> + <FairwayProfile></FairwayProfile> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import Maplayer from "./Maplayer"; +import FairwayProfile from "./fairway/Fairwayprofile"; + +export default { + name: "mainview", + components: { + Maplayer, + FairwayProfile + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Maplayer.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,383 @@ +<template> + <div id="map" :class="mapStyle"></div> +</template> + +<style lang="scss" scoped> +.nocursor { + cursor: none; +} + +.mapsplit { + height: 50vh; +} + +.mapfull { + height: 100vh; +} + +// the following css part is for browser-printing based pdf generation +@page { + size: A4 landscape !important; + margin: 4mm !important; + // according to https://www.w3.org/TR/css-page-3/#page-size-prop + // we shall now have 210 - 2*4 = 202 mm width and 297 - 2*4 = 289 mm height +} + +@media print { + .mapfull { + width: 2000px; + height: 2828px; + } + .mapsplit { + width: 2000px; + height: 2828px; + } +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * * Thomas Junk <thomas.junk@intevation.de> + * * Bernhard E. Reiter <bernhard.reiter@intevation.de> + */ +import { HTTP } from "../lib/http"; +import { mapGetters, mapState } from "vuex"; +import "ol/ol.css"; +import { Map, View } from "ol"; +import { WFS, GeoJSON } from "ol/format.js"; +import { Stroke, Style, Fill } from "ol/style.js"; + +/* for the sake of debugging */ +/* eslint-disable no-console */ +export default { + name: "maplayer", + data() { + return { + projection: "EPSG:3857" + }; + }, + computed: { + ...mapGetters("map", ["getLayerByName", "getVSourceByName"]), + ...mapState("map", [ + "extent", + "layers", + "openLayersMap", + "lineTool", + "polygonTool", + "cutTool" + ]), + ...mapState("bottlenecks", ["selectedSurvey"]), + ...mapState("application", ["showSplitscreen"]), + mapStyle() { + return { + mapfull: !this.showSplitscreen, + mapsplit: this.showSplitscreen, + nocursor: this.hasActiveInteractions + }; + }, + hasActiveInteractions() { + return ( + (this.lineTool && this.lineTool.getActive()) || + (this.polygonTool && this.polygonTool.getActive()) || + (this.cutTool && this.cutTool.getActive()) + ); + } + }, + methods: { + buildVectorLoader(featureRequestOptions, endpoint, vectorSource) { + // build a function to be used for VectorSource.setLoader() + // make use of WFS().writeGetFeature to build the request + // and use our HTTP library to actually do it + // NOTE: a) the geometryName has to be given in featureRequestOptions, + // because we want to load depending on the bbox + // b) the VectorSource has to have the option strategy: bbox + featureRequestOptions["outputFormat"] = "application/json"; + var loader = function(extent, resolution, projection) { + featureRequestOptions["bbox"] = extent; + featureRequestOptions["srsName"] = projection.getCode(); + var featureRequest = new WFS().writeGetFeature(featureRequestOptions); + // DEBUG console.log(featureRequest); + HTTP.post( + endpoint, + new XMLSerializer().serializeToString(featureRequest), + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + } + ) + .then(response => { + var features = new GeoJSON().readFeatures( + JSON.stringify(response.data) + ); + vectorSource.addFeatures(features); + // console.log( + // "loaded", + // features.length, + // featureRequestOptions.featureTypes, + // "features" + // ); + // DEBUG console.log("loaded ", features, "for", vectorSource); + // eslint-disable-next-line + }) + .catch(() => { + vectorSource.removeLoadedExtent(extent); + }); + }; + return loader; + }, + updateBottleneckFilter(bottleneck_id, datestr) { + console.log("updating filter with", bottleneck_id, datestr); + const layer = this.getLayerByName("Bottleneck isolines"); + const wmsSrc = layer.data.getSource(); + const exists = bottleneck_id != "does_not_exist"; + + if (exists) { + wmsSrc.updateParams({ + cql_filter: + "date_info='" + + datestr + + "' AND bottleneck_id='" + + bottleneck_id + + "'" + }); + } + layer.isVisible = exists; + layer.data.setVisible(exists); + }, + onBeforePrint(/* evt */) { + // console.log("onBeforePrint(", evt ,")"); + // + // the following code shows how to get the current map canvas + // and change it, however this does not work well enough, as + // another mechanism seems to update the size again before the rendering + // for printing is done: + // console.log(this.openLayersMap.getViewport()); + // var canvas = this.openLayersMap.getViewport().getElementsByTagName("canvas")[0]; + // console.log(canvas); + // canvas.width=1000; + // canvas.height=1414; + // + // An experiment which also did not work: + // this.openLayersMap.setSize([1000, 1414]); // estimate portait DIN A4 + // + // according to documentation + // http://openlayers.org/en/latest/apidoc/module-ol_PluggableMap-PluggableMap.html#updateSize + // "Force a recalculation of the map viewport size. This should be called when third-party code changes the size of the map viewport." + // but did not help + // this.openLayersMap.updateSize(); + }, + onAfterPrint(/* evt */) { + // could be used to undo changes that have been done for printing + // though https://www.tjvantoll.com/2012/06/15/detecting-print-requests-with-javascript/ + // reported that this was not feasable (back then). + // console.log("onAfterPrint(", evt, ")"); + } + }, + watch: { + showSplitscreen() { + const map = this.openLayersMap; + this.$nextTick(() => { + map && map.updateSize(); + }); + }, + selectedSurvey(newSelectedSurvey) { + if (newSelectedSurvey) { + this.updateBottleneckFilter( + newSelectedSurvey.bottleneck_id, + newSelectedSurvey.date_info + ); + } else { + this.updateBottleneckFilter("does_not_exist", "1999-10-01"); + } + } + }, + mounted() { + let map = new Map({ + layers: [...this.layers.map(x => x.data)], + target: "map", + controls: [], + view: new View({ + center: [this.extent.lon, this.extent.lat], + zoom: this.extent.zoom, + projection: this.projection + }) + }); + map.on("moveend", event => { + const center = event.map.getView().getCenter(); + this.$store.commit("map/extent", { + lat: center[1], + lon: center[0], + zoom: event.map.getView().getZoom() + }); + }); + this.$store.dispatch("map/openLayersMap", map); + + // TODO make display of layers more dynamic, e.g. from a list + + // loading the full WFS layer, by not setting the loader function + // and without bboxStrategy + var featureRequest = new WFS().writeGetFeature({ + srsName: "EPSG:3857", + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["fairway_dimensions"], + outputFormat: "application/json" + }); + + // NOTE: loading the full fairway_dimensions makes sure + // that all are available for the intersection with the profile + HTTP.post( + "/internal/wfs", + new XMLSerializer().serializeToString(featureRequest), + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + } + ).then(response => { + this.getVSourceByName("Fairway Dimensions").addFeatures( + new GeoJSON().readFeatures(JSON.stringify(response.data)) + ); + // would scale to the extend of all resulting features + // this.openLayersMap.getView().fit(vectorSrc.getExtent()); + }); + + // load following layers with bboxStrategy (using our request builder) + var layer = null; + + layer = this.getLayerByName("Waterway Area"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featurePrefix: "ws-wamos", + featureTypes: ["ienc_wtware"], + geometryName: "geom" + }, + "/external/d4d", + layer.data.getSource() + ) + ); + + layer = this.getLayerByName("Waterway Axis"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featurePrefix: "ws-wamos", + featureTypes: ["ienc_wtwaxs"], + geometryName: "geom" + }, + "/external/d4d", + layer.data.getSource() + ) + ); + + layer = this.getLayerByName("Distance marks"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featurePrefix: "ws-wamos", + featureTypes: ["ienc_dismar"], + geometryName: "geom" //, + /* restrict loading approximately to extend of danube in Austria */ + // filter: bboxFilter("geom", [13.3, 48.0, 17.1, 48.6], "EPSG:4326") + }, + "/external/d4d", + layer.data.getSource() + ) + ); + layer.data.setVisible(layer.isVisible); + + layer = this.getLayerByName("Distance marks, Axis"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["distance_marks_geoserver"], + geometryName: "geom" + }, + "/internal/wfs", + layer.data.getSource() + ) + ); + + layer = this.getLayerByName("Waterway Area, named"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["hydro_seaare"], + geometryName: "geom" + }, + "/external/d4d", + layer.data.getSource() + ) + ); + layer.data.setVisible(layer.isVisible); + + layer = this.getLayerByName("Bottlenecks"); + layer.data.getSource().setLoader( + this.buildVectorLoader( + { + featureNS: "gemma", + featurePrefix: "gemma", + featureTypes: ["bottlenecks"], + geometryName: "area" + }, + "/internal/wfs", + layer.data.getSource() + ) + ); + HTTP.get("/system/style/Bottlenecks/stroke", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.btlnStrokeC = response.data.code; + HTTP.get("/system/style/Bottlenecks/fill", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.btlnFillC = response.data.code; + var newstyle = new Style({ + stroke: new Stroke({ + color: this.btlnStrokeC, + width: 4 + }), + fill: new Fill({ + color: this.btlnFillC + }) + }); + layer.data.setStyle(newstyle); + }) + .catch(error => { + console.log(error); + }); + }) + .catch(error => { + console.log(error); + }); + + window.addEventListener("beforeprint", this.onBeforePrint); + window.addEventListener("afterprint", this.onAfterPrint); + + // so none is shown + this.updateBottleneckFilter("does_not_exist", "1999-10-01"); + this.$store.dispatch("map/enableIdentifyTool"); + this.$store.dispatch("bottlenecks/loadBottlenecks"); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Pdftool.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,110 @@ +<template> + <div + :class="[ + 'box ui-element rounded bg-white text-nowrap', + { expanded: showPdfTool } + ]" + > + <div style="width: 20rem"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="file-pdf" class="mr-2"></font-awesome-icon + ><translate>Generate PDF</translate> + <font-awesome-icon + icon="times" + class="ml-auto text-muted" + @click="$store.commit('application/showPdfTool', false)" + ></font-awesome-icon> + </h6> + <div class="p-3"> + <b><translate>Chose format:</translate></b> + <select v-model="form.format" class="form-control d-block w-100"> + <option><translate>landscape</translate></option> + <option><translate>portrait</translate></option> + </select> + <small class="d-block my-2"> + <input + type="radio" + id="pdfexport-downloadtype-download" + value="download" + v-model="form.downloadType" + selected + /> + <label for="pdfexport-downloadtype-download" class="ml-1 mr-2" + ><translate>Download</translate></label + > + <input + type="radio" + id="pdfexport-downloadtype-open" + value="open" + v-model="form.downloadType" + /> + <label for="pdfexport-downloadtype-open" class="ml-1" + ><translate>Open in new window</translate></label + > + </small> + <button + @click="download" + type="button" + class="btn btn-sm btn-info d-block w-100" + > + <translate>Generate PDF</translate> + </button> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; +//import { HTTP } from "../application/lib/http"; + +export default { + name: "pdftool", + data() { + return { + form: { + format: "landscape", + downloadType: "download" + } + }; + }, + computed: { + ...mapState("application", ["showPdfTool"]), + ...mapState("bottlenecks", ["selectedSurvey"]) + }, + methods: { + download() { + // generate PDF and open it + // TODO: replace this src with an API reponse after actually generating PDFs + let src = + this.form.format === "landscape" + ? "/img/PrintTemplate-Var2-Landscape.pdf" + : "/img/PrintTemplate-Var2-Portrait.pdf"; + + let a = document.createElement("a"); + a.href = src; + + if (this.form.downloadType === "download") + a.download = src.substr(src.lastIndexOf("/") + 1); + else a.target = "_blank"; + + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Search.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,292 @@ +<template> + <div :class="searchbarContainerStyle"> + <div class="input-group-prepend m-0 d-print-none"> + <span @click="toggleSearchbar" :class="searchButtonStyle" for="search"> + <font-awesome-icon icon="search"></font-awesome-icon> + </span> + </div> + <div + :class="[ + 'searchgroup', + { + 'searchgroup-collapsed': !showSearchbar, + big: + showContextBox && + ['bottlenecks', 'staging'].indexOf(contextBoxContent) !== -1 + } + ]" + > + <input + @keyup.enter="takeFirstSearchresult" + id="search" + v-model="searchQuery" + type="text" + :class="searchInputStyle" + /> + </div> + <div + v-if="showSearchbar && searchResults !== null && !showContextBox" + class="searchresults border-top ui-element bg-white rounded-bottom d-print-none position-absolute" + > + <div + v-for="entry of searchResults" + :key="entry.name" + class="border-top text-left" + > + <a + href="#" + @click.prevent="moveToSearchResult(entry)" + class="p-2 d-block text-nowrap" + > + <font-awesome-icon + icon="ship" + v-if="entry.type === 'bottleneck'" + class="mr-1" + fixed-width + /> + <font-awesome-icon + icon="water" + v-if="entry.type === 'rhm'" + class="mr-1" + fixed-width + /> + <font-awesome-icon + icon="city" + v-if="entry.type === 'city'" + class="mr-1" + fixed-width + /> + {{ entry.name }} + </a> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.searchcontainer { + opacity: 0.96; +} + +.searchcontainer .searchbar { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.searchgroup { + margin-left: -3px; + transition: width 0.3s; + width: 300px; + overflow: hidden; +} + +.searchgroup.big { + width: 571px; +} + +.searchgroup-collapsed { + width: 0; +} + +.searchbar { + height: 2rem !important; + box-shadow: none !important; +} + +.searchbar.rounded-top-right { + border-radius: 0 !important; + border-top-right-radius: 0.25rem !important; +} + +.searchlabel.rounded-top-left { + border-radius: 0 !important; + border-top-left-radius: 0.25rem !important; +} + +.input-group-text { + height: 2rem; + width: 2rem; +} + +.input-group-prepend svg path { + fill: #666; +} + +.searchresults { + box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2); + top: 2rem; + left: 0; + right: 0; + max-height: 24rem; + overflow: auto; +} + +.searchresults > div:first-child { + border-top: 0 !important; +} + +.searchresults a { + text-decoration: none; +} + +.searchresults a:hover { + background: #f8f8f8; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import debounce from "lodash.debounce"; +import { mapState } from "vuex"; + +import { displayError } from "../lib/errors.js"; +import { HTTP } from "../lib/http"; + +const setFocus = () => document.querySelector("#search").focus(); + +export default { + name: "search", + data() { + return { + searchQueryIsDirty: false, + searchResults: null, + isSearching: false + }; + }, + computed: { + ...mapState("application", [ + "showSearchbar", + "showContextBox", + "contextBoxContent" + ]), + searchQuery: { + get() { + return this.$store.state.application.searchQuery; + }, + set(value) { + this.$store.commit("application/searchQuery", value); + } + }, + searchIndicator: function() { + if (this.isSearching) { + return "⟳"; + } else if (this.searchQueryIsDirty) { + return ""; + } else { + return "✓"; + } + }, + searchbarContainerStyle() { + return [ + "input-group searchcontainer shadow-xs", + { + "d-flex": this.contextBoxContent !== "imports", + "d-none": this.contextBoxContent === "imports" && this.showContextBox + } + ]; + }, + searchInputStyle() { + return [ + "form-control ui-element search searchbar d-print-none border-0", + { "rounded-top-right": this.showContextBox || this.searchResults } + ]; + }, + searchButtonStyle() { + return [ + "ui-element input-group-text p-0 d-flex border-0 justify-content-center searchlabel bg-white d-print-none", + { + rounded: !this.showSearchbar, + "rounded-left": this.showSearchbar, + "rounded-top-left": + this.showSearchbar && (this.showContextBox || this.searchResults) + } + ]; + } + }, + watch: { + searchQuery: function() { + this.searchQueryIsDirty = true; + this.triggerSearch(); + } + }, + methods: { + takeFirstSearchresult() { + if (!this.searchResults || this.searchResults.length != 1) return; + this.moveToSearchResult(this.searchResults[0]); + }, + triggerSearch: debounce(function() { + this.doSearch(); + }, 500), + doSearch() { + this.isCalculating = true; + this.searchResults = null; + + if (this.searchQuery == "") { + return; + } + + HTTP.post( + "/search", + { string: this.searchQuery }, + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + } + ) + .then(response => { + // console.log("got:", response.data); + this.searchResults = response.data; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + + this.isCalculating = false; + this.searchQueryIsDirty = false; + }, + moveToSearchResult(resultEntry) { + // DEBUG console.log("Moving to", resultEntry); + if (resultEntry.geom.type == "Point") { + let zoom = 11; + if (resultEntry.type === "bottleneck") zoom = 17; + if (resultEntry.type === "rhm") zoom = 15; + if (resultEntry.type === "city") zoom = 13; + + this.$store.commit("map/moveMap", { + coordinates: resultEntry.geom.coordinates, + zoom, + preventZoomOut: true + }); + } + // this.searchQuery = ""; // clear search query again + this.toggleSearchbar(); + }, + toggleSearchbar() { + if (!this.showContextBox) { + if (!this.showSearchbar) { + setTimeout(setFocus, 300); + } + this.$store.commit("application/showSearchbar", !this.showSearchbar); + } + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Staging.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,236 @@ +<template> + <div class="w-90 stagingcard"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon + class="mr-2" + icon="clipboard-check" + ></font-awesome-icon> + <translate>Staging Area</translate> + </h6> + <table class="table"> + <thead> + <tr> + <th><translate>Name</translate></th> + <th><translate>Type</translate></th> + <th><translate>Date</translate></th> + <th><translate>Imported</translate></th> + <th><translate>Username</translate></th> + <th> </th> + <th> </th> + </tr> + </thead> + <tbody v-if="filteredData.length"> + <tr :key="data.id" v-for="data in filteredData"> + <td> + <a @click="zoomTo(data.id)" href="#">{{ + data.summary.bottleneck + }}</a> + </td> + <td>{{ data.kind.toUpperCase() }}</td> + <td>{{ formatSurveyDate(data.summary.date) }}</td> + <td>{{ formatSurveyDate(data.enqueued.split("T")[0]) }}</td> + <td>{{ data.user }}</td> + <td> + <button + :class="{ + btn: true, + 'btn-sm': true, + 'btn-outline-success': needsApproval(data) || isRejected(data), + 'btn-success': isApproved(data) + }" + @click="toggleApproval(data.id, $options.STATES.APPROVED)" + > + <font-awesome-icon icon="check"></font-awesome-icon> + </button> + </td> + <td> + <button + :class="{ + btn: true, + 'btn-sm': true, + 'btn-outline-danger': needsApproval(data) || isApproved(data), + 'btn-danger': isRejected(data) + }" + @click="toggleApproval(data.id, $options.STATES.REJECTED)" + > + <font-awesome-icon icon="times"></font-awesome-icon> + </button> + </td> + </tr> + </tbody> + <tbody v-else> + <tr> + <td class="text-center" colspan="6"> + <translate>No results.</translate> + </td> + </tr> + </tbody> + </table> + <div class="p-3" v-if="filteredData.length"> + <button @click="confirmReview" class="confirm-button btn btn-info"> + <translate>Confirm</translate> + </button> + </div> + <div class="p-3"> + <button @click="loadData" class="refresh btn btn-dark">Refresh</button> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus@intevation.de> + */ +import { mapState } from "vuex"; +import { HTTP } from "../lib/http.js"; +import { STATES } from "../store/imports.js"; +import { displayError, displayInfo } from "../lib/errors.js"; +import { formatSurveyDate } from "../lib/date.js"; + +export default { + data() { + return {}; + }, + mounted() { + this.loadData(); + }, + computed: { + ...mapState("application", ["searchQuery"]), + ...mapState("imports", ["staging"]), + filteredData() { + return this.staging.filter(data => { + const result = [data.id + "", data.enqueued, data.kind, data.user].some( + x => x.toLowerCase().includes(this.searchQuery.toLowerCase()) + ); + return result; + }); + } + }, + STATES: STATES, + methods: { + formatSurveyDate(date) { + return formatSurveyDate(date); + }, + loadData() { + this.$store.dispatch("imports/getStaging").catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }, + confirmReview() { + const reviewResults = this.staging + .filter(x => x.status !== STATES.NEEDSAPPROVAL) + .map(r => { + return { + id: r.id, + state: r.status + }; + }); + if (!reviewResults.length) return; + HTTP.patch("/imports", reviewResults, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + const messages = response.data + .map(x => { + if (x.message) return x.message; + if (x.error) return x.error; + }) + .join("\n\n"); + displayInfo({ + title: "Staging Area", + message: messages, + options: { + timeout: 0, + buttons: [{ text: "Ok", action: null, bold: true }] + } + }); + this.loadData(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }, + needsApproval(item) { + return item.status === STATES.NEEDSAPPROVAL; + }, + isRejected(item) { + return item.status === STATES.REJECTED; + }, + isApproved(item) { + return item.status === STATES.APPROVED; + }, + zoomTo(id) { + if (!id) return; + const soundingResult = this.filteredData.filter(x => x.id == id)[0]; + const { lat, lon, bottleneck, date } = soundingResult.summary; + const coordinates = [lat, lon]; + + this.$store.commit("map/moveMap", { + coordinates: coordinates, + zoom: 17, + preventZoomOut: true + }); + this.$store + .dispatch("bottlenecks/setSelectedBottleneck", bottleneck) + .then(() => { + this.$store.commit("bottlenecks/setSelectedSurveyByDate", date); + }); + }, + toggleApproval(id, newStatus) { + this.$store.commit("imports/toggleApproval", { + id: id, + newStatus: newStatus + }); + } + } +}; +</script> + +<style lang="scss" scoped> +.refresh { + position: absolute; + left: $offset; + bottom: $offset; +} +.table th, +td { + font-size: 0.9rem; + border-top: 0px !important; + border-bottom-width: 1px; + text-align: left; + padding: 0.5rem !important; +} + +.stagingcard { + position: relative; + min-height: 150px; +} + +.confirm-button { + position: absolute; + right: $offset; + bottom: $offset; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Systemconfiguration.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,191 @@ +<template> + <div class="d-flex flex-row"> + <div :class="spacerStyle"></div> + <div class="card sysconfig mt-3 shadow-xs"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon icon="wrench" class="mr-2"></font-awesome-icon> + <translate class="headline">Systemconfiguration</translate> + </h6> + <div class="card-body config"> + <section class="configsection"> + <h4 class="card-title"> + <translate>Bottleneck Areas stroke-color</translate> + </h4> + <compact-picker v-model="strokeColor" /> + </section> + <section> + <h4 class="card-title"> + <translate>Bottleneck Areas fill-color</translate> + </h4> + <chrome-picker v-model="fillColor" /> + </section> + <div class="sendbutton"> + <a @click.prevent="submit" class="btn btn-info text-white"> + <translate>Send</translate> + </a> + </div> + </div> + <!-- card-body --> + </div> + </div> +</template> + +<style scoped lang="scss"> +.config { + text-align: left; +} + +.configsection { + margin-bottom: $large-offset; +} + +.sendbutton { + position: absolute; + right: $offset; + bottom: $offset; +} + +.inputs { + margin-left: auto; + margin-right: auto; +} + +.sysconfig { + margin-right: $offset; + width: 100%; + height: 100%; +} + +.spacer { + height: 100vh; +} + +.spacer-collapsed { + min-width: $icon-width + $offset; + transition: $transition-fast; +} + +.spacer-expanded { + min-width: $sidebar-width + $offset; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Bernhard Reiter <bernhard@intevation.de> + */ +import { Chrome } from "vue-color"; +import { Compact } from "vue-color"; + +import { HTTP } from "../lib/http"; +import { displayError } from "../lib/errors.js"; +import { mapState } from "vuex"; +export default { + name: "systemconfiguration", + data() { + return { + sent: false, + strokeColor: { r: 0, g: 0, b: 0, a: 1.0 }, + fillColor: { r: 0, g: 0, b: 0, a: 1.0 }, + currentConfig: null + }; + }, + components: { + "chrome-picker": Chrome, + "compact-picker": Compact + }, + computed: { + ...mapState("application", ["showSidebar"]), + spacerStyle() { + return [ + "spacer ml-3", + { + "spacer-expanded": this.showSidebar, + "spacer-collapsed": !this.showSidebar + } + ]; + } + }, + methods: { + submit() { + HTTP.put("/system/style/Bottlenecks/stroke", this.strokeColor.rgba, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then() + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + + HTTP.put("/system/style/Bottlenecks/fill", this.fillColor.rgba, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then() + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + mounted() { + HTTP.get("/system/style/Bottlenecks/stroke", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + this.strokeColor = response.data.colour; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + + HTTP.get("/system/style/Bottlenecks/fill", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "application/json" + } + }) + .then(response => { + this.fillColor = response.data.colour; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Zoom.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,76 @@ +<template> + <div + class="d-flex buttoncontainer shadow-xs mb-3 position-absolute" + :style="showSplitscreen ? 'margin-bottom: 51vh !important' : ''" + > + <button + class="zoomButton border-0 bg-white rounded-left ui-element" + @click="zoomOut" + > + <font-awesome-icon icon="minus"></font-awesome-icon> + </button> + <button + class="zoomButton border-0 bg-white rounded-right ui-element border-right" + @click="zoomIn" + > + <font-awesome-icon icon="plus"></font-awesome-icon> + </button> + </div> +</template> + +<style lang="scss" scoped> +.buttoncontainer { + bottom: 0; + left: 50%; + margin-left: -$icon-width; +} + +.zoomButton { + min-height: $icon-width; + min-width: $icon-width; + z-index: 1; + outline: none; + color: #666; +} +</style> +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus@intevation.de> + * Thomas Junk <thomas.junk@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "zoom", + computed: { + ...mapState("map", ["openLayersMap"]), + ...mapState("application", ["showSplitscreen"]), + zoomLevel: { + get() { + return this.openLayersMap.getView().getZoom(); + }, + set(value) { + this.openLayersMap.getView().animate({ zoom: value, duration: 300 }); + } + } + }, + methods: { + zoomIn() { + this.zoomLevel = this.zoomLevel + 1; + }, + zoomOut() { + this.zoomLevel = this.zoomLevel - 1; + } + } +}; +</script>
--- a/client/src/components/admin/Importqueue.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,362 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <div :class="spacerStyle"></div> - <div class="mt-3 importqueuecard flex-grow-1"> - <div class="card shadow-xs"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon icon="tasks" class="mr-2"></font-awesome-icon> - <translate class="headline">Importqueue</translate> - </h6> - <div class="card-body importcardbody"> - <div class="card-body importcardbody"> - <div class="searchandfilter d-flex flex-row"> - <div class="searchgroup input-group"> - <div class="input-group-prepend"> - <span class="input-group-text" id="search"> - <font-awesome-icon icon="search"></font-awesome-icon> - </span> - </div> - <input - v-model="searchQuery" - type="text" - class="form-control" - placeholder - aria-label="Search" - aria-describedby="search" - /> - </div> - <div class="filters"> - <button - @click="setFilter('successful')" - :class="successfulStyle" - > - <translate>Successful</translate> - </button> - <button @click="setFilter('failed')" :class="failedStyle"> - <translate>Failed</translate> - </button> - <button @click="setFilter('pending')" :class="pendingStyle"> - <translate>Pending</translate> - </button> - <button @click="setFilter('rejected')" :class="rejectedStyle"> - <translate>Rejected</translate> - </button> - <button @click="setFilter('accepted')" :class="acceptedStyle"> - <translate>Accepted</translate> - </button> - </div> - </div> - <div class="text-left d-flex flex-row w-50 border-bottom"> - <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="text-left" v-for="job in filteredImports" :key="job.id"> - <Importqueuedetail :job="job"></Importqueuedetail> - </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"; -import Importqueuedetail from "./Importqueuedetail"; - -export default { - name: "importqueue", - components: { - Importqueuedetail - }, - data() { - return { - searchQuery: "", - successful: false, - failed: false, - pending: false, - rejected: false, - accepted: false - }; - }, - mounted() { - this.loadQueue(); - }, - methods: { - setFilter(name) { - this[name] = !this[name]; - const allSet = - this.successful && - this.failed && - this.pending && - this.accepted && - this.rejected; - if (allSet) { - this.successful = false; - this.failed = false; - this.pending = false; - this.accepted = false; - this.rejected = false; - } - }, - loadQueue() { - this.$store.dispatch("imports/getImports").catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - refresh() { - this.loadQueue(); - }, - showDetails(id) { - HTTP.get("/imports/" + id, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - const { entries } = response.data; - this.entries = entries; - this.$modal.show("details"); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - close() { - this.$modal.hide("details"); - } - }, - computed: { - ...mapState("imports", ["imports"]), - ...mapState("application", ["showSidebar"]), - sortIcon() { - return this.sortAsc ? "sort-amount-down" : "sort-amount-up"; - }, - filteredImports() { - const filtered = this.imports - .filter(element => { - if (!this.searchQuery) return true; - return [(element.kind, element.user, element.enqueued)].some(x => { - return x.toLowerCase().includes(this.searchQuery.toLowerCase()); - }); - }) - .filter(y => { - if ( - !this.successful && - !this.failed && - !this.pending && - !this.accepted && - !this.rejected - ) - return true; - let filterCriteria = []; - if (this.successful) filterCriteria.push("successful"); - if (this.failed) filterCriteria.push("failed"); - if (this.pending) filterCriteria.push("pending"); - if (this.accepted) filterCriteria.push("accepted"); - if (this.rejected) filterCriteria.push("rejected"); - const result = filterCriteria.map(selectedState => { - return y.state === selectedState; - }); - return result.some(x => x); - }); - return filtered; - }, - spacerStyle() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - }, - successfulStyle() { - return { - btn: true, - "btn-light": !this.successful, - "btn-dark": this.successful - }; - }, - pendingStyle() { - return { - btn: true, - "btn-light": !this.pending, - "btn-dark": this.pending - }; - }, - failedStyle() { - return { - btn: true, - "btn-light": !this.failed, - "btn-dark": this.failed - }; - }, - rejectedStyle() { - return { - btn: true, - "btn-light": !this.rejected, - "btn-dark": this.rejected - }; - }, - acceptedStyle() { - return { - btn: true, - "btn-light": !this.accepted, - "btn-dark": this.accepted - }; - } - } -}; -</script> - -<style lang="scss" scoped> -.jobid { - width: 80px; -} - -.enqueued { - width: 120px; -} - -.user { - width: 80px; -} - -.signer { - width: 80px; -} - -.kind { - width: 80px; -} - -.state { - width: 80px; -} - -.header { - font-weight: bold; - font-size: 0.9em; -} - -.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; -} - -.spacer { - height: 100vh; -} - -.spacer-collapsed { - min-width: $icon-width + $offset; - transition: $transition-fast; -} - -.spacer-expanded { - min-width: $sidebar-width; -} - -.importqueuecard { - width: 97%; - margin-left: $offset; - 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/admin/Importqueuedetail.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,275 +0,0 @@ -<template> - <div class="entry d-flex flex-column py-1 border-bottom w-50"> - <div class="d-flex flex-row position-relative"> - <div @click="showDetails(job.id)" class="jobid ml-2 mt-2 mr-2"> - {{ job.id }} - </div> - <div @click="showDetails(job.id)" class="enqueued mt-2 mr-2"> - {{ formatDate(job.enqueued) }} - </div> - <div @click="showDetails(job.id)" class="kind mt-2 mr-2"> - {{ job.kind }} - </div> - <div @click="showDetails(job.id)" class="user mt-2 mr-2"> - {{ job.user }} - </div> - <div @click="showDetails(job.id)" class="signer mt-2 mr-2"> - {{ job.signer }} - </div> - <div @click="showDetails(job.id)" class="state mt-2 mr-2"> - {{ job.state }} - </div> - <div - @click="showDetails(job.id)" - class="btn btn-sm h-100 rounded-0 btn-info detailsbutton" - > - <font-awesome-icon - v-if="show" - icon="angle-up" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - v-else - 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="first pb-0"> - <small class="condensed"><translate>Kind</translate></small> - </th> - <th class="second 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="third 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="first"> - <span class="condensed">{{ entry.kind }}</span> - </td> - <td class="second"> - <span class="condensed">{{ formatDate(entry.time) }}</span> - </td> - <td class="third"> - <span class="condensed">{{ entry.message }}</span> - </td> - </tr> - </tbody> - </table> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import { HTTP } from "../../lib/http.js"; -import { displayError } from "../../lib/errors.js"; -import locale2 from "locale2"; - -export default { - name: "importqueuedetail", - props: ["job"], - data() { - return { - show: false, - entries: [], - sortAsc: true - }; - }, - methods: { - formatDate(date) { - return date - ? new Date(date).toLocaleDateString(locale2, { - day: "2-digit", - month: "2-digit", - year: "numeric" - }) - : ""; - }, - showDetails(id) { - if (this.show) { - this.show = false; - return; - } - if (this.entries.length === 0) { - HTTP.get("/imports/" + id, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - const { entries } = response.data; - this.entries = entries; - this.show = true; - }) - .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; -} - -.entry:hover { - background-color: #f0f0f0; - transition: 1s; -} - -.detailstable { - margin-left: $offset; - margin-right: $large-offset; -} - -.detailsbutton { - position: absolute; - top: 0; - right: 0; - height: 100%; -} -.jobid { - width: 80px; -} - -.enqueued { - width: 120px; -} - -.user { - width: 80px; -} - -.signer { - width: 80px; -} - -.kind { - width: 80px; -} - -.state { - width: 80px; -} - -.details { - width: 50%; -} - -.detailsrow { - line-height: 0.1em; -} - -.first { - width: 65px; - padding-left: 0px; - border-top: 0px; - padding-bottom: $small-offset; -} - -.second { - width: 100px; - padding-left: 0px; - border-top: 0px; - padding-bottom: $small-offset; -} - -.third { - width: 600px; - padding-left: 0px; - border-top: 0px; - padding-bottom: $small-offset; -} - -thead, -tbody { - display: block; -} - -tbody { - height: 150px; - overflow-y: auto; - overflow-x: hidden; -} -</style>
--- a/client/src/components/admin/Logs.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,198 +0,0 @@ -<template> - <div class="main d-flex flex-column"> - <div class="d-flex flex-row"> - <div :class="spacer"></div> - <div class="card logs shadow-xs mt-3 mr-3"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon class="mr-2 fa-fw" icon="book"></font-awesome-icon> - <translate class="headline">Logs</translate> - </h6> - <div class="logoutput text-left bg-white"> - <pre id="code" v-highlightjs="logs"> - <code class="bash hljs hljs-string"></code> - </pre> - </div> - <div class="logmenu"> - <div class="d-flex align-self-center"> - <ul class="nav nav-pills"> - <li class="nav-item"> - <a - :class="accesslogStyle" - @click="fetch('system/log/apache2/access.log', 'accesslog')" - href="#" - > - <translate>Accesslog</translate> - </a> - </li> - <li class="nav-item"> - <a - :class="errorlogStyle" - @click="fetch('system/log/apache2/error.log', 'errorlog')" - href="#" - > - <translate>Errorlog</translate> - </a> - </li> - </ul> - </div> - <div class="statuscontainer d-flex flex-row mb-3"> - <div class="statusline align-self-center"> - <h3><translate>Last refresh:</translate> {{ refreshed }}</h3> - </div> - <div class="refresh"> - <button - @click="fetch(currentFile, currentLog)" - class="btn btn-dark" - > - <translate>Refresh</translate> - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.statuscontainer { - width: 87%; - position: relative; -} - -.logmenu { - position: relative; - margin-left: $offset; - margin-top: $offset; -} - -.logs { - height: 85vh; -} - -#code { - overflow: auto; -} - -.refresh { - position: absolute; - right: $offset; - bottom: 0; -} - -.logoutput { - margin-left: $offset; - margin-right: $offset; - margin-top: $offset; - height: 90%; - overflow: auto; - transition: $transition-fast; -} - -.spacer { - height: 90vh; -} - -.spacer-collapsed { - min-width: $icon-width + $offset; - transition: $transition-fast; -} - -.spacer-expanded { - min-width: $sidebar-width + $offset; -} - -.statusline { - position: absolute; - right: 0; - margin-right: 9rem; - bottom: -0.5rem; -} - -.statuscontainer { - width: 100%; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { mapState } from "vuex"; -import { HTTP } from "../../lib/http.js"; -import "../../../node_modules/highlight.js/styles/paraiso-dark.css"; -import Vue from "vue"; -import VueHighlightJS from "vue-highlightjs"; -Vue.use(VueHighlightJS); - -const ACCESSLOG = "accesslog"; -const ERRORLOG = "errorlog"; - -export default { - name: "logs", - mounted() { - this.fetch("system/log/apache2/access.log", ACCESSLOG); - }, - data() { - return { - logs: null, - currentLog: null, - currentFile: null, - refreshed: null - }; - }, - methods: { - fetch(file, type) { - HTTP.get(file, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - this.logs = response.data.content; - this.currentLog = type; - this.refreshed = new Date().toLocaleString(); - this.currentFile = file; - }) - .catch(); - }, - disallow(e) { - e.target.blur(); - } - }, - computed: { - ...mapState("application", ["showSidebar"]), - accesslogStyle() { - return { - active: this.currentLog == ACCESSLOG, - "nav-link": true - }; - }, - errorlogStyle() { - return { - active: this.currentLog == ERRORLOG, - "nav-link": true - }; - }, - spacer() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - } - } -}; -</script>
--- a/client/src/components/admin/Systemconfiguration.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,191 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <div :class="spacerStyle"></div> - <div class="card sysconfig mt-3 shadow-xs"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon icon="wrench" class="mr-2"></font-awesome-icon> - <translate class="headline">Systemconfiguration</translate> - </h6> - <div class="card-body config"> - <section class="configsection"> - <h4 class="card-title"> - <translate>Bottleneck Areas stroke-color</translate> - </h4> - <compact-picker v-model="strokeColor" /> - </section> - <section> - <h4 class="card-title"> - <translate>Bottleneck Areas fill-color</translate> - </h4> - <chrome-picker v-model="fillColor" /> - </section> - <div class="sendbutton"> - <a @click.prevent="submit" class="btn btn-info text-white"> - <translate>Send</translate> - </a> - </div> - </div> - <!-- card-body --> - </div> - </div> -</template> - -<style scoped lang="scss"> -.config { - text-align: left; -} - -.configsection { - margin-bottom: $large-offset; -} - -.sendbutton { - position: absolute; - right: $offset; - bottom: $offset; -} - -.inputs { - margin-left: auto; - margin-right: auto; -} - -.sysconfig { - margin-right: $offset; - width: 100%; - height: 100%; -} - -.spacer { - height: 100vh; -} - -.spacer-collapsed { - min-width: $icon-width + $offset; - transition: $transition-fast; -} - -.spacer-expanded { - min-width: $sidebar-width + $offset; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Bernhard Reiter <bernhard@intevation.de> - */ -import { Chrome } from "vue-color"; -import { Compact } from "vue-color"; - -import { HTTP } from "../../lib/http"; -import { displayError } from "../../lib/errors.js"; -import { mapState } from "vuex"; -export default { - name: "systemconfiguration", - data() { - return { - sent: false, - strokeColor: { r: 0, g: 0, b: 0, a: 1.0 }, - fillColor: { r: 0, g: 0, b: 0, a: 1.0 }, - currentConfig: null - }; - }, - components: { - "chrome-picker": Chrome, - "compact-picker": Compact - }, - computed: { - ...mapState("application", ["showSidebar"]), - spacerStyle() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - } - }, - methods: { - submit() { - HTTP.put("/system/style/Bottlenecks/stroke", this.strokeColor.rgba, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then() - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - - HTTP.put("/system/style/Bottlenecks/fill", this.fillColor.rgba, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then() - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - mounted() { - HTTP.get("/system/style/Bottlenecks/stroke", { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - this.strokeColor = response.data.colour; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - - HTTP.get("/system/style/Bottlenecks/fill", { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - this.fillColor = response.data.colour; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } -}; -</script>
--- a/client/src/components/admin/importschedule/Importschedule.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <div :class="spacerStyle"></div> - <div class="mt-3 w-100"> - <div class="card flex-grow-1 schedulecard shadow-xs"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon icon="clock" class="mr-2"></font-awesome-icon> - <translate class="headline">Importschedule</translate> - </h6> - <div class="card-body schedulecardbody"> - <div class="card-body schedulecardbody"> - <div class="searchandfilter w-50 d-flex flex-row"> - <div class="searchgroup input-group"> - <div class="input-group-prepend"> - <span class="input-group-text" id="search"> - <font-awesome-icon icon="search"></font-awesome-icon> - </span> - </div> - <input - v-model="searchQuery" - type="text" - class="form-control" - placeholder - aria-label="Search" - aria-describedby="search" - /> - </div> - </div> - <table v-if="schedules.length" class="table"> - <thead> - <tr> - <th><translate>Import</translate></th> - <th><translate>Type</translate></th> - <th><translate>Author</translate></th> - <th><translate>Schedule</translate></th> - <th><translate>Email</translate></th> - <th> </th> - <th> </th> - </tr> - </thead> - <tbody> - <tr v-for="(schedule, index) in schedules" :key="index"> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - <td> - <font-awesome-icon - icon="pencil-alt" - fixed-width - ></font-awesome-icon> - </td> - <td> - <font-awesome-icon - @click="deleteSchedule" - icon="trash" - fixed-width - ></font-awesome-icon> - </td> - </tr> - </tbody> - </table> - <div v-else class="mt-4 small text-center py-3"> - <translate>No schedules</translate> - </div> - <button - @click="newImport" - class="btn btn-info position-absolute newbutton" - > - <translate>New Import</translate> - </button> - </div> - </div> - </div> - </div> - <Importscheduledetail></Importscheduledetail> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import { mapState } from "vuex"; -import Importscheduledetail from "./Importscheduledetail"; -//import { SCHEDULES } from "../../store/imports.js"; - -export default { - name: "importschedule", - components: { - Importscheduledetail - }, - data() { - return { - searchQuery: "" - }; - }, - methods: { - newImport() { - this.$store.commit("imports/setImportScheduleDetailVisible"); - }, - deleteSchedule(index) { - this.$store.commit("imports/deleteSchedule", index); - } - }, - computed: { - ...mapState("application", ["showSidebar"]), - ...mapState("imports", ["schedules"]), - spacerStyle() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - } - } -}; -</script> - -<style lang="scss" scoped> -.spacer { - height: 100vh; -} - -.spacer-collapsed { - min-width: $icon-width + $offset; - transition: $transition-fast; -} - -.spacer-expanded { - min-width: $sidebar-width + $offset; -} - -.schedulecard { - margin-right: $offset; - min-height: 20rem; -} - -.schedulecard-body { - width: 100%; - margin-left: auto; - margin-right: auto; -} - -.newbutton { - position: absolute; - bottom: $offset; - right: $offset; -} -</style>
--- a/client/src/components/admin/importschedule/Importscheduledetail.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,146 +0,0 @@ -<template> - <div - class="importscheduledetails fadeIn animated" - v-if="importScheduleDetailVisible" - > - <div class="card h-100 shadow-xs"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <translate>New import</translate> - <span @click="closeDetailview" class="closebutton"> - <font-awesome-icon icon="times"></font-awesome-icon> - </span> - </h6> - <div class="card-body"> - <form @submit.prevent="save" class="ml-3"> - <div class="d-flex flex-row w-100"> - <div class="flex-column w-100"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Imports</translate> - </small> - </div> - <select v-model="import_" class="custom-select" id="import_"> - <option v-for="option in this.$options.imports" :key="option">{{ - option - }}</option> - </select> - </div> - </div> - <div class="d-flex flex-row mt-3 w-100 justify-content-between"> - <div class="flex-column w-100 mr-2"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Importtype</translate> - </small> - </div> - <select v-model="import_" class="custom-select" id="importtype"> - <option - v-for="option in this.$options.importtype" - :key="option" - >{{ option }}</option - > - </select> - </div> - <div class="flex-column w-100 ml-2"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Schedule</translate> - </small> - </div> - <select v-model="schedule" class="custom-select" id="period"> - <option v-for="option in this.$options.periods" :key="option">{{ - option - }}</option> - </select> - </div> - </div> - <div class="flex-column mt-3 w-100 mr-2"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Email Notification</translate> - </small> - </div> - <div class="flex-flex-row text-left"> - <toggle-button - v-model="eMailNotification" - class="mt-2" - :speed="100" - :labels="{ - checked: this.$options.on, - unchecked: this.$options.off - }" - :width="50" - :height="20" - /> - </div> - </div> - <div v-if="eMailNotification" class="flex-column w-100 mr-2"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>Email</translate> </small> - </div> - <input class="form-control" type="text" /> - </div> - <button type="submit" class="shadow-sm btn btn-info submit-button"> - <translate>Submit</translate> - </button> - </form> - </div> - </div> - </div> -</template> - -<script> -import { mapState } from "vuex"; -import { displayInfo } from "../../../lib/errors.js"; - -export default { - name: "importscheduledetail", - data() { - return { - schedule: null, - import_: null, - eMailNotification: false - }; - }, - computed: { - ...mapState("imports", ["importScheduleDetailVisible"]) - }, - methods: { - save() { - displayInfo({ - title: "Import", - message: "under construction" - }); - }, - closeDetailview() { - this.$store.commit("imports/clearImportScheduleDetail"); - this.$store.commit("imports/setImportScheduleDetailInvisible"); - } - }, - imports: [], - importtype: [], - on: "on", - off: "off", - periods: { - DAILY: "daily", - MONTHLY: "monthly" - } -}; -</script> - -<style lang="scss" scoped> -.importscheduledetails { - height: 420px; - width: 45%; - margin-top: $offset; - margin-right: $offset; -} - -.submit-button { - position: absolute; - right: $offset; - bottom: $offset; -} -</style>
--- a/client/src/components/admin/usermanagement/Passwordfield.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -<template> - <div class="w-100"> - <div class="d-flex flex-row"> - <label for="password">{{ this.label }}</label> - </div> - <div class="d-flex d-row"> - <input - :type="isPasswordVisible" - @change="fieldChanged" - class="form-control" - :placeholder="placeholder" - :required="required" - /> - <span class="input-group-text" @click="showPassword"> - <font-awesome-icon - :icon="readablePassword ? 'eye-slash' : 'eye'" - ></font-awesome-icon> - </span> - </div> - <div v-show="passworderrors" class="text-danger"> - <small> - <font-awesome-icon icon="exclamation-triangle"></font-awesome-icon> - {{ this.passworderrors }} - </small> - </div> - </div> -</template> - -<style> -/* FIXME does not work here, unclear why, so added to Login.vue -input[type="password"]::-ms-reveal { - display: none; -} */ -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -export default { - name: "passwordfield", - props: ["model", "placeholder", "label", "passworderrors", "required"], - data() { - return { - password: "", - readablePassword: false - }; - }, - methods: { - showPassword() { - this.readablePassword = !this.readablePassword; - }, - fieldChanged(e) { - this.$emit("fieldchange", e.target.value); - } - }, - computed: { - isPasswordVisible() { - return this.readablePassword ? "text" : "password"; - } - } -}; -</script>
--- a/client/src/components/admin/usermanagement/Userdetail.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,393 +0,0 @@ -<template> - <div class="userdetails mt-3 shadow fadeIn animated card"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - {{ this.cardHeader }} - <span @click="closeDetailview" class="closebutton"> - <font-awesome-icon icon="times"></font-awesome-icon> - </span> - </h6> - <div class="card-body"> - <form @submit.prevent="save" class="ml-3"> - <div class="formfields"> - <div v-if="currentUser.isNew" class="form-group row"> - <label for="user"> <translate>Username</translate> </label> - <input - type="user" - :placeholder="userNamePlaceholder" - class="form-control form-control-sm" - id="user" - aria-describedby="userHelp" - v-model="currentUser.user" - /> - <div v-show="errors.user" class="text-danger"> - <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> - {{ errors.user }} - </small> - </div> - </div> - <div class="form-group row"> - <label for="country"> <translate>Country</translate> </label> - <select - class="form-control form-control-sm" - v-on:change="validateCountry" - v-model="currentUser.country" - > - <option disabled value> - <translate>Please select one</translate> - </option> - <option - v-for="country in countries" - v-bind:value="country" - v-bind:key="country" - >{{ country }}</option - > - </select> - <div v-show="errors.country" class="text-danger"> - <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> - {{ errors.country }} - </small> - </div> - </div> - <div class="form-group row"> - <label for="email"> <translate>Email address</translate> </label> - <input - type="email" - v-on:change="validateEmailaddress" - class="form-control form-control-sm" - id="email" - aria-describedby="emailHelp" - v-model="currentUser.email" - /> - <div v-show="errors.email" class="text-danger"> - <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> - {{ errors.email }} - </small> - </div> - </div> - <div class="form-group row"> - <label for="role"> <translate>Role</translate> </label> - <select - class="form-control form-control-sm" - v-on:change="validateRole" - v-model="currentUser.role" - > - <option disabled value> - <translate>Please select one</translate> - </option> - <option value="sys_admin"> - <translate>Sysadmin</translate> - </option> - <option value="waterway_admin"> - <translate>Waterway Admin</translate> - </option> - <option value="waterway_user"> - <translate>Waterway User</translate> - </option> - </select> - <div v-show="errors.role" class="text-danger"> - <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> - {{ errors.role }} - </small> - </div> - </div> - <div class="form-group row"> - <PasswordField - @fieldchange="passwordChanged" - :placeholder="passwordPlaceholder" - :label="passwordLabel" - :passworderrors="errors.password" - ></PasswordField> - </div> - <div class="form-group row"> - <PasswordField - @fieldchange="passwordReChanged" - :placeholder="passwordRePlaceholder" - :label="passwordReLabel" - :passworderrors="errors.passwordre" - ></PasswordField> - </div> - </div> - <div> - <button - type="submit" - :disabled="submitted" - class="shadow-sm btn btn-info submit-button" - > - <translate>Submit</translate> - </button> - </div> - <div - v-if="currentUser.role != 'waterway_user'" - class="form-group row d-flex flex-row justify-content-start mailbutton" - > - <a @click="sendTestMail" class="btn btn-light"> - <font-awesome-icon icon="paper-plane"></font-awesome-icon> - <translate>Send testmail</translate> - </a> - <div v-if="mailsent"><translate>Mail was sent</translate></div> - </div> - </form> - </div> - </div> -</template> - -<style lang="scss" scoped> -.submit-button { - position: absolute; - right: $offset; - bottom: $offset; -} -.mailbutton { - width: 12vw; - position: absolute; - left: $large-offset; - bottom: 0; -} - -.formfields { - width: 60%; -} - -.userdetails { - height: 600px; - margin-top: $offset; - margin-left: $offset; - margin-right: $offset; -} - -form { - font-size: $smaller; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { HTTP } from "../../../lib/http"; -import { displayError } from "../../../lib/errors.js"; -import { mapState } from "vuex"; -import PasswordField from "./Passwordfield"; - -const emptyErrormessages = () => { - return { - email: "", - country: "", - role: "", - password: "", - passwordre: "" - }; -}; - -const isEmailValid = email => { - /** - * - * For convenience purposes the same regex used as in the go code - * cf. types.go - * - */ - // eslint-disable-next-line - return /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test( - email - ); -}; - -const violatedPasswordRules = password => { - return ( - // rules according to issue 70 - password.length < 7 || - /\W/.test(password) == false || - /\d/.test(password) == false - ); -}; - -export default { - name: "userdetail", - components: { - PasswordField - }, - data() { - return { - mailsent: false, - passwordLabel: this.$gettext("Password"), - passwordReLabel: this.$gettext("Repeat Password"), - passwordPlaceholder: this.$gettext("password"), - passwordRePlaceholder: this.$gettext("password again"), - password: "", - passwordre: "", - currentUser: {}, - path: null, - submitted: false, - errors: { - email: "", - country: "", - role: "", - password: "", - passwordre: "" - } - }; - }, - mounted() { - this.currentUser = { ...this.user }; - this.path = this.user.name; - }, - watch: { - user() { - this.currentUser = { ...this.user }; - this.path = this.user.name; - this.clearPassword(); - this.clearErrors(); - } - }, - computed: { - cardHeader() { - if (this.currentUser.isNew) return "N.N"; - return this.currentUser.user; - }, - userNamePlaceholder() { - if (this.currentUser.isNew) return "N.N"; - return ""; - }, - ...mapState("application", ["countries"]), - user() { - return this.$store.getters["usermanagement/currentUser"]; - }, - isFormValid() { - return ( - isEmailValid(this.currentUser.email) && - this.currentUser.country && - this.password === this.passwordre && - (this.password === "" || !violatedPasswordRules(this.password)) - ); - } - }, - methods: { - sendTestMail() { - if (this.mailsent) return; - HTTP.get("/testmail/" + this.currentUser.user, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - }) - .then(() => { - this.mailsent = true; - }) - .catch(error => { - this.loginFailed = true; - this.submitted = false; - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - passwordChanged(value) { - this.password = value; - this.validatePassword(); - }, - passwordReChanged(value) { - this.passwordre = value; - this.validatePassword(); - }, - clearErrors() { - this.errors = emptyErrormessages(); - }, - clearPassword() { - this.password = ""; - this.passwordre = ""; - }, - closeDetailview() { - this.$store.commit("usermanagement/clearCurrentUser"); - this.$store.commit("usermanagement/setUserDetailsInvisible"); - }, - validateCountry() { - this.errors.country = this.currentUser.country - ? "" - : this.$gettext("Please choose a country"); - }, - validateRole() { - this.errors.role = this.currentUser.role - ? "" - : this.$gettext("Please choose a role"); - }, - validatePassword() { - this.errors.passwordre = - this.password === this.passwordre - ? "" - : this.$gettext("Passwords do not match!"); - this.errors.password = - this.password === "" || !violatedPasswordRules(this.password) - ? "" - : this.$gettext( - "Password should at least be 8 char long including 1 digit and 1 special char like $" - ); - }, - validateEmailaddress() { - this.errors.email = isEmailValid(this.currentUser.email) - ? "" - : this.$gettext("invalid email"); - }, - validate() { - this.validateCountry(); - this.validateRole(); - this.validatePassword(); - this.validateEmailaddress(); - }, - save() { - this.validate(); - if (!this.isFormValid) return; - if (this.password) this.currentUser.password = this.password; - this.submitted = true; - this.$store - .dispatch("usermanagement/saveCurrentUser", { - path: this.user.user, - user: this.currentUser - }) - .then(() => { - this.submitted = false; - this.$store.dispatch("usermanagement/loadUsers").catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - this.submitted = false; - const { status, data } = error.response; - displayError({ - title: this.$gettext("Error while saving user"), - message: `${status}: ${data.message || data}` - }); - }); - } - } -}; -</script>
--- a/client/src/components/admin/usermanagement/Usermanagement.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,358 +0,0 @@ -<template> - <div class="main d-flex flex-row"> - <div :class="spacerStyle"></div> - <div class="d-flex content flex-column"> - <div class="d-flex flex-row"> - <div :class="userlistStyle"> - <div class="card"> - <h6 - class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" - > - <font-awesome-icon - icon="users-cog" - class="mr-2 fa-fw" - ></font-awesome-icon> - <translate class="headline">Users</translate> - </h6> - <div class="card-body"> - <table id="datatable" :class="tableStyle"> - <thead> - <tr> - <th scope="col" @click="sortBy('user')"> - <span - >Username - <font-awesome-icon - v-if="sortCriterion == 'user'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col" @click="sortBy('country')"> - <span - >Country - <font-awesome-icon - v-if="sortCriterion == 'country'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col" @click="sortBy('email')"> - <span - >Email - <font-awesome-icon - v-if="sortCriterion == 'email'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col" @click="sortBy('role')"> - <span - >Role - <font-awesome-icon - v-if="sortCriterion == 'role'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col"></th> - </tr> - </thead> - <tbody> - <tr - v-for="user in users" - :key="user.user" - @click="selectUser(user.user)" - > - <td>{{ user.user }}</td> - <td>{{ user.country }}</td> - <td>{{ user.email }}</td> - <td> - <font-awesome-icon - :icon="roleIcon(user.role)" - @click="deleteUser(user.user)" - ></font-awesome-icon> - </td> - <td> - <font-awesome-icon - icon="trash" - @click="deleteUser(user.user)" - ></font-awesome-icon> - </td> - </tr> - </tbody> - </table> - </div> - <div class="d-flex mx-auto align-items-center"> - <button - @click="prevPage" - v-if="this.currentPage !== 1" - class="mr-2 btn btn-sm btn-light align-self-center" - > - <font-awesome-icon icon="angle-left"></font-awesome-icon> - </button> - {{ this.currentPage }} / {{ this.pages }} - <button - @click="nextPage" - v-if="this.currentPage !== this.pages" - class="ml-2 btn btn-sm btn-light align-self-center" - > - <font-awesome-icon icon="angle-right"></font-awesome-icon> - </button> - </div> - <div class="mr-3 pb-3"> - <button @click="addUser" class="btn btn-info addbutton shadow-sm"> - <translate>Add User</translate> - </button> - </div> - </div> - </div> - <Userdetail - class="d-flex userdetails" - v-if="isUserDetailsVisible" - ></Userdetail> - </div> - </div> - </div> -</template> - -<style scoped lang="scss"> -@import "../../../assets/tooltip.scss"; - -.addbutton { - position: absolute; - bottom: $offset; - right: $offset; -} - -.content { - width: 100%; -} - -.userdetails { - width: 50%; -} -.spacer { - height: 100vh; - margin-left: $offset; -} - -.spacer-collapsed { - min-width: $icon-width + $offset; - transition: $transition-fast; -} - -.spacer-expanded { - min-width: $sidebar-width + $offset; -} - -.main { - height: 100vh; -} - -.icon { - font-size: large; -} - -.userlist { - min-width: 520px; - height: 100%; -} - -.userlistsmall { - width: 100%; -} - -.userlistextended { - width: 100%; -} - -.table { - width: 90% !important; - margin: auto; -} - -.table th { - cursor: pointer; -} - -.table th, -td { - font-size: $smaller; - border-top: 0px !important; - text-align: left; - padding: $small-offset !important; -} - -.table td { - font-size: $smaller; - cursor: pointer; -} - -tr span { - display: flex; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import Userdetail from "./Userdetail"; -import store from "../../../store"; -import { mapGetters, mapState } from "vuex"; -import { displayError } from "../../../lib/errors.js"; - -export default { - name: "userview", - data() { - return { - sortCriterion: "user", - pageSize: 10, - currentPage: 1 - }; - }, - components: { - Userdetail - }, - computed: { - ...mapGetters("usermanagement", ["isUserDetailsVisible"]), - ...mapState("application", ["showSidebar"]), - spacerStyle() { - return [ - "spacer", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - }, - users() { - let users = [...this.$store.getters["usermanagement/users"]]; - users.sort((a, b) => { - if ( - a[this.sortCriterion].toLowerCase() < - b[this.sortCriterion].toLowerCase() - ) - return -1; - if ( - a[this.sortCriterion].toLowerCase() > - b[this.sortCriterion].toLowerCase() - ) - return 1; - return 0; - }); - const start = (this.currentPage - 1) * this.pageSize; - return users.slice(start, start + this.pageSize); - }, - pages() { - let users = [...this.$store.getters["usermanagement/users"]]; - return Math.ceil(users.length / this.pageSize); - }, - tableStyle() { - return { - table: true, - "table-hover": true, - "table-sm": this.isUserDetailsVisible, - fadeIn: true, - animated: true - }; - }, - userlistStyle() { - return [ - "userlist mt-3 mr-3 shadow-xs", - { - userlistsmall: this.isUserDetailsVisible, - userlistextended: !this.isUserDetailsVisible - } - ]; - } - }, - methods: { - tween() {}, - nextPage() { - if (this.currentPage < this.pages) { - document.querySelector("#datatable").classList.add("fadeOut"); - setTimeout(() => { - document.querySelector("#datatable").classList.remove("fadeOut"); - this.currentPage += 1; - }, 10); - } - return; - }, - prevPage() { - if (this.currentPage > 0) { - document.querySelector("#datatable").classList.add("fadeOut"); - setTimeout(() => { - document.querySelector("#datatable").classList.remove("fadeOut"); - this.currentPage -= 1; - }, 10); - } - return; - }, - sortBy(criterion) { - this.sortCriterion = criterion; - }, - deleteUser(name) { - this.$store - .dispatch("usermanagement/deleteUser", { name: name }) - .then(() => { - this.submitted = false; - this.$store.dispatch("usermanagement/loadUsers").catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - addUser() { - this.$store.commit("usermanagement/clearCurrentUser"); - this.$store.commit("usermanagement/setUserDetailsVisible"); - }, - selectUser(name) { - const user = this.$store.getters["usermanagement/getUserByName"](name); - this.$store.commit("usermanagement/setCurrentUser", user); - }, - roleIcon(role) { - if (role === "sys_admin") return "star"; - if (role === "waterway_admin") return ["fab", "adn"]; - return "user"; - } - }, - beforeRouteEnter(to, from, next) { - store - .dispatch("usermanagement/loadUsers") - .then(next) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data}` - }); - }); - }, - beforeRouteLeave(to, from, next) { - store.commit("usermanagement/clearCurrentUser"); - store.commit("usermanagement/setUserDetailsInvisible"); - next(); - } -}; -</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/Fairwayprofile.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,413 @@ +<template> + <div :class="['position-relative', { show: showSplitscreen }]"> + <button + class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle" + @click="$store.commit('application/showSplitscreen', false)" + v-if="showSplitscreen" + > + <font-awesome-icon icon="angle-down" /> + </button> + <button + class="rounded-bottom bg-white border-0 position-absolute clear-selection" + @click="$store.dispatch('fairwayprofile/clearSelection')" + v-if="showSplitscreen" + > + <font-awesome-icon icon="times" /> + </button> + <div class="profile bg-white position-relative d-flex flex-column"> + <h5 + class="headline border-bottom mb-0 py-2" + v-if="selectedBottleneck && selectedSurvey" + > + {{ selectedBottleneck }} ({{ selectedSurvey.date_info }}) + </h5> + <div class="d-flex flex-fill"> + <div + class="loading d-flex justify-content-center align-items-center" + v-if="surveysLoading || profileLoading" + > + <font-awesome-icon icon="spinner" spin /> + </div> + <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.profile { + width: 100vw; + height: 0; + overflow: hidden; + z-index: 2; +} + +.splitscreen-toggle, +.clear-selection { + width: 2rem; + height: 2rem; + margin-top: 8px; + z-index: 3; + outline: none; +} + +.splitscreen-toggle svg path, +.clear-selection svg path { + fill: #666; +} + +.splitscreen-toggle { + right: 2.5rem; +} + +.clear-selection { + right: 0.5rem; +} + +.show .profile { + height: 50vh; +} + +.loading { + background: rgba(255, 255, 255, 0.96); + position: absolute; + z-index: 99; + top: 0; + right: 0; + bottom: 0; + left: 0; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import * as d3 from "d3"; +import { mapState, mapGetters } from "vuex"; +import debounce from "debounce"; + +const GROUND_COLOR = "#4A2F06"; + +export default { + name: "fairwayprofile", + data() { + return { + coordinatesInput: "", + coordinatesSelect: null, + cutLabel: "", + showLabelInput: false, + width: null, + height: null, + margin: { + top: 20, + right: 40, + bottom: 30, + left: 40 + } + }; + }, + computed: { + ...mapGetters("fairwayprofile", ["totalLength"]), + ...mapState("application", ["showSplitscreen"]), + ...mapState("fairwayprofile", [ + "startPoint", + "endPoint", + "currentProfile", + "additionalSurvey", + "minAlt", + "maxAlt", + "fairwayCoordinates", + "waterLevels", + "selectedWaterLevel", + "profileLoading" + ]), + ...mapState("bottlenecks", [ + "selectedBottleneck", + "selectedSurvey", + "surveysLoading" + ]), + currentData() { + if ( + !this.selectedSurvey || + !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info) + ) + return []; + return this.currentProfile[this.selectedSurvey.date_info].points; + }, + additionalData() { + if ( + !this.additionalSurvey || + !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info) + ) + return []; + return this.currentProfile[this.additionalSurvey.date_info].points; + }, + waterColor() { + const result = this.waterLevels.find( + x => x.level === this.selectedWaterLevel + ); + return result.color; + }, + xScale() { + return [0, this.totalLength]; + }, + yScaleLeft() { + const hi = Math.max(this.maxAlt, this.selectedWaterLevel); + return [this.minAlt, hi]; + }, + yScaleRight() { + const DELTA = this.maxAlt * 1.1 - this.maxAlt; + return [this.maxAlt * 1 + DELTA, -DELTA]; + } + }, + watch: { + currentData() { + this.drawDiagram(); + }, + additionalData() { + this.drawDiagram(); + }, + width() { + this.drawDiagram(); + }, + height() { + this.drawDiagram(); + }, + waterLevels() { + this.drawDiagram(); + }, + selectedWaterLevel() { + this.drawDiagram(); + }, + fairwayCoordinates() { + this.drawDiagram(); + } + }, + methods: { + drawDiagram() { + this.coordinatesSelect = null; + const chartDiv = document.querySelector(".fairwayprofile"); + d3.select(".fairwayprofile svg").remove(); + this.scaleFairwayProfile(); + let svg = d3.select(chartDiv).append("svg"); + svg.attr("width", this.width); + svg.attr("height", this.height); + const width = this.width - this.margin.right - 1.5 * this.margin.left; + const height = this.height - this.margin.top - 2 * this.margin.bottom; + const currentData = this.currentData; + const additionalData = this.additionalData; + const { xScale, yScaleRight, graph } = this.generateCoordinates( + svg, + height, + width + ); + this.drawWaterlevel({ graph, xScale, yScaleRight, height }); + this.drawLabels({ graph, height }); + this.drawFairway({ graph, xScale, yScaleRight }); + if (currentData) { + this.drawProfile({ + graph, + xScale, + yScaleRight, + currentData, + height, + color: GROUND_COLOR, + strokeColor: "black", + opacity: 1 + }); + } + if (additionalData) { + this.drawProfile({ + graph, + xScale, + yScaleRight, + currentData: additionalData, + height, + color: GROUND_COLOR, + strokeColor: "#943007", + opacity: 0.6 + }); + } + }, + drawFairway({ graph, xScale, yScaleRight }) { + for (let coordinates of this.fairwayCoordinates) { + const [startPoint, endPoint, depth] = coordinates; + let fairwayArea = d3 + .area() + .x(function(d) { + return xScale(d.x); + }) + .y0(yScaleRight(0)) + .y1(function(d) { + return yScaleRight(d.y); + }); + graph + .append("path") + .datum([{ x: startPoint, y: depth }, { x: endPoint, y: depth }]) + .attr("fill", "#002AFF") + .attr("stroke-opacity", 0.65) + .attr("fill-opacity", 0.65) + .attr("stroke", "#FFD20D") + .attr("d", fairwayArea); + } + }, + drawLabels({ graph, height }) { + graph + .append("text") + .attr("transform", ["rotate(-90)"]) + .attr("y", this.width - 60) + .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2) + .attr("dy", "1em") + .attr("fill", "black") + .style("text-anchor", "middle") + .text("Depth [m]"); + graph + .append("text") + .attr("y", 0 - this.margin.left) + .attr("x", 0 - height / 4) + .attr("dy", "1em") + .attr("fill", "black") + .style("text-anchor", "middle") + .attr("transform", [ + "translate(" + this.width / 2 + "," + this.height + ")", + "rotate(0)" + ]) + .text("Width [m]"); + }, + generateCoordinates(svg, height, width) { + let xScale = d3 + .scaleLinear() + .domain(this.xScale) + .rangeRound([0, width]); + + xScale.ticks(5); + let yScaleLeft = d3 + .scaleLinear() + .domain(this.yScaleLeft) + .rangeRound([height, 0]); + + let yScaleRight = d3 + .scaleLinear() + .domain(this.yScaleRight) + .rangeRound([height, 0]); + + let xAxis = d3.axisBottom(xScale); + let yAxis2 = d3.axisRight(yScaleRight); + let graph = svg + .append("g") + .attr( + "transform", + "translate(" + this.margin.left + "," + this.margin.top + ")" + ); + graph + .append("g") + .attr("transform", "translate(0," + height + ")") + .call(xAxis.ticks(5)); + graph + .append("g") + .attr("transform", "translate(" + width + ",0)") + .call(yAxis2); + return { xScale, yScaleLeft, yScaleRight, graph }; + }, + drawWaterlevel({ graph, xScale, yScaleRight, height }) { + let waterArea = d3 + .area() + .x(function(d) { + return xScale(d.x); + }) + .y0(height) + .y1(function(d) { + return yScaleRight(d.y); + }); + graph + .append("path") + .datum([{ x: 0, y: 0 }, { x: this.totalLength, y: 0 }]) + .attr("fill", this.waterColor) + .attr("stroke", this.waterColor) + .attr("d", waterArea); + }, + drawProfile({ + graph, + xScale, + yScaleRight, + currentData, + height, + color, + strokeColor, + opacity + }) { + for (let part of currentData) { + let profileLine = d3 + .line() + .x(d => { + return xScale(d.x); + }) + .y(d => { + return yScaleRight(d.y); + }); + let profileArea = d3 + .area() + .x(function(d) { + return xScale(d.x); + }) + .y0(height) + .y1(function(d) { + return yScaleRight(d.y); + }); + graph + .append("path") + .datum(part) + .attr("fill", color) + .attr("stroke", color) + .attr("stroke-width", 3) + .attr("stroke-opacity", opacity) + .attr("fill-opacity", opacity) + .attr("d", profileArea); + graph + .append("path") + .datum(part) + .attr("fill", "none") + .attr("stroke", strokeColor) + .attr("stroke-linejoin", "round") + .attr("stroke-linecap", "round") + .attr("stroke-width", 3) + .attr("stroke-opacity", opacity) + .attr("fill-opacity", opacity) + .attr("d", profileLine); + } + }, + scaleFairwayProfile() { + if (!document.querySelector(".fairwayprofile")) return; + const clientHeight = document.querySelector(".fairwayprofile") + .clientHeight; + const clientWidth = document.querySelector(".fairwayprofile").clientWidth; + if (!clientHeight || !clientWidth) return; + this.height = clientHeight; + this.width = clientWidth; + } + }, + created() { + window.addEventListener("resize", debounce(this.drawDiagram), 100); + }, + mounted() { + this.drawDiagram(); + }, + updated() { + this.scaleFairwayProfile(); + }, + destroyed() { + window.removeEventListener("resize", debounce(this.drawDiagram)); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/Infobar.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,61 @@ +<template> + <div + v-if="Object.keys(currentProfile).length && !showSplitscreen" + class="ui-element shadow-xs infobar rounded bg-white ml-auto mb-3 mr-3" + > + <div class="d-flex flex-row justify-content-between h-100"> + <h6 class="my-auto px-2"> + {{ selectedBottleneck }} ({{ selectedSurvey.date_info }}) + </h6> + <span + class="p-2 border-left d-flex align-items-center" + @click="$store.commit('application/showSplitscreen', true)" + > + <font-awesome-icon icon="angle-up"></font-awesome-icon> + </span> + <span + class="p-2 border-left d-flex align-items-center" + @click="$store.dispatch('fairwayprofile/clearSelection')" + > + <font-awesome-icon icon="times"></font-awesome-icon> + </span> + </div> + </div> +</template> + +<style lang="scss" scoped> +.infobar { + height: 2.2rem; + z-index: 2; +} + +.infobar svg path { + fill: #666; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "infobar", + computed: { + ...mapState("application", ["showSplitscreen"]), + ...mapState("fairwayprofile", ["currentProfile"]), + ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]) + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/Profiles.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,471 @@ +<template> + <div + :class="[ + 'box ui-element rounded bg-white text-nowrap', + { expanded: showProfiles } + ]" + > + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="chart-area" class="mr-2"></font-awesome-icon> + <translate>Profiles</translate> + <font-awesome-icon + icon="times" + class="ml-auto text-muted" + @click="$store.commit('application/showProfiles', false)" + ></font-awesome-icon> + </h6> + <div + class="d-flex flex-column p-3 flex-grow-1 text-left position-relative" + > + <div + class="loading d-flex justify-content-center align-items-center" + v-if="surveysLoading || profileLoading" + > + <font-awesome-icon icon="spinner" spin /> + </div> + <select + @click="moveToBottleneck" + v-model="selectedBottleneck" + class="form-control font-weight-bold" + > + <option :value="null"> + <translate>Select Bottleneck</translate> + </option> + <option + v-for="bn in bottlenecks" + :key="bn.properties.name" + :value="bn.properties.name" + >{{ bn.properties.name }}</option + > + </select> + <div v-if="selectedBottleneck"> + <div class="d-flex mt-2"> + <div class="flex-fill"> + <small class="text-muted"> + <translate>Sounding Result</translate>: + </small> + <select + v-model="selectedSurvey" + class="form-control form-control-sm" + > + <option + v-for="survey in surveys" + :key="survey.date_info" + :value="survey" + >{{ formatSurveyDate(survey.date_info) }}</option + > + </select> + </div> + <div + class="flex-fill ml-3" + v-if="selectedSurvey && surveys.length > 1" + > + <small class="text-muted mt-1"> + <translate>Compare with</translate>: + </small> + <select + v-model="additionalSurvey" + class="form-control form-control-sm" + > + <option :value="null">None</option> + <option + v-for="survey in additionalSurveys" + :key="survey.date_info" + :value="survey" + >{{ formatSurveyDate(survey.date_info) }}</option + > + </select> + </div> + </div> + <hr class="w-100 mb-0" /> + <small class="text-muted d-block mt-2"> + <translate>Saved cross profiles</translate>: + </small> + <div class="d-flex"> + <select + :class="[ + 'form-control form-control-sm flex-fill', + { 'rounded-left-only': selectedCut } + ]" + v-model="selectedCut" + > + <option></option> + <option + v-for="(cut, index) in previousCuts" + :value="cut" + :key="index" + >{{ cut.label }}</option + > + </select> + <button + class="btn btn-sm btn-danger input-button-right" + @click="confirmDeleteSelectedCut = true" + v-if="selectedCut && !confirmDeleteSelectedCut" + > + <font-awesome-icon icon="trash" /> + </button> + <button + class="btn btn-sm btn-info rounded-0" + @click="confirmDeleteSelectedCut = false" + v-if="selectedCut && confirmDeleteSelectedCut" + > + <font-awesome-icon icon="times" /> + </button> + <button + class="btn btn-sm btn-danger input-button-right" + @click="deleteSelectedCut" + v-if="selectedCut && confirmDeleteSelectedCut" + > + <font-awesome-icon icon="check" /> + </button> + </div> + <small class="text-muted d-block mt-2"> + <translate>Enter coordinates manually</translate>: + </small> + <div class="position-relative"> + <input + class="form-control form-control-sm pr-5" + placeholder="Lat,Lon,Lat,Lon" + v-model="coordinatesInput" + /> + <button + class="btn btn-sm btn-info position-absolute input-button-right" + @click="applyManualCoordinates" + style="top: 0; right: 0;" + v-if="coordinatesInputIsValid" + > + <font-awesome-icon icon="check" /> + </button> + </div> + <small class="d-flex text-left mt-2" v-if="startPoint && endPoint"> + <div class="text-nowrap mr-3"> + <b> <translate>Start</translate>: </b> <br /> + Lat: {{ startPoint[1] }} <br /> + Lon: {{ startPoint[0] }} + </div> + <div class="text-nowrap"> + <b>End:</b> <br /> + Lat: {{ endPoint[1] }} <br /> + Lon: {{ endPoint[0] }} + </div> + <button + v-clipboard:copy="coordinatesForClipboard" + v-clipboard:success="onCopyCoordinates" + class="btn btn-info btn-sm ml-auto mt-auto" + > + <font-awesome-icon icon="copy" /> + </button> + </small> + <div class="d-flex mt-3"> + <div + class="pr-3 w-50" + v-if="startPoint && endPoint && !selectedCut" + > + <button + class="btn btn-info btn-sm w-100" + @click="showLabelInput = !showLabelInput" + > + <font-awesome-icon :icon="showLabelInput ? 'times' : 'check'" /> + {{ showLabelInput ? "Cancel" : "Save" }} + </button> + </div> + <div + :class="startPoint && endPoint && !selectedCut ? 'w-50' : 'w-100'" + > + <button class="btn btn-info btn-sm w-100" @click="toggleCutTool"> + <font-awesome-icon + :icon="cutTool && cutTool.getActive() ? 'times' : 'plus'" + ></font-awesome-icon> + {{ cutTool && cutTool.getActive() ? "Cancel" : "New" }} + </button> + </div> + </div> + <div v-if="showLabelInput" class="mt-2"> + <small class="text-muted"> + <translate>Enter label for cross profile</translate>: + </small> + <div class="position-relative"> + <input + class="form-control form-control-sm pr-5" + v-model="cutLabel" + /> + <button + class="btn btn-sm btn-info position-absolute input-button-right" + @click="saveCut" + v-if="cutLabel" + style="top: 0; right: 0;" + > + <font-awesome-icon icon="check" /> + </button> + </div> + </div> + </div> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.loading { + background: rgba(255, 255, 255, 0.9); + position: absolute; + z-index: 99; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.input-button-right { + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.rounded-left-only { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; +import Feature from "ol/Feature"; +import LineString from "ol/geom/LineString"; +import { displayError, displayInfo } from "../../lib/errors.js"; +import { formatSurveyDate } from "../../lib/date.js"; + +export default { + name: "profiles", + data() { + return { + coordinatesInput: "", + cutLabel: "", + showLabelInput: false, + confirmDeleteSelectedCut: false + }; + }, + computed: { + ...mapGetters("map", ["getVSourceByName"]), + ...mapState("application", ["showProfiles"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), + ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]), + ...mapState("fairwayprofile", [ + "previousCuts", + "startPoint", + "endPoint", + "profileLoading" + ]), + selectedBottleneck: { + get() { + return this.$store.state.bottlenecks.selectedBottleneck; + }, + set(name) { + this.$store + .dispatch("bottlenecks/setSelectedBottleneck", name) + .then(() => { + this.$store.commit("bottlenecks/setFirstSurveySelected"); + }); + } + }, + selectedSurvey: { + get() { + return this.$store.state.bottlenecks.selectedSurvey; + }, + set(survey) { + this.$store.commit("fairwayprofile/additionalSurvey", null); + this.$store.commit("bottlenecks/selectedSurvey", survey); + } + }, + additionalSurvey: { + get() { + return this.$store.state.fairwayprofile.additionalSurvey; + }, + set(survey) { + this.$store.commit("fairwayprofile/additionalSurvey", survey); + } + }, + selectedCut: { + get() { + return this.$store.state.fairwayprofile.selectedCut; + }, + set(cut) { + this.$store.commit("fairwayprofile/selectedCut", cut); + if (!cut) { + this.$store.commit("fairwayprofile/clearCurrentProfile"); + this.$store.commit("application/showSplitscreen", false); + this.getVSourceByName("Cut Tool").clear(); + } + } + }, + additionalSurveys() { + return this.surveys.filter(survey => survey !== this.selectedSurvey); + }, + coordinatesForClipboard() { + return ( + this.startPoint[1] + + "," + + this.startPoint[0] + + "," + + this.endPoint[1] + + "," + + this.endPoint[0] + ); + }, + coordinatesInputIsValid() { + const coordinates = this.coordinatesInput + .split(",") + .map(coord => parseFloat(coord.trim())) + .filter(c => Number(c) === c); + return coordinates.length === 4; + } + }, + watch: { + selectedBottleneck() { + this.$store.dispatch("fairwayprofile/previousCuts"); + this.cutLabel = + this.selectedBottleneck + " (" + new Date().toISOString() + ")"; + }, + selectedSurvey(survey) { + this.loadProfile(survey); + }, + additionalSurvey(survey) { + this.loadProfile(survey); + }, + selectedCut(cut) { + if (cut) { + this.confirmDeleteSelectedCut = false; + this.applyCoordinates(cut.coordinates); + } + } + }, + methods: { + formatSurveyDate(date) { + return formatSurveyDate(date); + }, + loadProfile(survey) { + if (survey) { + this.$store.commit("fairwayprofile/profileLoading", true); + this.$store + .dispatch("fairwayprofile/loadProfile", survey) + .finally(() => + this.$store.commit("fairwayprofile/profileLoading", false) + ); + } + }, + toggleCutTool() { + this.cutTool.setActive(!this.cutTool.getActive()); + this.lineTool.setActive(false); + this.polygonTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + }, + onCopyCoordinates() { + displayInfo({ + title: this.$gettext("Success"), + message: this.$gettext("Coordinates copied to clipboard!") + }); + }, + applyManualCoordinates() { + const coordinates = this.coordinatesInput + .split(",") + .map(coord => parseFloat(coord.trim())); + this.selectedCut = null; + this.coordinatesInput = ""; + this.applyCoordinates([ + coordinates[1], + coordinates[0], + coordinates[3], + coordinates[2] + ]); + }, + applyCoordinates(coordinates) { + // allow only numbers + coordinates = coordinates.filter(c => Number(c) === c); + if (coordinates.length === 4) { + // draw line on map + this.getVSourceByName("Cut Tool").clear(); + const cut = new Feature({ + geometry: new LineString([ + [coordinates[0], coordinates[1]], + [coordinates[2], coordinates[3]] + ]).transform("EPSG:4326", "EPSG:3857") + }); + this.getVSourceByName("Cut Tool").addFeature(cut); + + // draw diagram + this.$store.dispatch("fairwayprofile/cut", cut); + } else { + displayError({ + title: this.$gettext("Invalid input"), + message: this.$gettext( + "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" + ) + }); + } + }, + saveCut() { + const previousCuts = + JSON.parse(localStorage.getItem("previousCuts")) || []; + const newEntry = { + label: this.cutLabel, + bottleneckName: this.selectedBottleneck, + coordinates: [...this.startPoint, ...this.endPoint], + timestamp: new Date().getTime() + }; + const existingEntry = previousCuts.find(cut => { + return JSON.stringify(cut) === JSON.stringify(newEntry); + }); + if (!existingEntry) previousCuts.push(newEntry); + if (previousCuts.length > 100) previousCuts.shift(); + localStorage.setItem("previousCuts", JSON.stringify(previousCuts)); + this.$store.dispatch("fairwayprofile/previousCuts"); + + this.showLabelInput = false; + displayInfo({ + title: this.$gettext("Profile saved!"), + message: this.$gettext( + 'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.' + ) + }); + }, + deleteSelectedCut() { + let previousCuts = JSON.parse(localStorage.getItem("previousCuts")) || []; + previousCuts = previousCuts.filter(cut => { + return JSON.stringify(cut) !== JSON.stringify(this.selectedCut); + }); + localStorage.setItem("previousCuts", JSON.stringify(previousCuts)); + this.$store.commit("fairwayprofile/selectedCut", null); + this.$store.dispatch("fairwayprofile/previousCuts"); + displayInfo({ title: this.$gettext("Profile deleted!") }); + }, + moveToBottleneck() { + const bottleneck = this.bottlenecks.find( + bn => bn.properties.name === this.selectedBottleneck + ); + if (!bottleneck) return; + this.$store.commit("map/moveMap", { + coordinates: bottleneck.geometry.coordinates, + zoom: 17, + preventZoomOut: true + }); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importschedule/Importschedule.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,166 @@ +<template> + <div class="d-flex flex-row"> + <div :class="spacerStyle"></div> + <div class="mt-3 w-100"> + <div class="card flex-grow-1 schedulecard shadow-xs"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon icon="clock" class="mr-2"></font-awesome-icon> + <translate class="headline">Importschedule</translate> + </h6> + <div class="card-body schedulecardbody"> + <div class="card-body schedulecardbody"> + <div class="searchandfilter w-50 d-flex flex-row"> + <div class="searchgroup input-group"> + <div class="input-group-prepend"> + <span class="input-group-text" id="search"> + <font-awesome-icon icon="search"></font-awesome-icon> + </span> + </div> + <input + v-model="searchQuery" + type="text" + class="form-control" + placeholder + aria-label="Search" + aria-describedby="search" + /> + </div> + </div> + <table v-if="schedules.length" class="table"> + <thead> + <tr> + <th><translate>Import</translate></th> + <th><translate>Type</translate></th> + <th><translate>Author</translate></th> + <th><translate>Schedule</translate></th> + <th><translate>Email</translate></th> + <th> </th> + <th> </th> + </tr> + </thead> + <tbody> + <tr v-for="(schedule, index) in schedules" :key="index"> + <td></td> + <td></td> + <td></td> + <td></td> + <td></td> + <td> + <font-awesome-icon + icon="pencil-alt" + fixed-width + ></font-awesome-icon> + </td> + <td> + <font-awesome-icon + @click="deleteSchedule" + icon="trash" + fixed-width + ></font-awesome-icon> + </td> + </tr> + </tbody> + </table> + <div v-else class="mt-4 small text-center py-3"> + <translate>No schedules</translate> + </div> + <button + @click="newImport" + class="btn btn-info position-absolute newbutton" + > + <translate>New Import</translate> + </button> + </div> + </div> + </div> + </div> + <Importscheduledetail></Importscheduledetail> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { mapState } from "vuex"; +import Importscheduledetail from "./Importscheduledetail"; +//import { SCHEDULES } from "../../store/imports.js"; + +export default { + name: "importschedule", + components: { + Importscheduledetail + }, + data() { + return { + searchQuery: "" + }; + }, + methods: { + newImport() { + this.$store.commit("imports/setImportScheduleDetailVisible"); + }, + deleteSchedule(index) { + this.$store.commit("imports/deleteSchedule", index); + } + }, + computed: { + ...mapState("application", ["showSidebar"]), + ...mapState("imports", ["schedules"]), + spacerStyle() { + return [ + "spacer ml-3", + { + "spacer-expanded": this.showSidebar, + "spacer-collapsed": !this.showSidebar + } + ]; + } + } +}; +</script> + +<style lang="scss" scoped> +.spacer { + height: 100vh; +} + +.spacer-collapsed { + min-width: $icon-width + $offset; + transition: $transition-fast; +} + +.spacer-expanded { + min-width: $sidebar-width + $offset; +} + +.schedulecard { + margin-right: $offset; + min-height: 20rem; +} + +.schedulecard-body { + width: 100%; + margin-left: auto; + margin-right: auto; +} + +.newbutton { + position: absolute; + bottom: $offset; + right: $offset; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importschedule/Importscheduledetail.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,146 @@ +<template> + <div + class="importscheduledetails fadeIn animated" + v-if="importScheduleDetailVisible" + > + <div class="card h-100 shadow-xs"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <translate>New import</translate> + <span @click="closeDetailview" class="closebutton"> + <font-awesome-icon icon="times"></font-awesome-icon> + </span> + </h6> + <div class="card-body"> + <form @submit.prevent="save" class="ml-3"> + <div class="d-flex flex-row w-100"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Imports</translate> + </small> + </div> + <select v-model="import_" class="custom-select" id="import_"> + <option v-for="option in this.$options.imports" :key="option">{{ + option + }}</option> + </select> + </div> + </div> + <div class="d-flex flex-row mt-3 w-100 justify-content-between"> + <div class="flex-column w-100 mr-2"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Importtype</translate> + </small> + </div> + <select v-model="import_" class="custom-select" id="importtype"> + <option + v-for="option in this.$options.importtype" + :key="option" + >{{ option }}</option + > + </select> + </div> + <div class="flex-column w-100 ml-2"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Schedule</translate> + </small> + </div> + <select v-model="schedule" class="custom-select" id="period"> + <option v-for="option in this.$options.periods" :key="option">{{ + option + }}</option> + </select> + </div> + </div> + <div class="flex-column mt-3 w-100 mr-2"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Email Notification</translate> + </small> + </div> + <div class="flex-flex-row text-left"> + <toggle-button + v-model="eMailNotification" + class="mt-2" + :speed="100" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="50" + :height="20" + /> + </div> + </div> + <div v-if="eMailNotification" class="flex-column w-100 mr-2"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>Email</translate> </small> + </div> + <input class="form-control" type="text" /> + </div> + <button type="submit" class="shadow-sm btn btn-info submit-button"> + <translate>Submit</translate> + </button> + </form> + </div> + </div> + </div> +</template> + +<script> +import { mapState } from "vuex"; +import { displayInfo } from "../../lib/errors.js"; + +export default { + name: "importscheduledetail", + data() { + return { + schedule: null, + import_: null, + eMailNotification: false + }; + }, + computed: { + ...mapState("imports", ["importScheduleDetailVisible"]) + }, + methods: { + save() { + displayInfo({ + title: "Import", + message: "under construction" + }); + }, + closeDetailview() { + this.$store.commit("imports/clearImportScheduleDetail"); + this.$store.commit("imports/setImportScheduleDetailInvisible"); + } + }, + imports: [], + importtype: [], + on: "on", + off: "off", + periods: { + DAILY: "daily", + MONTHLY: "monthly" + } +}; +</script> + +<style lang="scss" scoped> +.importscheduledetails { + height: 420px; + width: 45%; + margin-top: $offset; + margin-right: $offset; +} + +.submit-button { + position: absolute; + right: $offset; + bottom: $offset; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/layers/Layers.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,64 @@ +<template> + <div + :class="[ + 'box ui-element rounded bg-white text-nowrap', + { expanded: showLayers } + ]" + > + <div style="width: 20rem"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> + <font-awesome-icon icon="layer-group" class="mr-2"></font-awesome-icon + ><translate>Layers</translate> + <font-awesome-icon + icon="times" + class="ml-auto text-muted" + @click="$store.commit('application/showLayers', false)" + ></font-awesome-icon> + </h6> + <div class="d-flex flex-column p-3 small"> + <Layerselect + v-for="(layer, index) in layersForLegend" + :layerindex="index" + :layername="layer.name" + :key="layer.name" + :isVisible="layer.isVisible" + @visibilityToggled="visibilityToggled" + ></Layerselect> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import Layerselect from "./Layerselect"; +import { mapGetters, mapState } from "vuex"; +export default { + name: "layers", + components: { + Layerselect + }, + computed: { + ...mapGetters("map", ["layersForLegend"]), + ...mapState("application", ["showLayers"]) + }, + methods: { + visibilityToggled(layer) { + this.$store.commit("map/toggleVisibility", layer); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/layers/Layerselect.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,85 @@ +<template> + <div> + <div class="form-check d-flex flex-row flex-start selection"> + <input + class="form-check-input" + @change="visibilityToggled" + :id="layername" + type="checkbox" + :checked="isVisible" + /> + <LegendElement + :layername="layername" + :layerindex="layerindex" + ></LegendElement> + <label class="layername form-check-label" @click="visibilityToggled">{{ + layername + }}</label> + </div> + <div v-if="isVisible && layername == 'Bottleneck isolines'"> + <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl" /> + </div> + </div> +</template> + +<style lang="scss" scoped> +.selection { + text-align: left; +} +.layername { + margin-left: $small-offset; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { HTTP } from "../../lib/http"; +import LegendElement from "./LegendElement.vue"; +export default { + props: ["layername", "layerindex", "isVisible"], + name: "layerselect", + data() { + return { + isolinesLegendImgUrl: "" + }; + }, + components: { + LegendElement + }, + methods: { + visibilityToggled() { + this.$emit("visibilityToggled", this.layerindex); + } + }, + created() { + // fetch legend image for bottleneck isolines + // TODO: move to store + if (this.layername == "Bottleneck isolines") { + const src = + "/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.0.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=sounding_results_contour_lines_geoserver&legend_options=columns:4;fontAntiAliasing:true"; + HTTP.get(src, { + headers: { + Accept: "image/png", + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + var urlCreator = window.URL || window.webkitURL; + this.isolinesLegendImgUrl = urlCreator.createObjectURL(response.data); + }); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/layers/LegendElement.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,124 @@ +<template> + <div :id="id" class="legendelement"></div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { mapGetters } from "vuex"; + +import { Map, View } from "ol"; +import Feature from "ol/Feature"; +import { Vector as VectorLayer } from "ol/layer.js"; +import { Vector as VectorSource } from "ol/source.js"; +import LineString from "ol/geom/LineString.js"; +import Point from "ol/geom/Point"; + +export default { + name: "legendelement", + props: ["layername", "layerindex"], + data: function() { + return { + myMap: null, + mapLayer: null + }; + }, + computed: { + ...mapGetters("map", ["getLayerByName"]), + id() { + return "legendelement" + this.layerindex; + }, + mstyle() { + if (this.mapLayer && this.mapLayer.data.getStyle) { + return this.mapLayer.data.getStyle(); + } + } + }, + watch: { + mstyle(newStyle, oldStyle) { + // only recreate if there already was a style before + if (oldStyle) { + let vector = this.createVectorLayer(); + + this.myMap.removeLayer(this.myMap.getLayers()[0]); + this.myMap.addLayer(vector); + } + } + }, + mounted() { + this.mapLayer = this.getLayerByName(this.layername); + if (this.mapLayer.data.getType() == "VECTOR") { + this.initMap(); + } else { + // TODO other tiles + } + }, + methods: { + initMap() { + let vector = this.createVectorLayer(); + + this.myMap = new Map({ + layers: [vector], + target: this.id, + controls: [], + interactions: [], + view: new View({ + center: [0, 0], + zoom: 3, + projection: "EPSG:4326" + }) + }); + }, + createVectorLayer() { + let mapStyle = this.mapLayer.data.getStyle(); + + let feature = new Feature({ + geometry: new LineString([[-1, 0.5], [0, 0], [0.7, 0], [1.3, -0.7]]) + }); + + // special case if we need to call the style function with a special + // parameter or to detect a point layer + if (this.mapLayer["forLegendStyle"]) { + if (this.mapLayer.forLegendStyle.point) { + feature.setGeometry(new Point([0, 0])); + } + mapStyle = this.mapLayer.data.getStyleFunction()( + feature, + this.mapLayer.forLegendStyle.resolution + ); + } + + // we could add extra properties here, if they are needed for + // the styling function in the future. An idea is to extend the + // this.mapLayer["forLegendStyle"] for it. + // FIXME, this is a special case for the Fairway Dimensions style + feature.set("level_of_service", ""); + return new VectorLayer({ + source: new VectorSource({ + features: [feature], + wrapX: false + }), + style: mapStyle + }); + } + } +}; +</script> + +<style lang="scss" scoped> +.legendelement { + max-height: 1.5rem; + width: 2rem; +} +</style>
--- a/client/src/components/map/Identify.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,124 +0,0 @@ -<template> - <div - :class="[ - 'box ui-element rounded bg-white text-nowrap', - { expanded: showIdentify } - ]" - > - <div style="width: 20rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="info" class="mr-2"></font-awesome-icon> - <translate>Identified</translate> - <font-awesome-icon - icon="times" - class="ml-auto text-muted" - @click="$store.commit('application/showIdentify', false)" - ></font-awesome-icon> - </h6> - <div class="d-flex flex-column features p-3 flex-grow-1 text-left"> - <div v-if="currentMeasurement"> - <b> - {{ currentMeasurement.quantity }} ({{ - currentMeasurement.unitSymbol - }}): - </b> - <br /> - <small>{{ currentMeasurement.value }}</small> - </div> - <div v-for="(feature, i) of identifiedFeatures" :key="feature.getId()"> - <div v-if="feature.getId()" :class="{ 'mt-2': i }"> - <strong> - {{ - feature.getId().replace(/[.][^.]*$/, "") - /* cut away everything from the last . to the end */ - }}: - </strong> - <small - v-for="(value, key) in prepareProperties(feature)" - :key="key" - > - <div v-if="value">{{ key }}:{{ value }}</div> - </small> - </div> - </div> - <div - v-if="!currentMeasurement && !identifiedFeatures.length" - class="text-muted small text-center my-auto" - > - <translate>No features identified.</translate> - </div> - </div> - <div class="versioninfo border-top p-3 text-left"> - <span v-translate="{ license: 'AGPL-3.0-or-later' }"> - This app uses <i>gemma</i>, which is Free Software under <br /> - %{ license } without warranty, see docs for details. - </span> - <br /> - <a href="https://hg.intevation.de/gemma/file/tip"> - <translate>source-code</translate> - </a> - {{ versionStr }} <br />© via donau. ⓔ Intevation. <br /> - <span v-translate="{ name: 'OpenSteetMap' }" - >Some data © - <a href="https://www.openstreetmap.org/copyright">%{ name }</a> - contributors. - </span> - <p v-translate="{ geoLicense: 'CC-BY-4.0' }"> - Uses - <a href="https://download.geonames.org/export/dump/readme.txt" - >GeoNames</a - > - under %{ geoLicense }. - </p> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.features { - max-height: 19rem; - overflow-y: auto; -} - -.versioninfo { - font-size: 60%; - white-space: normal; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Bernhard E. Reiter <bernhard.reiter@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; - -export default { - name: "identify", - computed: { - ...mapGetters("application", ["versionStr"]), - ...mapState("application", ["showIdentify"]), - ...mapState("map", ["identifiedFeatures", "currentMeasurement"]) - }, - methods: { - prepareProperties(feature) { - // return dict object with propertyname:plainvalue prepared for display - var properties = feature.getProperties(); - delete properties[feature.getGeometryName()]; - return properties; - } - } -}; -</script>
--- a/client/src/components/map/Main.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ -<template> - <div class="main d-flex flex-column"> - <Maplayer></Maplayer> - <FairwayProfile></FairwayProfile> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import Maplayer from "./Maplayer"; -import FairwayProfile from "./fairway/Fairwayprofile"; - -export default { - name: "mainview", - components: { - Maplayer, - FairwayProfile - } -}; -</script>
--- a/client/src/components/map/Maplayer.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,383 +0,0 @@ -<template> - <div id="map" :class="mapStyle"></div> -</template> - -<style lang="scss" scoped> -.nocursor { - cursor: none; -} - -.mapsplit { - height: 50vh; -} - -.mapfull { - height: 100vh; -} - -// the following css part is for browser-printing based pdf generation -@page { - size: A4 landscape !important; - margin: 4mm !important; - // according to https://www.w3.org/TR/css-page-3/#page-size-prop - // we shall now have 210 - 2*4 = 202 mm width and 297 - 2*4 = 289 mm height -} - -@media print { - .mapfull { - width: 2000px; - height: 2828px; - } - .mapsplit { - width: 2000px; - height: 2828px; - } -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * * Thomas Junk <thomas.junk@intevation.de> - * * Bernhard E. Reiter <bernhard.reiter@intevation.de> - */ -import { HTTP } from "../../lib/http"; -import { mapGetters, mapState } from "vuex"; -import "ol/ol.css"; -import { Map, View } from "ol"; -import { WFS, GeoJSON } from "ol/format.js"; -import { Stroke, Style, Fill } from "ol/style.js"; - -/* for the sake of debugging */ -/* eslint-disable no-console */ -export default { - name: "maplayer", - data() { - return { - projection: "EPSG:3857" - }; - }, - computed: { - ...mapGetters("map", ["getLayerByName", "getVSourceByName"]), - ...mapState("map", [ - "extent", - "layers", - "openLayersMap", - "lineTool", - "polygonTool", - "cutTool" - ]), - ...mapState("bottlenecks", ["selectedSurvey"]), - ...mapState("application", ["showSplitscreen"]), - mapStyle() { - return { - mapfull: !this.showSplitscreen, - mapsplit: this.showSplitscreen, - nocursor: this.hasActiveInteractions - }; - }, - hasActiveInteractions() { - return ( - (this.lineTool && this.lineTool.getActive()) || - (this.polygonTool && this.polygonTool.getActive()) || - (this.cutTool && this.cutTool.getActive()) - ); - } - }, - methods: { - buildVectorLoader(featureRequestOptions, endpoint, vectorSource) { - // build a function to be used for VectorSource.setLoader() - // make use of WFS().writeGetFeature to build the request - // and use our HTTP library to actually do it - // NOTE: a) the geometryName has to be given in featureRequestOptions, - // because we want to load depending on the bbox - // b) the VectorSource has to have the option strategy: bbox - featureRequestOptions["outputFormat"] = "application/json"; - var loader = function(extent, resolution, projection) { - featureRequestOptions["bbox"] = extent; - featureRequestOptions["srsName"] = projection.getCode(); - var featureRequest = new WFS().writeGetFeature(featureRequestOptions); - // DEBUG console.log(featureRequest); - HTTP.post( - endpoint, - new XMLSerializer().serializeToString(featureRequest), - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - } - ) - .then(response => { - var features = new GeoJSON().readFeatures( - JSON.stringify(response.data) - ); - vectorSource.addFeatures(features); - // console.log( - // "loaded", - // features.length, - // featureRequestOptions.featureTypes, - // "features" - // ); - // DEBUG console.log("loaded ", features, "for", vectorSource); - // eslint-disable-next-line - }) - .catch(() => { - vectorSource.removeLoadedExtent(extent); - }); - }; - return loader; - }, - updateBottleneckFilter(bottleneck_id, datestr) { - console.log("updating filter with", bottleneck_id, datestr); - const layer = this.getLayerByName("Bottleneck isolines"); - const wmsSrc = layer.data.getSource(); - const exists = bottleneck_id != "does_not_exist"; - - if (exists) { - wmsSrc.updateParams({ - cql_filter: - "date_info='" + - datestr + - "' AND bottleneck_id='" + - bottleneck_id + - "'" - }); - } - layer.isVisible = exists; - layer.data.setVisible(exists); - }, - onBeforePrint(/* evt */) { - // console.log("onBeforePrint(", evt ,")"); - // - // the following code shows how to get the current map canvas - // and change it, however this does not work well enough, as - // another mechanism seems to update the size again before the rendering - // for printing is done: - // console.log(this.openLayersMap.getViewport()); - // var canvas = this.openLayersMap.getViewport().getElementsByTagName("canvas")[0]; - // console.log(canvas); - // canvas.width=1000; - // canvas.height=1414; - // - // An experiment which also did not work: - // this.openLayersMap.setSize([1000, 1414]); // estimate portait DIN A4 - // - // according to documentation - // http://openlayers.org/en/latest/apidoc/module-ol_PluggableMap-PluggableMap.html#updateSize - // "Force a recalculation of the map viewport size. This should be called when third-party code changes the size of the map viewport." - // but did not help - // this.openLayersMap.updateSize(); - }, - onAfterPrint(/* evt */) { - // could be used to undo changes that have been done for printing - // though https://www.tjvantoll.com/2012/06/15/detecting-print-requests-with-javascript/ - // reported that this was not feasable (back then). - // console.log("onAfterPrint(", evt, ")"); - } - }, - watch: { - showSplitscreen() { - const map = this.openLayersMap; - this.$nextTick(() => { - map && map.updateSize(); - }); - }, - selectedSurvey(newSelectedSurvey) { - if (newSelectedSurvey) { - this.updateBottleneckFilter( - newSelectedSurvey.bottleneck_id, - newSelectedSurvey.date_info - ); - } else { - this.updateBottleneckFilter("does_not_exist", "1999-10-01"); - } - } - }, - mounted() { - let map = new Map({ - layers: [...this.layers.map(x => x.data)], - target: "map", - controls: [], - view: new View({ - center: [this.extent.lon, this.extent.lat], - zoom: this.extent.zoom, - projection: this.projection - }) - }); - map.on("moveend", event => { - const center = event.map.getView().getCenter(); - this.$store.commit("map/extent", { - lat: center[1], - lon: center[0], - zoom: event.map.getView().getZoom() - }); - }); - this.$store.dispatch("map/openLayersMap", map); - - // TODO make display of layers more dynamic, e.g. from a list - - // loading the full WFS layer, by not setting the loader function - // and without bboxStrategy - var featureRequest = new WFS().writeGetFeature({ - srsName: "EPSG:3857", - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["fairway_dimensions"], - outputFormat: "application/json" - }); - - // NOTE: loading the full fairway_dimensions makes sure - // that all are available for the intersection with the profile - HTTP.post( - "/internal/wfs", - new XMLSerializer().serializeToString(featureRequest), - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - } - ).then(response => { - this.getVSourceByName("Fairway Dimensions").addFeatures( - new GeoJSON().readFeatures(JSON.stringify(response.data)) - ); - // would scale to the extend of all resulting features - // this.openLayersMap.getView().fit(vectorSrc.getExtent()); - }); - - // load following layers with bboxStrategy (using our request builder) - var layer = null; - - layer = this.getLayerByName("Waterway Area"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featurePrefix: "ws-wamos", - featureTypes: ["ienc_wtware"], - geometryName: "geom" - }, - "/external/d4d", - layer.data.getSource() - ) - ); - - layer = this.getLayerByName("Waterway Axis"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featurePrefix: "ws-wamos", - featureTypes: ["ienc_wtwaxs"], - geometryName: "geom" - }, - "/external/d4d", - layer.data.getSource() - ) - ); - - layer = this.getLayerByName("Distance marks"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featurePrefix: "ws-wamos", - featureTypes: ["ienc_dismar"], - geometryName: "geom" //, - /* restrict loading approximately to extend of danube in Austria */ - // filter: bboxFilter("geom", [13.3, 48.0, 17.1, 48.6], "EPSG:4326") - }, - "/external/d4d", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName("Distance marks, Axis"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["distance_marks_geoserver"], - geometryName: "geom" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - - layer = this.getLayerByName("Waterway Area, named"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["hydro_seaare"], - geometryName: "geom" - }, - "/external/d4d", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName("Bottlenecks"); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["bottlenecks"], - geometryName: "area" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - HTTP.get("/system/style/Bottlenecks/stroke", { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - this.btlnStrokeC = response.data.code; - HTTP.get("/system/style/Bottlenecks/fill", { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - this.btlnFillC = response.data.code; - var newstyle = new Style({ - stroke: new Stroke({ - color: this.btlnStrokeC, - width: 4 - }), - fill: new Fill({ - color: this.btlnFillC - }) - }); - layer.data.setStyle(newstyle); - }) - .catch(error => { - console.log(error); - }); - }) - .catch(error => { - console.log(error); - }); - - window.addEventListener("beforeprint", this.onBeforePrint); - window.addEventListener("afterprint", this.onAfterPrint); - - // so none is shown - this.updateBottleneckFilter("does_not_exist", "1999-10-01"); - this.$store.dispatch("map/enableIdentifyTool"); - this.$store.dispatch("bottlenecks/loadBottlenecks"); - } -}; -</script>
--- a/client/src/components/map/Pdftool.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ -<template> - <div - :class="[ - 'box ui-element rounded bg-white text-nowrap', - { expanded: showPdfTool } - ]" - > - <div style="width: 20rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="file-pdf" class="mr-2"></font-awesome-icon - ><translate>Generate PDF</translate> - <font-awesome-icon - icon="times" - class="ml-auto text-muted" - @click="$store.commit('application/showPdfTool', false)" - ></font-awesome-icon> - </h6> - <div class="p-3"> - <b><translate>Chose format:</translate></b> - <select v-model="form.format" class="form-control d-block w-100"> - <option><translate>landscape</translate></option> - <option><translate>portrait</translate></option> - </select> - <small class="d-block my-2"> - <input - type="radio" - id="pdfexport-downloadtype-download" - value="download" - v-model="form.downloadType" - selected - /> - <label for="pdfexport-downloadtype-download" class="ml-1 mr-2" - ><translate>Download</translate></label - > - <input - type="radio" - id="pdfexport-downloadtype-open" - value="open" - v-model="form.downloadType" - /> - <label for="pdfexport-downloadtype-open" class="ml-1" - ><translate>Open in new window</translate></label - > - </small> - <button - @click="download" - type="button" - class="btn btn-sm btn-info d-block w-100" - > - <translate>Generate PDF</translate> - </button> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; -//import { HTTP } from "../application/lib/http"; - -export default { - name: "pdftool", - data() { - return { - form: { - format: "landscape", - downloadType: "download" - } - }; - }, - computed: { - ...mapState("application", ["showPdfTool"]), - ...mapState("bottlenecks", ["selectedSurvey"]) - }, - methods: { - download() { - // generate PDF and open it - // TODO: replace this src with an API reponse after actually generating PDFs - let src = - this.form.format === "landscape" - ? "/img/PrintTemplate-Var2-Landscape.pdf" - : "/img/PrintTemplate-Var2-Portrait.pdf"; - - let a = document.createElement("a"); - a.href = src; - - if (this.form.downloadType === "download") - a.download = src.substr(src.lastIndexOf("/") + 1); - else a.target = "_blank"; - - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - } - } -}; -</script>
--- a/client/src/components/map/Search.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,292 +0,0 @@ -<template> - <div :class="searchbarContainerStyle"> - <div class="input-group-prepend m-0 d-print-none"> - <span @click="toggleSearchbar" :class="searchButtonStyle" for="search"> - <font-awesome-icon icon="search"></font-awesome-icon> - </span> - </div> - <div - :class="[ - 'searchgroup', - { - 'searchgroup-collapsed': !showSearchbar, - big: - showContextBox && - ['bottlenecks', 'staging'].indexOf(contextBoxContent) !== -1 - } - ]" - > - <input - @keyup.enter="takeFirstSearchresult" - id="search" - v-model="searchQuery" - type="text" - :class="searchInputStyle" - /> - </div> - <div - v-if="showSearchbar && searchResults !== null && !showContextBox" - class="searchresults border-top ui-element bg-white rounded-bottom d-print-none position-absolute" - > - <div - v-for="entry of searchResults" - :key="entry.name" - class="border-top text-left" - > - <a - href="#" - @click.prevent="moveToSearchResult(entry)" - class="p-2 d-block text-nowrap" - > - <font-awesome-icon - icon="ship" - v-if="entry.type === 'bottleneck'" - class="mr-1" - fixed-width - /> - <font-awesome-icon - icon="water" - v-if="entry.type === 'rhm'" - class="mr-1" - fixed-width - /> - <font-awesome-icon - icon="city" - v-if="entry.type === 'city'" - class="mr-1" - fixed-width - /> - {{ entry.name }} - </a> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.searchcontainer { - opacity: 0.96; -} - -.searchcontainer .searchbar { - border-top-left-radius: 0 !important; - border-bottom-left-radius: 0 !important; -} - -.searchgroup { - margin-left: -3px; - transition: width 0.3s; - width: 300px; - overflow: hidden; -} - -.searchgroup.big { - width: 571px; -} - -.searchgroup-collapsed { - width: 0; -} - -.searchbar { - height: 2rem !important; - box-shadow: none !important; -} - -.searchbar.rounded-top-right { - border-radius: 0 !important; - border-top-right-radius: 0.25rem !important; -} - -.searchlabel.rounded-top-left { - border-radius: 0 !important; - border-top-left-radius: 0.25rem !important; -} - -.input-group-text { - height: 2rem; - width: 2rem; -} - -.input-group-prepend svg path { - fill: #666; -} - -.searchresults { - box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2); - top: 2rem; - left: 0; - right: 0; - max-height: 24rem; - overflow: auto; -} - -.searchresults > div:first-child { - border-top: 0 !important; -} - -.searchresults a { - text-decoration: none; -} - -.searchresults a:hover { - background: #f8f8f8; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import debounce from "lodash.debounce"; -import { mapState } from "vuex"; - -import { displayError } from "../../lib/errors.js"; -import { HTTP } from "../../lib/http"; - -const setFocus = () => document.querySelector("#search").focus(); - -export default { - name: "search", - data() { - return { - searchQueryIsDirty: false, - searchResults: null, - isSearching: false - }; - }, - computed: { - ...mapState("application", [ - "showSearchbar", - "showContextBox", - "contextBoxContent" - ]), - searchQuery: { - get() { - return this.$store.state.application.searchQuery; - }, - set(value) { - this.$store.commit("application/searchQuery", value); - } - }, - searchIndicator: function() { - if (this.isSearching) { - return "⟳"; - } else if (this.searchQueryIsDirty) { - return ""; - } else { - return "✓"; - } - }, - searchbarContainerStyle() { - return [ - "input-group searchcontainer shadow-xs", - { - "d-flex": this.contextBoxContent !== "imports", - "d-none": this.contextBoxContent === "imports" && this.showContextBox - } - ]; - }, - searchInputStyle() { - return [ - "form-control ui-element search searchbar d-print-none border-0", - { "rounded-top-right": this.showContextBox || this.searchResults } - ]; - }, - searchButtonStyle() { - return [ - "ui-element input-group-text p-0 d-flex border-0 justify-content-center searchlabel bg-white d-print-none", - { - rounded: !this.showSearchbar, - "rounded-left": this.showSearchbar, - "rounded-top-left": - this.showSearchbar && (this.showContextBox || this.searchResults) - } - ]; - } - }, - watch: { - searchQuery: function() { - this.searchQueryIsDirty = true; - this.triggerSearch(); - } - }, - methods: { - takeFirstSearchresult() { - if (!this.searchResults || this.searchResults.length != 1) return; - this.moveToSearchResult(this.searchResults[0]); - }, - triggerSearch: debounce(function() { - this.doSearch(); - }, 500), - doSearch() { - this.isCalculating = true; - this.searchResults = null; - - if (this.searchQuery == "") { - return; - } - - HTTP.post( - "/search", - { string: this.searchQuery }, - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - } - ) - .then(response => { - // console.log("got:", response.data); - this.searchResults = response.data; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - - this.isCalculating = false; - this.searchQueryIsDirty = false; - }, - moveToSearchResult(resultEntry) { - // DEBUG console.log("Moving to", resultEntry); - if (resultEntry.geom.type == "Point") { - let zoom = 11; - if (resultEntry.type === "bottleneck") zoom = 17; - if (resultEntry.type === "rhm") zoom = 15; - if (resultEntry.type === "city") zoom = 13; - - this.$store.commit("map/moveMap", { - coordinates: resultEntry.geom.coordinates, - zoom, - preventZoomOut: true - }); - } - // this.searchQuery = ""; // clear search query again - this.toggleSearchbar(); - }, - toggleSearchbar() { - if (!this.showContextBox) { - if (!this.showSearchbar) { - setTimeout(setFocus, 300); - } - this.$store.commit("application/showSearchbar", !this.showSearchbar); - } - } - } -}; -</script>
--- a/client/src/components/map/Zoom.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -<template> - <div - class="d-flex buttoncontainer shadow-xs mb-3 position-absolute" - :style="showSplitscreen ? 'margin-bottom: 51vh !important' : ''" - > - <button - class="zoomButton border-0 bg-white rounded-left ui-element" - @click="zoomOut" - > - <font-awesome-icon icon="minus"></font-awesome-icon> - </button> - <button - class="zoomButton border-0 bg-white rounded-right ui-element border-right" - @click="zoomIn" - > - <font-awesome-icon icon="plus"></font-awesome-icon> - </button> - </div> -</template> - -<style lang="scss" scoped> -.buttoncontainer { - bottom: 0; - left: 50%; - margin-left: -$icon-width; -} - -.zoomButton { - min-height: $icon-width; - min-width: $icon-width; - z-index: 1; - outline: none; - color: #666; -} -</style> -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus@intevation.de> - * Thomas Junk <thomas.junk@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "zoom", - computed: { - ...mapState("map", ["openLayersMap"]), - ...mapState("application", ["showSplitscreen"]), - zoomLevel: { - get() { - return this.openLayersMap.getView().getZoom(); - }, - set(value) { - this.openLayersMap.getView().animate({ zoom: value, duration: 300 }); - } - } - }, - methods: { - zoomIn() { - this.zoomLevel = this.zoomLevel + 1; - }, - zoomOut() { - this.zoomLevel = this.zoomLevel - 1; - } - } -}; -</script>
--- a/client/src/components/map/contextbox/Bottlenecks.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,324 +0,0 @@ -<template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="ship" class="mr-2"></font-awesome-icon> - <translate>Bottlenecks</translate> - </h6> - <div class="row p-2 text-left small"> - <div class="col-5"> - <a href="#" @click="sortBy('name')" class="sort-link"> - <translate>Name</translate> - </a> - <font-awesome-icon - :icon="sortIcon" - class="ml-1" - v-if="sortColumn === 'name'" - ></font-awesome-icon> - </div> - <div class="col-2"> - <a href="#" @click="sortBy('latestMeasurement')" class="sort-link"> - <translate>Latest</translate> <br /> - <translate>Measurement</translate> - </a> - <font-awesome-icon - :icon="sortIcon" - class="ml-1" - v-if="sortColumn === 'latestMeasurement'" - ></font-awesome-icon> - </div> - <div class="col-3"> - <a href="#" @click="sortBy('chainage')" class="sort-link"> - <translate>Chainage</translate> - </a> - <font-awesome-icon - :icon="sortIcon" - class="ml-1" - v-if="sortColumn === 'chainage'" - ></font-awesome-icon> - </div> - <div class="col-2"></div> - </div> - <div - class="bottleneck-list small text-left" - :style="'max-height: ' + (showSplitscreen ? 18 : 35) + 'rem'" - v-if="filteredAndSortedBottlenecks().length" - > - <div - v-for="bottleneck in filteredAndSortedBottlenecks()" - :key="bottleneck.properties.name" - class="border-top row bottleneck-row mx-0" - > - <div class="col-5 py-2 text-left"> - <a href="#" @click="selectBottleneck(bottleneck)">{{ - bottleneck.properties.name - }}</a> - </div> - <div class="col-2 py-2"> - {{ formatSurveyDate(bottleneck.properties.current) }} - </div> - <div class="col-3 py-2"> - {{ - displayCurrentChainage( - bottleneck.properties.from, - bottleneck.properties.to - ) - }} - </div> - <div class="col-2 pr-0 text-right"> - <button - type="button" - class="btn btn-sm btn-info rounded-0 h-100" - @click="loadSurveys(bottleneck.properties.name)" - v-if="bottleneck.properties.current" - > - <font-awesome-icon - icon="spinner" - fixed-width - spin - v-if="loading === bottleneck.properties.name" - ></font-awesome-icon> - <font-awesome-icon - icon="angle-down" - fixed-width - v-if=" - loading !== bottleneck.properties.name && - openBottleneck !== bottleneck.properties.name - " - ></font-awesome-icon> - <font-awesome-icon - icon="angle-up" - fixed-width - v-if=" - loading !== bottleneck.properties.name && - openBottleneck === bottleneck.properties.name - " - ></font-awesome-icon> - </button> - </div> - <div - :class="[ - 'col-12 p-0', - 'surveys', - { open: openBottleneck === bottleneck.properties.name } - ]" - > - <a - href="#" - class="d-block px-3 py-2" - v-for="(survey, index) in openBottleneckSurveys" - :key="index" - @click="selectSurvey(survey, bottleneck)" - >{{ formatSurveyDate(survey.date_info) }}</a - > - </div> - </div> - </div> - <div v-else class="small text-center py-3 border-top"> - <translate>No results.</translate> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; -import { HTTP } from "../../../lib/http"; -import { displayError } from "../../../lib/errors.js"; -import { formatSurveyDate } from "../../../lib/date.js"; - -export default { - name: "bottlenecks", - data() { - return { - sortColumn: "name", - sortDirection: "ASC", - openBottleneck: null, - openBottleneckSurveys: null, - loading: null - }; - }, - computed: { - ...mapState("application", [ - "searchQuery", - "showSearchbarLastState", - "showSplitscreen" - ]), - ...mapState("bottlenecks", ["bottlenecks"]), - sortIcon() { - return this.sortDirection === "ASC" - ? "sort-amount-down" - : "sort-amount-up"; - } - }, - methods: { - formatSurveyDate(date) { - return formatSurveyDate(date); - }, - filteredAndSortedBottlenecks() { - return this.bottlenecks - .filter(bn => { - return bn.properties.name - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - }) - .sort((bnA, bnB) => { - switch (this.sortColumn) { - case "name": - if ( - bnA.properties.name.toLowerCase() < - bnB.properties.name.toLowerCase() - ) - return this.sortDirection === "ASC" ? -1 : 1; - if ( - bnA.properties.name.toLowerCase() > - bnB.properties.name.toLowerCase() - ) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - - case "latestMeasurement": { - if ( - (bnA.properties.current || "") < (bnB.properties.current || "") - ) - return this.sortDirection === "ASC" ? -1 : 1; - if ( - (bnA.properties.current || "") > (bnB.properties.current || "") - ) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - } - - case "chainage": - if (bnA.properties.from < bnB.properties.from) - return this.sortDirection === "ASC" ? -1 : 1; - if (bnA.properties.from > bnB.properties.from) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - - default: - return 0; - } - }); - }, - selectSurvey(survey, bottleneck) { - this.$store - .dispatch( - "bottlenecks/setSelectedBottleneck", - bottleneck.properties.name - ) - .then(() => { - this.$store.commit("bottlenecks/selectedSurvey", survey); - }) - .then(() => { - this.$store.commit("map/moveMap", { - coordinates: bottleneck.geometry.coordinates, - zoom: 17, - preventZoomOut: true - }); - }); - }, - selectBottleneck(bottleneck) { - this.$store - .dispatch( - "bottlenecks/setSelectedBottleneck", - bottleneck.properties.name - ) - .then(() => { - this.$store.commit("bottlenecks/setFirstSurveySelected"); - }) - .then(() => { - this.$store.commit("map/moveMap", { - coordinates: bottleneck.geometry.coordinates, - zoom: 17, - preventZoomOut: true - }); - }); - }, - sortBy(column) { - this.sortColumn = column; - this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC"; - }, - loadSurveys(name) { - this.openBottleneckSurveys = null; - if (name === this.openBottleneck) { - this.openBottleneck = null; - } else { - this.openBottleneck = name; - this.loading = name; - - HTTP.get("/surveys/" + name, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - }) - .then(response => { - this.openBottleneckSurveys = response.data.surveys.sort((a, b) => { - return a.date_info < b.date_info ? 1 : -1; - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }) - .finally(() => (this.loading = null)); - } - }, - displayCurrentChainage(from, to) { - return from / 10 + " - " + to / 10; - } - }, - mounted() { - this.$store.dispatch("bottlenecks/loadBottlenecks"); - } -}; -</script> - -<style lang="scss" scoped> -.bottleneck-list { - overflow-y: auto; -} - -.bottleneck-list .bottleneck-row a { - text-decoration: none; -} - -.bottleneck-list .bottleneck-row:hover { - background: #fbfbfb; -} - -.surveys { - max-height: 0; - min-height: 0; - overflow: hidden; -} - -.surveys a:hover { - background: #f3f3f3; -} - -.surveys.open { - max-height: 250px; - overflow: auto; -} - -.sort-link { - color: #444; - font-weight: bold; -} -</style>
--- a/client/src/components/map/contextbox/Contextbox.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -<template> - <div :class="style"> - <div @click="close" class="ui-element close-contextbox text-muted"> - <font-awesome-icon icon="times"></font-awesome-icon> - </div> - <Bottlenecks v-if="contextBoxContent === 'bottlenecks'"></Bottlenecks> - <Importsounding v-if="contextBoxContent === 'imports'"></Importsounding> - <Staging v-if="contextBoxContent === 'staging'"></Staging> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "contextbox", - components: { - Bottlenecks: () => import("./Bottlenecks"), - Importsounding: () => import("./ImportSoundingresults.vue"), - Staging: () => import("./Staging.vue") - }, - computed: { - ...mapState("application", [ - "showSearchbarLastState", - "contextBoxContent", - "showContextBox" - ]), - style() { - return [ - "ui-element shadow-xs contextbox", - { - contextboxcollapsed: !this.showContextBox, - contextboxextended: this.showContextBox, - "rounded-bottom": this.contextBoxContent !== "imports", - rounded: this.contextBoxContent === "imports" - } - ]; - } - }, - methods: { - close() { - this.$store.commit("application/showContextBox", false); - this.$store.commit( - "application/showSearchbar", - this.showSearchbarLastState - ); - } - } -}; -</script> - -<style lang="scss" scoped> -.contextbox { - position: relative; - background-color: #ffffff; - opacity: $slight-transparent; - transition: max-width 0.3s, max-height 0.3s; - overflow: hidden; - background: #fff; -} -.contextbox > div:last-child { - width: 600px; -} - -.contextboxcollapsed { - max-width: 0; - max-height: 0; -} - -.contextboxextended { - max-width: 600px; - max-height: 640px; -} - -.close-contextbox { - position: absolute; - z-index: 2; - right: 0; - top: 7px; - height: $icon-width; - width: $icon-height; -} -</style>
--- a/client/src/components/map/contextbox/ImportSoundingresults.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,379 +0,0 @@ -<template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="upload" class="mr-2"></font-awesome-icon> - <translate>Import Soundingresults</translate> - </h6> - <div v-if="editState" class="ml-auto mr-auto mt-4 w-95"> - <div class="d-flex flex-column"> - <div class="d-flex flex-row"> - <div class="mt-1 text-left w-50 ml-2 mr-4"> - <small class="text-muted"> - <translate>Bottleneck</translate> - </small> - <select v-model="bottleneck" class="custom-select"> - <option - v-for="bottleneck in availableBottlenecks" - :key="bottleneck" - >{{ bottleneck }}</option - > - </select> - <span class="text-danger"> - <small v-if="!bottleneck"> - <translate>Please select a bottleneck</translate> - </small> - </span> - </div> - <div class="d-flex flex-column mt-1 text-left w-50 mr-2"> - <small class="text-muted"> - <translate>Projection</translate> (EPSG) - </small> - <input - class="form-control" - v-model="projection" - value="4326" - placeholder="e.g. 4326" - type="number" - /> - <span class="text-left text-danger"> - <small v-if="!projection"> - <translate>Please enter a projection</translate> - </small> - </span> - </div> - </div> - <div class="d-flex flex-row"> - <div class="mt-1 text-left w-50 ml-2 mr-4"> - <small class="text-muted"> - <translate>Depthreference</translate> - </small> - <select - v-model="depthReference" - class="custom-select" - id="depthreference" - > - <option - v-for="option in this.$options.depthReferenceOptions" - :key="option" - >{{ option }}</option - > - </select> - <span class="text-left text-danger"> - <small v-if="!depthReference"> - <translate>Please enter a reference</translate> - </small> - </span> - </div> - <div class="mt-1 text-left w-50 mr-2"> - <small class="text-muted"> <translate>Date</translate> </small> - <input - id="importdate" - type="date" - class="form-control" - placeholder="Date of import" - aria-label="bottleneck" - aria-describedby="bottlenecklabel" - v-model="importDate" - /> - <span class="text-left text-danger"> - <small v-if="!importDate"> - <translate>Please enter a date</translate> - </small> - </span> - </div> - </div> - </div> - <div class="ml-2 mt-2 text-left"> - <small v-for="(message, index) in messages" :key="index"> - {{ message }} - </small> - </div> - </div> - <div class="w-95 ml-auto mr-auto mt-4 mb-4"> - <div v-if="uploadState" class="d-flex flex-row input-group mb-4"> - <div class="custom-file"> - <input - accept=".zip" - type="file" - @change="fileSelected" - class="custom-file-input" - id="uploadFile" - /> - <label class="custom-file-label" for="uploadFile"> - {{ uploadLabel }} - </label> - </div> - </div> - <div class="buttons text-right"> - <a - v-if="editState" - download="meta.json" - :href="dataLink" - class="btn btn-outline-info pull-left" - > - <translate>Download Meta.json</translate> - </a> - <button - v-if="editState" - @click="deleteTempData" - class="btn btn-danger" - type="button" - > - <translate>Cancel Upload</translate> - </button> - <button - :disabled="disableUploadButton" - @click="submit" - class="btn btn-info" - type="button" - > - {{ uploadState ? Upload : Confirm }} - </button> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { HTTP } from "../../../lib/http"; -import { displayError, displayInfo } from "../../../lib/errors.js"; -import { mapState } from "vuex"; - -const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; - -export default { - name: "imports", - data() { - return { - importState: IMPORTSTATE.UPLOAD, - depthReference: "", - bottleneck: "", - projection: "", - importDate: "", - uploadLabel: this.$gettext("choose .zip- file"), - uploadFile: null, - disableUpload: false, - token: null, - messages: [] - }; - }, - methods: { - initialState() { - this.importState = IMPORTSTATE.UPLOAD; - this.depthReference = ""; - this.bottleneck = ""; - this.projection = ""; - this.importDate = ""; - this.uploadLabel = this.$gettext("choose .zip- file"); - this.uploadFile = null; - this.disableUpload = false; - this.token = null; - this.messages = []; - }, - fileSelected(e) { - const files = e.target.files || e.dataTransfer.files; - if (!files) return; - this.uploadLabel = files[0].name; - this.uploadFile = files[0]; - }, - deleteTempData() { - HTTP.delete("/imports/soundingresult-upload/" + this.token, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - } - }) - .then(() => { - this.initialState(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - submit() { - if (!this.uploadFile || this.disableUpload) return; - if (this.importState === IMPORTSTATE.UPLOAD) { - this.upload(); - } else { - this.confirm(); - } - }, - upload() { - let formData = new FormData(); - formData.append("soundingresult", this.uploadFile); - HTTP.post("/imports/soundingresult-upload", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(response => { - if (response.data.meta) { - const { bottleneck, date, epsg } = response.data.meta; - const depthReference = response.data.meta["depth-reference"]; - this.bottleneck = bottleneck; - this.depthReference = depthReference; - this.importDate = new Date(date).toISOString().split("T")[0]; - this.projection = epsg; - } - this.importState = IMPORTSTATE.EDIT; - this.token = response.data.token; - this.messages = response.data.messages; - }) - .catch(error => { - const { status, data } = error.response; - const messages = data.messages ? data.messages.join(", ") : ""; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${messages}` - }); - }); - }, - confirm() { - let formData = new FormData(); - formData.append("token", this.token); - if (this.bottleneck) formData.append("bottleneck", this.bottleneck); - if (this.importDate) - formData.append("date", this.importDate.split("T")[0]); - if (this.depthReference) - formData.append("depth-reference", this.depthReference); - if (this.projection) formData.append("", this.projection); - - HTTP.post("/imports/soundingresult", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(() => { - displayInfo({ - title: this.$gettext("Import"), - message: this.$gettext("Starting import for ") + this.bottleneck - }); - this.initialState(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - mounted() { - this.$store.dispatch("bottlenecks/loadBottlenecks"); - }, - watch: { - showContextBox() { - if (!this.showContextBox && this.token) this.deleteTempData(); - } - }, - computed: { - ...mapState("application", ["showContextBox"]), - ...mapState("bottlenecks", ["bottlenecks"]), - disableUploadButton() { - if (this.importState === IMPORTSTATE.UPLOAD) return this.disableUpload; - if ( - !this.bottleneck || - !this.importDate || - !this.depthReference || - !this.projection - ) - return true; - return this.disableUpload; - }, - availableBottlenecks() { - return this.bottlenecks.map(x => x.properties.name); - }, - editState() { - return this.importState === IMPORTSTATE.EDIT; - }, - uploadState() { - return this.importState === IMPORTSTATE.UPLOAD; - }, - Upload() { - return this.$gettext("Upload"); - }, - Confirm() { - return this.$gettext("Confirm"); - }, - dataLink() { - return ( - "data:text/json;charset=utf-8," + - encodeURIComponent( - JSON.stringify({ - depthReference: this.depthReference, - bottleneck: this.bottleneck, - date: this.importDate - }) - ) - ); - } - }, - depthReferenceOptions: [ - "", - // "NAP", - // "KP", - // "FZP", - // "ADR", - // "TAW", - // "PUL", - // "NGM", - // "ETRS", - // "POT", - // "LDC", - // "HDC", - // "ZPG", - // "GLW", - // "HSW", - // "LNW", - // "HNW", - // "IGN", - // "WGS", - "RN" //, - // "HBO" - ] -}; -</script> - -<style lang="scss" scoped> -.projectionLabel { - margin-left: $small-offset; -} - -.depthreferencelabel { - margin-left: $small-offset; -} - -.offset-r { - margin-right: $small-offset; -} - -.buttons button { - margin-left: $offset !important; -} - -.label-text { - width: 5rem; - text-align: left; - line-height: 2.25rem; -} -</style>
--- a/client/src/components/map/contextbox/Staging.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,236 +0,0 @@ -<template> - <div class="w-90 stagingcard"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon - class="mr-2" - icon="clipboard-check" - ></font-awesome-icon> - <translate>Staging Area</translate> - </h6> - <table class="table"> - <thead> - <tr> - <th><translate>Name</translate></th> - <th><translate>Type</translate></th> - <th><translate>Date</translate></th> - <th><translate>Imported</translate></th> - <th><translate>Username</translate></th> - <th> </th> - <th> </th> - </tr> - </thead> - <tbody v-if="filteredData.length"> - <tr :key="data.id" v-for="data in filteredData"> - <td> - <a @click="zoomTo(data.id)" href="#">{{ - data.summary.bottleneck - }}</a> - </td> - <td>{{ data.kind.toUpperCase() }}</td> - <td>{{ formatSurveyDate(data.summary.date) }}</td> - <td>{{ formatSurveyDate(data.enqueued.split("T")[0]) }}</td> - <td>{{ data.user }}</td> - <td> - <button - :class="{ - btn: true, - 'btn-sm': true, - 'btn-outline-success': needsApproval(data) || isRejected(data), - 'btn-success': isApproved(data) - }" - @click="toggleApproval(data.id, $options.STATES.APPROVED)" - > - <font-awesome-icon icon="check"></font-awesome-icon> - </button> - </td> - <td> - <button - :class="{ - btn: true, - 'btn-sm': true, - 'btn-outline-danger': needsApproval(data) || isApproved(data), - 'btn-danger': isRejected(data) - }" - @click="toggleApproval(data.id, $options.STATES.REJECTED)" - > - <font-awesome-icon icon="times"></font-awesome-icon> - </button> - </td> - </tr> - </tbody> - <tbody v-else> - <tr> - <td class="text-center" colspan="6"> - <translate>No results.</translate> - </td> - </tr> - </tbody> - </table> - <div class="p-3" v-if="filteredData.length"> - <button @click="confirmReview" class="confirm-button btn btn-info"> - <translate>Confirm</translate> - </button> - </div> - <div class="p-3"> - <button @click="loadData" class="refresh btn btn-dark">Refresh</button> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus@intevation.de> - */ -import { mapState } from "vuex"; -import { HTTP } from "../../../lib/http.js"; -import { STATES } from "../../../store/imports.js"; -import { displayError, displayInfo } from "../../../lib/errors.js"; -import { formatSurveyDate } from "../../../lib/date.js"; - -export default { - data() { - return {}; - }, - mounted() { - this.loadData(); - }, - computed: { - ...mapState("application", ["searchQuery"]), - ...mapState("imports", ["staging"]), - filteredData() { - return this.staging.filter(data => { - const result = [data.id + "", data.enqueued, data.kind, data.user].some( - x => x.toLowerCase().includes(this.searchQuery.toLowerCase()) - ); - return result; - }); - } - }, - STATES: STATES, - methods: { - formatSurveyDate(date) { - return formatSurveyDate(date); - }, - loadData() { - this.$store.dispatch("imports/getStaging").catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }, - confirmReview() { - const reviewResults = this.staging - .filter(x => x.status !== STATES.NEEDSAPPROVAL) - .map(r => { - return { - id: r.id, - state: r.status - }; - }); - if (!reviewResults.length) return; - HTTP.patch("/imports", reviewResults, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "application/json" - } - }) - .then(response => { - const messages = response.data - .map(x => { - if (x.message) return x.message; - if (x.error) return x.error; - }) - .join("\n\n"); - displayInfo({ - title: "Staging Area", - message: messages, - options: { - timeout: 0, - buttons: [{ text: "Ok", action: null, bold: true }] - } - }); - this.loadData(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }, - needsApproval(item) { - return item.status === STATES.NEEDSAPPROVAL; - }, - isRejected(item) { - return item.status === STATES.REJECTED; - }, - isApproved(item) { - return item.status === STATES.APPROVED; - }, - zoomTo(id) { - if (!id) return; - const soundingResult = this.filteredData.filter(x => x.id == id)[0]; - const { lat, lon, bottleneck, date } = soundingResult.summary; - const coordinates = [lat, lon]; - - this.$store.commit("map/moveMap", { - coordinates: coordinates, - zoom: 17, - preventZoomOut: true - }); - this.$store - .dispatch("bottlenecks/setSelectedBottleneck", bottleneck) - .then(() => { - this.$store.commit("bottlenecks/setSelectedSurveyByDate", date); - }); - }, - toggleApproval(id, newStatus) { - this.$store.commit("imports/toggleApproval", { - id: id, - newStatus: newStatus - }); - } - } -}; -</script> - -<style lang="scss" scoped> -.refresh { - position: absolute; - left: $offset; - bottom: $offset; -} -.table th, -td { - font-size: 0.9rem; - border-top: 0px !important; - border-bottom-width: 1px; - text-align: left; - padding: 0.5rem !important; -} - -.stagingcard { - position: relative; - min-height: 150px; -} - -.confirm-button { - position: absolute; - right: $offset; - bottom: $offset; -} -</style>
--- a/client/src/components/map/fairway/Fairwayprofile.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,413 +0,0 @@ -<template> - <div :class="['position-relative', { show: showSplitscreen }]"> - <button - class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle" - @click="$store.commit('application/showSplitscreen', false)" - v-if="showSplitscreen" - > - <font-awesome-icon icon="angle-down" /> - </button> - <button - class="rounded-bottom bg-white border-0 position-absolute clear-selection" - @click="$store.dispatch('fairwayprofile/clearSelection')" - v-if="showSplitscreen" - > - <font-awesome-icon icon="times" /> - </button> - <div class="profile bg-white position-relative d-flex flex-column"> - <h5 - class="headline border-bottom mb-0 py-2" - v-if="selectedBottleneck && selectedSurvey" - > - {{ selectedBottleneck }} ({{ selectedSurvey.date_info }}) - </h5> - <div class="d-flex flex-fill"> - <div - class="loading d-flex justify-content-center align-items-center" - v-if="surveysLoading || profileLoading" - > - <font-awesome-icon icon="spinner" spin /> - </div> - <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.profile { - width: 100vw; - height: 0; - overflow: hidden; - z-index: 2; -} - -.splitscreen-toggle, -.clear-selection { - width: 2rem; - height: 2rem; - margin-top: 8px; - z-index: 3; - outline: none; -} - -.splitscreen-toggle svg path, -.clear-selection svg path { - fill: #666; -} - -.splitscreen-toggle { - right: 2.5rem; -} - -.clear-selection { - right: 0.5rem; -} - -.show .profile { - height: 50vh; -} - -.loading { - background: rgba(255, 255, 255, 0.96); - position: absolute; - z-index: 99; - top: 0; - right: 0; - bottom: 0; - left: 0; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import * as d3 from "d3"; -import { mapState, mapGetters } from "vuex"; -import debounce from "debounce"; - -const GROUND_COLOR = "#4A2F06"; - -export default { - name: "fairwayprofile", - data() { - return { - coordinatesInput: "", - coordinatesSelect: null, - cutLabel: "", - showLabelInput: false, - width: null, - height: null, - margin: { - top: 20, - right: 40, - bottom: 30, - left: 40 - } - }; - }, - computed: { - ...mapGetters("fairwayprofile", ["totalLength"]), - ...mapState("application", ["showSplitscreen"]), - ...mapState("fairwayprofile", [ - "startPoint", - "endPoint", - "currentProfile", - "additionalSurvey", - "minAlt", - "maxAlt", - "fairwayCoordinates", - "waterLevels", - "selectedWaterLevel", - "profileLoading" - ]), - ...mapState("bottlenecks", [ - "selectedBottleneck", - "selectedSurvey", - "surveysLoading" - ]), - currentData() { - if ( - !this.selectedSurvey || - !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info) - ) - return []; - return this.currentProfile[this.selectedSurvey.date_info].points; - }, - additionalData() { - if ( - !this.additionalSurvey || - !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info) - ) - return []; - return this.currentProfile[this.additionalSurvey.date_info].points; - }, - waterColor() { - const result = this.waterLevels.find( - x => x.level === this.selectedWaterLevel - ); - return result.color; - }, - xScale() { - return [0, this.totalLength]; - }, - yScaleLeft() { - const hi = Math.max(this.maxAlt, this.selectedWaterLevel); - return [this.minAlt, hi]; - }, - yScaleRight() { - const DELTA = this.maxAlt * 1.1 - this.maxAlt; - return [this.maxAlt * 1 + DELTA, -DELTA]; - } - }, - watch: { - currentData() { - this.drawDiagram(); - }, - additionalData() { - this.drawDiagram(); - }, - width() { - this.drawDiagram(); - }, - height() { - this.drawDiagram(); - }, - waterLevels() { - this.drawDiagram(); - }, - selectedWaterLevel() { - this.drawDiagram(); - }, - fairwayCoordinates() { - this.drawDiagram(); - } - }, - methods: { - drawDiagram() { - this.coordinatesSelect = null; - const chartDiv = document.querySelector(".fairwayprofile"); - d3.select(".fairwayprofile svg").remove(); - this.scaleFairwayProfile(); - let svg = d3.select(chartDiv).append("svg"); - svg.attr("width", this.width); - svg.attr("height", this.height); - const width = this.width - this.margin.right - 1.5 * this.margin.left; - const height = this.height - this.margin.top - 2 * this.margin.bottom; - const currentData = this.currentData; - const additionalData = this.additionalData; - const { xScale, yScaleRight, graph } = this.generateCoordinates( - svg, - height, - width - ); - this.drawWaterlevel({ graph, xScale, yScaleRight, height }); - this.drawLabels({ graph, height }); - this.drawFairway({ graph, xScale, yScaleRight }); - if (currentData) { - this.drawProfile({ - graph, - xScale, - yScaleRight, - currentData, - height, - color: GROUND_COLOR, - strokeColor: "black", - opacity: 1 - }); - } - if (additionalData) { - this.drawProfile({ - graph, - xScale, - yScaleRight, - currentData: additionalData, - height, - color: GROUND_COLOR, - strokeColor: "#943007", - opacity: 0.6 - }); - } - }, - drawFairway({ graph, xScale, yScaleRight }) { - for (let coordinates of this.fairwayCoordinates) { - const [startPoint, endPoint, depth] = coordinates; - let fairwayArea = d3 - .area() - .x(function(d) { - return xScale(d.x); - }) - .y0(yScaleRight(0)) - .y1(function(d) { - return yScaleRight(d.y); - }); - graph - .append("path") - .datum([{ x: startPoint, y: depth }, { x: endPoint, y: depth }]) - .attr("fill", "#002AFF") - .attr("stroke-opacity", 0.65) - .attr("fill-opacity", 0.65) - .attr("stroke", "#FFD20D") - .attr("d", fairwayArea); - } - }, - drawLabels({ graph, height }) { - graph - .append("text") - .attr("transform", ["rotate(-90)"]) - .attr("y", this.width - 60) - .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2) - .attr("dy", "1em") - .attr("fill", "black") - .style("text-anchor", "middle") - .text("Depth [m]"); - graph - .append("text") - .attr("y", 0 - this.margin.left) - .attr("x", 0 - height / 4) - .attr("dy", "1em") - .attr("fill", "black") - .style("text-anchor", "middle") - .attr("transform", [ - "translate(" + this.width / 2 + "," + this.height + ")", - "rotate(0)" - ]) - .text("Width [m]"); - }, - generateCoordinates(svg, height, width) { - let xScale = d3 - .scaleLinear() - .domain(this.xScale) - .rangeRound([0, width]); - - xScale.ticks(5); - let yScaleLeft = d3 - .scaleLinear() - .domain(this.yScaleLeft) - .rangeRound([height, 0]); - - let yScaleRight = d3 - .scaleLinear() - .domain(this.yScaleRight) - .rangeRound([height, 0]); - - let xAxis = d3.axisBottom(xScale); - let yAxis2 = d3.axisRight(yScaleRight); - let graph = svg - .append("g") - .attr( - "transform", - "translate(" + this.margin.left + "," + this.margin.top + ")" - ); - graph - .append("g") - .attr("transform", "translate(0," + height + ")") - .call(xAxis.ticks(5)); - graph - .append("g") - .attr("transform", "translate(" + width + ",0)") - .call(yAxis2); - return { xScale, yScaleLeft, yScaleRight, graph }; - }, - drawWaterlevel({ graph, xScale, yScaleRight, height }) { - let waterArea = d3 - .area() - .x(function(d) { - return xScale(d.x); - }) - .y0(height) - .y1(function(d) { - return yScaleRight(d.y); - }); - graph - .append("path") - .datum([{ x: 0, y: 0 }, { x: this.totalLength, y: 0 }]) - .attr("fill", this.waterColor) - .attr("stroke", this.waterColor) - .attr("d", waterArea); - }, - drawProfile({ - graph, - xScale, - yScaleRight, - currentData, - height, - color, - strokeColor, - opacity - }) { - for (let part of currentData) { - let profileLine = d3 - .line() - .x(d => { - return xScale(d.x); - }) - .y(d => { - return yScaleRight(d.y); - }); - let profileArea = d3 - .area() - .x(function(d) { - return xScale(d.x); - }) - .y0(height) - .y1(function(d) { - return yScaleRight(d.y); - }); - graph - .append("path") - .datum(part) - .attr("fill", color) - .attr("stroke", color) - .attr("stroke-width", 3) - .attr("stroke-opacity", opacity) - .attr("fill-opacity", opacity) - .attr("d", profileArea); - graph - .append("path") - .datum(part) - .attr("fill", "none") - .attr("stroke", strokeColor) - .attr("stroke-linejoin", "round") - .attr("stroke-linecap", "round") - .attr("stroke-width", 3) - .attr("stroke-opacity", opacity) - .attr("fill-opacity", opacity) - .attr("d", profileLine); - } - }, - scaleFairwayProfile() { - if (!document.querySelector(".fairwayprofile")) return; - const clientHeight = document.querySelector(".fairwayprofile") - .clientHeight; - const clientWidth = document.querySelector(".fairwayprofile").clientWidth; - if (!clientHeight || !clientWidth) return; - this.height = clientHeight; - this.width = clientWidth; - } - }, - created() { - window.addEventListener("resize", debounce(this.drawDiagram), 100); - }, - mounted() { - this.drawDiagram(); - }, - updated() { - this.scaleFairwayProfile(); - }, - destroyed() { - window.removeEventListener("resize", debounce(this.drawDiagram)); - } -}; -</script>
--- a/client/src/components/map/fairway/Infobar.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +0,0 @@ -<template> - <div - v-if="Object.keys(currentProfile).length && !showSplitscreen" - class="ui-element shadow-xs infobar rounded bg-white ml-auto mb-3 mr-3" - > - <div class="d-flex flex-row justify-content-between h-100"> - <h6 class="my-auto px-2"> - {{ selectedBottleneck }} ({{ selectedSurvey.date_info }}) - </h6> - <span - class="p-2 border-left d-flex align-items-center" - @click="$store.commit('application/showSplitscreen', true)" - > - <font-awesome-icon icon="angle-up"></font-awesome-icon> - </span> - <span - class="p-2 border-left d-flex align-items-center" - @click="$store.dispatch('fairwayprofile/clearSelection')" - > - <font-awesome-icon icon="times"></font-awesome-icon> - </span> - </div> - </div> -</template> - -<style lang="scss" scoped> -.infobar { - height: 2.2rem; - z-index: 2; -} - -.infobar svg path { - fill: #666; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "infobar", - computed: { - ...mapState("application", ["showSplitscreen"]), - ...mapState("fairwayprofile", ["currentProfile"]), - ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]) - } -}; -</script>
--- a/client/src/components/map/fairway/Profiles.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,471 +0,0 @@ -<template> - <div - :class="[ - 'box ui-element rounded bg-white text-nowrap', - { expanded: showProfiles } - ]" - > - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="chart-area" class="mr-2"></font-awesome-icon> - <translate>Profiles</translate> - <font-awesome-icon - icon="times" - class="ml-auto text-muted" - @click="$store.commit('application/showProfiles', false)" - ></font-awesome-icon> - </h6> - <div - class="d-flex flex-column p-3 flex-grow-1 text-left position-relative" - > - <div - class="loading d-flex justify-content-center align-items-center" - v-if="surveysLoading || profileLoading" - > - <font-awesome-icon icon="spinner" spin /> - </div> - <select - @click="moveToBottleneck" - v-model="selectedBottleneck" - class="form-control font-weight-bold" - > - <option :value="null"> - <translate>Select Bottleneck</translate> - </option> - <option - v-for="bn in bottlenecks" - :key="bn.properties.name" - :value="bn.properties.name" - >{{ bn.properties.name }}</option - > - </select> - <div v-if="selectedBottleneck"> - <div class="d-flex mt-2"> - <div class="flex-fill"> - <small class="text-muted"> - <translate>Sounding Result</translate>: - </small> - <select - v-model="selectedSurvey" - class="form-control form-control-sm" - > - <option - v-for="survey in surveys" - :key="survey.date_info" - :value="survey" - >{{ formatSurveyDate(survey.date_info) }}</option - > - </select> - </div> - <div - class="flex-fill ml-3" - v-if="selectedSurvey && surveys.length > 1" - > - <small class="text-muted mt-1"> - <translate>Compare with</translate>: - </small> - <select - v-model="additionalSurvey" - class="form-control form-control-sm" - > - <option :value="null">None</option> - <option - v-for="survey in additionalSurveys" - :key="survey.date_info" - :value="survey" - >{{ formatSurveyDate(survey.date_info) }}</option - > - </select> - </div> - </div> - <hr class="w-100 mb-0" /> - <small class="text-muted d-block mt-2"> - <translate>Saved cross profiles</translate>: - </small> - <div class="d-flex"> - <select - :class="[ - 'form-control form-control-sm flex-fill', - { 'rounded-left-only': selectedCut } - ]" - v-model="selectedCut" - > - <option></option> - <option - v-for="(cut, index) in previousCuts" - :value="cut" - :key="index" - >{{ cut.label }}</option - > - </select> - <button - class="btn btn-sm btn-danger input-button-right" - @click="confirmDeleteSelectedCut = true" - v-if="selectedCut && !confirmDeleteSelectedCut" - > - <font-awesome-icon icon="trash" /> - </button> - <button - class="btn btn-sm btn-info rounded-0" - @click="confirmDeleteSelectedCut = false" - v-if="selectedCut && confirmDeleteSelectedCut" - > - <font-awesome-icon icon="times" /> - </button> - <button - class="btn btn-sm btn-danger input-button-right" - @click="deleteSelectedCut" - v-if="selectedCut && confirmDeleteSelectedCut" - > - <font-awesome-icon icon="check" /> - </button> - </div> - <small class="text-muted d-block mt-2"> - <translate>Enter coordinates manually</translate>: - </small> - <div class="position-relative"> - <input - class="form-control form-control-sm pr-5" - placeholder="Lat,Lon,Lat,Lon" - v-model="coordinatesInput" - /> - <button - class="btn btn-sm btn-info position-absolute input-button-right" - @click="applyManualCoordinates" - style="top: 0; right: 0;" - v-if="coordinatesInputIsValid" - > - <font-awesome-icon icon="check" /> - </button> - </div> - <small class="d-flex text-left mt-2" v-if="startPoint && endPoint"> - <div class="text-nowrap mr-3"> - <b> <translate>Start</translate>: </b> <br /> - Lat: {{ startPoint[1] }} <br /> - Lon: {{ startPoint[0] }} - </div> - <div class="text-nowrap"> - <b>End:</b> <br /> - Lat: {{ endPoint[1] }} <br /> - Lon: {{ endPoint[0] }} - </div> - <button - v-clipboard:copy="coordinatesForClipboard" - v-clipboard:success="onCopyCoordinates" - class="btn btn-info btn-sm ml-auto mt-auto" - > - <font-awesome-icon icon="copy" /> - </button> - </small> - <div class="d-flex mt-3"> - <div - class="pr-3 w-50" - v-if="startPoint && endPoint && !selectedCut" - > - <button - class="btn btn-info btn-sm w-100" - @click="showLabelInput = !showLabelInput" - > - <font-awesome-icon :icon="showLabelInput ? 'times' : 'check'" /> - {{ showLabelInput ? "Cancel" : "Save" }} - </button> - </div> - <div - :class="startPoint && endPoint && !selectedCut ? 'w-50' : 'w-100'" - > - <button class="btn btn-info btn-sm w-100" @click="toggleCutTool"> - <font-awesome-icon - :icon="cutTool && cutTool.getActive() ? 'times' : 'plus'" - ></font-awesome-icon> - {{ cutTool && cutTool.getActive() ? "Cancel" : "New" }} - </button> - </div> - </div> - <div v-if="showLabelInput" class="mt-2"> - <small class="text-muted"> - <translate>Enter label for cross profile</translate>: - </small> - <div class="position-relative"> - <input - class="form-control form-control-sm pr-5" - v-model="cutLabel" - /> - <button - class="btn btn-sm btn-info position-absolute input-button-right" - @click="saveCut" - v-if="cutLabel" - style="top: 0; right: 0;" - > - <font-awesome-icon icon="check" /> - </button> - </div> - </div> - </div> - </div> - </div> - </div> -</template> - -<style lang="scss" scoped> -.loading { - background: rgba(255, 255, 255, 0.9); - position: absolute; - z-index: 99; - top: 0; - right: 0; - bottom: 0; - left: 0; -} - -.input-button-right { - border-top-right-radius: $border-radius; - border-bottom-right-radius: $border-radius; - border-top-left-radius: 0 !important; - border-bottom-left-radius: 0 !important; -} - -.rounded-left-only { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; - border-top-left-radius: $border-radius; - border-bottom-left-radius: $border-radius; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; -import Feature from "ol/Feature"; -import LineString from "ol/geom/LineString"; -import { displayError, displayInfo } from "../../../lib/errors.js"; -import { formatSurveyDate } from "../../../lib/date.js"; - -export default { - name: "profiles", - data() { - return { - coordinatesInput: "", - cutLabel: "", - showLabelInput: false, - confirmDeleteSelectedCut: false - }; - }, - computed: { - ...mapGetters("map", ["getVSourceByName"]), - ...mapState("application", ["showProfiles"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), - ...mapState("bottlenecks", ["bottlenecks", "surveys", "surveysLoading"]), - ...mapState("fairwayprofile", [ - "previousCuts", - "startPoint", - "endPoint", - "profileLoading" - ]), - selectedBottleneck: { - get() { - return this.$store.state.bottlenecks.selectedBottleneck; - }, - set(name) { - this.$store - .dispatch("bottlenecks/setSelectedBottleneck", name) - .then(() => { - this.$store.commit("bottlenecks/setFirstSurveySelected"); - }); - } - }, - selectedSurvey: { - get() { - return this.$store.state.bottlenecks.selectedSurvey; - }, - set(survey) { - this.$store.commit("fairwayprofile/additionalSurvey", null); - this.$store.commit("bottlenecks/selectedSurvey", survey); - } - }, - additionalSurvey: { - get() { - return this.$store.state.fairwayprofile.additionalSurvey; - }, - set(survey) { - this.$store.commit("fairwayprofile/additionalSurvey", survey); - } - }, - selectedCut: { - get() { - return this.$store.state.fairwayprofile.selectedCut; - }, - set(cut) { - this.$store.commit("fairwayprofile/selectedCut", cut); - if (!cut) { - this.$store.commit("fairwayprofile/clearCurrentProfile"); - this.$store.commit("application/showSplitscreen", false); - this.getVSourceByName("Cut Tool").clear(); - } - } - }, - additionalSurveys() { - return this.surveys.filter(survey => survey !== this.selectedSurvey); - }, - coordinatesForClipboard() { - return ( - this.startPoint[1] + - "," + - this.startPoint[0] + - "," + - this.endPoint[1] + - "," + - this.endPoint[0] - ); - }, - coordinatesInputIsValid() { - const coordinates = this.coordinatesInput - .split(",") - .map(coord => parseFloat(coord.trim())) - .filter(c => Number(c) === c); - return coordinates.length === 4; - } - }, - watch: { - selectedBottleneck() { - this.$store.dispatch("fairwayprofile/previousCuts"); - this.cutLabel = - this.selectedBottleneck + " (" + new Date().toISOString() + ")"; - }, - selectedSurvey(survey) { - this.loadProfile(survey); - }, - additionalSurvey(survey) { - this.loadProfile(survey); - }, - selectedCut(cut) { - if (cut) { - this.confirmDeleteSelectedCut = false; - this.applyCoordinates(cut.coordinates); - } - } - }, - methods: { - formatSurveyDate(date) { - return formatSurveyDate(date); - }, - loadProfile(survey) { - if (survey) { - this.$store.commit("fairwayprofile/profileLoading", true); - this.$store - .dispatch("fairwayprofile/loadProfile", survey) - .finally(() => - this.$store.commit("fairwayprofile/profileLoading", false) - ); - } - }, - toggleCutTool() { - this.cutTool.setActive(!this.cutTool.getActive()); - this.lineTool.setActive(false); - this.polygonTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - }, - onCopyCoordinates() { - displayInfo({ - title: this.$gettext("Success"), - message: this.$gettext("Coordinates copied to clipboard!") - }); - }, - applyManualCoordinates() { - const coordinates = this.coordinatesInput - .split(",") - .map(coord => parseFloat(coord.trim())); - this.selectedCut = null; - this.coordinatesInput = ""; - this.applyCoordinates([ - coordinates[1], - coordinates[0], - coordinates[3], - coordinates[2] - ]); - }, - applyCoordinates(coordinates) { - // allow only numbers - coordinates = coordinates.filter(c => Number(c) === c); - if (coordinates.length === 4) { - // draw line on map - this.getVSourceByName("Cut Tool").clear(); - const cut = new Feature({ - geometry: new LineString([ - [coordinates[0], coordinates[1]], - [coordinates[2], coordinates[3]] - ]).transform("EPSG:4326", "EPSG:3857") - }); - this.getVSourceByName("Cut Tool").addFeature(cut); - - // draw diagram - this.$store.dispatch("fairwayprofile/cut", cut); - } else { - displayError({ - title: this.$gettext("Invalid input"), - message: this.$gettext( - "Please enter correct coordinates in the format: Lat,Lon,Lat,Lon" - ) - }); - } - }, - saveCut() { - const previousCuts = - JSON.parse(localStorage.getItem("previousCuts")) || []; - const newEntry = { - label: this.cutLabel, - bottleneckName: this.selectedBottleneck, - coordinates: [...this.startPoint, ...this.endPoint], - timestamp: new Date().getTime() - }; - const existingEntry = previousCuts.find(cut => { - return JSON.stringify(cut) === JSON.stringify(newEntry); - }); - if (!existingEntry) previousCuts.push(newEntry); - if (previousCuts.length > 100) previousCuts.shift(); - localStorage.setItem("previousCuts", JSON.stringify(previousCuts)); - this.$store.dispatch("fairwayprofile/previousCuts"); - - this.showLabelInput = false; - displayInfo({ - title: this.$gettext("Profile saved!"), - message: this.$gettext( - 'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.' - ) - }); - }, - deleteSelectedCut() { - let previousCuts = JSON.parse(localStorage.getItem("previousCuts")) || []; - previousCuts = previousCuts.filter(cut => { - return JSON.stringify(cut) !== JSON.stringify(this.selectedCut); - }); - localStorage.setItem("previousCuts", JSON.stringify(previousCuts)); - this.$store.commit("fairwayprofile/selectedCut", null); - this.$store.dispatch("fairwayprofile/previousCuts"); - displayInfo({ title: this.$gettext("Profile deleted!") }); - }, - moveToBottleneck() { - const bottleneck = this.bottlenecks.find( - bn => bn.properties.name === this.selectedBottleneck - ); - if (!bottleneck) return; - this.$store.commit("map/moveMap", { - coordinates: bottleneck.geometry.coordinates, - zoom: 17, - preventZoomOut: true - }); - } - } -}; -</script>
--- a/client/src/components/map/layers/Layers.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -<template> - <div - :class="[ - 'box ui-element rounded bg-white text-nowrap', - { expanded: showLayers } - ]" - > - <div style="width: 20rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="layer-group" class="mr-2"></font-awesome-icon - ><translate>Layers</translate> - <font-awesome-icon - icon="times" - class="ml-auto text-muted" - @click="$store.commit('application/showLayers', false)" - ></font-awesome-icon> - </h6> - <div class="d-flex flex-column p-3 small"> - <Layerselect - v-for="(layer, index) in layersForLegend" - :layerindex="index" - :layername="layer.name" - :key="layer.name" - :isVisible="layer.isVisible" - @visibilityToggled="visibilityToggled" - ></Layerselect> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import Layerselect from "./Layerselect"; -import { mapGetters, mapState } from "vuex"; -export default { - name: "layers", - components: { - Layerselect - }, - computed: { - ...mapGetters("map", ["layersForLegend"]), - ...mapState("application", ["showLayers"]) - }, - methods: { - visibilityToggled(layer) { - this.$store.commit("map/toggleVisibility", layer); - } - } -}; -</script>
--- a/client/src/components/map/layers/Layerselect.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,85 +0,0 @@ -<template> - <div> - <div class="form-check d-flex flex-row flex-start selection"> - <input - class="form-check-input" - @change="visibilityToggled" - :id="layername" - type="checkbox" - :checked="isVisible" - /> - <LegendElement - :layername="layername" - :layerindex="layerindex" - ></LegendElement> - <label class="layername form-check-label" @click="visibilityToggled">{{ - layername - }}</label> - </div> - <div v-if="isVisible && layername == 'Bottleneck isolines'"> - <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl" /> - </div> - </div> -</template> - -<style lang="scss" scoped> -.selection { - text-align: left; -} -.layername { - margin-left: $small-offset; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { HTTP } from "../../../lib/http"; -import LegendElement from "./LegendElement.vue"; -export default { - props: ["layername", "layerindex", "isVisible"], - name: "layerselect", - data() { - return { - isolinesLegendImgUrl: "" - }; - }, - components: { - LegendElement - }, - methods: { - visibilityToggled() { - this.$emit("visibilityToggled", this.layerindex); - } - }, - created() { - // fetch legend image for bottleneck isolines - // TODO: move to store - if (this.layername == "Bottleneck isolines") { - const src = - "/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.0.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=sounding_results_contour_lines_geoserver&legend_options=columns:4;fontAntiAliasing:true"; - HTTP.get(src, { - headers: { - Accept: "image/png", - "X-Gemma-Auth": localStorage.getItem("token") - }, - responseType: "blob" - }).then(response => { - var urlCreator = window.URL || window.webkitURL; - this.isolinesLegendImgUrl = urlCreator.createObjectURL(response.data); - }); - } - } -}; -</script>
--- a/client/src/components/map/layers/LegendElement.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,124 +0,0 @@ -<template> - <div :id="id" class="legendelement"></div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { mapGetters } from "vuex"; - -import { Map, View } from "ol"; -import Feature from "ol/Feature"; -import { Vector as VectorLayer } from "ol/layer.js"; -import { Vector as VectorSource } from "ol/source.js"; -import LineString from "ol/geom/LineString.js"; -import Point from "ol/geom/Point"; - -export default { - name: "legendelement", - props: ["layername", "layerindex"], - data: function() { - return { - myMap: null, - mapLayer: null - }; - }, - computed: { - ...mapGetters("map", ["getLayerByName"]), - id() { - return "legendelement" + this.layerindex; - }, - mstyle() { - if (this.mapLayer && this.mapLayer.data.getStyle) { - return this.mapLayer.data.getStyle(); - } - } - }, - watch: { - mstyle(newStyle, oldStyle) { - // only recreate if there already was a style before - if (oldStyle) { - let vector = this.createVectorLayer(); - - this.myMap.removeLayer(this.myMap.getLayers()[0]); - this.myMap.addLayer(vector); - } - } - }, - mounted() { - this.mapLayer = this.getLayerByName(this.layername); - if (this.mapLayer.data.getType() == "VECTOR") { - this.initMap(); - } else { - // TODO other tiles - } - }, - methods: { - initMap() { - let vector = this.createVectorLayer(); - - this.myMap = new Map({ - layers: [vector], - target: this.id, - controls: [], - interactions: [], - view: new View({ - center: [0, 0], - zoom: 3, - projection: "EPSG:4326" - }) - }); - }, - createVectorLayer() { - let mapStyle = this.mapLayer.data.getStyle(); - - let feature = new Feature({ - geometry: new LineString([[-1, 0.5], [0, 0], [0.7, 0], [1.3, -0.7]]) - }); - - // special case if we need to call the style function with a special - // parameter or to detect a point layer - if (this.mapLayer["forLegendStyle"]) { - if (this.mapLayer.forLegendStyle.point) { - feature.setGeometry(new Point([0, 0])); - } - mapStyle = this.mapLayer.data.getStyleFunction()( - feature, - this.mapLayer.forLegendStyle.resolution - ); - } - - // we could add extra properties here, if they are needed for - // the styling function in the future. An idea is to extend the - // this.mapLayer["forLegendStyle"] for it. - // FIXME, this is a special case for the Fairway Dimensions style - feature.set("level_of_service", ""); - return new VectorLayer({ - source: new VectorSource({ - features: [feature], - wrapX: false - }), - style: mapStyle - }); - } - } -}; -</script> - -<style lang="scss" scoped> -.legendelement { - max-height: 1.5rem; - width: 2rem; -} -</style>
--- a/client/src/components/map/toolbar/Identify.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -<template> - <div - @click="$store.commit('application/showIdentify', !showIdentify)" - class="toolbar-button" - > - <font-awesome-icon - icon="info" - :class="{ 'text-info': showIdentify }" - ></font-awesome-icon> - <span - :class="[ - 'indicator', - { - show: - !showIdentify && (identifiedFeatures.length || currentMeasurement) - } - ]" - > - {{ badgeCount }} - </span> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "identify", - computed: { - ...mapState("application", ["showIdentify"]), - ...mapState("map", ["identifiedFeatures", "currentMeasurement"]), - badgeCount() { - return this.identifiedFeatures.length + !!this.currentMeasurement; - } - } -}; -</script>
--- a/client/src/components/map/toolbar/Layers.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +0,0 @@ -<template> - <div - @click="$store.commit('application/showLayers', !showLayers)" - class="toolbar-button" - > - <font-awesome-icon - icon="layer-group" - :class="{ 'text-info': showLayers }" - ></font-awesome-icon> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "layers", - computed: { - ...mapState("application", ["showLayers"]) - } -}; -</script>
--- a/client/src/components/map/toolbar/Linetool.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -<template> - <div @click="toggleLineTool" class="toolbar-button"> - <font-awesome-icon - icon="ruler" - :class="{ 'text-info': lineTool && lineTool.getActive() }" - ></font-awesome-icon> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; - -export default { - name: "linetool", - computed: { - ...mapGetters("map", ["getLayerByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]) - }, - methods: { - toggleLineTool() { - this.lineTool.setActive(!this.lineTool.getActive()); - this.polygonTool.setActive(false); - this.cutTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - this.getVSourceByName("Draw Tool").clear(); - } - } -}; -</script>
--- a/client/src/components/map/toolbar/Pdftool.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +0,0 @@ -<template> - <div - @click="$store.commit('application/showPdfTool', !showPdfTool)" - class="toolbar-button" - > - <font-awesome-icon - icon="file-pdf" - :class="{ 'text-info': showPdfTool }" - ></font-awesome-icon> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "pdftool", - computed: { - ...mapState("application", ["showPdfTool"]) - } -}; -</script>
--- a/client/src/components/map/toolbar/Polygontool.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -<template> - <div @click="togglePolygonTool" class="toolbar-button"> - <font-awesome-icon - icon="draw-polygon" - :class="{ 'text-info': polygonTool && polygonTool.getActive() }" - ></font-awesome-icon> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; - -export default { - name: "polygontool", - computed: { - ...mapGetters("map", ["getLayerByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]) - }, - methods: { - togglePolygonTool() { - this.polygonTool.setActive(!this.polygonTool.getActive()); - this.lineTool.setActive(false); - this.cutTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - this.getVSourceByName("Draw Tool").clear(); - } - } -}; -</script>
--- a/client/src/components/map/toolbar/Profiles.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +0,0 @@ -<template> - <div - @click="$store.commit('application/showProfiles', !showProfiles)" - class="toolbar-button" - > - <font-awesome-icon - icon="chart-area" - :class="{ 'text-info': showProfiles }" - ></font-awesome-icon> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "profiles", - computed: { - ...mapState("application", ["showProfiles"]) - } -}; -</script>
--- a/client/src/components/map/toolbar/Toolbar.vue Tue Dec 11 22:59:10 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,142 +0,0 @@ -<template> - <div class="ml-2"> - <div - :class=" - 'rounded-top toolbar toolbar-' + - (expandToolbar ? 'expanded' : 'collapsed') - " - > - <Identify></Identify> - <Layers></Layers> - <Profiles></Profiles> - <Linetool></Linetool> - <Polygontool></Polygontool> - <Pdftool></Pdftool> - </div> - <div - @click="$store.commit('application/expandToolbar', !expandToolbar)" - class="toolbar-button toolbar-toggle rounded-bottom bg-info text-white" - > - <font-awesome-icon - :icon="expandToolbar ? 'angle-up' : 'angle-down'" - ></font-awesome-icon> - </div> - </div> -</template> - -<style lang="scss"> -// not scoped to affect nested components -// doen't work when put in application/assets/application.sass... why??? o_O -.toolbar { - box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2); - overflow: hidden; - transition: max-height 0.4s; - margin-bottom: auto; -} - -.toolbar-collapsed { - max-height: 6rem; -} - -.toolbar-expanded { - max-height: 100%; -} - -.toolbar-button { - opacity: 0.96; - color: #666; - height: 2rem; - width: 2rem; - align-items: center; - justify-content: center; - display: flex; - background: #fff; - border-bottom: 1px solid #dee2e6; - z-index: 2; - pointer-events: auto; - position: relative; - overflow: hidden; -} - -.toolbar-button:last-child { - border-bottom: none; -} - -.toolbar-button .inverted { - color: #17a2b8; -} - -.toolbar-button .grey { - color: #ddd; -} - -.toolbar-button .indicator { - color: #fff; - background: #17a2b8; - position: absolute; - bottom: -14px; - left: -14px; - padding: 2px 4px 1px; - font-size: 11px; - line-height: 11px; - border-top-right-radius: 0.25rem; - transition: bottom 0.3s, left 0.3s; -} - -.toolbar-button .indicator.show { - left: 0; - bottom: 0; -} - -.toolbar-toggle { - height: 1.2rem; - border-bottom: none; -} -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; - -export default { - name: "toolbar", - components: { - Identify: () => import("./Identify.vue"), - Layers: () => import("./Layers.vue"), - Linetool: () => import("./Linetool.vue"), - Polygontool: () => import("./Polygontool.vue"), - Profiles: () => import("./Profiles.vue"), - Pdftool: () => import("./Pdftool.vue") - }, - computed: { - ...mapGetters("map", ["getVSourceByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), - ...mapState("application", ["expandToolbar"]) - }, - mounted() { - window.addEventListener("keydown", e => { - // Escape - if (e.keyCode === 27) { - this.lineTool.setActive(false); - this.polygonTool.setActive(false); - this.cutTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - this.$store.dispatch("map/enableIdentifyTool"); - this.getVSourceByName("Draw Tool").clear(); - } - }); - } -}; -</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Identify.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,50 @@ +<template> + <div + @click="$store.commit('application/showIdentify', !showIdentify)" + class="toolbar-button" + > + <font-awesome-icon + icon="info" + :class="{ 'text-info': showIdentify }" + ></font-awesome-icon> + <span + :class="[ + 'indicator', + { + show: + !showIdentify && (identifiedFeatures.length || currentMeasurement) + } + ]" + > + {{ badgeCount }} + </span> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "identify", + computed: { + ...mapState("application", ["showIdentify"]), + ...mapState("map", ["identifiedFeatures", "currentMeasurement"]), + badgeCount() { + return this.identifiedFeatures.length + !!this.currentMeasurement; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Layers.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,35 @@ +<template> + <div + @click="$store.commit('application/showLayers', !showLayers)" + class="toolbar-button" + > + <font-awesome-icon + icon="layer-group" + :class="{ 'text-info': showLayers }" + ></font-awesome-icon> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "layers", + computed: { + ...mapState("application", ["showLayers"]) + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Linetool.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,42 @@ +<template> + <div @click="toggleLineTool" class="toolbar-button"> + <font-awesome-icon + icon="ruler" + :class="{ 'text-info': lineTool && lineTool.getActive() }" + ></font-awesome-icon> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; + +export default { + name: "linetool", + computed: { + ...mapGetters("map", ["getLayerByName"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool"]) + }, + methods: { + toggleLineTool() { + this.lineTool.setActive(!this.lineTool.getActive()); + this.polygonTool.setActive(false); + this.cutTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + this.getVSourceByName("Draw Tool").clear(); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Pdftool.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,35 @@ +<template> + <div + @click="$store.commit('application/showPdfTool', !showPdfTool)" + class="toolbar-button" + > + <font-awesome-icon + icon="file-pdf" + :class="{ 'text-info': showPdfTool }" + ></font-awesome-icon> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "pdftool", + computed: { + ...mapState("application", ["showPdfTool"]) + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Polygontool.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,42 @@ +<template> + <div @click="togglePolygonTool" class="toolbar-button"> + <font-awesome-icon + icon="draw-polygon" + :class="{ 'text-info': polygonTool && polygonTool.getActive() }" + ></font-awesome-icon> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; + +export default { + name: "polygontool", + computed: { + ...mapGetters("map", ["getLayerByName"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool"]) + }, + methods: { + togglePolygonTool() { + this.polygonTool.setActive(!this.polygonTool.getActive()); + this.lineTool.setActive(false); + this.cutTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + this.getVSourceByName("Draw Tool").clear(); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Profiles.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,35 @@ +<template> + <div + @click="$store.commit('application/showProfiles', !showProfiles)" + class="toolbar-button" + > + <font-awesome-icon + icon="chart-area" + :class="{ 'text-info': showProfiles }" + ></font-awesome-icon> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "profiles", + computed: { + ...mapState("application", ["showProfiles"]) + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/Toolbar.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,142 @@ +<template> + <div class="ml-2"> + <div + :class=" + 'rounded-top toolbar toolbar-' + + (expandToolbar ? 'expanded' : 'collapsed') + " + > + <Identify></Identify> + <Layers></Layers> + <Profiles></Profiles> + <Linetool></Linetool> + <Polygontool></Polygontool> + <Pdftool></Pdftool> + </div> + <div + @click="$store.commit('application/expandToolbar', !expandToolbar)" + class="toolbar-button toolbar-toggle rounded-bottom bg-info text-white" + > + <font-awesome-icon + :icon="expandToolbar ? 'angle-up' : 'angle-down'" + ></font-awesome-icon> + </div> + </div> +</template> + +<style lang="scss"> +// not scoped to affect nested components +// doen't work when put in application/assets/application.sass... why??? o_O +.toolbar { + box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2); + overflow: hidden; + transition: max-height 0.4s; + margin-bottom: auto; +} + +.toolbar-collapsed { + max-height: 6rem; +} + +.toolbar-expanded { + max-height: 100%; +} + +.toolbar-button { + opacity: 0.96; + color: #666; + height: 2rem; + width: 2rem; + align-items: center; + justify-content: center; + display: flex; + background: #fff; + border-bottom: 1px solid #dee2e6; + z-index: 2; + pointer-events: auto; + position: relative; + overflow: hidden; +} + +.toolbar-button:last-child { + border-bottom: none; +} + +.toolbar-button .inverted { + color: #17a2b8; +} + +.toolbar-button .grey { + color: #ddd; +} + +.toolbar-button .indicator { + color: #fff; + background: #17a2b8; + position: absolute; + bottom: -14px; + left: -14px; + padding: 2px 4px 1px; + font-size: 11px; + line-height: 11px; + border-top-right-radius: 0.25rem; + transition: bottom 0.3s, left 0.3s; +} + +.toolbar-button .indicator.show { + left: 0; + bottom: 0; +} + +.toolbar-toggle { + height: 1.2rem; + border-bottom: none; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; + +export default { + name: "toolbar", + components: { + Identify: () => import("./Identify.vue"), + Layers: () => import("./Layers.vue"), + Linetool: () => import("./Linetool.vue"), + Polygontool: () => import("./Polygontool.vue"), + Profiles: () => import("./Profiles.vue"), + Pdftool: () => import("./Pdftool.vue") + }, + computed: { + ...mapGetters("map", ["getVSourceByName"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), + ...mapState("application", ["expandToolbar"]) + }, + mounted() { + window.addEventListener("keydown", e => { + // Escape + if (e.keyCode === 27) { + this.lineTool.setActive(false); + this.polygonTool.setActive(false); + this.cutTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + this.$store.dispatch("map/enableIdentifyTool"); + this.getVSourceByName("Draw Tool").clear(); + } + }); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/usermanagement/Passwordfield.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,73 @@ +<template> + <div class="w-100"> + <div class="d-flex flex-row"> + <label for="password">{{ this.label }}</label> + </div> + <div class="d-flex d-row"> + <input + :type="isPasswordVisible" + @change="fieldChanged" + class="form-control" + :placeholder="placeholder" + :required="required" + /> + <span class="input-group-text" @click="showPassword"> + <font-awesome-icon + :icon="readablePassword ? 'eye-slash' : 'eye'" + ></font-awesome-icon> + </span> + </div> + <div v-show="passworderrors" class="text-danger"> + <small> + <font-awesome-icon icon="exclamation-triangle"></font-awesome-icon> + {{ this.passworderrors }} + </small> + </div> + </div> +</template> + +<style> +/* FIXME does not work here, unclear why, so added to Login.vue +input[type="password"]::-ms-reveal { + display: none; +} */ +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +export default { + name: "passwordfield", + props: ["model", "placeholder", "label", "passworderrors", "required"], + data() { + return { + password: "", + readablePassword: false + }; + }, + methods: { + showPassword() { + this.readablePassword = !this.readablePassword; + }, + fieldChanged(e) { + this.$emit("fieldchange", e.target.value); + } + }, + computed: { + isPasswordVisible() { + return this.readablePassword ? "text" : "password"; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/usermanagement/Userdetail.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,393 @@ +<template> + <div class="userdetails mt-3 shadow fadeIn animated card"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + {{ this.cardHeader }} + <span @click="closeDetailview" class="closebutton"> + <font-awesome-icon icon="times"></font-awesome-icon> + </span> + </h6> + <div class="card-body"> + <form @submit.prevent="save" class="ml-3"> + <div class="formfields"> + <div v-if="currentUser.isNew" class="form-group row"> + <label for="user"> <translate>Username</translate> </label> + <input + type="user" + :placeholder="userNamePlaceholder" + class="form-control form-control-sm" + id="user" + aria-describedby="userHelp" + v-model="currentUser.user" + /> + <div v-show="errors.user" class="text-danger"> + <small> + <font-awesome-icon + icon="exclamation-triangle" + ></font-awesome-icon> + {{ errors.user }} + </small> + </div> + </div> + <div class="form-group row"> + <label for="country"> <translate>Country</translate> </label> + <select + class="form-control form-control-sm" + v-on:change="validateCountry" + v-model="currentUser.country" + > + <option disabled value> + <translate>Please select one</translate> + </option> + <option + v-for="country in countries" + v-bind:value="country" + v-bind:key="country" + >{{ country }}</option + > + </select> + <div v-show="errors.country" class="text-danger"> + <small> + <font-awesome-icon + icon="exclamation-triangle" + ></font-awesome-icon> + {{ errors.country }} + </small> + </div> + </div> + <div class="form-group row"> + <label for="email"> <translate>Email address</translate> </label> + <input + type="email" + v-on:change="validateEmailaddress" + class="form-control form-control-sm" + id="email" + aria-describedby="emailHelp" + v-model="currentUser.email" + /> + <div v-show="errors.email" class="text-danger"> + <small> + <font-awesome-icon + icon="exclamation-triangle" + ></font-awesome-icon> + {{ errors.email }} + </small> + </div> + </div> + <div class="form-group row"> + <label for="role"> <translate>Role</translate> </label> + <select + class="form-control form-control-sm" + v-on:change="validateRole" + v-model="currentUser.role" + > + <option disabled value> + <translate>Please select one</translate> + </option> + <option value="sys_admin"> + <translate>Sysadmin</translate> + </option> + <option value="waterway_admin"> + <translate>Waterway Admin</translate> + </option> + <option value="waterway_user"> + <translate>Waterway User</translate> + </option> + </select> + <div v-show="errors.role" class="text-danger"> + <small> + <font-awesome-icon + icon="exclamation-triangle" + ></font-awesome-icon> + {{ errors.role }} + </small> + </div> + </div> + <div class="form-group row"> + <PasswordField + @fieldchange="passwordChanged" + :placeholder="passwordPlaceholder" + :label="passwordLabel" + :passworderrors="errors.password" + ></PasswordField> + </div> + <div class="form-group row"> + <PasswordField + @fieldchange="passwordReChanged" + :placeholder="passwordRePlaceholder" + :label="passwordReLabel" + :passworderrors="errors.passwordre" + ></PasswordField> + </div> + </div> + <div> + <button + type="submit" + :disabled="submitted" + class="shadow-sm btn btn-info submit-button" + > + <translate>Submit</translate> + </button> + </div> + <div + v-if="currentUser.role != 'waterway_user'" + class="form-group row d-flex flex-row justify-content-start mailbutton" + > + <a @click="sendTestMail" class="btn btn-light"> + <font-awesome-icon icon="paper-plane"></font-awesome-icon> + <translate>Send testmail</translate> + </a> + <div v-if="mailsent"><translate>Mail was sent</translate></div> + </div> + </form> + </div> + </div> +</template> + +<style lang="scss" scoped> +.submit-button { + position: absolute; + right: $offset; + bottom: $offset; +} +.mailbutton { + width: 12vw; + position: absolute; + left: $large-offset; + bottom: 0; +} + +.formfields { + width: 60%; +} + +.userdetails { + height: 600px; + margin-top: $offset; + margin-left: $offset; + margin-right: $offset; +} + +form { + font-size: $smaller; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { HTTP } from "../../lib/http"; +import { displayError } from "../../lib/errors.js"; +import { mapState } from "vuex"; +import PasswordField from "./Passwordfield"; + +const emptyErrormessages = () => { + return { + email: "", + country: "", + role: "", + password: "", + passwordre: "" + }; +}; + +const isEmailValid = email => { + /** + * + * For convenience purposes the same regex used as in the go code + * cf. types.go + * + */ + // eslint-disable-next-line + return /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test( + email + ); +}; + +const violatedPasswordRules = password => { + return ( + // rules according to issue 70 + password.length < 7 || + /\W/.test(password) == false || + /\d/.test(password) == false + ); +}; + +export default { + name: "userdetail", + components: { + PasswordField + }, + data() { + return { + mailsent: false, + passwordLabel: this.$gettext("Password"), + passwordReLabel: this.$gettext("Repeat Password"), + passwordPlaceholder: this.$gettext("password"), + passwordRePlaceholder: this.$gettext("password again"), + password: "", + passwordre: "", + currentUser: {}, + path: null, + submitted: false, + errors: { + email: "", + country: "", + role: "", + password: "", + passwordre: "" + } + }; + }, + mounted() { + this.currentUser = { ...this.user }; + this.path = this.user.name; + }, + watch: { + user() { + this.currentUser = { ...this.user }; + this.path = this.user.name; + this.clearPassword(); + this.clearErrors(); + } + }, + computed: { + cardHeader() { + if (this.currentUser.isNew) return "N.N"; + return this.currentUser.user; + }, + userNamePlaceholder() { + if (this.currentUser.isNew) return "N.N"; + return ""; + }, + ...mapState("application", ["countries"]), + user() { + return this.$store.getters["usermanagement/currentUser"]; + }, + isFormValid() { + return ( + isEmailValid(this.currentUser.email) && + this.currentUser.country && + this.password === this.passwordre && + (this.password === "" || !violatedPasswordRules(this.password)) + ); + } + }, + methods: { + sendTestMail() { + if (this.mailsent) return; + HTTP.get("/testmail/" + this.currentUser.user, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(() => { + this.mailsent = true; + }) + .catch(error => { + this.loginFailed = true; + this.submitted = false; + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + passwordChanged(value) { + this.password = value; + this.validatePassword(); + }, + passwordReChanged(value) { + this.passwordre = value; + this.validatePassword(); + }, + clearErrors() { + this.errors = emptyErrormessages(); + }, + clearPassword() { + this.password = ""; + this.passwordre = ""; + }, + closeDetailview() { + this.$store.commit("usermanagement/clearCurrentUser"); + this.$store.commit("usermanagement/setUserDetailsInvisible"); + }, + validateCountry() { + this.errors.country = this.currentUser.country + ? "" + : this.$gettext("Please choose a country"); + }, + validateRole() { + this.errors.role = this.currentUser.role + ? "" + : this.$gettext("Please choose a role"); + }, + validatePassword() { + this.errors.passwordre = + this.password === this.passwordre + ? "" + : this.$gettext("Passwords do not match!"); + this.errors.password = + this.password === "" || !violatedPasswordRules(this.password) + ? "" + : this.$gettext( + "Password should at least be 8 char long including 1 digit and 1 special char like $" + ); + }, + validateEmailaddress() { + this.errors.email = isEmailValid(this.currentUser.email) + ? "" + : this.$gettext("invalid email"); + }, + validate() { + this.validateCountry(); + this.validateRole(); + this.validatePassword(); + this.validateEmailaddress(); + }, + save() { + this.validate(); + if (!this.isFormValid) return; + if (this.password) this.currentUser.password = this.password; + this.submitted = true; + this.$store + .dispatch("usermanagement/saveCurrentUser", { + path: this.user.user, + user: this.currentUser + }) + .then(() => { + this.submitted = false; + this.$store.dispatch("usermanagement/loadUsers").catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + this.submitted = false; + const { status, data } = error.response; + displayError({ + title: this.$gettext("Error while saving user"), + message: `${status}: ${data.message || data}` + }); + }); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/usermanagement/Usermanagement.vue Wed Dec 12 09:22:20 2018 +0100 @@ -0,0 +1,358 @@ +<template> + <div class="main d-flex flex-row"> + <div :class="spacerStyle"></div> + <div class="d-flex content flex-column"> + <div class="d-flex flex-row"> + <div :class="userlistStyle"> + <div class="card"> + <h6 + class="mb-0 py-2 px-3 border-bottom d-flex text-info align-items-center" + > + <font-awesome-icon + icon="users-cog" + class="mr-2 fa-fw" + ></font-awesome-icon> + <translate class="headline">Users</translate> + </h6> + <div class="card-body"> + <table id="datatable" :class="tableStyle"> + <thead> + <tr> + <th scope="col" @click="sortBy('user')"> + <span + >Username + <font-awesome-icon + v-if="sortCriterion == 'user'" + icon="angle-down" + ></font-awesome-icon> + </span> + </th> + <th scope="col" @click="sortBy('country')"> + <span + >Country + <font-awesome-icon + v-if="sortCriterion == 'country'" + icon="angle-down" + ></font-awesome-icon> + </span> + </th> + <th scope="col" @click="sortBy('email')"> + <span + >Email + <font-awesome-icon + v-if="sortCriterion == 'email'" + icon="angle-down" + ></font-awesome-icon> + </span> + </th> + <th scope="col" @click="sortBy('role')"> + <span + >Role + <font-awesome-icon + v-if="sortCriterion == 'role'" + icon="angle-down" + ></font-awesome-icon> + </span> + </th> + <th scope="col"></th> + </tr> + </thead> + <tbody> + <tr + v-for="user in users" + :key="user.user" + @click="selectUser(user.user)" + > + <td>{{ user.user }}</td> + <td>{{ user.country }}</td> + <td>{{ user.email }}</td> + <td> + <font-awesome-icon + :icon="roleIcon(user.role)" + @click="deleteUser(user.user)" + ></font-awesome-icon> + </td> + <td> + <font-awesome-icon + icon="trash" + @click="deleteUser(user.user)" + ></font-awesome-icon> + </td> + </tr> + </tbody> + </table> + </div> + <div class="d-flex mx-auto align-items-center"> + <button + @click="prevPage" + v-if="this.currentPage !== 1" + class="mr-2 btn btn-sm btn-light align-self-center" + > + <font-awesome-icon icon="angle-left"></font-awesome-icon> + </button> + {{ this.currentPage }} / {{ this.pages }} + <button + @click="nextPage" + v-if="this.currentPage !== this.pages" + class="ml-2 btn btn-sm btn-light align-self-center" + > + <font-awesome-icon icon="angle-right"></font-awesome-icon> + </button> + </div> + <div class="mr-3 pb-3"> + <button @click="addUser" class="btn btn-info addbutton shadow-sm"> + <translate>Add User</translate> + </button> + </div> + </div> + </div> + <Userdetail + class="d-flex userdetails" + v-if="isUserDetailsVisible" + ></Userdetail> + </div> + </div> + </div> +</template> + +<style scoped lang="scss"> +@import "../../assets/tooltip.scss"; + +.addbutton { + position: absolute; + bottom: $offset; + right: $offset; +} + +.content { + width: 100%; +} + +.userdetails { + width: 50%; +} +.spacer { + height: 100vh; + margin-left: $offset; +} + +.spacer-collapsed { + min-width: $icon-width + $offset; + transition: $transition-fast; +} + +.spacer-expanded { + min-width: $sidebar-width + $offset; +} + +.main { + height: 100vh; +} + +.icon { + font-size: large; +} + +.userlist { + min-width: 520px; + height: 100%; +} + +.userlistsmall { + width: 100%; +} + +.userlistextended { + width: 100%; +} + +.table { + width: 90% !important; + margin: auto; +} + +.table th { + cursor: pointer; +} + +.table th, +td { + font-size: $smaller; + border-top: 0px !important; + text-align: left; + padding: $small-offset !important; +} + +.table td { + font-size: $smaller; + cursor: pointer; +} + +tr span { + display: flex; +} +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import Userdetail from "./Userdetail"; +import store from "../../store"; +import { mapGetters, mapState } from "vuex"; +import { displayError } from "../../lib/errors.js"; + +export default { + name: "userview", + data() { + return { + sortCriterion: "user", + pageSize: 10, + currentPage: 1 + }; + }, + components: { + Userdetail + }, + computed: { + ...mapGetters("usermanagement", ["isUserDetailsVisible"]), + ...mapState("application", ["showSidebar"]), + spacerStyle() { + return [ + "spacer", + { + "spacer-expanded": this.showSidebar, + "spacer-collapsed": !this.showSidebar + } + ]; + }, + users() { + let users = [...this.$store.getters["usermanagement/users"]]; + users.sort((a, b) => { + if ( + a[this.sortCriterion].toLowerCase() < + b[this.sortCriterion].toLowerCase() + ) + return -1; + if ( + a[this.sortCriterion].toLowerCase() > + b[this.sortCriterion].toLowerCase() + ) + return 1; + return 0; + }); + const start = (this.currentPage - 1) * this.pageSize; + return users.slice(start, start + this.pageSize); + }, + pages() { + let users = [...this.$store.getters["usermanagement/users"]]; + return Math.ceil(users.length / this.pageSize); + }, + tableStyle() { + return { + table: true, + "table-hover": true, + "table-sm": this.isUserDetailsVisible, + fadeIn: true, + animated: true + }; + }, + userlistStyle() { + return [ + "userlist mt-3 mr-3 shadow-xs", + { + userlistsmall: this.isUserDetailsVisible, + userlistextended: !this.isUserDetailsVisible + } + ]; + } + }, + methods: { + tween() {}, + nextPage() { + if (this.currentPage < this.pages) { + document.querySelector("#datatable").classList.add("fadeOut"); + setTimeout(() => { + document.querySelector("#datatable").classList.remove("fadeOut"); + this.currentPage += 1; + }, 10); + } + return; + }, + prevPage() { + if (this.currentPage > 0) { + document.querySelector("#datatable").classList.add("fadeOut"); + setTimeout(() => { + document.querySelector("#datatable").classList.remove("fadeOut"); + this.currentPage -= 1; + }, 10); + } + return; + }, + sortBy(criterion) { + this.sortCriterion = criterion; + }, + deleteUser(name) { + this.$store + .dispatch("usermanagement/deleteUser", { name: name }) + .then(() => { + this.submitted = false; + this.$store.dispatch("usermanagement/loadUsers").catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + addUser() { + this.$store.commit("usermanagement/clearCurrentUser"); + this.$store.commit("usermanagement/setUserDetailsVisible"); + }, + selectUser(name) { + const user = this.$store.getters["usermanagement/getUserByName"](name); + this.$store.commit("usermanagement/setCurrentUser", user); + }, + roleIcon(role) { + if (role === "sys_admin") return "star"; + if (role === "waterway_admin") return ["fab", "adn"]; + return "user"; + } + }, + beforeRouteEnter(to, from, next) { + store + .dispatch("usermanagement/loadUsers") + .then(next) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data}` + }); + }); + }, + beforeRouteLeave(to, from, next) { + store.commit("usermanagement/clearCurrentUser"); + store.commit("usermanagement/setUserDetailsInvisible"); + next(); + } +}; +</script>
--- a/client/src/router.js Tue Dec 11 22:59:10 2018 +0100 +++ b/client/src/router.js Wed Dec 12 09:22:20 2018 +0100 @@ -20,15 +20,15 @@ /* facilitate codesplitting */ const Login = () => import("./components/Login.vue"); -const Main = () => import("./components/map/Main.vue"); +const Main = () => import("./components/Main.vue"); const Usermanagement = () => - import("./components/admin/usermanagement/Usermanagement.vue"); -const Logs = () => import("./components/admin/Logs.vue"); -const Importqueue = () => import("./components/admin/Importqueue.vue"); + import("./components/usermanagement/Usermanagement.vue"); +const Logs = () => import("./components/Logs.vue"); +const Importqueue = () => import("./components/Importqueue.vue"); const Importschedule = () => - import("./components/admin/importschedule/Importschedule.vue"); + import("./components/importschedule/Importschedule.vue"); const Systemconfiguration = () => - import("./components/admin/Systemconfiguration.vue"); + import("./components/Systemconfiguration.vue"); Vue.use(Router);