Mercurial > gemma
changeset 1272:bc55ffaeb639
cleaned up client/src directory
better organization of files and directories, better naming, separation of admin and map context
line wrap: on
line diff
--- a/client/src/App.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -<template> - <div id="app" class="main"> - <div v-if="isAuthenticated" class="d-flex flex-column userinterface"> - <div class="topbar d-flex pt-3 mx-3"> - <div class="mr-auto d-flex"> - <Sidebar :routeName="routeName"></Sidebar> - <div class="d-flex flex-column" style="max-width: 600px;"> - <Search v-if="routeName == 'mainview'"></Search> - <Contextbox v-if="routeName == 'mainview'"></Contextbox> - </div> - </div> - <div class="ml-auto d-flex"> - <Pdftool v-if="routeName == 'mainview'"></Pdftool> - <Layers v-if="routeName == 'mainview'"></Layers> - <Identify v-if="routeName == 'mainview'"></Identify> - <Toolbar v-if="routeName == 'mainview'"></Toolbar> - </div> - </div> - <div class="flex-fill"></div> - <div class="d-flex flex-row align-items-end"> - <Surveys v-if="routeName == 'mainview'"></Surveys> - <Infobar v-if="routeName == 'mainview'"></Infobar> - </div> - <Zoom v-if="routeName == 'mainview'"></Zoom> - </div> - <div class="d-flex flex-column"> - <router-view/> - </div> - </div> -</template> - -<style lang="sass" scoped> -.userinterface - position: absolute - top: 0 - left: 0 - height: 100vh - width: 100vw - z-index: 4 - pointer-events: none - -.topbar - position: relative - z-index: 2 - -#app - height: 100vh - width: 100vw - font-family: "Avenir", Helvetica, Arial, sans-serif - -webkit-font-smoothing: antialiased - -moz-osx-font-smoothing: grayscale - text-align: center - color: #2c3e50 -</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> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "app", - computed: { - ...mapState("user", ["isAuthenticated"]), - routeName() { - const routeName = this.$route.name; - return routeName; - } - }, - components: { - Surveys: () => import("./fairway/Surveys"), - Infobar: () => import("./fairway/Infobar"), - Pdftool: () => import("./pdftool/Pdftool"), - Zoom: () => import("./zoom/zoom"), - Identify: () => import("./identify/Identify"), - Layers: () => import("./layers/Layers"), - Sidebar: () => import("./application/Sidebar"), - Search: () => import("./application/Search"), - Contextbox: () => import("./application/Contextbox"), - Toolbar: () => import("./toolbar/Toolbar") - } -}; -</script>
--- a/client/src/application/Contextbox.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,91 +0,0 @@ -<template> - <div :class="style"> - <div @click="close" class="ui-element close-contextbox"> - <i class="fa fa-close"></i> - </div> - <Bottlenecks v-if="showInContextBox === 'bottlenecks'"></Bottlenecks> - <Imports v-if="showInContextBox === 'imports'"></Imports> - <Staging v-if="showInContextBox === '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/Bottlenecks"), - Imports: () => import("../imports/Imports.vue"), - Staging: () => import("../staging/Staging.vue") - }, - computed: { - ...mapState("application", ["showSearchbarLastState", "showInContextBox"]), - style() { - return [ - "ui-element shadow-xs contextbox ml-3", - { - contextboxcollapsed: !this.showInContextBox, - contextboxextended: this.showInContextBox, - "rounded-bottom": this.showInContextBox !== "imports", - rounded: this.showInContextBox === "imports" - } - ]; - } - }, - methods: { - close() { - this.$store.commit("application/showInContextBox", null); - this.$store.commit( - "application/showSearchbar", - this.showSearchbarLastState - ); - } - } -}; -</script> - -<style lang="sass" scoped> -.contextbox - position: relative - background-color: #ffffff - opacity: $slight-transparent - transition: left 0.3s ease - overflow: hidden - background: #fff - -.contextboxcollapsed - width: 0 - height: 0 - transition: $transition-fast - -.contextboxextended - min-width: 600px - -.close-contextbox - position: absolute - z-index: 2 - right: 0 - top: 7px - height: $icon-width - width: $icon-height - display: none - color: #fff - -.contextboxextended .close-contextbox - display: block -</style>
--- a/client/src/application/Main.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,126 +0,0 @@ -<template> - <div class="main d-flex flex-column"> - <Maplayer :split="showSplitscreen" :lat="6155376" :long="1819178" :zoom="11"></Maplayer> - <FairwayProfile - :additionalSurveys="additionalSurveys" - :height="height" - :width="width" - :xScale="xAxis" - :yScaleLeft="yAxisLeft" - :yScaleRight="yAxisRight" - :margin="margins" - ></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 "../map/Maplayer"; -import FairwayProfile from "../fairway/Fairwayprofile"; -import { mapState } from "vuex"; -import debounce from "debounce"; - -export default { - name: "mainview", - components: { - Maplayer, - FairwayProfile - }, - data() { - return { - width: null, - height: null, - margin: { - top: 20, - right: 40, - bottom: 30, - left: 40 - } - }; - }, - computed: { - ...mapState("application", ["showSplitscreen"]), - ...mapState("fairwayprofile", [ - "currentProfile", - "minAlt", - "maxAlt", - "totalLength", - "waterLevels", - "fairwayCoordinates", - "selectedWaterLevel" - ]), - ...mapState("bottlenecks", ["surveys", "selectedSurvey"]), - additionalSurveys() { - if (!this.surveys) return []; - if (!this.selectedSurvey) return this.surveys; - return this.surveys.filter(survey => { - return survey.date_info !== this.selectedSurvey.date_info; - }); - }, - xAxis() { - return [this.xScale.x, this.xScale.y]; - }, - yAxisLeft() { - const hi = Math.max(this.maxAlt, this.selectedWaterLevel); - return [this.yScaleLeft.lo, hi]; - }, - yAxisRight() { - const DELTA = this.maxAlt * 1.1 - this.maxAlt; - return [this.maxAlt * 1 + DELTA, -DELTA]; - }, - margins() { - return this.margin; - }, - yScaleLeft() { - return { - lo: this.minAlt, - hi: this.maxAlt - }; - }, - xScale() { - return { - x: 0, - y: this.totalLength - }; - } - }, - created() { - window.addEventListener("resize", debounce(this.scaleFairwayProfile), 100); - window.addEventListener("onbeforeprint", this.test); - }, - updated() { - this.scaleFairwayProfile(); - }, - destroyed() { - window.removeEventListener("resize", debounce(this.scaleFairwayProfile)); - }, - methods: { - test(evt) { - console.log("test: ", evt); - }, - 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; - } - } -}; -</script>
--- a/client/src/application/Search.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,223 +0,0 @@ -<template> - <div :class="searchbarContainerStyle"> - <div class="input-group-prepend"> - <span @click="toggleSearchbar" :class="searchButtonStyle" for="search"> - <i class="fa fa-search d-print-none"></i> - </span> - </div> - <div class="searchgroup flex-fill"> - <input - @keyup.enter="takeFirstSearchresult" - v-if="showSearchbar" - id="search" - v-model="searchQuery" - type="text" - :class="searchInputStyle" - > - <div v-if="showSearchbar && searchResults !== null && !showInContextBox" class="searchresults border-top ui-element bg-white rounded-bottom d-print-none"> - <div v-for="entry of searchResults" :key="entry.name" class="border-top py-2"> - <a href="#" @click.prevent="moveToSearchResult(entry)">{{ entry.name }}</a> - </div> - </div> - </div> - </div> -</template> - -<style lang="sass" scoped> - .searchcontainer - height: $icon-height - opacity: $slight-transparent - - .searchbar-expanded - min-width: 600px - .searchbar - border-top-left-radius: 0 !important - border-bottom-left-radius: 0 !important - - .searchbar-collapsed - width: $icon-width !important - transition: $transition-fast - - .searchbar - height: $icon-height !important - box-shadow: none !important - &.rounded-top-right - border-radius: 0 !important - border-top-right-radius: $border-radius !important - - .searchlabel - &.rounded-top-left - border-radius: 0 !important - border-top-left-radius: $border-radius !important - - .input-group-text - height: $icon-height - width: $icon-width - - .input-group-prepend - .fa-search - color: #666 - - .searchresults - margin-left: -31px - max-height: 20rem - overflow: auto - > div:first-child - border-top: 0 !important -</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 "../application/lib/errors.js"; -import { HTTP } from "../application/lib/http"; - -const setFocus = () => document.querySelector("#search").focus(); - -export default { - name: "search", - data() { - return { - searchQueryIsDirty: false, - searchResults: null, - isSearching: false - }; - }, - computed: { - ...mapState("application", ["showSearchbar", "showInContextBox"]), - 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 ml-3 shadow-xs", - { - "searchbar-collapsed": !this.showSearchbar, - "searchbar-expanded": this.showSearchbar, - "d-flex": this.showInContextBox !== "imports", - "d-none": this.showInContextBox === "imports" - } - ]; - }, - searchInputStyle() { - return [ - "form-control ui-element search searchbar d-print-none border-0", - { "rounded-top-right": this.showInContextBox || 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.showInContextBox || 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: "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.showInContextBox) { - if (!this.showSearchbar) { - setTimeout(setFocus, 300); - } - this.$store.commit("application/showSearchbar", !this.showSearchbar); - } - } - } -}; -</script>
--- a/client/src/application/Sidebar.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,186 +0,0 @@ -<template> - <div :class="sidebarStyle"> - <div - @click="$store.commit('application/showSidebar', !showSidebar)" - class="menubutton p-2 bg-white rounded position-absolute d-flex justify-content-center" - > - <i class="ui-element d-print-none fa fa-bars"></i> - </div> - <div :class="menuStyle"> - <div class="menupoints" v-if="this.showSidebar"> - <router-link to="/" class="text-body d-flex flex-row nav-link"> - <i class="fa fa-map-o align-self-center navicon"></i>Map - </router-link> - <div class="d-flex flex-row nav-link"> - <i class="fa fa-ship align-self-center navicon"></i> - <a - class="text-body d-flex flex-row" - href="#" - @click="toggleContextBox('bottlenecks')" - >Bottlenecks</a> - </div> - <div v-if="isSysAdmin"> - <hr> - <div class="nav-link d-flex menupadding text-muted">Administration</div> - </div> - <div v-if="isWaterwayAdmin"> - <div class="d-flex flex-row nav-link"> - <i class="fa fa-upload align-self-center navicon"></i> - <a - href="#" - class="text-body" - @click="toggleContextBox('imports')" - >Import soundingresults</a> - </div> - <div class="d-flex flex-row nav-link"> - <i class="fa fa-list-ol align-self-center navicon"></i> - <a - href="#" - class="text-body" - @click="toggleContextBox('staging')" - >Staging area</a> - </div> - <div class="nav-link d-flex menupadding text-muted">Systemadministration</div> - <router-link class="text-body d-flex flex-row nav-link" to="usermanagement"> - <i class="fa fa-address-card-o align-self-center navicon"></i>Users - </router-link> - </div> - <div v-if="isSysAdmin"> - <router-link - class="text-body d-flex flex-row nav-link" - to="systemconfiguration" - > - <i class="fa fa-wrench align-self-center navicon"></i>Systemconfiguration - </router-link> - <router-link class="text-body d-flex flex-row nav-link" to="logs"> - <i class="fa fa-book align-self-center navicon"></i>Logs - </router-link> - <router-link class="text-body d-flex flex-row nav-link" to="importqueue"> - <i class="fa fa-exchange align-self-center navicon"></i>Importqueue - </router-link> - </div> - <hr> - <a href="#" @click="logoff" class="text-body d-flex flex-row nav-link"> - <i class="fa fa-power-off align-self-center navicon"></i> - Logout {{ user }} - </a> - </div> - </div> - </div> -</template> - -<script> -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapGetters, mapState } from "vuex"; - -export default { - name: "sidebar", - props: ["routeName"], - computed: { - ...mapGetters("user", ["isSysAdmin", "isWaterwayAdmin"]), - ...mapState("user", ["user"]), - ...mapState("application", [ - "showSidebar", - "showSearchbarLastState", - "showInContextBox" - ]), - menuStyle() { - return { - menu: true, - nav: true, - "flex-column": true - }; - }, - sidebarStyle() { - return [ - "ui-element position-relative sidebar rounded shadow-xs d-print-none mb-auto", - { - sidebarcollapsed: !this.showSidebar, - sidebarextended: this.showSidebar - } - ]; - } - }, - methods: { - logoff() { - this.$store.commit("user/clearAuth"); - this.$store.commit("application/showSidebar", false); - this.$store.commit("application/showUsermenu", false); - this.$store.commit("application/showSplitscreen", false); - this.$router.push("/login"); - }, - toggleContextBox(context) { - const SHOW = context; - const HIDE = null; - const isElementAlreadyShown = this.showInContextBox === context; - let toggleState = - isElementAlreadyShown && this.routeName === "mainview" ? HIDE : SHOW; - this.$router.push("/"); - this.$store.commit("application/showInContextBox", toggleState); - if (this.showInContextBox === context) { - this.$store.commit("application/showSearchbar", true); - } else { - this.$store.commit( - "application/showSearchbar", - this.showSearchbarLastState - ); - } - } - } -}; -</script> - -<style lang="sass" scoped> - -a:hover - text-decoration: none - -.menupoints - text-align: left - -.menubutton - height: $icon-height - width: $icon-width - top: 0 - left: 0 - color: #666 - -.router-link-exact-active - background-color: #f2f2f2 - -.navicon - margin-right: $small-offset - color: #666 - -.menu - padding-top: $small-offset - -.sidebar - background-color: #ffffff - padding-top: $large-offset - opacity: $slight-transparent - -.sidebarcollapsed - height: 30px - width: 30px - transition: $transition-fast - -.sidebarextended - height: 35rem - width: $sidebar-width - min-width: $sidebar-width -</style>
--- a/client/src/application/assets/application.sass Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,66 +0,0 @@ -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation> - */ -$shadow-xs: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2) -$border-radius: 0.25rem -$icon-height: 2rem -$icon-width: 2rem -$large-offset: 2rem -$offset: 1rem -$sidebar-width: 15rem -$slight-transparent: 0.96 -$small-offset: 0.5rem -$smaller: 0.9rem -$transition-fast: 0.3s -$x-large-offset: 3rem -$xx-large-offset: 5rem -$color-info: #17a2b8 - -a - color: $color-info - -.w-90 - width: 90% - -.debug - border: 1px solid red - -%fully-centered - position: absolute - top: 50% - left: 50% - transform: translate(-50%, -50%) - -.ui-element - pointer-events: auto - -.shadow-xs - box-shadow: $shadow-xs - -.box - opacity: $slight-transparent - max-height: 0 - max-width: 0 - overflow: hidden - margin-left: 0 - margin-right: 0 - box-shadow: $shadow-xs - transition: max-width .4s, max-height .4s, margin-left .4s, margin-right .4s - -.expanded - max-height: 999px - max-width: 999px - margin-left: 0.5rem - margin-right: 0.5rem
--- a/client/src/application/assets/tooltip.sass Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,107 +0,0 @@ -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -.tooltip - display: block !important - z-index: 10000 - - .tooltip-inner - background: black - color: white - border-radius: 16px - padding: 5px 10px 4px - - .tooltip-arrow - width: 0 - height: 0 - border-style: solid - position: absolute - margin: 5px - border-color: black - z-index: 1 - - &[x-placement^="top"] - margin-bottom: 5px - - .tooltip-arrow - border-width: 5px 5px 0 5px - border-left-color: transparent !important - border-right-color: transparent !important - border-bottom-color: transparent !important - bottom: -5px - left: calc(50% - 5px) - margin-top: 0 - margin-bottom: 0 - - &[x-placement^="bottom"] - margin-top: 5px - - .tooltip-arrow - border-width: 0 5px 5px 5px - border-left-color: transparent !important - border-right-color: transparent !important - border-top-color: transparent !important - top: -5px - left: calc(50% - 5px) - margin-top: 0 - margin-bottom: 0 - - &[x-placement^="right"] - margin-left: 5px - - .tooltip-arrow - border-width: 5px 5px 5px 0 - border-left-color: transparent !important - border-top-color: transparent !important - border-bottom-color: transparent !important - left: -5px - top: calc(50% - 5px) - margin-left: 0 - margin-right: 0 - - &[x-placement^="left"] - margin-right: 5px - - .tooltip-arrow - border-width: 5px 0 5px 5px - border-top-color: transparent !important - border-right-color: transparent !important - border-bottom-color: transparent !important - right: -5px - top: calc(50% - 5px) - margin-left: 0 - margin-right: 0 - - &.popover - $color: #f9f9f9 - - .popover-inner - background: $color - color: black - padding: 24px - border-radius: 5px - box-shadow: 0 5px 30px rgba(black, 0.1) - - .popover-arrow - border-color: $color - - &[aria-hidden="true"] - visibility: hidden - opacity: 0 - transition: opacity 0.15s, visibility 0.15s - - &[aria-hidden="false"] - visibility: visible - opacity: 1 - transition: opacity 0.15s
--- a/client/src/application/lib/errors.js Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -/** facade to wrap calls to vue2-toastr */ -import app from "../../main"; - -const displayError = ({ title, message }) => { - app.$toast.error({ - title: title, - message: message - }); -}; - -const displayInfo = ({ title, message }) => { - app.$toast.info({ - title: title, - message: message - }); -}; - -export { displayError, displayInfo };
--- a/client/src/application/lib/geo.js Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,208 +0,0 @@ -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -/** - * - * Distance calculations - * JS transposition of cross.go functions - * - */ - -import { GeoJSON } from "ol/format.js"; -import Feature from "ol/Feature"; -import distance from "@turf/distance"; -import { - lineString as turfLineString, - polygon as turfPolygon -} from "@turf/helpers"; -import lineIntersect from "@turf/line-intersect"; - -const EARTHRADIUS = 6378137.0; - -/** - * Converts to radiant - * @param {degree} d - */ -const deg2rad = d => { - return (d * Math.PI) / 180.0; -}; - -/** - * Calculates the difference between two points in m - * - * Points are given with {lat: $lat, lon: $lon} - * - * @param {object} P1 - * @param {object} P2 - */ -const distanceBetween = (P1, P2) => { - const dLat = deg2rad(P2.lat - P1.lat); - let dLng = Math.abs(deg2rad(P2.lon - P1.lon)); - if (dLng > Math.PI) { - dLng = 2 * Math.PI - dLng; - } - const x = dLng * Math.cos(deg2rad((P1.lat + P2.lat) / 2.0)); - return Math.sqrt(dLat * dLat + x * x) * EARTHRADIUS; -}; - -/** - * Takes a triple of [lat, long, alt] and generates - * an object with according attributes - * { - * lat: $lat, - * lon: $lon, - * alt: $alt - * } - * - * @param {array} coords - */ -const Point = coords => { - return { - lon: coords[0], - lat: coords[1], - alt: coords[2] - }; -}; - -/** - * Has geoJSON as its input and transforms - * given coordinates into points representing - * distance from startpoint / altitude information - * - * a) extracting the minimum altitude - * b) extracting the maximum altitude - * c) calculating the total length of the given profile - * d) transposes the datapoints relative to a given start point - * - * The calculation of total equals the sum of partial distances between points - * - * The x-value of a point is equal to the total distance up to this point - * - * The distance between the last point of the last segment and the end point is added - * to the total - * - * @param {object} geoJSON, startPoint, endPoint - */ -const transform = ({ geoJSON, startPoint, endPoint }) => { - const lineSegments = geoJSON.geometry.coordinates; - let segmentPoints = []; - let lengthPolyLine = 0; - let referencePoint = Point(startPoint); - let minAlt = Math.abs(lineSegments[0][0][2]); - let maxAlt = Math.abs(lineSegments[0][0][2]); - let currentPoint = null; - for (let segment of lineSegments) { - let points = []; - for (let coordinateTriplet of segment) { - currentPoint = Point(coordinateTriplet); - lengthPolyLine += distanceBetween(referencePoint, currentPoint); - let y = Math.abs(currentPoint.alt); - points.push({ - x: lengthPolyLine, - y: y - }); - if (y < minAlt) minAlt = y; - if (y > maxAlt) maxAlt = y; - referencePoint = currentPoint; - } - segmentPoints.push(points); - } - lengthPolyLine += distanceBetween(currentPoint, Point(endPoint)); - return { segmentPoints, lengthPolyLine, minAlt, maxAlt }; -}; - -/** - * Prepare profile takes geoJSON data in form of - * a MultiLineString, e.g. - * - * { - * type: "Feature", - * geometry: { - * type: "MultiLineString", - * coordinates: [ - * [ - * [16.53593398, 48.14694085, -146.52392755] - * ... - * ]] - * - * and transforms it to a structure representing the number of sections - * where data is present with according lengths and the points - * - * { - * { points: - * [ - * [ { x: 0.005798201616417183, y: -146.52419461 }, - * { x: 0, y: -146.52394016 } - * ... - * ] - * ] - * lengthPolyLine: 160.06814078495722, - * minAlt: -146.73122231, - * maxAlt: -145.65155866 - * } - * - * @param {object} geoJSON - */ -const prepareProfile = ({ geoJSON, startPoint, endPoint }) => { - const { segmentPoints, lengthPolyLine, minAlt, maxAlt } = transform({ - geoJSON, - startPoint, - endPoint - }); - return { - points: segmentPoints, - lengthPolyLine: lengthPolyLine, - minAlt: minAlt, - maxAlt: maxAlt - }; -}; - -const generateFeatureRequest = (profileLine, bottleneck_id, date_info) => { - const feature = new Feature({ - geometry: profileLine, - bottleneck: bottleneck_id, - date: date_info - }); - return new GeoJSON({ geometryName: "geometry" }).writeFeature(feature); -}; - -const calculateFairwayCoordinates = (profileLine, fairwayGeometry, depth) => { - // both geometries have to be in EPSG:4326 - // uses turfjs distance() function - let fairwayCoordinates = []; - var line = turfLineString(profileLine.getCoordinates()); - var polygon = turfPolygon(fairwayGeometry.getCoordinates()); - var intersects = lineIntersect(line, polygon); - var l = intersects.features.length; - if (l % 2 != 0) { - console.log("Ignoring fairway because profile only intersects once."); - } else { - for (let i = 0; i < l; i += 2) { - let pStartPoint = profileLine.getCoordinates()[0]; - let fStartPoint = intersects.features[i].geometry.coordinates; - let fEndPoint = intersects.features[i + 1].geometry.coordinates; - let opts = { units: "kilometers" }; - - fairwayCoordinates.push([ - distance(pStartPoint, fStartPoint, opts) * 1000, - distance(pStartPoint, fEndPoint, opts) * 1000, - depth - ]); - } - } - return fairwayCoordinates; -}; - -export { generateFeatureRequest, prepareProfile, calculateFairwayCoordinates };
--- a/client/src/application/lib/http.js Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import axios from "axios"; - -export const HTTP = axios.create({ - baseURL: process.env.VUE_APP_API_URL || "/api" - /* headers: { - Authorization: 'Bearer {token}' - }*/ -});
--- a/client/src/application/lib/session.js Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -/** - * Compares whether session is current - * based on the expiry information and the - * current date - * - * @param {number} expiresFromPastSession - */ -function sessionStillActive(expiresFromPastSession) { - if (!expiresFromPastSession) return false; - const now = Date.now(); - const stillActive = now < expiresFromPastSession; - return stillActive; -} -/** - * Converts a given unix time to Milliseconds - * - * @param {string} timestring - */ -function toMillisFromString(timestring) { - return timestring * 1000; -} - -export { sessionStillActive, toMillisFromString };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/assets/application.sass Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,66 @@ +/* + * 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> + */ +$shadow-xs: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2) +$border-radius: 0.25rem +$icon-height: 2rem +$icon-width: 2rem +$large-offset: 2rem +$offset: 1rem +$sidebar-width: 15rem +$slight-transparent: 0.96 +$small-offset: 0.5rem +$smaller: 0.9rem +$transition-fast: 0.3s +$x-large-offset: 3rem +$xx-large-offset: 5rem +$color-info: #17a2b8 + +a + color: $color-info + +.w-90 + width: 90% + +.debug + border: 1px solid red + +%fully-centered + position: absolute + top: 50% + left: 50% + transform: translate(-50%, -50%) + +.ui-element + pointer-events: auto + +.shadow-xs + box-shadow: $shadow-xs + +.box + opacity: $slight-transparent + max-height: 0 + max-width: 0 + overflow: hidden + margin-left: 0 + margin-right: 0 + box-shadow: $shadow-xs + transition: max-width .4s, max-height .4s, margin-left .4s, margin-right .4s + +.expanded + max-height: 999px + max-width: 999px + margin-left: 0.5rem + margin-right: 0.5rem
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/assets/tooltip.sass Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,107 @@ +/* + * 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> + */ +.tooltip + display: block !important + z-index: 10000 + + .tooltip-inner + background: black + color: white + border-radius: 16px + padding: 5px 10px 4px + + .tooltip-arrow + width: 0 + height: 0 + border-style: solid + position: absolute + margin: 5px + border-color: black + z-index: 1 + + &[x-placement^="top"] + margin-bottom: 5px + + .tooltip-arrow + border-width: 5px 5px 0 5px + border-left-color: transparent !important + border-right-color: transparent !important + border-bottom-color: transparent !important + bottom: -5px + left: calc(50% - 5px) + margin-top: 0 + margin-bottom: 0 + + &[x-placement^="bottom"] + margin-top: 5px + + .tooltip-arrow + border-width: 0 5px 5px 5px + border-left-color: transparent !important + border-right-color: transparent !important + border-top-color: transparent !important + top: -5px + left: calc(50% - 5px) + margin-top: 0 + margin-bottom: 0 + + &[x-placement^="right"] + margin-left: 5px + + .tooltip-arrow + border-width: 5px 5px 5px 0 + border-left-color: transparent !important + border-top-color: transparent !important + border-bottom-color: transparent !important + left: -5px + top: calc(50% - 5px) + margin-left: 0 + margin-right: 0 + + &[x-placement^="left"] + margin-right: 5px + + .tooltip-arrow + border-width: 5px 0 5px 5px + border-top-color: transparent !important + border-right-color: transparent !important + border-bottom-color: transparent !important + right: -5px + top: calc(50% - 5px) + margin-left: 0 + margin-right: 0 + + &.popover + $color: #f9f9f9 + + .popover-inner + background: $color + color: black + padding: 24px + border-radius: 5px + box-shadow: 0 5px 30px rgba(black, 0.1) + + .popover-arrow + border-color: $color + + &[aria-hidden="true"] + visibility: hidden + opacity: 0 + transition: opacity 0.15s, visibility 0.15s + + &[aria-hidden="false"] + visibility: visible + opacity: 1 + transition: opacity 0.15s
--- a/client/src/bottlenecks/Bottlenecks.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,233 +0,0 @@ -<template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> - <i class="fa fa-ship mr-2"></i> - Bottlenecks - </h6> - <div class="row p-2 text-left small"> - <div class="col-5"> - <a href="#" @click="sortBy('name')" class="sort-link">Name</a> - <i :class="sortClass" v-if="sortColumn === 'name'"></i> - </div> - <div class="col-2"> - <a - href="#" - @click="sortBy('latestMeasurement')" - class="sort-link" - >Latest Measurement</a> - <i :class="sortClass" v-if="sortColumn === 'latestMeasurement'"></i> - </div> - <div class="col-3"> - <a href="#" @click="sortBy('chainage')" class="sort-link">Chainage</a> - <i :class="sortClass" v-if="sortColumn === 'chainage'"></i> - </div> - <div class="col-2"></div> - </div> - <div class="bottleneck-list small text-left" v-if="filteredAndSortedBottlenecks().length"> - <div - v-for="bottleneck in filteredAndSortedBottlenecks()" - :key="bottleneck.properties.name" - class="border-top row mx-0 py-2" - > - <div class="col-5 text-left"> - <a - href="#" - class="d-block" - @click="moveToBottleneck(bottleneck)" - >{{ bottleneck.properties.name }}</a> - </div> - <div class="col-2">{{ displayCurrentSurvey(bottleneck.properties.current) }}</div> - <div - class="col-3" - >{{ displayCurrentChainage(bottleneck.properties.from, bottleneck.properties.from) }}</div> - <div class="col-2 text-right"> - <button - type="button" - class="btn btn-sm btn-outline-secondary" - @click="toggleBottleneck(bottleneck.properties.name)" - > - <i class="fa fa-angle-down"></i> - </button> - </div> - <div - :class="['col-12', 'surveys', {open: openBottleneck === bottleneck.properties.name}]" - > - <a - href="#" - class="d-block p-2" - v-for="(survey, index) in openBottleneckSurveys" - :key="index" - @click="selectSurvey(survey, bottleneck)" - >{{ survey.date_info }}</a> - </div> - </div> - </div> - <div v-else class="small text-center py-3 border-top"> - No results. - </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"; -import { displayError } from "../application/lib/errors.js"; - -export default { - name: "bottlenecks", - data() { - return { - sortColumn: "name", - sortDirection: "ASC", - openBottleneck: null, - openBottleneckSurveys: null - }; - }, - computed: { - ...mapState("application", ["searchQuery", "showSearchbarLastState"]), - ...mapState("bottlenecks", ["bottlenecks"]), - sortClass() { - return [ - "fa ml-1", - { - "fa-sort-amount-asc": this.sortDirection === "ASC", - "fa-sort-amount-desc": this.sortDirection === "DESC" - } - ]; - } - }, - methods: { - filteredAndSortedBottlenecks() { - return this.bottlenecks - .filter(bn => { - return bn.properties.name - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - }) - .sort((bnA, bnB) => { - switch (this.sortColumn) { - case "name": - if ( - bnA.properties.name.toLowerCase() < - bnB.properties.name.toLowerCase() - ) - return this.sortDirection === "ASC" ? -1 : 1; - if ( - bnA.properties.name.toLowerCase() > - bnB.properties.name.toLowerCase() - ) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - - case "latestMeasurement": { - if ( - (bnA.properties.current || "") < (bnB.properties.current || "") - ) - return this.sortDirection === "ASC" ? -1 : 1; - if ( - (bnA.properties.current || "") > (bnB.properties.current || "") - ) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - } - - case "chainage": - if (bnA.properties.from < bnB.properties.from) - return this.sortDirection === "ASC" ? -1 : 1; - if (bnA.properties.from > bnB.properties.from) - return this.sortDirection === "ASC" ? 1 : -1; - return 0; - - default: - return 0; - } - }); - }, - selectSurvey(survey, bottleneck) { - this.$store.dispatch( - "bottlenecks/setSelectedBottleneck", - bottleneck.properties.name - ); - this.$store.commit("bottlenecks/setSelectedSurvey", survey); - this.moveToBottleneck(bottleneck); - }, - moveToBottleneck(bottleneck) { - 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"; - }, - toggleBottleneck(name) { - this.openBottleneckSurveys = null; - if (name === this.openBottleneck) { - this.openBottleneck = null; - } else { - this.openBottleneck = 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; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } - }, - displayCurrentSurvey(current) { - return current ? current.substr(0, current.length - 1) : ""; - }, - displayCurrentChainage(from, to) { - return from / 10 + " - " + to / 10; - } - }, - mounted() { - this.$store.dispatch("bottlenecks/loadBottlenecks"); - } -}; -</script> - -<style lang="sass" scoped> -.bottleneck-list - overflow-y: auto - max-height: 500px - -.surveys - max-height: 0 - overflow: hidden - transition: max-height 0.3s ease - -.surveys.open - max-height: 999px - -.sort-link - color: #444 - font-weight: bold -</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/App.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,96 @@ +<template> + <div id="app" class="main"> + <div v-if="isAuthenticated" class="d-flex flex-column userinterface"> + <div class="topbar d-flex pt-3 mx-3"> + <div class="mr-auto d-flex"> + <Sidebar :routeName="routeName"></Sidebar> + <div class="d-flex flex-column" style="max-width: 600px;"> + <Search v-if="routeName == 'mainview'"></Search> + <Contextbox v-if="routeName == 'mainview'"></Contextbox> + </div> + </div> + <div class="ml-auto d-flex"> + <Pdftool v-if="routeName == 'mainview'"></Pdftool> + <Layers v-if="routeName == 'mainview'"></Layers> + <Identify v-if="routeName == 'mainview'"></Identify> + <Toolbar v-if="routeName == 'mainview'"></Toolbar> + </div> + </div> + <div class="flex-fill"></div> + <div class="d-flex flex-row align-items-end"> + <Surveys v-if="routeName == 'mainview'"></Surveys> + <Infobar v-if="routeName == 'mainview'"></Infobar> + </div> + <Zoom v-if="routeName == 'mainview'"></Zoom> + </div> + <div class="d-flex flex-column"> + <router-view/> + </div> + </div> +</template> + +<style lang="sass" scoped> +.userinterface + position: absolute + top: 0 + left: 0 + height: 100vh + width: 100vw + z-index: 4 + pointer-events: none + +.topbar + position: relative + z-index: 2 + +#app + height: 100vh + width: 100vw + font-family: "Avenir", Helvetica, Arial, sans-serif + -webkit-font-smoothing: antialiased + -moz-osx-font-smoothing: grayscale + text-align: center + color: #2c3e50 +</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> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "app", + computed: { + ...mapState("user", ["isAuthenticated"]), + routeName() { + const routeName = this.$route.name; + return routeName; + } + }, + components: { + Surveys: () => import("./map/fairway/Surveys"), + Infobar: () => import("./map/fairway/Infobar"), + Pdftool: () => import("./map/Pdftool"), + Zoom: () => import("./map/Zoom"), + Identify: () => import("./map/Identify"), + Layers: () => import("./map/layers/Layers"), + Sidebar: () => import("./Sidebar"), + Search: () => import("./map/Search"), + Contextbox: () => import("./map/Contextbox"), + Toolbar: () => import("./map/toolbar/Toolbar") + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Login.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,182 @@ +(<template> + <div class="d-flex flex-column login bg-white shadow"> + <div class="m-5"> + <!-- logo section --> + <div class="d-flex flex-row justify-content-center mb-3"> + <div class="logo mr-3"><img src="../assets/logo.png"></div> + <div class="title"> + <h1>{{ appTitle }}</h1> + </div> + </div> + <!-- end logo section --> + <form class="loginform mx-auto" @submit.prevent="login"> + <div id="alert" :style="errorMessageStyle" :class="errorMessageClass" role="alert"> + <span>{{ errorMessage }}</span> + </div> + <div class="input-group mb-3"> + <input type="text" v-model="user" id="inputUsername" class="form-control shadow-sm" :placeholder="usernameLabel" required autofocus> + </div> + <div class="input-group mb-3"> + <input :type="isPasswordVisible" v-model="password" id="inputPassword" class="form-control shadow-sm" :placeholder='passwordLabel' :required='!showPasswordReset' :disabled='showPasswordReset'> + <div class="input-group-append"> + <span class="input-group-text disabled" id="basic-addon2" @click="showPassword"> + <font-awesome-icon icon="eye" v-if="!readablePassword" /> + <font-awesome-icon icon="eye-slash" v-if="readablePassword" /> + </span> + </div> + </div> + <button v-if="showPasswordReset==false" class="btn btn-primary btn-block shadow-sm" :disabled="submitted || showPasswordReset" type="submit"> + <translate>Login</translate> + </button> + <div v-if="showPasswordReset" class="passwordreset"> + <button class="btn btn-block btn-info" type="button" @click="resetPassword"> + <translate>Request password reset!</translate> + </button> + <div class="pull-right"> + <a href="#" @click.prevent="togglePasswordReset"> + <translate>back to login</translate> + </a> + </div> + </div> + <div v-else class="pull-right"> + <a href="#" @click.prevent="togglePasswordReset"> + <translate>Forgot password</translate> + </a> + </div> + </form> + + <!-- bottom logo section --> + <div class="mb-3 secondary-logo mx-auto mb-auto"><img :src="secondaryLogo"></div> + </div> + </div> +</template>) + +<style lang="sass" scoped> +.login + min-width: 375px + min-height: 500px + @extend %fully-centered + +.loginform + max-width: 375px + +.alert + padding: 0.5rem + +.secondary-logo + max-width: 375px + +/* avoid IE and Edge show a password reveal as we do our own */ +input[type="password"]::-ms-reveal + display: none +</style> + +<script> +import { mapState } from "vuex"; +import { HTTP } from "../lib/http.js"; +import { displayError } from "../lib/errors.js"; + +const UNAUTHORIZED = 401; + +export default { + name: "login", + data() { + return { + user: "", + password: "", + submitted: false, + loginFailed: false, + passwordJustResetted: false, + readablePassword: false, + showPasswordReset: false, + usernameToReset: "" + }; + }, + computed: { + errorMessage() { + if (this.loginFailed) return this.$gettext("Login failed"); + if (this.passwordJustResetted) + return this.$gettext("Password reset requested!"); + return "&npsp;"; + }, + passwordLabel() { + return this.$gettext("Enter passphrase"); + }, + usernameLabel() { + return this.$gettext("Enter username"); + }, + isPasswordVisible() { + return this.readablePassword ? "text" : "password"; + }, + errorMessageStyle() { + if (this.loginFailed || this.passwordJustResetted) { + return "visibility:visible"; + } + return "visibility:hidden"; + }, + errorMessageClass() { + let result = { + "mb-3": true, + errormessage: true, + alert: true + }; + if (this.loginFailed) { + result["alert-danger"] = true; + } + if (this.passwordJustResetted) { + result["alert-info"] = true; + } + return result; + }, + ...mapState("application", ["appTitle", "secondaryLogo"]) + }, + methods: { + login() { + this.submitted = true; + this.passwordJustResetted = false; + const { user, password } = this; + this.$store + .dispatch("user/login", { user, password }) + .then(() => { + this.loginFailed = false; + this.$router.push("/"); + }) + .catch(error => { + this.loginFailed = true; + this.submitted = false; + const { status, data } = error.response; + if (status !== UNAUTHORIZED) { + //Unauthorized is handled in alert-div + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + } + }); + }, + showPassword() { + // disallowing toggle when in reset mode + if (this.showPasswordReset) return; + this.readablePassword = !this.readablePassword; + }, + togglePasswordReset() { + this.passwordJustResetted = false; + this.showPasswordReset = !this.showPasswordReset; + this.loginFailed = false; + }, + resetPassword() { + if (this.user) { + HTTP.post("/users/passwordreset", { user: this.user }).catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + this.togglePasswordReset(); + this.passwordJustResetted = true; + } + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Sidebar.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,186 @@ +<template> + <div :class="sidebarStyle"> + <div + @click="$store.commit('application/showSidebar', !showSidebar)" + class="menubutton p-2 bg-white rounded position-absolute d-flex justify-content-center" + > + <i class="ui-element d-print-none fa fa-bars"></i> + </div> + <div :class="menuStyle"> + <div class="menupoints" v-if="this.showSidebar"> + <router-link to="/" class="text-body d-flex flex-row nav-link"> + <i class="fa fa-map-o align-self-center navicon"></i>Map + </router-link> + <div class="d-flex flex-row nav-link"> + <i class="fa fa-ship align-self-center navicon"></i> + <a + class="text-body d-flex flex-row" + href="#" + @click="toggleContextBox('bottlenecks')" + >Bottlenecks</a> + </div> + <div v-if="isSysAdmin"> + <hr> + <div class="nav-link d-flex menupadding text-muted">Administration</div> + </div> + <div v-if="isWaterwayAdmin"> + <div class="d-flex flex-row nav-link"> + <i class="fa fa-upload align-self-center navicon"></i> + <a + href="#" + class="text-body" + @click="toggleContextBox('imports')" + >Import soundingresults</a> + </div> + <div class="d-flex flex-row nav-link"> + <i class="fa fa-list-ol align-self-center navicon"></i> + <a + href="#" + class="text-body" + @click="toggleContextBox('staging')" + >Staging area</a> + </div> + <div class="nav-link d-flex menupadding text-muted">Systemadministration</div> + <router-link class="text-body d-flex flex-row nav-link" to="usermanagement"> + <i class="fa fa-address-card-o align-self-center navicon"></i>Users + </router-link> + </div> + <div v-if="isSysAdmin"> + <router-link + class="text-body d-flex flex-row nav-link" + to="systemconfiguration" + > + <i class="fa fa-wrench align-self-center navicon"></i>Systemconfiguration + </router-link> + <router-link class="text-body d-flex flex-row nav-link" to="logs"> + <i class="fa fa-book align-self-center navicon"></i>Logs + </router-link> + <router-link class="text-body d-flex flex-row nav-link" to="importqueue"> + <i class="fa fa-exchange align-self-center navicon"></i>Importqueue + </router-link> + </div> + <hr> + <a href="#" @click="logoff" class="text-body d-flex flex-row nav-link"> + <i class="fa fa-power-off align-self-center navicon"></i> + Logout {{ user }} + </a> + </div> + </div> + </div> +</template> + +<script> +/* + * This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapGetters, mapState } from "vuex"; + +export default { + name: "sidebar", + props: ["routeName"], + computed: { + ...mapGetters("user", ["isSysAdmin", "isWaterwayAdmin"]), + ...mapState("user", ["user"]), + ...mapState("application", [ + "showSidebar", + "showSearchbarLastState", + "showInContextBox" + ]), + menuStyle() { + return { + menu: true, + nav: true, + "flex-column": true + }; + }, + sidebarStyle() { + return [ + "ui-element position-relative sidebar rounded shadow-xs d-print-none mb-auto", + { + sidebarcollapsed: !this.showSidebar, + sidebarextended: this.showSidebar + } + ]; + } + }, + methods: { + logoff() { + this.$store.commit("user/clearAuth"); + this.$store.commit("application/showSidebar", false); + this.$store.commit("application/showUsermenu", false); + this.$store.commit("application/showSplitscreen", false); + this.$router.push("/login"); + }, + toggleContextBox(context) { + const SHOW = context; + const HIDE = null; + const isElementAlreadyShown = this.showInContextBox === context; + let toggleState = + isElementAlreadyShown && this.routeName === "mainview" ? HIDE : SHOW; + this.$router.push("/"); + this.$store.commit("application/showInContextBox", toggleState); + if (this.showInContextBox === context) { + this.$store.commit("application/showSearchbar", true); + } else { + this.$store.commit( + "application/showSearchbar", + this.showSearchbarLastState + ); + } + } + } +}; +</script> + +<style lang="sass" scoped> + +a:hover + text-decoration: none + +.menupoints + text-align: left + +.menubutton + height: $icon-height + width: $icon-width + top: 0 + left: 0 + color: #666 + +.router-link-exact-active + background-color: #f2f2f2 + +.navicon + margin-right: $small-offset + color: #666 + +.menu + padding-top: $small-offset + +.sidebar + background-color: #ffffff + padding-top: $large-offset + opacity: $slight-transparent + +.sidebarcollapsed + height: 30px + width: 30px + transition: $transition-fast + +.sidebarextended + height: 35rem + width: $sidebar-width + min-width: $sidebar-width +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/admin/logs.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,158 @@ +<template> + <div class="main d-flex flex-column"> + <div class="d-flex flex-row"> + <div :class="spacer"></div> + <div class="logoutput text-left bg-white shadow mt-3 mx-3"> + <pre id="code" v-highlightjs="logs"><code class="bash hljs hljs-string"></code></pre> + </div> + </div> + <div class="d-flex flex-row logmenu"> + <div class="d-flex align-self-center"> + <ul class="nav nav-pills"> + <li class="nav-item"> + <a + @click="fetch('system/log/apache2/access.log', 'accesslog')" + :class="accesslogStyle" + href="#" + >Accesslog</a> + </li> + <li class="nav-item"> + <a + @click="fetch('system/log/apache2/error.log', 'errorlog')" + :class="errorlogStyle" + href="#" + >Errorlog</a> + </li> + </ul> + </div> + <div class="statuscontainer d-flex flex-row"> + <div class="statusline ml-3 mt-1 align-self-center"> + <h3>Last refresh: {{refreshed}}</h3> + </div> + <div class="refresh"> + <button class="btn btn-dark" @click="fetch(currentFile, currentLog)">Refresh</button> + </div> + </div> + </div> + </div> +</template> + +<style lang="sass" scoped> +.statuscontainer + width: 87% + position: relative + +.logmenu + margin-left: 5rem + min-width: 60vw + +#code + overflow: auto + +.refresh + position: absolute + right: 0 + +.logoutput + width: 95% + height: 85vh + 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: 7rem +</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/admin/systemconfiguration.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,144 @@ +<template> + <div class="d-flex flex-row"> + <div class="card sysconfig mt-3 mx-auto"> + <div class="card-header shadow-sm text-white bg-info mb-6"> + Systemconfiguration + </div> + <div class="card-body config"> + <section class="configsection"> + <h4 class="card-title">Bottleneck Areas stroke-color</h4> + <compact-picker v-model="strokeColor" /> + </section> + <section> + <h4 class="card-title">Bottleneck Areas fill-color</h4> + <chrome-picker v-model="fillColor" /> + </section> + <div class="sendbutton"> + <a @click.prevent="submit" class="btn btn-info">Send</a> + </div> + </div> <!-- card-body --> + </div> + </div> +</template> + +<style scoped lang="sass"> +.config + text-align: left + +.configsection + margin-bottom: $large-offset + +.sendbutton + position: absolute + right: $offset + bottom: $offset + +.inputs + margin-left: auto + margin-right: auto + +.sysconfig + width: 30vw +</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 { Chrome } from "vue-color"; +import { Compact } from "vue-color"; + +import { HTTP } from "../../lib/http"; +import { displayError } from "../../lib/errors.js"; +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 }, + 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: "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: "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: "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: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/admin/usermanagement/Passwordfield.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,64 @@ +<template> + <div> + <label for="password">{{this.label}}</label> + <div class="d-flex d-row"> + <input :type="isPasswordVisible" @change="fieldChanged" class="form-control" :placeholder='placeholder' :required="required"> + <span class="input-group-text" @click="showPassword"><i :class="eyeIcon"></i></span> + </div> + <div v-show="passworderrors" class="text-danger"><small><i class="fa fa-warning"></i> {{ 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"; + }, + eyeIcon() { + return { + fa: true, + "fa-eye": !this.readablePassword, + "fa-eye-slash": this.readablePassword + }; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/admin/usermanagement/Userdetail.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,298 @@ +<template> + <div class="userdetails h-100 mt-3 mr-auto shadow fadeIn animated"> + <div class="card"> + <div class="card-header shadow-sm text-white bg-info mb-3"> + {{ this.cardHeader }} + <span @click="closeDetailview" class="pull-right"> + <i class="fa fa-close"></i> + </span> + </div> + <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">Username</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> + <i class="fa fa-warning"></i> {{ errors.user }}</small> + </div> + </div> + <div class="form-group row"> + <label for="country">Country</label> + <select class="form-control form-control-sm" v-on:change="validateCountry" v-model="currentUser.country"> + <option disabled value="">Please select one</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> + <i class="fa fa-warning"></i> {{ errors.country }}</small> + </div> + </div> + <div class="form-group row"> + <label for="email">Email address</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> + <i class="fa fa-warning"></i> {{ errors.email }}</small> + </div> + </div> + <div class="form-group row"> + <label for="role">Role</label> + <select class="form-control form-control-sm" v-on:change="validateRole" v-model="currentUser.role"> + <option disabled value="">Please select one</option> + <option value="sys_admin">Sysadmin</option> + <option value="waterway_admin">Waterway Admin</option> + <option value="waterway_user">Waterway User</option> + </select> + <div v-show="errors.role" class="text-danger"> + <small> + <i class="fa fa-warning"></i> {{ errors.role }}</small> + </div> + </div> + <div class="form-group row"> + <PasswordField @fieldchange="passwordChanged" :placeholder="passwordPlaceholder" :label="passwordLabel" :passworderrors="errors.password"></PasswordField> + </div> + <div class="form-group row"> + <PasswordField @fieldchange="passwordReChanged" :placeholder="passwordRePlaceholder" :label="passwordReLabel" :passworderrors="errors.passwordre"></PasswordField> + </div> + </div> + <div> + <button type="submit" :disabled="submitted" class="shadow-sm btn btn-info pull-right">Submit</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"><i class="fa fa-telegram"> Send testmail</i></a> + <div v-if="mailsent">Mail was sent</div> + </div> + </form> + </div> + </div> + </div> +</template> + +<style lang="sass" scoped> +.mailbutton + width: 12vw + +.formfields + width: 10vw + +.userdetails + min-width: 40vw + +form + font-size: $smaller +</style> + +<script> +/* + * This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +import { HTTP } from "../../../lib/http"; +import { displayError } from "../../../lib/errors.js"; +import { mapState } from "vuex"; +import PasswordField from "./Passwordfield"; + +const emptyErrormessages = () => { + return { + email: "", + country: "", + role: "", + password: "", + passwordre: "" + }; +}; + +const isEmailValid = email => { + /** + * + * For convenience purposes the same regex used as in the go code + * cf. types.go + * + */ + // eslint-disable-next-line + return /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test( + email + ); +}; + +const violatedPasswordRules = password => { + return ( + // rules according to issue 70 + password.length < 7 || + /\W/.test(password) == false || + /\d/.test(password) == false + ); +}; + +export default { + name: "userdetail", + components: { + PasswordField + }, + data() { + return { + mailsent: false, + passwordLabel: "Password", + passwordReLabel: "Repeat Password", + passwordPlaceholder: "password", + passwordRePlaceholder: "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: "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 + ? "" + : "Please choose a country"; + }, + validateRole() { + this.errors.role = this.currentUser.role ? "" : "Please choose a role"; + }, + validatePassword() { + this.errors.passwordre = + this.password === this.passwordre ? "" : "Passwords do not match!"; + this.errors.password = + this.password === "" || !violatedPasswordRules(this.password) + ? "" + : "Password should at least be 8 char long including 1 digit and 1 special char like $"; + }, + validateEmailaddress() { + this.errors.email = isEmailValid(this.currentUser.email) + ? "" + : "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: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + this.submitted = false; + const { status, data } = error.response; + displayError({ + title: "Error while saving user", + message: `${status}: ${data.message || data}` + }); + }); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/admin/usermanagement/Usermanagement.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,302 @@ +<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"> + <div class="card-header shadow-sm text-white bg-info mb-3"> + Users + </div> + <div class="card-body"> + <table id="datatable" :class="tableStyle"> + <thead> + <tr> + <th scope="col" @click="sortBy('user')"> + <span>Username + <i v-if="sortCriterion=='user'" class="fa fa-angle-down"></i> + </span> + </th> + <th scope="col" @click="sortBy('country')"> + <span>Country + <i v-if="sortCriterion=='country'" class="fa fa-angle-down"></i> + </span> + </th> + <th scope="col" @click="sortBy('email')"> + <span>Email + <i v-if="sortCriterion=='email'" class="fa fa-angle-down"></i> + </span> + </th> + <th scope="col" @click="sortBy('role')"> + <span>Role + <i v-if="sortCriterion=='role'" class="fa fa-angle-down"></i> + </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> + <i v-tooltip="user.roleLabel" :class="{ + fa:true, + icon:true, + 'fa-user':user.role==='waterway_user', + 'fa-star':user.role=='sys_admin', + 'fa-adn':user.role==='waterway_admin'}"></i> + </td> + <td> + <i @click="deleteUser(user.user)" class="icon fa fa-trash-o"></i> + </td> + </tr> + </tbody> + </table> + </div> + <div class="d-flex flex-row pagination"> + <i @click=" prevPage " v-if="this.currentPage!=1 " class="mr-2 btn btn-sm btn-light align-self-center pages fa fa-caret-left "></i> {{this.currentPage}} / {{this.pages}} + <i @click="nextPage " class="ml-2 btn btn-sm btn-light align-self-center pages fa fa-caret-right "></i> + </div> + <div class="mr-3 pb-3"> + <button @click="addUser " class="btn btn-info pull-right shadow-sm ">Add User</button> + </div> + </div> + </div> + <Userdetail v-if="isUserDetailsVisible "></Userdetail> + </div> + </div> + </div> +</template> + +<style scoped lang="sass"> +@import "../../../assets/tooltip.sass" + +.spacer + height: 100vh + +.spacer-collapsed + min-width: $icon-width + $offset + transition: $transition-fast + +@media screen and (min-width: 600px) + .spacer-expanded + min-width: $icon-width + $offset + +@media screen and (max-width: 1650px) + .spacer-expanded + min-width: $sidebar-width + $offset + +.main + height: 100vh + +@media screen and (min-width: 600px) + .content + margin-left: $sidebar-width + margin-right: auto + +@media screen and (min-width: 1650px) + .content + margin-left: $sidebar-width + margin-right: auto + +.icon + font-size: large + +.userlist + min-width: 520px + height: 100% + +.pagination + margin-left: auto + margin-right: auto + +.userlistsmall + width: 30vw + +.userlistextended + width: 70vw + +.table + width: 90% !important + margin: auto + +.table th, +.pages + 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", "showUsermenu"]), + spacerStyle() { + return [ + "spacer ml-3", + { + "spacer-expanded": this.showUsermenu && this.showSidebar, + "spacer-collapsed": !this.showUsermenu && 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", + { + 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: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "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); + } + }, + beforeRouteEnter(to, from, next) { + store + .dispatch("usermanagement/loadUsers") + .then(next) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "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/admin/usermanagement/Users.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,279 @@ +<template> + <div class="main d-flex flex-column"> + <div class="d-flex content flex-column"> + <div class="d-flex flex-row"> + <div :class="userlistStyle"> + <div class="card"> + <div class="card-header shadow-sm text-white bg-info mb-3"> + Users + </div> + <div class="card-body"> + <table id="datatable" :class="tableStyle"> + <thead> + <tr> + <th scope="col" @click="sortBy('user')"> + <span>Username + <i v-if="sortCriterion=='user'" class="fa fa-angle-down"></i> + </span> + </th> + <th scope="col" @click="sortBy('country')"> + <span>Country + <i v-if="sortCriterion=='country'" class="fa fa-angle-down"></i> + </span> + </th> + <th scope="col" @click="sortBy('email')"> + <span>Email + <i v-if="sortCriterion=='email'" class="fa fa-angle-down"></i> + </span> + </th> + <th scope="col" @click="sortBy('role')"> + <span>Role + <i v-if="sortCriterion=='role'" class="fa fa-angle-down"></i> + </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> + <i v-tooltip="user.roleLabel" :class="{ + fa:true, + icon:true, + 'fa-user':user.role==='waterway_user', + 'fa-star':user.role=='sys_admin', + 'fa-adn':user.role==='waterway_admin'}"></i> + </td> + <td> + <i @click="deleteUser(user.user)" class="icon fa fa-trash-o"></i> + </td> + </tr> + </tbody> + </table> + </div> + <div class="d-flex flex-row pagination"> + <i @click=" prevPage " v-if="this.currentPage!=1 " class="backwards btn btn-sm btn-light align-self-center pages fa fa-caret-left "></i> {{this.currentPage}} / {{this.pages}} + <i @click="nextPage " class="forwards btn btn-sm btn-light align-self-center pages fa fa-caret-right "></i> + </div> + <div class="mr-3 pb-3 "> + <button @click="addUser " class="btn btn-info pull-right shadow-sm ">Add User</button> + </div> + </div> + </div> + <Userdetail v-if="isUserDetailsVisible "></Userdetail> + </div> + </div> + </div> +</template> + +<style lang="sass" scoped> +@import "../../../assets/tooltip.sass" + +.main + height: 100vh + +.backwards + margin-right: 0.5rem + +.forwards + margin-left: 0.5rem + +.content + margin-top: $large-offset + margin-left: auto + margin-right: auto + +.icon + font-size: large + +.userlist + margin-top: $topbarheight + min-width: 520px + height: 100% + +.pagination + margin-left: auto + margin-right: auto + +.userlistsmall + width: 30vw + +.userlistextended + width: 70vw + +.table + width: 90% !important + margin: auto + +.table th, +.pages + cursor: pointer + +.table th, +td + font-size: 0.9rem + border-top: 0px !important + text-align: left + padding: 0.5rem !important + +.table td + font-size: 0.9rem + 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 } 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"]), + ...mapGetters("application", ["sidebarCollapsed"]), + 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 mr-3 shadow", + { + 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: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "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); + } + }, + beforeRouteEnter(to, from, next) { + store + .dispatch("usermanagement/loadUsers") + .then(next) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "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/map/Bottlenecks.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,233 @@ +<template> + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> + <i class="fa fa-ship mr-2"></i> + Bottlenecks + </h6> + <div class="row p-2 text-left small"> + <div class="col-5"> + <a href="#" @click="sortBy('name')" class="sort-link">Name</a> + <i :class="sortClass" v-if="sortColumn === 'name'"></i> + </div> + <div class="col-2"> + <a + href="#" + @click="sortBy('latestMeasurement')" + class="sort-link" + >Latest Measurement</a> + <i :class="sortClass" v-if="sortColumn === 'latestMeasurement'"></i> + </div> + <div class="col-3"> + <a href="#" @click="sortBy('chainage')" class="sort-link">Chainage</a> + <i :class="sortClass" v-if="sortColumn === 'chainage'"></i> + </div> + <div class="col-2"></div> + </div> + <div class="bottleneck-list small text-left" v-if="filteredAndSortedBottlenecks().length"> + <div + v-for="bottleneck in filteredAndSortedBottlenecks()" + :key="bottleneck.properties.name" + class="border-top row mx-0 py-2" + > + <div class="col-5 text-left"> + <a + href="#" + class="d-block" + @click="moveToBottleneck(bottleneck)" + >{{ bottleneck.properties.name }}</a> + </div> + <div class="col-2">{{ displayCurrentSurvey(bottleneck.properties.current) }}</div> + <div + class="col-3" + >{{ displayCurrentChainage(bottleneck.properties.from, bottleneck.properties.from) }}</div> + <div class="col-2 text-right"> + <button + type="button" + class="btn btn-sm btn-outline-secondary" + @click="toggleBottleneck(bottleneck.properties.name)" + > + <i class="fa fa-angle-down"></i> + </button> + </div> + <div + :class="['col-12', 'surveys', {open: openBottleneck === bottleneck.properties.name}]" + > + <a + href="#" + class="d-block p-2" + v-for="(survey, index) in openBottleneckSurveys" + :key="index" + @click="selectSurvey(survey, bottleneck)" + >{{ survey.date_info }}</a> + </div> + </div> + </div> + <div v-else class="small text-center py-3 border-top"> + No results. + </div> + </div> +</template> + +<script> +/* + * This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; +import { HTTP } from "../../lib/http"; +import { displayError } from "../../lib/errors.js"; + +export default { + name: "bottlenecks", + data() { + return { + sortColumn: "name", + sortDirection: "ASC", + openBottleneck: null, + openBottleneckSurveys: null + }; + }, + computed: { + ...mapState("application", ["searchQuery", "showSearchbarLastState"]), + ...mapState("bottlenecks", ["bottlenecks"]), + sortClass() { + return [ + "fa ml-1", + { + "fa-sort-amount-asc": this.sortDirection === "ASC", + "fa-sort-amount-desc": this.sortDirection === "DESC" + } + ]; + } + }, + methods: { + filteredAndSortedBottlenecks() { + return this.bottlenecks + .filter(bn => { + return bn.properties.name + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + }) + .sort((bnA, bnB) => { + switch (this.sortColumn) { + case "name": + if ( + bnA.properties.name.toLowerCase() < + bnB.properties.name.toLowerCase() + ) + return this.sortDirection === "ASC" ? -1 : 1; + if ( + bnA.properties.name.toLowerCase() > + bnB.properties.name.toLowerCase() + ) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + + case "latestMeasurement": { + if ( + (bnA.properties.current || "") < (bnB.properties.current || "") + ) + return this.sortDirection === "ASC" ? -1 : 1; + if ( + (bnA.properties.current || "") > (bnB.properties.current || "") + ) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + } + + case "chainage": + if (bnA.properties.from < bnB.properties.from) + return this.sortDirection === "ASC" ? -1 : 1; + if (bnA.properties.from > bnB.properties.from) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + + default: + return 0; + } + }); + }, + selectSurvey(survey, bottleneck) { + this.$store.dispatch( + "bottlenecks/setSelectedBottleneck", + bottleneck.properties.name + ); + this.$store.commit("bottlenecks/setSelectedSurvey", survey); + this.moveToBottleneck(bottleneck); + }, + moveToBottleneck(bottleneck) { + 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"; + }, + toggleBottleneck(name) { + this.openBottleneckSurveys = null; + if (name === this.openBottleneck) { + this.openBottleneck = null; + } else { + this.openBottleneck = 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; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } + }, + displayCurrentSurvey(current) { + return current ? current.substr(0, current.length - 1) : ""; + }, + displayCurrentChainage(from, to) { + return from / 10 + " - " + to / 10; + } + }, + mounted() { + this.$store.dispatch("bottlenecks/loadBottlenecks"); + } +}; +</script> + +<style lang="sass" scoped> +.bottleneck-list + overflow-y: auto + max-height: 500px + +.surveys + max-height: 0 + overflow: hidden + transition: max-height 0.3s ease + +.surveys.open + max-height: 999px + +.sort-link + color: #444 + font-weight: bold +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/Contextbox.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,91 @@ +<template> + <div :class="style"> + <div @click="close" class="ui-element close-contextbox"> + <i class="fa fa-close"></i> + </div> + <Bottlenecks v-if="showInContextBox === 'bottlenecks'"></Bottlenecks> + <Imports v-if="showInContextBox === 'imports'"></Imports> + <Staging v-if="showInContextBox === '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"), + Imports: () => import("./imports/Imports.vue"), + Staging: () => import("./Staging.vue") + }, + computed: { + ...mapState("application", ["showSearchbarLastState", "showInContextBox"]), + style() { + return [ + "ui-element shadow-xs contextbox ml-3", + { + contextboxcollapsed: !this.showInContextBox, + contextboxextended: this.showInContextBox, + "rounded-bottom": this.showInContextBox !== "imports", + rounded: this.showInContextBox === "imports" + } + ]; + } + }, + methods: { + close() { + this.$store.commit("application/showInContextBox", null); + this.$store.commit( + "application/showSearchbar", + this.showSearchbarLastState + ); + } + } +}; +</script> + +<style lang="sass" scoped> +.contextbox + position: relative + background-color: #ffffff + opacity: $slight-transparent + transition: left 0.3s ease + overflow: hidden + background: #fff + +.contextboxcollapsed + width: 0 + height: 0 + transition: $transition-fast + +.contextboxextended + min-width: 600px + +.close-contextbox + position: absolute + z-index: 2 + right: 0 + top: 7px + height: $icon-width + width: $icon-height + display: none + color: #fff + +.contextboxextended .close-contextbox + display: block +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/Identify.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,86 @@ +<template> + <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showIdentify }]"> + <div style="width: 20rem"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> + <i class="fa fa-info mr-2"></i> + Identified + <i class="fa fa-times ml-auto" @click="$store.commit('application/showIdentify', false)"></i> + </h6> + <div class="d-flex flex-column features p-3 flex-grow-1 text-left"> + <div v-if="currentMeasurement"> + <b> + {{ currentMeasurement.quantity }} + ({{ currentMeasurement.unitSymbol }}): + </b><br> + <small>{{ currentMeasurement.value }}</small> + </div> + <div v-for="(feature, i) of identifiedFeatures" :key="feature.getId()" > + <div v-if="feature.getId()" :class="{ 'mt-2': i }"> + <b>{{ feature.getId().replace(/[.][^.]*$/,"") /* cut away everything from the last . to the end */}}:</b> + <small + v-for="(value, key) in prepareProperties(feature)" + :key="key" + > + <div v-if="value">{{key}}:{{value}}</div> + </small> + </div> + </div> + <div v-if="!currentMeasurement && !identifiedFeatures.length" class="text-muted small text-center my-auto"> + No features identified. + </div> + </div> + <div class="versioninfo border-top p-3 text-left"> + gemma + <a href="https://hg.intevation.de/gemma/file/tip">source-code</a> + {{ versionStr }} + <br>Some data © + <a href="https://www.openstreetmap.org/copyright">OpenSteetMap</a>contributors + </div> + </div> + </div> +</template> + +<style lang="sass" scoped> +.features + max-height: 19rem + overflow-y: auto + +.versioninfo + font-size: 60% +</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/map/Main.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,126 @@ +<template> + <div class="main d-flex flex-column"> + <Maplayer :split="showSplitscreen" :lat="6155376" :long="1819178" :zoom="11"></Maplayer> + <FairwayProfile + :additionalSurveys="additionalSurveys" + :height="height" + :width="width" + :xScale="xAxis" + :yScaleLeft="yAxisLeft" + :yScaleRight="yAxisRight" + :margin="margins" + ></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"; +import { mapState } from "vuex"; +import debounce from "debounce"; + +export default { + name: "mainview", + components: { + Maplayer, + FairwayProfile + }, + data() { + return { + width: null, + height: null, + margin: { + top: 20, + right: 40, + bottom: 30, + left: 40 + } + }; + }, + computed: { + ...mapState("application", ["showSplitscreen"]), + ...mapState("fairwayprofile", [ + "currentProfile", + "minAlt", + "maxAlt", + "totalLength", + "waterLevels", + "fairwayCoordinates", + "selectedWaterLevel" + ]), + ...mapState("bottlenecks", ["surveys", "selectedSurvey"]), + additionalSurveys() { + if (!this.surveys) return []; + if (!this.selectedSurvey) return this.surveys; + return this.surveys.filter(survey => { + return survey.date_info !== this.selectedSurvey.date_info; + }); + }, + xAxis() { + return [this.xScale.x, this.xScale.y]; + }, + yAxisLeft() { + const hi = Math.max(this.maxAlt, this.selectedWaterLevel); + return [this.yScaleLeft.lo, hi]; + }, + yAxisRight() { + const DELTA = this.maxAlt * 1.1 - this.maxAlt; + return [this.maxAlt * 1 + DELTA, -DELTA]; + }, + margins() { + return this.margin; + }, + yScaleLeft() { + return { + lo: this.minAlt, + hi: this.maxAlt + }; + }, + xScale() { + return { + x: 0, + y: this.totalLength + }; + } + }, + created() { + window.addEventListener("resize", debounce(this.scaleFairwayProfile), 100); + window.addEventListener("onbeforeprint", this.test); + }, + updated() { + this.scaleFairwayProfile(); + }, + destroyed() { + window.removeEventListener("resize", debounce(this.scaleFairwayProfile)); + }, + methods: { + test(evt) { + console.log("test: ", evt); + }, + 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; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/Maplayer.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,430 @@ +<template> + <div id="map" :class="mapStyle"></div> +</template> + +<style lang="sass" scoped> +.nocursor + cursor: none + +.mapsplit + height: 50vh + +.mapfull + height: 100vh + +@media print + .mapfull + width: 2000px + height: 2828px + + .mapsplit + width: 2000px + height: 2828px +</style> + +<script> +/* + * This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * * Thomas Junk <thomas.junk@intevation.de> + * * Bernhard E. Reiter <bernhard.reiter@intevation.de> + */ +import { HTTP } from "../../lib/http"; +import { mapGetters, mapState } from "vuex"; +import "ol/ol.css"; +import { Map, View } from "ol"; +import { WFS, GeoJSON } from "ol/format.js"; +import { Stroke, Style, Fill } from "ol/style.js"; +import { getCenter } from "ol/extent"; + +/* for the sake of debugging */ +/* eslint-disable no-console */ +export default { + name: "maplayer", + props: ["lat", "long", "zoom", "split"], + data() { + return { + projection: "EPSG:3857" + }; + }, + computed: { + ...mapGetters("map", ["getLayerByName"]), + ...mapState("map", [ + "layers", + "openLayersMap", + "lineTool", + "polygonTool", + "cutTool" + ]), + ...mapState("bottlenecks", ["selectedSurvey"]), + mapStyle() { + return { + mapfull: !this.split, + mapsplit: this.split, + nocursor: this.hasActiveInteractions + }; + }, + hasActiveInteractions() { + return ( + (this.lineTool && this.lineTool.getActive()) || + (this.polygonTool && this.polygonTool.getActive()) || + (this.cutTool && this.cutTool.getActive()) + ); + } + }, + methods: { + identify(coordinate, pixel) { + if (!this.hasActiveInteractions) { + this.$store.commit("map/setIdentifiedFeatures", []); + // checking our WFS layers + var features = this.openLayersMap.getFeaturesAtPixel(pixel); + if (features) { + this.$store.commit("map/setIdentifiedFeatures", features); + + // get selected bottleneck from identified features + for (let feature of features) { + let id = feature.getId(); + // RegExp.prototype.test() works with number, str and undefined + if (/^bottlenecks\./.test(id)) { + this.$store.dispatch( + "bottlenecks/setSelectedBottleneck", + feature.get("objnam") + ); + this.$store.commit("map/moveMap", { + coordinates: getCenter( + feature + .getGeometry() + .clone() + .transform("EPSG:3857", "EPSG:4326") + .getExtent() + ), + zoom: 17, + preventZoomOut: true + }); + } + } + } + + // DEBUG output and example how to remove the GeometryName + /* + for (let feature of features) { + console.log("Identified:", feature.getId()); + for (let key of feature.getKeys()) { + if (key != feature.getGeometryName()) { + console.log(key, feature.get(key)); + } + } + } + */ + + // trying the GetFeatureInfo way for WMS + var wmsSource = this.getLayerByName( + "Inland ECDIS chart Danube" + ).data.getSource(); + var url = wmsSource.getGetFeatureInfoUrl( + coordinate, + 100 /* resolution */, + "EPSG:3857", + // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d + { INFO_FORMAT: "text/plain" } + ); + + if (url) { + // cannot directly query here because of SOP + console.log("GetFeatureInfo url:", url); + } + } + }, + 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); + var layer = this.getLayerByName("Bottleneck isolines"); + var wmsSrc = layer.data.getSource(); + + if (bottleneck_id != "does_not_exist") { + wmsSrc.updateParams({ + cql_filter: + "date_info='" + + datestr + + "' AND bottleneck_id='" + + bottleneck_id + + "'" + }); + layer.isVisible = true; + layer.data.setVisible(true); + } else { + layer.isVisible = false; + layer.data.setVisible(false); + } + }, + 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: { + split() { + const map = this.openLayersMap; + this.$nextTick(() => { + 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.long, this.lat], + zoom: this.zoom, + projection: this.projection + }) + }); + this.$store.commit("map/setOpenLayersMap", 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 => { + var features = new GeoJSON().readFeatures(JSON.stringify(response.data)); + var vectorSrc = this.getLayerByName( + "Fairway Dimensions" + ).data.getSource(); + vectorSrc.addFeatures(features); + // 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.openLayersMap.on(["singleclick", "dblclick"], event => { + this.identify(event.coordinate, event.pixel); + }); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/Pdftool.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,96 @@ +<template> + <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showPdfTool }]"> + <div style="width: 15rem"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> + <i class="fa fa-file-pdf-o mr-2"></i> + Generate PDF + <i class="fa fa-times ml-auto" @click="$store.commit('application/showPdfTool', false)"></i> + </h6> + <div class="p-3"> + <b>Chose format:</b> + <select v-model="form.format" class="form-control d-block w-100"> + <option>landscape</option> + <option>portrait</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">Download</label> + <input + type="radio" + id="pdfexport-downloadtype-open" + value="open" + v-model="form.downloadType" + > + <label for="pdfexport-downloadtype-open" class="ml-1">Open in new window</label> + </small> + <button + @click="download" + type="button" + class="btn btn-sm btn-info d-block w-100" + >Generate PDF</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/map/Search.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,223 @@ +<template> + <div :class="searchbarContainerStyle"> + <div class="input-group-prepend"> + <span @click="toggleSearchbar" :class="searchButtonStyle" for="search"> + <i class="fa fa-search d-print-none"></i> + </span> + </div> + <div class="searchgroup flex-fill"> + <input + @keyup.enter="takeFirstSearchresult" + v-if="showSearchbar" + id="search" + v-model="searchQuery" + type="text" + :class="searchInputStyle" + > + <div v-if="showSearchbar && searchResults !== null && !showInContextBox" class="searchresults border-top ui-element bg-white rounded-bottom d-print-none"> + <div v-for="entry of searchResults" :key="entry.name" class="border-top py-2"> + <a href="#" @click.prevent="moveToSearchResult(entry)">{{ entry.name }}</a> + </div> + </div> + </div> + </div> +</template> + +<style lang="sass" scoped> + .searchcontainer + height: $icon-height + opacity: $slight-transparent + + .searchbar-expanded + min-width: 600px + .searchbar + border-top-left-radius: 0 !important + border-bottom-left-radius: 0 !important + + .searchbar-collapsed + width: $icon-width !important + transition: $transition-fast + + .searchbar + height: $icon-height !important + box-shadow: none !important + &.rounded-top-right + border-radius: 0 !important + border-top-right-radius: $border-radius !important + + .searchlabel + &.rounded-top-left + border-radius: 0 !important + border-top-left-radius: $border-radius !important + + .input-group-text + height: $icon-height + width: $icon-width + + .input-group-prepend + .fa-search + color: #666 + + .searchresults + margin-left: -31px + max-height: 20rem + overflow: auto + > div:first-child + border-top: 0 !important +</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", "showInContextBox"]), + 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 ml-3 shadow-xs", + { + "searchbar-collapsed": !this.showSearchbar, + "searchbar-expanded": this.showSearchbar, + "d-flex": this.showInContextBox !== "imports", + "d-none": this.showInContextBox === "imports" + } + ]; + }, + searchInputStyle() { + return [ + "form-control ui-element search searchbar d-print-none border-0", + { "rounded-top-right": this.showInContextBox || 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.showInContextBox || 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: "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.showInContextBox) { + 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/map/Staging.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,148 @@ +<template> + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> + <i class="fa fa-list-ol mr-2"></i> + Staging Area + </h6> + <table class="table mb-0"> + <thead> + <tr> + <th>Name</th> + <th>Datatype</th> + <th>Importdate</th> + <th>ImportID</th> + <th> </th> + </tr> + </thead> + <tbody v-if="filteredData.length"> + <tr v-for="data in filteredData" :key="data.id"> + <td> + <a @click="zoomTo(data.location)" href="#">{{ data.name }}</a> + </td> + <td>{{ data.type }}</td> + <td>{{ data.date }}</td> + <td>{{ data.importID }}</td> + <td> + <button class="btn btn-outline-info"> + <i class="fa fa-thumbs-up"></i> + </button> + + <button class="btn btn-outline-info"> + <i class="fa fa-thumbs-down"></i> + </button> + </td> + </tr> + </tbody> + <tbody v-else> + <tr> + <td class="text-center" colspan="6">No results.</td> + </tr> + </tbody> + </table> + <div class="p-3" v-if="filteredData.length"> + <button class="btn btn-info">Confirm</button> + </div> + </div> +</template> + +<script> +import { mapState } from "vuex"; + +const demodata = [ + { + id: 1, + name: "B1", + date: "2018-11-19 10:23", + location: [16.5364, 48.1471], + status: "Not approved", + importID: "123456789", + type: "bottleneck" + }, + { + id: 2, + name: "B2", + date: "2018-11-19 10:24", + location: [16.5364, 48.1472], + status: "Not approved", + importID: "123456789", + type: "bottleneck" + }, + { + id: 3, + name: "s1", + date: "2018-11-13 10:25", + location: [16.5364, 48.1473], + status: "Not approved", + importID: "987654321", + type: "soundingresult" + }, + { + id: 4, + name: "s2", + date: "2018-11-13 10:26", + location: [16.5364, 48.1474], + status: "Not approved", + importID: "987654321", + type: "soundingresult" + } +]; + +export default { + computed: { + ...mapState("application", ["searchQuery"]), + filteredData() { + return demodata.filter(data => { + const nameFound = data.name + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + const dateFound = data.date + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + const locationFound = data.location.find(coord => { + return coord + .toString() + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + }); + const statusFound = data.status + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + const importIDFound = data.importID + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + const typeFound = data.type + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + + return ( + nameFound || + dateFound || + locationFound || + statusFound || + importIDFound || + typeFound + ); + }); + } + }, + methods: { + zoomTo(coordinates) { + this.$store.commit("map/moveMap", { + coordinates: coordinates, + zoom: 17, + preventZoomOut: true + }); + } + } +}; +</script> + +<style lang="sass" scoped> +.table th, +td + font-size: 0.9rem + border-top: 0px !important + border-bottom-width: 1px + text-align: left + padding: 0.5rem !important +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/Zoom.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,51 @@ +<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 border-right" @click="zoomIn"> + <i class="fa fa-plus"></i> + </button> + <button class="zoomButton border-0 bg-white rounded-right ui-element" @click="zoomOut"> + <i class="fa fa-minus"></i> + </button> + </div> +</template> + +<style lang="sass" scoped> +.buttoncontainer + bottom: 0 + left: 50% + margin-left: -$icon-width + +.zoomButton + min-height: $icon-width + min-width: $icon-width + z-index: 2 + outline: none + color: #666 +</style> +<script> +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>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/fairway/Fairwayprofile.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,579 @@ +<template> + <div :class="['position-relative', {show: showSplitscreen}]" v-if="Object.keys(currentProfile).length"> + <button + class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle shadow-sm" + @click="$store.commit('application/showSplitscreen', false)" + v-if="showSplitscreen"> + <i class="fa fa-angle-down"></i> + </button> + <button + class="rounded-bottom bg-white border-0 position-absolute clear-selection shadow-sm" + @click="$store.dispatch('fairwayprofile/clearSelection');" + v-if="showSplitscreen"> + <i class="fa fa-times text-danger"></i> + </button> + <div class="profile bg-white position-relative d-flex flex-column pr-5"> + <h5 class="mb-0 mt-2">{{ selectedBottleneck }} ({{ selectedSurvey.date_info }})</h5> + <div class="d-flex flex-fill"> + <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div> + <div class="additionalsurveys d-flex flex-column"> + <small> + Additional Surveys + <select v-model="additionalSurvey" class="form-control form-control-sm"> + <option value="">None</option> + <option + v-for="survey in additionalSurveys" + :key="survey.date_info" + :value="survey" + >{{survey.date_info}}</option> + </select> + <hr> + <div class="d-flex text-left mb-2"> + <div class="text-nowrap mr-1"> + <b>Start:</b> + <br> + Lat: {{ startPoint[1] }} + <br> + Lon: {{ startPoint[0] }} + </div> + <div class="text-nowrap ml-1"> + <b>End:</b> + <br> + Lat: {{ endPoint[1] }} + <br> + Lon: {{ endPoint[0] }} + </div> + <button class="btn btn-outline-secondary btn-sm ml-2 mt-auto" + @click="showLabelInput = !showLabelInput"> + <i :class="'fa fa-' + (showLabelInput ? 'times' : 'save')"></i> + </button> + <button v-clipboard:copy="coordinatesForClipboard" + v-clipboard:success="onCopyCoordinates" + class="btn btn-outline-secondary btn-sm ml-2 mt-auto"> + <i class="fa fa-copy"></i> + </button> + </div> + <div v-if="showLabelInput"> + Enter label for cross profile: + <div class="position-relative"> + <input class="form-control form-control-sm pr-5" v-model="cutLabel" /><br> + <button class="btn btn-sm btn-outline-secondary position-absolute" + @click="saveCut" + v-if="cutLabel" + style="top: 0; right: 0;"> + <i class="fa fa-check"></i> + </button> + </div> + </div> + Saved cross profiles: + <select class="form-control form-control-sm mb-2" v-model="coordinatesSelect"> + <option></option> + <option v-for="(cut, index) in previousCuts" :value="cut.coordinates" :key="index"> + {{ cut.label }} + </option> + </select> + Enter coordinates manually: + <div class="position-relative"> + <input class="form-control form-control-sm pr-5" placeholder="Lat,Lon,Lat,Lon" v-model="coordinatesInput" /><br> + <button class="btn btn-sm btn-outline-secondary position-absolute" + @click="applyManualCoordinates" + style="top: 0; right: 0;" + v-if="coordinatesInput"> + <i class="fa fa-check"></i> + </button> + </div> + </small> + </div> + </div> + </div> + </div> +</template> + +<style lang="sass" scoped> +.profile + width: 100vw + height: 0 + overflow: hidden + z-index: 2 + +.splitscreen-toggle, +.clear-selection + right: $icon-width + $offset + width: $icon-width + height: $icon-height + margin-top: 2px + z-index: 3 + outline: none + +.clear-selection + right: $offset + +.show + .profile + height: 50vh + +.waterlevelselection + margin-top: $large-offset + margin-right: $large-offset + +.additionalsurveys + margin-top: $large-offset + margin-bottom: auto + margin-right: $large-offset + margin-left: auto + max-width: 300px + +.additionalsurveys input + margin-right: $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 * as d3 from "d3"; +import { mapState, mapGetters } from "vuex"; +import { displayError, displayInfo } from "../../../lib/errors.js"; +import Feature from "ol/Feature"; +import LineString from "ol/geom/LineString"; + +const GROUND_COLOR = "#4A2F06"; + +export default { + name: "fairwayprofile", + props: [ + "width", + "height", + "xScale", + "yScaleLeft", + "yScaleRight", + "margin", + "additionalSurveys" + ], + data() { + return { + wait: false, + coordinatesInput: "", + coordinatesSelect: null, + cutLabel: "", + showLabelInput: false + }; + }, + computed: { + ...mapGetters("map", ["getLayerByName"]), + ...mapState("application", ["showSplitscreen"]), + ...mapState("fairwayprofile", [ + "startPoint", + "endPoint", + "currentProfile", + "minAlt", + "maxAlt", + "totalLength", + "fairwayCoordinates", + "waterLevels", + "selectedWaterLevel", + "previousCuts" + ]), + ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]), + additionalSurvey: { + get() { + return this.$store.getters["fairwayprofile/additionalSurvey"]; + }, + set(value) { + this.$store.commit("fairwayprofile/setAdditionalSurvey", value); + this.selectAdditionalSurveyData(); + } + }, + currentData() { + if ( + !this.selectedSurvey || + !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info) + ) + return []; + return this.currentProfile[this.selectedSurvey.date_info]; + }, + additionalData() { + if ( + !this.additionalSurvey || + !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info) + ) + return []; + return this.currentProfile[this.additionalSurvey.date_info]; + }, + waterColor() { + const result = this.waterLevels.find( + x => x.level === this.selectedWaterLevel + ); + return result.color; + }, + coordinatesForClipboard() { + return ( + this.startPoint[1] + + "," + + this.startPoint[0] + + "," + + this.endPoint[1] + + "," + + this.endPoint[0] + ); + } + }, + watch: { + showSplitscreen() { + this.drawDiagram(); + }, + currentData() { + this.drawDiagram(); + }, + width() { + this.drawDiagram(); + }, + height() { + this.drawDiagram(); + }, + waterLevels() { + this.drawDiagram(); + }, + selectedWaterLevel() { + this.drawDiagram(); + }, + fairwayCoordinates() { + this.drawDiagram(); + }, + selectedBottleneck() { + this.$store.dispatch("fairwayprofile/previousCuts"); + this.cutLabel = + this.selectedBottleneck + " (" + new Date().toISOString() + ")"; + }, + coordinatesSelect(newValue) { + if (newValue) { + this.applyCoordinates(newValue); + } + } + }, + methods: { + selectAdditionalSurveyData() { + if ( + !this.additionalSurvey || + this.wait || + this.currentProfile[this.additionalSurvey.date_info] + ) { + this.drawDiagram(); + return; + } + this.$store + .dispatch("fairwayprofile/loadProfile", this.additionalSurvey) + .then(() => { + this.wait = false; + }) + .catch(error => { + this.wait = false; + let status = "ERROR"; + let data = error; + const response = error.response; + if (response) { + status = response.status; + data = response.data; + } + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }, + drawDiagram() { + this.coordinatesSelect = null; + const chartDiv = document.querySelector(".fairwayprofile"); + d3.select("svg").remove(); + 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, + yScaleLeft, + graph + } = this.generateCoordinates(svg, height, width); + this.drawWaterlevel({ + graph, + xScale, + yScaleRight, + height, + width + }); + if (currentData) { + this.drawProfile({ + graph, + xScale, + yScaleRight, + currentData, + height, + width, + color: GROUND_COLOR, + strokeColor: "black", + opacity: 1 + }); + } + if (additionalData) { + this.drawProfile({ + graph, + xScale, + yScaleRight, + currentData: additionalData, + height, + width, + color: GROUND_COLOR, + strokeColor: "#943007", + opacity: 0.6 + }); + } + this.drawLabels({ + graph, + xScale, + yScaleLeft, + currentData, + height, + width + }); + this.drawFairway({ + graph, + xScale, + yScaleRight, + currentData, + height, + width + }); + }, + 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); + } + }, + onCopyCoordinates() { + displayInfo({ + title: "Success", + message: "Coordinates copied to clipboard!" + }); + }, + applyManualCoordinates() { + const coordinates = this.coordinatesInput + .split(",") + .map(coord => parseFloat(coord.trim())); + 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 + const cutLayer = this.getLayerByName("Cut Tool"); + cutLayer.data.getSource().clear(); + const cut = new Feature({ + geometry: new LineString([ + [coordinates[0], coordinates[1]], + [coordinates[2], coordinates[3]] + ]).transform("EPSG:4326", "EPSG:3857") + }); + cutLayer.data.getSource().addFeature(cut); + + // draw diagram + this.$store.dispatch("fairwayprofile/cut", cut); + } else { + displayError({ + title: "Invalid input", + message: + "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] + }; + 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: "Coordinates saved!", + message: + 'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.' + }); + } + }, + mounted() { + this.drawDiagram(); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/fairway/Infobar.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,45 @@ +<template> + <div v-if="selectedSurvey && !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"> + <h6 class="my-auto px-2"> + {{ selectedBottleneck }} + ({{ selectedSurvey.date_info }}) + </h6> + <i class="fa fa-angle-up py-2 px-2 border-left" @click="$store.commit('application/showSplitscreen', true)" v-if="Object.keys(currentProfile).length"></i> + <i class="fa fa-close text-danger py-2 px-2 border-left" @click="$store.dispatch('fairwayprofile/clearSelection');"></i> + </div> + </div> +</template> + +<style lang="sass" scoped> +.infobar + height: $icon-width + z-index: 2 +</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/map/fairway/Surveys.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,50 @@ +<template> + <div class="box expanded ui-element rounded bg-white ml-auto mr-3 mb-3 text-nowrap" v-if="selectedBottleneck && surveys && !selectedSurvey"> + <div style="width: 15rem"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> + {{ selectedBottleneck }} + <i class="fa fa-times ml-auto" @click="$store.dispatch('fairwayprofile/clearSelection');"></i> + </h6> + <div class="p-3"> + <div + v-for="(survey, i) of surveys" + :key="survey.data_info" + :class="{ 'mt-1': i }" + @click.prevent="$store.commit('bottlenecks/setSelectedSurvey', survey)" + > + <a href="#" @click.prevent>{{ survey.date_info }}</a> + </div> + </div> + </div> + </div> +</template> + +<script> +/* + * This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + name: "surveys", + computed: { + ...mapState("bottlenecks", [ + "selectedBottleneck", + "surveys", + "selectedSurvey" + ]) + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/imports/Importqueue.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,170 @@ +<template> + <div class="d-flex flex-row"> + <div :class="spacerStyle"></div> + <div class="mt-3 mx-auto"> + <div class="card importqueuecard"> + <div class="card-header shadow-sm text-white bg-info mb-3">Importqueue</div> + <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"> + <i class="fa fa-search"></i> + </span> + </div> + <input + type="text" + class="form-control" + placeholder="" + aria-label="Search" + aria-describedby="search" + > + </div> + <div class="filters"> + <button + @click="setFilter('successful')" + :class="successfulStyle" + >Successful</button> + <button @click="setFilter('failed')" :class="failedStyle">Failed</button> + <button @click="setFilter('pending')" :class="pendingStyle">Pending</button> + </div> + </div> + <table class="table"> + <thead> + <tr> + <th>Enqueued</th> + <th>Kind</th> + <th>User</th> + <th>State</th> + </tr> + </thead> + <tbody> + <tr v-for="job in imports" :key="job.id"> + <td>{{job.enqueued}}</td> + <td>{{job.kind}}</td> + <td>{{job.user}}</td> + <td>{{job.state}}</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + </div> +</template> + +<script> +import { displayError } from "../../../lib/errors.js"; +import { mapState } from "vuex"; + +export default { + name: "importqueue", + data() { + return { + successful: false, + failed: false, + pending: false + }; + }, + methods: { + setFilter(name) { + this[name] = !this[name]; + const allSet = this.successful && this.failed && this.pending; + if (allSet) { + this.all = false; + this.successful = false; + this.failed = false; + this.pending = false; + } + } + }, + computed: { + ...mapState("imports", ["imports"]), + ...mapState("application", ["showSidebar"]), + 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 + }; + } + }, + mounted() { + this.$store.dispatch("imports/getImports").catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } +}; +</script> + +<style lang="sass" scoped> +.spacer + height: 100vh + +.spacer-collapsed + min-width: $icon-width + $offset + transition: $transition-fast + +.spacer-expanded + min-width: $sidebar-width + $offset + +.importqueuecard + width: 80vw + 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: 50% +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/imports/Imports.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,260 @@ +<template> + <div> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> + <i class="fa fa-upload mr-2"></i> + Import Soundingresults + </h6> + <hr class="mr-auto ml-auto mb-0 w-90"> + <div v-if="editState" class="ml-auto mr-auto mt-4 w-90"> + <div class="d-flex flex-row input-group mb-4"> + <div class="offset-r"> + <label for="bottleneck" class="label-text" id="bottlenecklabel">Bottleneck</label> + </div> + <input + id="bottleneck" + type="text" + class="form-control" + placeholder="Name of Bottleneck" + aria-label="bottleneck" + aria-describedby="bottlenecklabel" + v-model="bottleneck" + > + </div> + <div class="d-flex flex-row input-group mb-4"> + <div class="offset-r"> + <label class="label-text" for="importdate" id="importdatelabel">Date</label> + </div> + <input + id="importdate" + type="date" + class="form-control" + placeholder="Date of import" + aria-label="bottleneck" + aria-describedby="bottlenecklabel" + v-model="importDate" + > + </div> + <div class="d-flex flex-row input-group mb-4"> + <div class="offset-r"> + <label class="label-text" for="depthreference">Depth reference</label> + </div> + <select v-model="depthReference" class="custom-select" id="depthreference"> + <option + v-for="option in this.$options.depthReferenceOptions" + :key="option" + >{{option}}</option> + </select> + </div> + </div> + <div class="w-90 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 + 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" + >Download Meta.json</a> + <button + v-if="editState" + @click="deleteTempData" + class="btn btn-danger" + type="button" + >Cancel Upload</button> + <button + :disabled="disableUpload" + @click="submit" + class="btn btn-info" + type="button" + >{{uploadState?"Upload":"Confirm"}}</button> + </div> + </div> + </div> +</template> + +<script> +import { HTTP } from "../../../lib/http"; +import { displayError, displayInfo } from "../../../lib/errors.js"; + +const defaultLabel = "Choose .zip-file"; +const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; + +export default { + name: "imports", + data() { + return { + importState: IMPORTSTATE.UPLOAD, + depthReference: "", + bottleneck: "", + importDate: "", + uploadLabel: defaultLabel, + uploadFile: null, + disableUpload: false, + token: null + }; + }, + methods: { + initialState() { + this.importState = IMPORTSTATE.UPLOAD; + this.depthReference = ""; + this.bottleneck = ""; + this.importDate = ""; + this.uploadLabel = defaultLabel; + this.uploadFile = null; + this.disableUpload = false; + this.token = null; + }, + 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: "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 => { + const { bottleneck, date } = response.data.meta; + const depthReference = response.data.meta["depth-reference"]; + this.importState = IMPORTSTATE.EDIT; + this.bottleneck = bottleneck; + this.depthReference = depthReference; + this.importDate = new Date(date).toISOString().split("T")[0]; + this.token = response.data.token; + }) + .catch(error => { + const { status, data } = error.response; + const messages = data.messages ? data.messages.join(", ") : ""; + displayError({ + title: "Backend Error", + message: `${status}: ${messages}` + }); + }); + }, + confirm() { + let formData = new FormData(); + formData.append("token", this.token); + ["bottleneck", "importDate", "depthReference"].forEach(x => { + if (this[x]) formData.append(x, this[x]); + }); + HTTP.post("/imports/soundingresult", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(() => { + displayInfo({ + title: "Import", + message: "Starting import for " + this.bottleneck + }); + this.initialState(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + } + }, + computed: { + editState() { + return this.importState === IMPORTSTATE.EDIT; + }, + uploadState() { + return this.importState === IMPORTSTATE.UPLOAD; + }, + 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="sass" scoped> +.offset-r + margin-right: $large-offset + +.buttons button + margin-left: $offset !important + +.label-text + width: 10rem + text-align: left + line-height: 2.25rem +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/layers/Layers.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,56 @@ +<template> + <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showLayers }]"> + <div style="width: 20rem"> + <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> + <i class="fa fa-th-list mr-2"></i> + Layers + <i class="fa fa-times ml-auto" @click="$store.commit('application/showLayers', false)"></i> + </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/map/layers/Layerselect.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,75 @@ +<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">{{layername}}</label> + </div> + <div v-if="isVisible && (layername == 'Bottleneck isolines')"> + <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl"> + </div> + </div> +</template> + +<style lang="sass" 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/map/layers/LegendElement.vue Thu Nov 22 07:07:12 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="sass" scoped> +.legendelement + max-height: 1.5rem + width: 2rem +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/toolbar/Cuttool.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,76 @@ +<template> + <div @click="toggleCutTool" class="toolbar-button"> + <i :class="['fa fa-area-chart', { inverted: cutTool && cutTool.getActive(), grey: !selectedSurvey }]"></i> + </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"; +import Draw from "ol/interaction/Draw.js"; +import { Stroke, Style, Circle, Fill } from "ol/style.js"; + +export default { + name: "cuttool", + computed: { + ...mapGetters("map", ["getLayerByName"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"]), + ...mapState("bottlenecks", ["selectedSurvey"]) + }, + methods: { + toggleCutTool() { + console.log(this.selectedSurvey); + if (this.selectedSurvey) { + this.cutTool.setActive(!this.cutTool.getActive()); + this.lineTool.setActive(false); + this.polygonTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + } + }, + cutEnd(event) { + this.$store.dispatch("fairwayprofile/cut", event.feature); + } + }, + created() { + if (!this.cutTool) { + const cutVectorSrc = this.getLayerByName("Cut Tool").data.getSource(); + const cutTool = new Draw({ + source: cutVectorSrc, + type: "LineString", + maxPoints: 2, + style: new Style({ + stroke: new Stroke({ + color: "#444", + width: 2, + lineDash: [7, 7] + }), + image: new Circle({ + fill: new Fill({ color: "#333" }), + stroke: new Stroke({ color: "#fff", width: 1.5 }), + radius: 6 + }) + }) + }); + cutTool.setActive(false); + cutTool.on("drawstart", () => { + cutVectorSrc.clear(); + }); + cutTool.on("drawend", this.cutEnd); + this.$store.commit("map/cutTool", cutTool); + this.openLayersMap.addInteraction(cutTool); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/toolbar/Identify.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,29 @@ +<template> + <div @click="$store.commit('application/showIdentify', !showIdentify)" class="toolbar-button"> + <i :class="['fa fa-info', {inverted: showIdentify}]"></i> + </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"]) + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/toolbar/Layers.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,29 @@ +<template> + <div @click="$store.commit('application/showLayers', !showLayers)" class="toolbar-button"> + <i :class="['fa fa-th-list', {inverted: showLayers}]"></i> + </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/map/toolbar/Linetool.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,70 @@ +<template> + <div @click="toggleLineTool" class="toolbar-button"> + <i :class="['fa fa-pencil', {inverted: lineTool && lineTool.getActive()}]"></i> + </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"; +import { getLength } from "ol/sphere.js"; +import Draw from "ol/interaction/Draw.js"; + +export default { + name: "linetool", + computed: { + ...mapGetters("map", ["getLayerByName"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"]) + }, + methods: { + toggleLineTool() { + this.lineTool.setActive(!this.lineTool.getActive()); + this.polygonTool.setActive(false); + this.cutTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + this.getLayerByName("Draw Tool") + .data.getSource() + .clear(); + }, + lineEnd(event) { + const length = getLength(event.feature.getGeometry()); + this.$store.commit("map/setCurrentMeasurement", { + quantity: "Length", + unitSymbol: "m", + value: Math.round(length * 10) / 10 + }); + this.$store.commit("application/showIdentify", true); + } + }, + created() { + if (!this.lineTool) { + const drawVectorSrc = this.getLayerByName("Draw Tool").data.getSource(); + const lineTool = new Draw({ + source: drawVectorSrc, + type: "LineString", + maxPoints: 2 + }); + lineTool.setActive(false); + lineTool.on("drawstart", () => { + drawVectorSrc.clear(); + this.$store.commit("map/setCurrentMeasurement", null); + }); + lineTool.on("drawend", this.lineEnd); + this.$store.commit("map/lineTool", lineTool); + this.openLayersMap.addInteraction(lineTool); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/toolbar/Pdftool.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,29 @@ +<template> + <div @click="$store.commit('application/showPdfTool', !showPdfTool)" class="toolbar-button"> + <i :class="['fa fa-file-pdf-o', {inverted: showPdfTool}]"></i> + </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/map/toolbar/Polygontool.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,73 @@ +<template> + <div @click="togglePolygonTool" class="toolbar-button"> + <i :class="['fa fa-edit', {inverted: polygonTool && polygonTool.getActive()}]"></i> + </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"; +import { getArea } from "ol/sphere.js"; +import Draw from "ol/interaction/Draw.js"; + +export default { + name: "polygontool", + computed: { + ...mapGetters("map", ["getLayerByName"]), + ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"]) + }, + methods: { + togglePolygonTool() { + this.polygonTool.setActive(!this.polygonTool.getActive()); + this.lineTool.setActive(false); + this.cutTool.setActive(false); + this.$store.commit("map/setCurrentMeasurement", null); + this.getLayerByName("Draw Tool") + .data.getSource() + .clear(); + }, + polygonEnd(event) { + const areaSize = getArea(event.feature.getGeometry()); + this.$store.commit("map/setCurrentMeasurement", { + quantity: "Area", + unitSymbol: areaSize > 100000 ? "km²" : "m²", + value: + areaSize > 100000 + ? Math.round(areaSize / 1000) / 1000 // convert into 1 km² == 1000*1000 m² and round to 1000 m² + : Math.round(areaSize) + }); + this.$store.commit("application/showIdentify", true); + } + }, + created() { + if (!this.polygonTool) { + const drawVectorSrc = this.getLayerByName("Draw Tool").data.getSource(); + const polygonTool = new Draw({ + source: drawVectorSrc, + type: "Polygon", + maxPoints: 50 + }); + polygonTool.setActive(false); + polygonTool.on("drawstart", () => { + drawVectorSrc.clear(); + this.$store.commit("map/setCurrentMeasurement", null); + }); + polygonTool.on("drawend", this.polygonEnd); + this.$store.commit("map/polygonTool", polygonTool); + this.openLayersMap.addInteraction(polygonTool); + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/toolbar/Toolbar.vue Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,96 @@ +<template> + <div class="ml-2"> + <div :class="'toolbar toolbar-' + (expandToolbar ? 'expanded' : 'collapsed')"> + <Identify></Identify> + <Layers></Layers> + <Cuttool></Cuttool> + <Linetool></Linetool> + <Polygontool></Polygontool> + <Pdftool></Pdftool> + </div> + <div @click="$store.commit('application/expandToolbar', !expandToolbar)" class="toolbar-button bg-info text-white"> + <i :class="'fa fa-angle-' + (expandToolbar ? 'up' : 'down')"></i> + </div> + </div> +</template> + +<style lang="sass"> +// not scoped to affect nested components +// doen't work when put in application/assets/application.sass... why??? o_O +.toolbar + overflow: hidden + transition: max-height 0.4s + +.toolbar-collapsed + max-height: (3 * $icon-height) + (3 * $offset) + +.toolbar-expanded + max-height: 100% + +.toolbar-button + opacity: $slight-transparent + color: #666 + height: $icon-width + width: $icon-height + align-items: center + justify-content: center + display: flex + background: #fff + margin-bottom: $offset + border-radius: $border-radius + box-shadow: $shadow-xs + z-index: 2 + pointer-events: auto + .inverted + color: $color-info + .grey + color: #ddd +</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"), + Cuttool: () => import("./Cuttool.vue"), + Pdftool: () => import("./Pdftool.vue") + }, + computed: { + ...mapGetters("map", ["getLayerByName"]), + ...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.getLayerByName("Draw Tool") + .data.getSource() + .clear(); + } + }); + } +}; +</script>
--- a/client/src/fairway/Fairwayprofile.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,579 +0,0 @@ -<template> - <div :class="['position-relative', {show: showSplitscreen}]" v-if="Object.keys(currentProfile).length"> - <button - class="rounded-bottom bg-white border-0 position-absolute splitscreen-toggle shadow-sm" - @click="$store.commit('application/showSplitscreen', false)" - v-if="showSplitscreen"> - <i class="fa fa-angle-down"></i> - </button> - <button - class="rounded-bottom bg-white border-0 position-absolute clear-selection shadow-sm" - @click="$store.dispatch('fairwayprofile/clearSelection');" - v-if="showSplitscreen"> - <i class="fa fa-times text-danger"></i> - </button> - <div class="profile bg-white position-relative d-flex flex-column pr-5"> - <h5 class="mb-0 mt-2">{{ selectedBottleneck }} ({{ selectedSurvey.date_info }})</h5> - <div class="d-flex flex-fill"> - <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div> - <div class="additionalsurveys d-flex flex-column"> - <small> - Additional Surveys - <select v-model="additionalSurvey" class="form-control form-control-sm"> - <option value="">None</option> - <option - v-for="survey in additionalSurveys" - :key="survey.date_info" - :value="survey" - >{{survey.date_info}}</option> - </select> - <hr> - <div class="d-flex text-left mb-2"> - <div class="text-nowrap mr-1"> - <b>Start:</b> - <br> - Lat: {{ startPoint[1] }} - <br> - Lon: {{ startPoint[0] }} - </div> - <div class="text-nowrap ml-1"> - <b>End:</b> - <br> - Lat: {{ endPoint[1] }} - <br> - Lon: {{ endPoint[0] }} - </div> - <button class="btn btn-outline-secondary btn-sm ml-2 mt-auto" - @click="showLabelInput = !showLabelInput"> - <i :class="'fa fa-' + (showLabelInput ? 'times' : 'save')"></i> - </button> - <button v-clipboard:copy="coordinatesForClipboard" - v-clipboard:success="onCopyCoordinates" - class="btn btn-outline-secondary btn-sm ml-2 mt-auto"> - <i class="fa fa-copy"></i> - </button> - </div> - <div v-if="showLabelInput"> - Enter label for cross profile: - <div class="position-relative"> - <input class="form-control form-control-sm pr-5" v-model="cutLabel" /><br> - <button class="btn btn-sm btn-outline-secondary position-absolute" - @click="saveCut" - v-if="cutLabel" - style="top: 0; right: 0;"> - <i class="fa fa-check"></i> - </button> - </div> - </div> - Saved cross profiles: - <select class="form-control form-control-sm mb-2" v-model="coordinatesSelect"> - <option></option> - <option v-for="(cut, index) in previousCuts" :value="cut.coordinates" :key="index"> - {{ cut.label }} - </option> - </select> - Enter coordinates manually: - <div class="position-relative"> - <input class="form-control form-control-sm pr-5" placeholder="Lat,Lon,Lat,Lon" v-model="coordinatesInput" /><br> - <button class="btn btn-sm btn-outline-secondary position-absolute" - @click="applyManualCoordinates" - style="top: 0; right: 0;" - v-if="coordinatesInput"> - <i class="fa fa-check"></i> - </button> - </div> - </small> - </div> - </div> - </div> - </div> -</template> - -<style lang="sass" scoped> -.profile - width: 100vw - height: 0 - overflow: hidden - z-index: 2 - -.splitscreen-toggle, -.clear-selection - right: $icon-width + $offset - width: $icon-width - height: $icon-height - margin-top: 2px - z-index: 3 - outline: none - -.clear-selection - right: $offset - -.show - .profile - height: 50vh - -.waterlevelselection - margin-top: $large-offset - margin-right: $large-offset - -.additionalsurveys - margin-top: $large-offset - margin-bottom: auto - margin-right: $large-offset - margin-left: auto - max-width: 300px - -.additionalsurveys input - margin-right: $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 * as d3 from "d3"; -import { mapState, mapGetters } from "vuex"; -import { displayError, displayInfo } from "../application/lib/errors.js"; -import Feature from "ol/Feature"; -import LineString from "ol/geom/LineString"; - -const GROUND_COLOR = "#4A2F06"; - -export default { - name: "fairwayprofile", - props: [ - "width", - "height", - "xScale", - "yScaleLeft", - "yScaleRight", - "margin", - "additionalSurveys" - ], - data() { - return { - wait: false, - coordinatesInput: "", - coordinatesSelect: null, - cutLabel: "", - showLabelInput: false - }; - }, - computed: { - ...mapGetters("map", ["getLayerByName"]), - ...mapState("application", ["showSplitscreen"]), - ...mapState("fairwayprofile", [ - "startPoint", - "endPoint", - "currentProfile", - "minAlt", - "maxAlt", - "totalLength", - "fairwayCoordinates", - "waterLevels", - "selectedWaterLevel", - "previousCuts" - ]), - ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]), - additionalSurvey: { - get() { - return this.$store.getters["fairwayprofile/additionalSurvey"]; - }, - set(value) { - this.$store.commit("fairwayprofile/setAdditionalSurvey", value); - this.selectAdditionalSurveyData(); - } - }, - currentData() { - if ( - !this.selectedSurvey || - !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info) - ) - return []; - return this.currentProfile[this.selectedSurvey.date_info]; - }, - additionalData() { - if ( - !this.additionalSurvey || - !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info) - ) - return []; - return this.currentProfile[this.additionalSurvey.date_info]; - }, - waterColor() { - const result = this.waterLevels.find( - x => x.level === this.selectedWaterLevel - ); - return result.color; - }, - coordinatesForClipboard() { - return ( - this.startPoint[1] + - "," + - this.startPoint[0] + - "," + - this.endPoint[1] + - "," + - this.endPoint[0] - ); - } - }, - watch: { - showSplitscreen() { - this.drawDiagram(); - }, - currentData() { - this.drawDiagram(); - }, - width() { - this.drawDiagram(); - }, - height() { - this.drawDiagram(); - }, - waterLevels() { - this.drawDiagram(); - }, - selectedWaterLevel() { - this.drawDiagram(); - }, - fairwayCoordinates() { - this.drawDiagram(); - }, - selectedBottleneck() { - this.$store.dispatch("fairwayprofile/previousCuts"); - this.cutLabel = - this.selectedBottleneck + " (" + new Date().toISOString() + ")"; - }, - coordinatesSelect(newValue) { - if (newValue) { - this.applyCoordinates(newValue); - } - } - }, - methods: { - selectAdditionalSurveyData() { - if ( - !this.additionalSurvey || - this.wait || - this.currentProfile[this.additionalSurvey.date_info] - ) { - this.drawDiagram(); - return; - } - this.$store - .dispatch("fairwayprofile/loadProfile", this.additionalSurvey) - .then(() => { - this.wait = false; - }) - .catch(error => { - this.wait = false; - let status = "ERROR"; - let data = error; - const response = error.response; - if (response) { - status = response.status; - data = response.data; - } - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }, - drawDiagram() { - this.coordinatesSelect = null; - const chartDiv = document.querySelector(".fairwayprofile"); - d3.select("svg").remove(); - 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, - yScaleLeft, - graph - } = this.generateCoordinates(svg, height, width); - this.drawWaterlevel({ - graph, - xScale, - yScaleRight, - height, - width - }); - if (currentData) { - this.drawProfile({ - graph, - xScale, - yScaleRight, - currentData, - height, - width, - color: GROUND_COLOR, - strokeColor: "black", - opacity: 1 - }); - } - if (additionalData) { - this.drawProfile({ - graph, - xScale, - yScaleRight, - currentData: additionalData, - height, - width, - color: GROUND_COLOR, - strokeColor: "#943007", - opacity: 0.6 - }); - } - this.drawLabels({ - graph, - xScale, - yScaleLeft, - currentData, - height, - width - }); - this.drawFairway({ - graph, - xScale, - yScaleRight, - currentData, - height, - width - }); - }, - 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); - } - }, - onCopyCoordinates() { - displayInfo({ - title: "Success", - message: "Coordinates copied to clipboard!" - }); - }, - applyManualCoordinates() { - const coordinates = this.coordinatesInput - .split(",") - .map(coord => parseFloat(coord.trim())); - 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 - const cutLayer = this.getLayerByName("Cut Tool"); - cutLayer.data.getSource().clear(); - const cut = new Feature({ - geometry: new LineString([ - [coordinates[0], coordinates[1]], - [coordinates[2], coordinates[3]] - ]).transform("EPSG:4326", "EPSG:3857") - }); - cutLayer.data.getSource().addFeature(cut); - - // draw diagram - this.$store.dispatch("fairwayprofile/cut", cut); - } else { - displayError({ - title: "Invalid input", - message: - "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] - }; - 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: "Coordinates saved!", - message: - 'You can now select these coordinates from the "Saved cross profiles" menu to restore this cross profile.' - }); - } - }, - mounted() { - this.drawDiagram(); - } -}; -</script>
--- a/client/src/fairway/Infobar.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,45 +0,0 @@ -<template> - <div v-if="selectedSurvey && !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"> - <h6 class="my-auto px-2"> - {{ selectedBottleneck }} - ({{ selectedSurvey.date_info }}) - </h6> - <i class="fa fa-angle-up py-2 px-2 border-left" @click="$store.commit('application/showSplitscreen', true)" v-if="Object.keys(currentProfile).length"></i> - <i class="fa fa-close text-danger py-2 px-2 border-left" @click="$store.dispatch('fairwayprofile/clearSelection');"></i> - </div> - </div> -</template> - -<style lang="sass" scoped> -.infobar - height: $icon-width - z-index: 2 -</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/fairway/Surveys.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -<template> - <div class="box expanded ui-element rounded bg-white ml-auto mr-3 mb-3 text-nowrap" v-if="selectedBottleneck && surveys && !selectedSurvey"> - <div style="width: 15rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> - {{ selectedBottleneck }} - <i class="fa fa-times ml-auto" @click="$store.dispatch('fairwayprofile/clearSelection');"></i> - </h6> - <div class="p-3"> - <div - v-for="(survey, i) of surveys" - :key="survey.data_info" - :class="{ 'mt-1': i }" - @click.prevent="$store.commit('bottlenecks/setSelectedSurvey', survey)" - > - <a href="#" @click.prevent>{{ survey.date_info }}</a> - </div> - </div> - </div> - </div> -</template> - -<script> -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "surveys", - computed: { - ...mapState("bottlenecks", [ - "selectedBottleneck", - "surveys", - "selectedSurvey" - ]) - } -}; -</script>
--- a/client/src/identify/Identify.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,86 +0,0 @@ -<template> - <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showIdentify }]"> - <div style="width: 20rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> - <i class="fa fa-info mr-2"></i> - Identified - <i class="fa fa-times ml-auto" @click="$store.commit('application/showIdentify', false)"></i> - </h6> - <div class="d-flex flex-column features p-3 flex-grow-1 text-left"> - <div v-if="currentMeasurement"> - <b> - {{ currentMeasurement.quantity }} - ({{ currentMeasurement.unitSymbol }}): - </b><br> - <small>{{ currentMeasurement.value }}</small> - </div> - <div v-for="(feature, i) of identifiedFeatures" :key="feature.getId()" > - <div v-if="feature.getId()" :class="{ 'mt-2': i }"> - <b>{{ feature.getId().replace(/[.][^.]*$/,"") /* cut away everything from the last . to the end */}}:</b> - <small - v-for="(value, key) in prepareProperties(feature)" - :key="key" - > - <div v-if="value">{{key}}:{{value}}</div> - </small> - </div> - </div> - <div v-if="!currentMeasurement && !identifiedFeatures.length" class="text-muted small text-center my-auto"> - No features identified. - </div> - </div> - <div class="versioninfo border-top p-3 text-left"> - gemma - <a href="https://hg.intevation.de/gemma/file/tip">source-code</a> - {{ versionStr }} - <br>Some data © - <a href="https://www.openstreetmap.org/copyright">OpenSteetMap</a>contributors - </div> - </div> - </div> -</template> - -<style lang="sass" scoped> -.features - max-height: 19rem - overflow-y: auto - -.versioninfo - font-size: 60% -</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/imports/Importqueue.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,170 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <div :class="spacerStyle"></div> - <div class="mt-3 mx-auto"> - <div class="card importqueuecard"> - <div class="card-header shadow-sm text-white bg-info mb-3">Importqueue</div> - <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"> - <i class="fa fa-search"></i> - </span> - </div> - <input - type="text" - class="form-control" - placeholder="" - aria-label="Search" - aria-describedby="search" - > - </div> - <div class="filters"> - <button - @click="setFilter('successful')" - :class="successfulStyle" - >Successful</button> - <button @click="setFilter('failed')" :class="failedStyle">Failed</button> - <button @click="setFilter('pending')" :class="pendingStyle">Pending</button> - </div> - </div> - <table class="table"> - <thead> - <tr> - <th>Enqueued</th> - <th>Kind</th> - <th>User</th> - <th>State</th> - </tr> - </thead> - <tbody> - <tr v-for="job in imports" :key="job.id"> - <td>{{job.enqueued}}</td> - <td>{{job.kind}}</td> - <td>{{job.user}}</td> - <td>{{job.state}}</td> - </tr> - </tbody> - </table> - </div> - </div> - </div> - </div> - </div> -</template> - -<script> -import { displayError } from "../application/lib/errors.js"; -import { mapState } from "vuex"; - -export default { - name: "importqueue", - data() { - return { - successful: false, - failed: false, - pending: false - }; - }, - methods: { - setFilter(name) { - this[name] = !this[name]; - const allSet = this.successful && this.failed && this.pending; - if (allSet) { - this.all = false; - this.successful = false; - this.failed = false; - this.pending = false; - } - } - }, - computed: { - ...mapState("imports", ["imports"]), - ...mapState("application", ["showSidebar"]), - 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 - }; - } - }, - mounted() { - this.$store.dispatch("imports/getImports").catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } -}; -</script> - -<style lang="sass" scoped> -.spacer - height: 100vh - -.spacer-collapsed - min-width: $icon-width + $offset - transition: $transition-fast - -.spacer-expanded - min-width: $sidebar-width + $offset - -.importqueuecard - width: 80vw - 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: 50% -</style>
--- a/client/src/imports/Imports.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,260 +0,0 @@ -<template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> - <i class="fa fa-upload mr-2"></i> - Import Soundingresults - </h6> - <hr class="mr-auto ml-auto mb-0 w-90"> - <div v-if="editState" class="ml-auto mr-auto mt-4 w-90"> - <div class="d-flex flex-row input-group mb-4"> - <div class="offset-r"> - <label for="bottleneck" class="label-text" id="bottlenecklabel">Bottleneck</label> - </div> - <input - id="bottleneck" - type="text" - class="form-control" - placeholder="Name of Bottleneck" - aria-label="bottleneck" - aria-describedby="bottlenecklabel" - v-model="bottleneck" - > - </div> - <div class="d-flex flex-row input-group mb-4"> - <div class="offset-r"> - <label class="label-text" for="importdate" id="importdatelabel">Date</label> - </div> - <input - id="importdate" - type="date" - class="form-control" - placeholder="Date of import" - aria-label="bottleneck" - aria-describedby="bottlenecklabel" - v-model="importDate" - > - </div> - <div class="d-flex flex-row input-group mb-4"> - <div class="offset-r"> - <label class="label-text" for="depthreference">Depth reference</label> - </div> - <select v-model="depthReference" class="custom-select" id="depthreference"> - <option - v-for="option in this.$options.depthReferenceOptions" - :key="option" - >{{option}}</option> - </select> - </div> - </div> - <div class="w-90 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 - 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" - >Download Meta.json</a> - <button - v-if="editState" - @click="deleteTempData" - class="btn btn-danger" - type="button" - >Cancel Upload</button> - <button - :disabled="disableUpload" - @click="submit" - class="btn btn-info" - type="button" - >{{uploadState?"Upload":"Confirm"}}</button> - </div> - </div> - </div> -</template> - -<script> -import { HTTP } from "../application/lib/http"; -import { displayError, displayInfo } from "../application/lib/errors.js"; - -const defaultLabel = "Choose .zip-file"; -const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; - -export default { - name: "imports", - data() { - return { - importState: IMPORTSTATE.UPLOAD, - depthReference: "", - bottleneck: "", - importDate: "", - uploadLabel: defaultLabel, - uploadFile: null, - disableUpload: false, - token: null - }; - }, - methods: { - initialState() { - this.importState = IMPORTSTATE.UPLOAD; - this.depthReference = ""; - this.bottleneck = ""; - this.importDate = ""; - this.uploadLabel = defaultLabel; - this.uploadFile = null; - this.disableUpload = false; - this.token = null; - }, - 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: "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 => { - const { bottleneck, date } = response.data.meta; - const depthReference = response.data.meta["depth-reference"]; - this.importState = IMPORTSTATE.EDIT; - this.bottleneck = bottleneck; - this.depthReference = depthReference; - this.importDate = new Date(date).toISOString().split("T")[0]; - this.token = response.data.token; - }) - .catch(error => { - const { status, data } = error.response; - const messages = data.messages ? data.messages.join(", ") : ""; - displayError({ - title: "Backend Error", - message: `${status}: ${messages}` - }); - }); - }, - confirm() { - let formData = new FormData(); - formData.append("token", this.token); - ["bottleneck", "importDate", "depthReference"].forEach(x => { - if (this[x]) formData.append(x, this[x]); - }); - HTTP.post("/imports/soundingresult", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(() => { - displayInfo({ - title: "Import", - message: "Starting import for " + this.bottleneck - }); - this.initialState(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } - }, - computed: { - editState() { - return this.importState === IMPORTSTATE.EDIT; - }, - uploadState() { - return this.importState === IMPORTSTATE.UPLOAD; - }, - 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="sass" scoped> -.offset-r - margin-right: $large-offset - -.buttons button - margin-left: $offset !important - -.label-text - width: 10rem - text-align: left - line-height: 2.25rem -</style>
--- a/client/src/layers/Layers.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -<template> - <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showLayers }]"> - <div style="width: 20rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> - <i class="fa fa-th-list mr-2"></i> - Layers - <i class="fa fa-times ml-auto" @click="$store.commit('application/showLayers', false)"></i> - </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/layers/Layerselect.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,75 +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">{{layername}}</label> - </div> - <div v-if="isVisible && (layername == 'Bottleneck isolines')"> - <img class="rounded my-1 d-block" :src="isolinesLegendImgUrl"> - </div> - </div> -</template> - -<style lang="sass" 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 "../application/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/layers/LegendElement.vue Wed Nov 21 15:07:39 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="sass" scoped> -.legendelement - max-height: 1.5rem - width: 2rem -</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/lib/errors.js Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,33 @@ +/* + * 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> + */ + +/** facade to wrap calls to vue2-toastr */ +import app from "../main"; + +const displayError = ({ title, message }) => { + app.$toast.error({ + title: title, + message: message + }); +}; + +const displayInfo = ({ title, message }) => { + app.$toast.info({ + title: title, + message: message + }); +}; + +export { displayError, displayInfo };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/lib/geo.js Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,208 @@ +/* + * 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> + */ + +/** + * + * Distance calculations + * JS transposition of cross.go functions + * + */ + +import { GeoJSON } from "ol/format.js"; +import Feature from "ol/Feature"; +import distance from "@turf/distance"; +import { + lineString as turfLineString, + polygon as turfPolygon +} from "@turf/helpers"; +import lineIntersect from "@turf/line-intersect"; + +const EARTHRADIUS = 6378137.0; + +/** + * Converts to radiant + * @param {degree} d + */ +const deg2rad = d => { + return (d * Math.PI) / 180.0; +}; + +/** + * Calculates the difference between two points in m + * + * Points are given with {lat: $lat, lon: $lon} + * + * @param {object} P1 + * @param {object} P2 + */ +const distanceBetween = (P1, P2) => { + const dLat = deg2rad(P2.lat - P1.lat); + let dLng = Math.abs(deg2rad(P2.lon - P1.lon)); + if (dLng > Math.PI) { + dLng = 2 * Math.PI - dLng; + } + const x = dLng * Math.cos(deg2rad((P1.lat + P2.lat) / 2.0)); + return Math.sqrt(dLat * dLat + x * x) * EARTHRADIUS; +}; + +/** + * Takes a triple of [lat, long, alt] and generates + * an object with according attributes + * { + * lat: $lat, + * lon: $lon, + * alt: $alt + * } + * + * @param {array} coords + */ +const Point = coords => { + return { + lon: coords[0], + lat: coords[1], + alt: coords[2] + }; +}; + +/** + * Has geoJSON as its input and transforms + * given coordinates into points representing + * distance from startpoint / altitude information + * + * a) extracting the minimum altitude + * b) extracting the maximum altitude + * c) calculating the total length of the given profile + * d) transposes the datapoints relative to a given start point + * + * The calculation of total equals the sum of partial distances between points + * + * The x-value of a point is equal to the total distance up to this point + * + * The distance between the last point of the last segment and the end point is added + * to the total + * + * @param {object} geoJSON, startPoint, endPoint + */ +const transform = ({ geoJSON, startPoint, endPoint }) => { + const lineSegments = geoJSON.geometry.coordinates; + let segmentPoints = []; + let lengthPolyLine = 0; + let referencePoint = Point(startPoint); + let minAlt = Math.abs(lineSegments[0][0][2]); + let maxAlt = Math.abs(lineSegments[0][0][2]); + let currentPoint = null; + for (let segment of lineSegments) { + let points = []; + for (let coordinateTriplet of segment) { + currentPoint = Point(coordinateTriplet); + lengthPolyLine += distanceBetween(referencePoint, currentPoint); + let y = Math.abs(currentPoint.alt); + points.push({ + x: lengthPolyLine, + y: y + }); + if (y < minAlt) minAlt = y; + if (y > maxAlt) maxAlt = y; + referencePoint = currentPoint; + } + segmentPoints.push(points); + } + lengthPolyLine += distanceBetween(currentPoint, Point(endPoint)); + return { segmentPoints, lengthPolyLine, minAlt, maxAlt }; +}; + +/** + * Prepare profile takes geoJSON data in form of + * a MultiLineString, e.g. + * + * { + * type: "Feature", + * geometry: { + * type: "MultiLineString", + * coordinates: [ + * [ + * [16.53593398, 48.14694085, -146.52392755] + * ... + * ]] + * + * and transforms it to a structure representing the number of sections + * where data is present with according lengths and the points + * + * { + * { points: + * [ + * [ { x: 0.005798201616417183, y: -146.52419461 }, + * { x: 0, y: -146.52394016 } + * ... + * ] + * ] + * lengthPolyLine: 160.06814078495722, + * minAlt: -146.73122231, + * maxAlt: -145.65155866 + * } + * + * @param {object} geoJSON + */ +const prepareProfile = ({ geoJSON, startPoint, endPoint }) => { + const { segmentPoints, lengthPolyLine, minAlt, maxAlt } = transform({ + geoJSON, + startPoint, + endPoint + }); + return { + points: segmentPoints, + lengthPolyLine: lengthPolyLine, + minAlt: minAlt, + maxAlt: maxAlt + }; +}; + +const generateFeatureRequest = (profileLine, bottleneck_id, date_info) => { + const feature = new Feature({ + geometry: profileLine, + bottleneck: bottleneck_id, + date: date_info + }); + return new GeoJSON({ geometryName: "geometry" }).writeFeature(feature); +}; + +const calculateFairwayCoordinates = (profileLine, fairwayGeometry, depth) => { + // both geometries have to be in EPSG:4326 + // uses turfjs distance() function + let fairwayCoordinates = []; + var line = turfLineString(profileLine.getCoordinates()); + var polygon = turfPolygon(fairwayGeometry.getCoordinates()); + var intersects = lineIntersect(line, polygon); + var l = intersects.features.length; + if (l % 2 != 0) { + console.log("Ignoring fairway because profile only intersects once."); + } else { + for (let i = 0; i < l; i += 2) { + let pStartPoint = profileLine.getCoordinates()[0]; + let fStartPoint = intersects.features[i].geometry.coordinates; + let fEndPoint = intersects.features[i + 1].geometry.coordinates; + let opts = { units: "kilometers" }; + + fairwayCoordinates.push([ + distance(pStartPoint, fStartPoint, opts) * 1000, + distance(pStartPoint, fEndPoint, opts) * 1000, + depth + ]); + } + } + return fairwayCoordinates; +}; + +export { generateFeatureRequest, prepareProfile, calculateFairwayCoordinates };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/lib/http.js Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,23 @@ +/* + * 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 axios from "axios"; + +export const HTTP = axios.create({ + baseURL: process.env.VUE_APP_API_URL || "/api" + /* headers: { + Authorization: 'Bearer {token}' + }*/ +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/lib/session.js Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,38 @@ +/* + * 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> + */ + +/** + * Compares whether session is current + * based on the expiry information and the + * current date + * + * @param {number} expiresFromPastSession + */ +function sessionStillActive(expiresFromPastSession) { + if (!expiresFromPastSession) return false; + const now = Date.now(); + const stillActive = now < expiresFromPastSession; + return stillActive; +} +/** + * Converts a given unix time to Milliseconds + * + * @param {string} timestring + */ +function toMillisFromString(timestring) { + return timestring * 1000; +} + +export { sessionStillActive, toMillisFromString };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/locale/translations.json Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,16 @@ +{ + "en_GB": { + "Enter username": "Enter username", + "Enter passphrase": "Enter passphrase", + "Login failed": "Login failed", + "Login": "Login", + "Forgot password": "Forgot password" + }, + "de_AT": { + "Enter username": "Benutzername", + "Enter passphrase": "Passphrase", + "Login failed": "Login fehlgeschlagen", + "Login": "Login", + "Forgot password": "Passwort vergessen" + } +}
--- a/client/src/login/Login.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,182 +0,0 @@ -(<template> - <div class="d-flex flex-column login bg-white shadow"> - <div class="m-5"> - <!-- logo section --> - <div class="d-flex flex-row justify-content-center mb-3"> - <div class="logo mr-3"><img src="../application/assets/logo.png"></div> - <div class="title"> - <h1>{{ appTitle }}</h1> - </div> - </div> - <!-- end logo section --> - <form class="loginform mx-auto" @submit.prevent="login"> - <div id="alert" :style="errorMessageStyle" :class="errorMessageClass" role="alert"> - <span>{{ errorMessage }}</span> - </div> - <div class="input-group mb-3"> - <input type="text" v-model="user" id="inputUsername" class="form-control shadow-sm" :placeholder="usernameLabel" required autofocus> - </div> - <div class="input-group mb-3"> - <input :type="isPasswordVisible" v-model="password" id="inputPassword" class="form-control shadow-sm" :placeholder='passwordLabel' :required='!showPasswordReset' :disabled='showPasswordReset'> - <div class="input-group-append"> - <span class="input-group-text disabled" id="basic-addon2" @click="showPassword"> - <font-awesome-icon icon="eye" v-if="!readablePassword" /> - <font-awesome-icon icon="eye-slash" v-if="readablePassword" /> - </span> - </div> - </div> - <button v-if="showPasswordReset==false" class="btn btn-primary btn-block shadow-sm" :disabled="submitted || showPasswordReset" type="submit"> - <translate>Login</translate> - </button> - <div v-if="showPasswordReset" class="passwordreset"> - <button class="btn btn-block btn-info" type="button" @click="resetPassword"> - <translate>Request password reset!</translate> - </button> - <div class="pull-right"> - <a href="#" @click.prevent="togglePasswordReset"> - <translate>back to login</translate> - </a> - </div> - </div> - <div v-else class="pull-right"> - <a href="#" @click.prevent="togglePasswordReset"> - <translate>Forgot password</translate> - </a> - </div> - </form> - - <!-- bottom logo section --> - <div class="mb-3 secondary-logo mx-auto mb-auto"><img :src="secondaryLogo"></div> - </div> - </div> -</template>) - -<style lang="sass" scoped> -.login - min-width: 375px - min-height: 500px - @extend %fully-centered - -.loginform - max-width: 375px - -.alert - padding: 0.5rem - -.secondary-logo - max-width: 375px - -/* avoid IE and Edge show a password reveal as we do our own */ -input[type="password"]::-ms-reveal - display: none -</style> - -<script> -import { mapState } from "vuex"; -import { HTTP } from "../application/lib/http.js"; -import { displayError } from "../application/lib/errors.js"; - -const UNAUTHORIZED = 401; - -export default { - name: "login", - data() { - return { - user: "", - password: "", - submitted: false, - loginFailed: false, - passwordJustResetted: false, - readablePassword: false, - showPasswordReset: false, - usernameToReset: "" - }; - }, - computed: { - errorMessage() { - if (this.loginFailed) return this.$gettext("Login failed"); - if (this.passwordJustResetted) - return this.$gettext("Password reset requested!"); - return "&npsp;"; - }, - passwordLabel() { - return this.$gettext("Enter passphrase"); - }, - usernameLabel() { - return this.$gettext("Enter username"); - }, - isPasswordVisible() { - return this.readablePassword ? "text" : "password"; - }, - errorMessageStyle() { - if (this.loginFailed || this.passwordJustResetted) { - return "visibility:visible"; - } - return "visibility:hidden"; - }, - errorMessageClass() { - let result = { - "mb-3": true, - errormessage: true, - alert: true - }; - if (this.loginFailed) { - result["alert-danger"] = true; - } - if (this.passwordJustResetted) { - result["alert-info"] = true; - } - return result; - }, - ...mapState("application", ["appTitle", "secondaryLogo"]) - }, - methods: { - login() { - this.submitted = true; - this.passwordJustResetted = false; - const { user, password } = this; - this.$store - .dispatch("user/login", { user, password }) - .then(() => { - this.loginFailed = false; - this.$router.push("/"); - }) - .catch(error => { - this.loginFailed = true; - this.submitted = false; - const { status, data } = error.response; - if (status !== UNAUTHORIZED) { - //Unauthorized is handled in alert-div - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - } - }); - }, - showPassword() { - // disallowing toggle when in reset mode - if (this.showPasswordReset) return; - this.readablePassword = !this.readablePassword; - }, - togglePasswordReset() { - this.passwordJustResetted = false; - this.showPasswordReset = !this.showPasswordReset; - this.loginFailed = false; - }, - resetPassword() { - if (this.user) { - HTTP.post("/users/passwordreset", { user: this.user }).catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - this.togglePasswordReset(); - this.passwordJustResetted = true; - } - } - } -}; -</script>
--- a/client/src/logs/logs.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,158 +0,0 @@ -<template> - <div class="main d-flex flex-column"> - <div class="d-flex flex-row"> - <div :class="spacer"></div> - <div class="logoutput text-left bg-white shadow mt-3 mx-3"> - <pre id="code" v-highlightjs="logs"><code class="bash hljs hljs-string"></code></pre> - </div> - </div> - <div class="d-flex flex-row logmenu"> - <div class="d-flex align-self-center"> - <ul class="nav nav-pills"> - <li class="nav-item"> - <a - @click="fetch('system/log/apache2/access.log', 'accesslog')" - :class="accesslogStyle" - href="#" - >Accesslog</a> - </li> - <li class="nav-item"> - <a - @click="fetch('system/log/apache2/error.log', 'errorlog')" - :class="errorlogStyle" - href="#" - >Errorlog</a> - </li> - </ul> - </div> - <div class="statuscontainer d-flex flex-row"> - <div class="statusline ml-3 mt-1 align-self-center"> - <h3>Last refresh: {{refreshed}}</h3> - </div> - <div class="refresh"> - <button class="btn btn-dark" @click="fetch(currentFile, currentLog)">Refresh</button> - </div> - </div> - </div> - </div> -</template> - -<style lang="sass" scoped> -.statuscontainer - width: 87% - position: relative - -.logmenu - margin-left: 5rem - min-width: 60vw - -#code - overflow: auto - -.refresh - position: absolute - right: 0 - -.logoutput - width: 95% - height: 85vh - 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: 7rem -</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 "../application/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/main.js Wed Nov 21 15:07:39 2018 +0100 +++ b/client/src/main.js Thu Nov 22 07:07:12 2018 +0100 @@ -13,11 +13,11 @@ */ import Vue from "vue"; -import App from "./App.vue"; +import App from "./components/App.vue"; import router from "./router"; import store from "./store"; import GetTextPlugin from "vue-gettext"; -import translations from "./translations.json"; +import translations from "./locale/translations.json"; import locale2 from "locale2"; import CxltToastr from "cxlt-vue2-toastr"; import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
--- a/client/src/map/Maplayer.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,430 +0,0 @@ -<template> - <div id="map" :class="mapStyle"></div> -</template> - -<style lang="sass" scoped> -.nocursor - cursor: none - -.mapsplit - height: 50vh - -.mapfull - height: 100vh - -@media print - .mapfull - width: 2000px - height: 2828px - - .mapsplit - width: 2000px - height: 2828px -</style> - -<script> -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * * Thomas Junk <thomas.junk@intevation.de> - * * Bernhard E. Reiter <bernhard.reiter@intevation.de> - */ -import { HTTP } from "../application/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"; -import { getCenter } from "ol/extent"; - -/* for the sake of debugging */ -/* eslint-disable no-console */ -export default { - name: "maplayer", - props: ["lat", "long", "zoom", "split"], - data() { - return { - projection: "EPSG:3857" - }; - }, - computed: { - ...mapGetters("map", ["getLayerByName"]), - ...mapState("map", [ - "layers", - "openLayersMap", - "lineTool", - "polygonTool", - "cutTool" - ]), - ...mapState("bottlenecks", ["selectedSurvey"]), - mapStyle() { - return { - mapfull: !this.split, - mapsplit: this.split, - nocursor: this.hasActiveInteractions - }; - }, - hasActiveInteractions() { - return ( - (this.lineTool && this.lineTool.getActive()) || - (this.polygonTool && this.polygonTool.getActive()) || - (this.cutTool && this.cutTool.getActive()) - ); - } - }, - methods: { - identify(coordinate, pixel) { - if (!this.hasActiveInteractions) { - this.$store.commit("map/setIdentifiedFeatures", []); - // checking our WFS layers - var features = this.openLayersMap.getFeaturesAtPixel(pixel); - if (features) { - this.$store.commit("map/setIdentifiedFeatures", features); - - // get selected bottleneck from identified features - for (let feature of features) { - let id = feature.getId(); - // RegExp.prototype.test() works with number, str and undefined - if (/^bottlenecks\./.test(id)) { - this.$store.dispatch( - "bottlenecks/setSelectedBottleneck", - feature.get("objnam") - ); - this.$store.commit("map/moveMap", { - coordinates: getCenter( - feature - .getGeometry() - .clone() - .transform("EPSG:3857", "EPSG:4326") - .getExtent() - ), - zoom: 17, - preventZoomOut: true - }); - } - } - } - - // DEBUG output and example how to remove the GeometryName - /* - for (let feature of features) { - console.log("Identified:", feature.getId()); - for (let key of feature.getKeys()) { - if (key != feature.getGeometryName()) { - console.log(key, feature.get(key)); - } - } - } - */ - - // trying the GetFeatureInfo way for WMS - var wmsSource = this.getLayerByName( - "Inland ECDIS chart Danube" - ).data.getSource(); - var url = wmsSource.getGetFeatureInfoUrl( - coordinate, - 100 /* resolution */, - "EPSG:3857", - // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d - { INFO_FORMAT: "text/plain" } - ); - - if (url) { - // cannot directly query here because of SOP - console.log("GetFeatureInfo url:", url); - } - } - }, - 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); - var layer = this.getLayerByName("Bottleneck isolines"); - var wmsSrc = layer.data.getSource(); - - if (bottleneck_id != "does_not_exist") { - wmsSrc.updateParams({ - cql_filter: - "date_info='" + - datestr + - "' AND bottleneck_id='" + - bottleneck_id + - "'" - }); - layer.isVisible = true; - layer.data.setVisible(true); - } else { - layer.isVisible = false; - layer.data.setVisible(false); - } - }, - 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: { - split() { - const map = this.openLayersMap; - this.$nextTick(() => { - 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.long, this.lat], - zoom: this.zoom, - projection: this.projection - }) - }); - this.$store.commit("map/setOpenLayersMap", 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 => { - var features = new GeoJSON().readFeatures(JSON.stringify(response.data)); - var vectorSrc = this.getLayerByName( - "Fairway Dimensions" - ).data.getSource(); - vectorSrc.addFeatures(features); - // 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.openLayersMap.on(["singleclick", "dblclick"], event => { - this.identify(event.coordinate, event.pixel); - }); - } -}; -</script>
--- a/client/src/pdftool/Pdftool.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -<template> - <div :class="['box ui-element rounded bg-white mb-auto text-nowrap', { expanded: showPdfTool }]"> - <div style="width: 15rem"> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> - <i class="fa fa-file-pdf-o mr-2"></i> - Generate PDF - <i class="fa fa-times ml-auto" @click="$store.commit('application/showPdfTool', false)"></i> - </h6> - <div class="p-3"> - <b>Chose format:</b> - <select v-model="form.format" class="form-control d-block w-100"> - <option>landscape</option> - <option>portrait</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">Download</label> - <input - type="radio" - id="pdfexport-downloadtype-open" - value="open" - v-model="form.downloadType" - > - <label for="pdfexport-downloadtype-open" class="ml-1">Open in new window</label> - </small> - <button - @click="download" - type="button" - class="btn btn-sm btn-info d-block w-100" - >Generate PDF</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/router.js Wed Nov 21 15:07:39 2018 +0100 +++ b/client/src/router.js Thu Nov 22 07:07:12 2018 +0100 @@ -15,19 +15,17 @@ import Vue from "vue"; import Router from "vue-router"; import store from "./store"; -import { - sessionStillActive, - toMillisFromString -} from "./application/lib/session"; +import { sessionStillActive, toMillisFromString } from "./lib/session"; /* facilitate codesplitting */ -const Login = () => import("./login/Login.vue"); -const Main = () => import("./application/Main.vue"); -const Usermanagement = () => import("./usermanagement/Usermanagement.vue"); -const Logs = () => import("./logs/logs.vue"); -const Importqueue = () => import("./imports/Importqueue.vue"); +const Login = () => import("./components/Login.vue"); +const Main = () => import("./components/map/Main.vue"); +const Usermanagement = () => + import("./components/admin/usermanagement/Usermanagement.vue"); +const Logs = () => import("./components/admin/logs.vue"); +const Importqueue = () => import("./components/map/imports/Importqueue.vue"); const Systemconfiguration = () => - import("./systemconfiguration/systemconfiguration.vue"); + import("./components/admin/systemconfiguration.vue"); Vue.use(Router);
--- a/client/src/staging/Staging.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,148 +0,0 @@ -<template> - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center bg-info text-white"> - <i class="fa fa-list-ol mr-2"></i> - Staging Area - </h6> - <table class="table mb-0"> - <thead> - <tr> - <th>Name</th> - <th>Datatype</th> - <th>Importdate</th> - <th>ImportID</th> - <th> </th> - </tr> - </thead> - <tbody v-if="filteredData.length"> - <tr v-for="data in filteredData" :key="data.id"> - <td> - <a @click="zoomTo(data.location)" href="#">{{ data.name }}</a> - </td> - <td>{{ data.type }}</td> - <td>{{ data.date }}</td> - <td>{{ data.importID }}</td> - <td> - <button class="btn btn-outline-info"> - <i class="fa fa-thumbs-up"></i> - </button> - - <button class="btn btn-outline-info"> - <i class="fa fa-thumbs-down"></i> - </button> - </td> - </tr> - </tbody> - <tbody v-else> - <tr> - <td class="text-center" colspan="6">No results.</td> - </tr> - </tbody> - </table> - <div class="p-3" v-if="filteredData.length"> - <button class="btn btn-info">Confirm</button> - </div> - </div> -</template> - -<script> -import { mapState } from "vuex"; - -const demodata = [ - { - id: 1, - name: "B1", - date: "2018-11-19 10:23", - location: [16.5364, 48.1471], - status: "Not approved", - importID: "123456789", - type: "bottleneck" - }, - { - id: 2, - name: "B2", - date: "2018-11-19 10:24", - location: [16.5364, 48.1472], - status: "Not approved", - importID: "123456789", - type: "bottleneck" - }, - { - id: 3, - name: "s1", - date: "2018-11-13 10:25", - location: [16.5364, 48.1473], - status: "Not approved", - importID: "987654321", - type: "soundingresult" - }, - { - id: 4, - name: "s2", - date: "2018-11-13 10:26", - location: [16.5364, 48.1474], - status: "Not approved", - importID: "987654321", - type: "soundingresult" - } -]; - -export default { - computed: { - ...mapState("application", ["searchQuery"]), - filteredData() { - return demodata.filter(data => { - const nameFound = data.name - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const dateFound = data.date - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const locationFound = data.location.find(coord => { - return coord - .toString() - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - }); - const statusFound = data.status - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const importIDFound = data.importID - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - const typeFound = data.type - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - - return ( - nameFound || - dateFound || - locationFound || - statusFound || - importIDFound || - typeFound - ); - }); - } - }, - methods: { - zoomTo(coordinates) { - this.$store.commit("map/moveMap", { - coordinates: coordinates, - zoom: 17, - preventZoomOut: true - }); - } - } -}; -</script> - -<style lang="sass" scoped> -.table th, -td - font-size: 0.9rem - border-top: 0px !important - border-bottom-width: 1px - text-align: left - padding: 0.5rem !important -</style>
--- a/client/src/store.js Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,39 +0,0 @@ -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ - -import Vue from "vue"; -import Vuex from "vuex"; -import application from "./store/application"; -import user from "./store/user"; -import usermanagement from "./store/usermanagement"; -import map from "./store/map"; -import fairwayprofile from "./store/fairway"; -import bottlenecks from "./store/bottlenecks"; -import imports from "./store/imports"; - -Vue.use(Vuex); - -export default new Vuex.Store({ - modules: { - application, - fairwayprofile, - imports, - bottlenecks, - map, - user, - usermanagement - } -});
--- a/client/src/store/bottlenecks.js Wed Nov 21 15:07:39 2018 +0100 +++ b/client/src/store/bottlenecks.js Thu Nov 22 07:07:12 2018 +0100 @@ -12,9 +12,9 @@ * Author(s): * Markus Kottländer <markuks.kottlaender@intevation.de> */ -import { HTTP } from "../application/lib/http"; +import { HTTP } from "../lib/http"; import { WFS } from "ol/format.js"; -import { displayError } from "../application/lib/errors.js"; +import { displayError } from "../lib/errors.js"; export default { namespaced: true,
--- a/client/src/store/fairway.js Wed Nov 21 15:07:39 2018 +0100 +++ b/client/src/store/fairway.js Thu Nov 22 07:07:12 2018 +0100 @@ -14,13 +14,13 @@ * Markus Kottländer <markuks.kottlaender@intevation.de> */ import Vue from "vue"; -import { HTTP } from "../application/lib/http"; -import { prepareProfile } from "../application/lib/geo"; +import { HTTP } from "../lib/http"; +import { prepareProfile } from "../lib/geo"; import LineString from "ol/geom/LineString.js"; -import { generateFeatureRequest } from "../application/lib/geo.js"; +import { generateFeatureRequest } from "../lib/geo.js"; import { getLength } from "ol/sphere.js"; -import { calculateFairwayCoordinates } from "../application/lib/geo.js"; -import { displayError } from "../application/lib/errors.js"; +import { calculateFairwayCoordinates } from "../lib/geo.js"; +import { displayError } from "../lib/errors.js"; const DEMOLEVEL = 149.345; const DEMODATA = 2.5;
--- a/client/src/store/imports.js Wed Nov 21 15:07:39 2018 +0100 +++ b/client/src/store/imports.js Thu Nov 22 07:07:12 2018 +0100 @@ -13,7 +13,7 @@ * Thomas Junk <thomas.junk@intevation.de> */ -import { HTTP } from "../application/lib/http"; +import { HTTP } from "../lib/http"; const Imports = { namespaced: true,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/store/index.js Thu Nov 22 07:07:12 2018 +0100 @@ -0,0 +1,39 @@ +/* + * 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 Vue from "vue"; +import Vuex from "vuex"; +import application from "./application"; +import user from "./user"; +import usermanagement from "./usermanagement"; +import map from "./map"; +import fairwayprofile from "./fairway"; +import bottlenecks from "./bottlenecks"; +import imports from "./imports"; + +Vue.use(Vuex); + +export default new Vuex.Store({ + modules: { + application, + fairwayprofile, + imports, + bottlenecks, + map, + user, + usermanagement + } +});
--- a/client/src/store/map.js Wed Nov 21 15:07:39 2018 +0100 +++ b/client/src/store/map.js Thu Nov 22 07:07:12 2018 +0100 @@ -29,7 +29,7 @@ import VectorSource from "ol/source/Vector.js"; import Point from "ol/geom/Point.js"; import { bbox as bboxStrategy } from "ol/loadingstrategy"; -import { HTTP } from "../application/lib/http"; +import { HTTP } from "../lib/http"; import { fromLonLat } from "ol/proj"; export default { @@ -263,7 +263,7 @@ geometry: new Point(end), image: new Icon({ // we need to make sure the image is loaded by Vue Loader - src: require("../application/assets/linestring_arrow.png"), + src: require("../assets/linestring_arrow.png"), // fiddling with the anchor's y value does not help to // position the image more centered on the line ending, as the // default line style seems to be slightly uncentered in the @@ -312,7 +312,7 @@ geometry: new Point(end), image: new Icon({ // we need to make sure the image is loaded by Vue Loader - src: require("../application/assets/linestring_arrow_grey.png"), + src: require("../assets/linestring_arrow_grey.png"), // fiddling with the anchor's y value does not help to // position the image more centered on the line ending, as the // default line style seems to be slightly uncentered in the
--- a/client/src/store/user.js Wed Nov 21 15:07:39 2018 +0100 +++ b/client/src/store/user.js Thu Nov 22 07:07:12 2018 +0100 @@ -13,7 +13,7 @@ * Thomas Junk <thomas.junk@intevation.de> */ -import { HTTP } from "../application/lib/http"; +import { HTTP } from "../lib/http"; export default { namespaced: true,
--- a/client/src/store/usermanagement.js Wed Nov 21 15:07:39 2018 +0100 +++ b/client/src/store/usermanagement.js Thu Nov 22 07:07:12 2018 +0100 @@ -13,7 +13,7 @@ * Thomas Junk <thomas.junk@intevation.de> */ -import { HTTP } from "../application/lib/http"; +import { HTTP } from "../lib/http"; const newUser = () => { return {
--- a/client/src/systemconfiguration/systemconfiguration.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,144 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <div class="card sysconfig mt-3 mx-auto"> - <div class="card-header shadow-sm text-white bg-info mb-6"> - Systemconfiguration - </div> - <div class="card-body config"> - <section class="configsection"> - <h4 class="card-title">Bottleneck Areas stroke-color</h4> - <compact-picker v-model="strokeColor" /> - </section> - <section> - <h4 class="card-title">Bottleneck Areas fill-color</h4> - <chrome-picker v-model="fillColor" /> - </section> - <div class="sendbutton"> - <a @click.prevent="submit" class="btn btn-info">Send</a> - </div> - </div> <!-- card-body --> - </div> - </div> -</template> - -<style scoped lang="sass"> -.config - text-align: left - -.configsection - margin-bottom: $large-offset - -.sendbutton - position: absolute - right: $offset - bottom: $offset - -.inputs - margin-left: auto - margin-right: auto - -.sysconfig - width: 30vw -</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 { Chrome } from "vue-color"; -import { Compact } from "vue-color"; - -import { HTTP } from "../application/lib/http"; -import { displayError } from "../application/lib/errors.js"; -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 }, - 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: "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: "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: "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: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - } -}; -</script>
--- a/client/src/toolbar/Toolbar.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -<template> - <div class="ml-2"> - <div :class="'toolbar toolbar-' + (expandToolbar ? 'expanded' : 'collapsed')"> - <Identify></Identify> - <Layers></Layers> - <Cuttool></Cuttool> - <Linetool></Linetool> - <Polygontool></Polygontool> - <Pdftool></Pdftool> - </div> - <div @click="$store.commit('application/expandToolbar', !expandToolbar)" class="toolbar-button bg-info text-white"> - <i :class="'fa fa-angle-' + (expandToolbar ? 'up' : 'down')"></i> - </div> - </div> -</template> - -<style lang="sass"> -// not scoped to affect nested components -// doen't work when put in application/assets/application.sass... why??? o_O -.toolbar - overflow: hidden - transition: max-height 0.4s - -.toolbar-collapsed - max-height: (3 * $icon-height) + (3 * $offset) - -.toolbar-expanded - max-height: 100% - -.toolbar-button - opacity: $slight-transparent - color: #666 - height: $icon-width - width: $icon-height - align-items: center - justify-content: center - display: flex - background: #fff - margin-bottom: $offset - border-radius: $border-radius - box-shadow: $shadow-xs - z-index: 2 - pointer-events: auto - .inverted - color: $color-info - .grey - color: #ddd -</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("./buttons/Identify.vue"), - Layers: () => import("./buttons/Layers.vue"), - Linetool: () => import("./buttons/Linetool.vue"), - Polygontool: () => import("./buttons/Polygontool.vue"), - Cuttool: () => import("./buttons/Cuttool.vue"), - Pdftool: () => import("./buttons/Pdftool.vue") - }, - computed: { - ...mapGetters("map", ["getLayerByName"]), - ...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.getLayerByName("Draw Tool") - .data.getSource() - .clear(); - } - }); - } -}; -</script>
--- a/client/src/toolbar/buttons/Cuttool.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -<template> - <div @click="toggleCutTool" class="toolbar-button"> - <i :class="['fa fa-area-chart', { inverted: cutTool && cutTool.getActive(), grey: !selectedSurvey }]"></i> - </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"; -import Draw from "ol/interaction/Draw.js"; -import { Stroke, Style, Circle, Fill } from "ol/style.js"; - -export default { - name: "cuttool", - computed: { - ...mapGetters("map", ["getLayerByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"]), - ...mapState("bottlenecks", ["selectedSurvey"]) - }, - methods: { - toggleCutTool() { - console.log(this.selectedSurvey); - if (this.selectedSurvey) { - this.cutTool.setActive(!this.cutTool.getActive()); - this.lineTool.setActive(false); - this.polygonTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - } - }, - cutEnd(event) { - this.$store.dispatch("fairwayprofile/cut", event.feature); - } - }, - created() { - if (!this.cutTool) { - const cutVectorSrc = this.getLayerByName("Cut Tool").data.getSource(); - const cutTool = new Draw({ - source: cutVectorSrc, - type: "LineString", - maxPoints: 2, - style: new Style({ - stroke: new Stroke({ - color: "#444", - width: 2, - lineDash: [7, 7] - }), - image: new Circle({ - fill: new Fill({ color: "#333" }), - stroke: new Stroke({ color: "#fff", width: 1.5 }), - radius: 6 - }) - }) - }); - cutTool.setActive(false); - cutTool.on("drawstart", () => { - cutVectorSrc.clear(); - }); - cutTool.on("drawend", this.cutEnd); - this.$store.commit("map/cutTool", cutTool); - this.openLayersMap.addInteraction(cutTool); - } - } -}; -</script>
--- a/client/src/toolbar/buttons/Identify.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -<template> - <div @click="$store.commit('application/showIdentify', !showIdentify)" class="toolbar-button"> - <i :class="['fa fa-info', {inverted: showIdentify}]"></i> - </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"]) - } -}; -</script>
--- a/client/src/toolbar/buttons/Layers.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -<template> - <div @click="$store.commit('application/showLayers', !showLayers)" class="toolbar-button"> - <i :class="['fa fa-th-list', {inverted: showLayers}]"></i> - </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/toolbar/buttons/Linetool.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,70 +0,0 @@ -<template> - <div @click="toggleLineTool" class="toolbar-button"> - <i :class="['fa fa-pencil', {inverted: lineTool && lineTool.getActive()}]"></i> - </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"; -import { getLength } from "ol/sphere.js"; -import Draw from "ol/interaction/Draw.js"; - -export default { - name: "linetool", - computed: { - ...mapGetters("map", ["getLayerByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"]) - }, - methods: { - toggleLineTool() { - this.lineTool.setActive(!this.lineTool.getActive()); - this.polygonTool.setActive(false); - this.cutTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - this.getLayerByName("Draw Tool") - .data.getSource() - .clear(); - }, - lineEnd(event) { - const length = getLength(event.feature.getGeometry()); - this.$store.commit("map/setCurrentMeasurement", { - quantity: "Length", - unitSymbol: "m", - value: Math.round(length * 10) / 10 - }); - this.$store.commit("application/showIdentify", true); - } - }, - created() { - if (!this.lineTool) { - const drawVectorSrc = this.getLayerByName("Draw Tool").data.getSource(); - const lineTool = new Draw({ - source: drawVectorSrc, - type: "LineString", - maxPoints: 2 - }); - lineTool.setActive(false); - lineTool.on("drawstart", () => { - drawVectorSrc.clear(); - this.$store.commit("map/setCurrentMeasurement", null); - }); - lineTool.on("drawend", this.lineEnd); - this.$store.commit("map/lineTool", lineTool); - this.openLayersMap.addInteraction(lineTool); - } - } -}; -</script>
--- a/client/src/toolbar/buttons/Pdftool.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -<template> - <div @click="$store.commit('application/showPdfTool', !showPdfTool)" class="toolbar-button"> - <i :class="['fa fa-file-pdf-o', {inverted: showPdfTool}]"></i> - </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/toolbar/buttons/Polygontool.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -<template> - <div @click="togglePolygonTool" class="toolbar-button"> - <i :class="['fa fa-edit', {inverted: polygonTool && polygonTool.getActive()}]"></i> - </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"; -import { getArea } from "ol/sphere.js"; -import Draw from "ol/interaction/Draw.js"; - -export default { - name: "polygontool", - computed: { - ...mapGetters("map", ["getLayerByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool", "openLayersMap"]) - }, - methods: { - togglePolygonTool() { - this.polygonTool.setActive(!this.polygonTool.getActive()); - this.lineTool.setActive(false); - this.cutTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - this.getLayerByName("Draw Tool") - .data.getSource() - .clear(); - }, - polygonEnd(event) { - const areaSize = getArea(event.feature.getGeometry()); - this.$store.commit("map/setCurrentMeasurement", { - quantity: "Area", - unitSymbol: areaSize > 100000 ? "km²" : "m²", - value: - areaSize > 100000 - ? Math.round(areaSize / 1000) / 1000 // convert into 1 km² == 1000*1000 m² and round to 1000 m² - : Math.round(areaSize) - }); - this.$store.commit("application/showIdentify", true); - } - }, - created() { - if (!this.polygonTool) { - const drawVectorSrc = this.getLayerByName("Draw Tool").data.getSource(); - const polygonTool = new Draw({ - source: drawVectorSrc, - type: "Polygon", - maxPoints: 50 - }); - polygonTool.setActive(false); - polygonTool.on("drawstart", () => { - drawVectorSrc.clear(); - this.$store.commit("map/setCurrentMeasurement", null); - }); - polygonTool.on("drawend", this.polygonEnd); - this.$store.commit("map/polygonTool", polygonTool); - this.openLayersMap.addInteraction(polygonTool); - } - } -}; -</script>
--- a/client/src/translations.json Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -{ - "en_GB": { - "Enter username": "Enter username", - "Enter passphrase": "Enter passphrase", - "Login failed": "Login failed", - "Login": "Login", - "Forgot password": "Forgot password" - }, - "de_AT": { - "Enter username": "Benutzername", - "Enter passphrase": "Passphrase", - "Login failed": "Login fehlgeschlagen", - "Login": "Login", - "Forgot password": "Passwort vergessen" - } -}
--- a/client/src/usermanagement/Passwordfield.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -<template> - <div> - <label for="password">{{this.label}}</label> - <div class="d-flex d-row"> - <input :type="isPasswordVisible" @change="fieldChanged" class="form-control" :placeholder='placeholder' :required="required"> - <span class="input-group-text" @click="showPassword"><i :class="eyeIcon"></i></span> - </div> - <div v-show="passworderrors" class="text-danger"><small><i class="fa fa-warning"></i> {{ 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"; - }, - eyeIcon() { - return { - fa: true, - "fa-eye": !this.readablePassword, - "fa-eye-slash": this.readablePassword - }; - } - } -}; -</script>
--- a/client/src/usermanagement/Userdetail.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,298 +0,0 @@ -<template> - <div class="userdetails h-100 mt-3 mr-auto shadow fadeIn animated"> - <div class="card"> - <div class="card-header shadow-sm text-white bg-info mb-3"> - {{ this.cardHeader }} - <span @click="closeDetailview" class="pull-right"> - <i class="fa fa-close"></i> - </span> - </div> - <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">Username</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> - <i class="fa fa-warning"></i> {{ errors.user }}</small> - </div> - </div> - <div class="form-group row"> - <label for="country">Country</label> - <select class="form-control form-control-sm" v-on:change="validateCountry" v-model="currentUser.country"> - <option disabled value="">Please select one</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> - <i class="fa fa-warning"></i> {{ errors.country }}</small> - </div> - </div> - <div class="form-group row"> - <label for="email">Email address</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> - <i class="fa fa-warning"></i> {{ errors.email }}</small> - </div> - </div> - <div class="form-group row"> - <label for="role">Role</label> - <select class="form-control form-control-sm" v-on:change="validateRole" v-model="currentUser.role"> - <option disabled value="">Please select one</option> - <option value="sys_admin">Sysadmin</option> - <option value="waterway_admin">Waterway Admin</option> - <option value="waterway_user">Waterway User</option> - </select> - <div v-show="errors.role" class="text-danger"> - <small> - <i class="fa fa-warning"></i> {{ errors.role }}</small> - </div> - </div> - <div class="form-group row"> - <PasswordField @fieldchange="passwordChanged" :placeholder="passwordPlaceholder" :label="passwordLabel" :passworderrors="errors.password"></PasswordField> - </div> - <div class="form-group row"> - <PasswordField @fieldchange="passwordReChanged" :placeholder="passwordRePlaceholder" :label="passwordReLabel" :passworderrors="errors.passwordre"></PasswordField> - </div> - </div> - <div> - <button type="submit" :disabled="submitted" class="shadow-sm btn btn-info pull-right">Submit</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"><i class="fa fa-telegram"> Send testmail</i></a> - <div v-if="mailsent">Mail was sent</div> - </div> - </form> - </div> - </div> - </div> -</template> - -<style lang="sass" scoped> -.mailbutton - width: 12vw - -.formfields - width: 10vw - -.userdetails - min-width: 40vw - -form - font-size: $smaller -</style> - -<script> -/* - * This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { HTTP } from "../application/lib/http"; -import { displayError } from "../application/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: "Password", - passwordReLabel: "Repeat Password", - passwordPlaceholder: "password", - passwordRePlaceholder: "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: "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 - ? "" - : "Please choose a country"; - }, - validateRole() { - this.errors.role = this.currentUser.role ? "" : "Please choose a role"; - }, - validatePassword() { - this.errors.passwordre = - this.password === this.passwordre ? "" : "Passwords do not match!"; - this.errors.password = - this.password === "" || !violatedPasswordRules(this.password) - ? "" - : "Password should at least be 8 char long including 1 digit and 1 special char like $"; - }, - validateEmailaddress() { - this.errors.email = isEmailValid(this.currentUser.email) - ? "" - : "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: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - this.submitted = false; - const { status, data } = error.response; - displayError({ - title: "Error while saving user", - message: `${status}: ${data.message || data}` - }); - }); - } - } -}; -</script>
--- a/client/src/usermanagement/Usermanagement.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,302 +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"> - <div class="card-header shadow-sm text-white bg-info mb-3"> - Users - </div> - <div class="card-body"> - <table id="datatable" :class="tableStyle"> - <thead> - <tr> - <th scope="col" @click="sortBy('user')"> - <span>Username - <i v-if="sortCriterion=='user'" class="fa fa-angle-down"></i> - </span> - </th> - <th scope="col" @click="sortBy('country')"> - <span>Country - <i v-if="sortCriterion=='country'" class="fa fa-angle-down"></i> - </span> - </th> - <th scope="col" @click="sortBy('email')"> - <span>Email - <i v-if="sortCriterion=='email'" class="fa fa-angle-down"></i> - </span> - </th> - <th scope="col" @click="sortBy('role')"> - <span>Role - <i v-if="sortCriterion=='role'" class="fa fa-angle-down"></i> - </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> - <i v-tooltip="user.roleLabel" :class="{ - fa:true, - icon:true, - 'fa-user':user.role==='waterway_user', - 'fa-star':user.role=='sys_admin', - 'fa-adn':user.role==='waterway_admin'}"></i> - </td> - <td> - <i @click="deleteUser(user.user)" class="icon fa fa-trash-o"></i> - </td> - </tr> - </tbody> - </table> - </div> - <div class="d-flex flex-row pagination"> - <i @click=" prevPage " v-if="this.currentPage!=1 " class="mr-2 btn btn-sm btn-light align-self-center pages fa fa-caret-left "></i> {{this.currentPage}} / {{this.pages}} - <i @click="nextPage " class="ml-2 btn btn-sm btn-light align-self-center pages fa fa-caret-right "></i> - </div> - <div class="mr-3 pb-3"> - <button @click="addUser " class="btn btn-info pull-right shadow-sm ">Add User</button> - </div> - </div> - </div> - <Userdetail v-if="isUserDetailsVisible "></Userdetail> - </div> - </div> - </div> -</template> - -<style scoped lang="sass"> -@import "../application/assets/tooltip.sass" - -.spacer - height: 100vh - -.spacer-collapsed - min-width: $icon-width + $offset - transition: $transition-fast - -@media screen and (min-width: 600px) - .spacer-expanded - min-width: $icon-width + $offset - -@media screen and (max-width: 1650px) - .spacer-expanded - min-width: $sidebar-width + $offset - -.main - height: 100vh - -@media screen and (min-width: 600px) - .content - margin-left: $sidebar-width - margin-right: auto - -@media screen and (min-width: 1650px) - .content - margin-left: $sidebar-width - margin-right: auto - -.icon - font-size: large - -.userlist - min-width: 520px - height: 100% - -.pagination - margin-left: auto - margin-right: auto - -.userlistsmall - width: 30vw - -.userlistextended - width: 70vw - -.table - width: 90% !important - margin: auto - -.table th, -.pages - 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 "../application/lib/errors.js"; - -export default { - name: "userview", - data() { - return { - sortCriterion: "user", - pageSize: 10, - currentPage: 1 - }; - }, - components: { - Userdetail - }, - computed: { - ...mapGetters("usermanagement", ["isUserDetailsVisible"]), - ...mapState("application", ["showSidebar", "showUsermenu"]), - spacerStyle() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showUsermenu && this.showSidebar, - "spacer-collapsed": !this.showUsermenu && 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", - { - 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: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "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); - } - }, - beforeRouteEnter(to, from, next) { - store - .dispatch("usermanagement/loadUsers") - .then(next) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data}` - }); - }); - }, - beforeRouteLeave(to, from, next) { - store.commit("usermanagement/clearCurrentUser"); - store.commit("usermanagement/setUserDetailsInvisible"); - next(); - } -}; -</script>
--- a/client/src/usermanagement/Users.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,279 +0,0 @@ -<template> - <div class="main d-flex flex-column"> - <div class="d-flex content flex-column"> - <div class="d-flex flex-row"> - <div :class="userlistStyle"> - <div class="card"> - <div class="card-header shadow-sm text-white bg-info mb-3"> - Users - </div> - <div class="card-body"> - <table id="datatable" :class="tableStyle"> - <thead> - <tr> - <th scope="col" @click="sortBy('user')"> - <span>Username - <i v-if="sortCriterion=='user'" class="fa fa-angle-down"></i> - </span> - </th> - <th scope="col" @click="sortBy('country')"> - <span>Country - <i v-if="sortCriterion=='country'" class="fa fa-angle-down"></i> - </span> - </th> - <th scope="col" @click="sortBy('email')"> - <span>Email - <i v-if="sortCriterion=='email'" class="fa fa-angle-down"></i> - </span> - </th> - <th scope="col" @click="sortBy('role')"> - <span>Role - <i v-if="sortCriterion=='role'" class="fa fa-angle-down"></i> - </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> - <i v-tooltip="user.roleLabel" :class="{ - fa:true, - icon:true, - 'fa-user':user.role==='waterway_user', - 'fa-star':user.role=='sys_admin', - 'fa-adn':user.role==='waterway_admin'}"></i> - </td> - <td> - <i @click="deleteUser(user.user)" class="icon fa fa-trash-o"></i> - </td> - </tr> - </tbody> - </table> - </div> - <div class="d-flex flex-row pagination"> - <i @click=" prevPage " v-if="this.currentPage!=1 " class="backwards btn btn-sm btn-light align-self-center pages fa fa-caret-left "></i> {{this.currentPage}} / {{this.pages}} - <i @click="nextPage " class="forwards btn btn-sm btn-light align-self-center pages fa fa-caret-right "></i> - </div> - <div class="mr-3 pb-3 "> - <button @click="addUser " class="btn btn-info pull-right shadow-sm ">Add User</button> - </div> - </div> - </div> - <Userdetail v-if="isUserDetailsVisible "></Userdetail> - </div> - </div> - </div> -</template> - -<style lang="sass" scoped> -@import "../application/assets/tooltip.sass" - -.main - height: 100vh - -.backwards - margin-right: 0.5rem - -.forwards - margin-left: 0.5rem - -.content - margin-top: $large-offset - margin-left: auto - margin-right: auto - -.icon - font-size: large - -.userlist - margin-top: $topbarheight - min-width: 520px - height: 100% - -.pagination - margin-left: auto - margin-right: auto - -.userlistsmall - width: 30vw - -.userlistextended - width: 70vw - -.table - width: 90% !important - margin: auto - -.table th, -.pages - cursor: pointer - -.table th, -td - font-size: 0.9rem - border-top: 0px !important - text-align: left - padding: 0.5rem !important - -.table td - font-size: 0.9rem - 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 } from "vuex"; -import { displayError } from "../application/lib/errors.js"; - -export default { - name: "userview", - data() { - return { - sortCriterion: "user", - pageSize: 10, - currentPage: 1 - }; - }, - components: { - Userdetail - }, - computed: { - ...mapGetters("usermanagement", ["isUserDetailsVisible"]), - ...mapGetters("application", ["sidebarCollapsed"]), - 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 mr-3 shadow", - { - 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: "Backend Error", - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "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); - } - }, - beforeRouteEnter(to, from, next) { - store - .dispatch("usermanagement/loadUsers") - .then(next) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status}: ${data}` - }); - }); - }, - beforeRouteLeave(to, from, next) { - store.commit("usermanagement/clearCurrentUser"); - store.commit("usermanagement/setUserDetailsInvisible"); - next(); - } -}; -</script>
--- a/client/src/zoom/zoom.vue Wed Nov 21 15:07:39 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +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 border-right" @click="zoomIn"> - <i class="fa fa-plus"></i> - </button> - <button class="zoomButton border-0 bg-white rounded-right ui-element" @click="zoomOut"> - <i class="fa fa-minus"></i> - </button> - </div> -</template> - -<style lang="sass" scoped> -.buttoncontainer - bottom: 0 - left: 50% - margin-left: -$icon-width - -.zoomButton - min-height: $icon-width - min-width: $icon-width - z-index: 2 - outline: none - color: #666 -</style> -<script> -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/vue.config.js Wed Nov 21 15:07:39 2018 +0100 +++ b/client/vue.config.js Thu Nov 22 07:07:12 2018 +0100 @@ -28,7 +28,7 @@ // pass options to sass-loader sass: { // @/ is an alias to src/ - data: `@import "@/application/assets/application.sass";` + data: `@import "@/assets/application.sass";` } } },