Mercurial > gemma
changeset 3567:2b002b042499
translations: merge translations from weblate
author | Fadi Abbud <fadi.abbud@intevation.de> |
---|---|
date | Mon, 03 Jun 2019 10:19:18 +0200 |
parents | 4c585b5d4fe8 (diff) a363d1529cf8 (current diff) |
children | c646bb821b69 |
files | |
diffstat | 215 files changed, 21279 insertions(+), 11012 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgignore Wed May 29 10:58:45 2019 +0200 +++ b/.hgignore Mon Jun 03 10:19:18 2019 +0200 @@ -108,4 +108,5 @@ # Import data schema/geonames-import/data/* -translations.json \ No newline at end of file +translations.json +pub-config.json
--- a/client/.env Wed May 29 10:58:45 2019 +0200 +++ b/client/.env Mon Jun 03 10:19:18 2019 +0200 @@ -11,3 +11,7 @@ #Logos to be potentially loaded by the SPA. Can be left blank. VUE_APP_SECONDARY_LOGO_URL= VUE_APP_LOGO_FOR_PDF_URL= +VUE_APP_SILENCE_TRANSLATIONWARNINGS = + +#Url of user manual +VUE_APP_USER_MANUAL_URL= \ No newline at end of file
--- a/client/package.json Wed May 29 10:58:45 2019 +0200 +++ b/client/package.json Mon Jun 03 10:19:18 2019 +0200 @@ -37,6 +37,7 @@ "d3-line-chunked": "^1.4.1", "date-fns": "^1.30.1", "debounce": "^1.2.0", + "file-saver": "^2.0.2", "glob-all": "^3.1.0", "jspdf": "^1.5.3", "locale2": "^2.2.0",
--- a/client/public/index.html Wed May 29 10:58:45 2019 +0200 +++ b/client/public/index.html Mon Jun 03 10:19:18 2019 +0200 @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later License-Filename: LICENSES/AGPL-3.0.txt - Copyright (C) 2018 by via donau + Copyright (C) 2018, 2019 by via donau – Österreichische Wasserstraßen-Gesellschaft mbH Software engineering by Intevation GmbH -->
--- a/client/src/assets/application.scss Wed May 29 10:58:45 2019 +0200 +++ b/client/src/assets/application.scss Mon Jun 03 10:19:18 2019 +0200 @@ -105,6 +105,22 @@ font-weight: bold; } +.box-control { + display: flex; + color: #888; + padding: 4px 7px; + border-radius: 0.25rem; + cursor: pointer; + transition: background-color 0.3s, color 0.3s; + &.small { + padding: 4px; + } + &:hover { + color: #666; + background-color: #eee; + } +} + .expanded { max-height: 999px; max-width: 999px; @@ -183,16 +199,12 @@ select.form-control-sm.small { padding: 0.25rem 0.1rem; - font-size: 80%; + font-size: 0.75rem; } input.form-control-sm.small { padding: 0.25rem 0.2rem; - font-size: 80%; -} - -.empty { - margin-right: 1.25rem; + font-size: 0.75rem; } .truncate { @@ -201,15 +213,12 @@ text-overflow: ellipsis; } -.loading { - background: rgba(255, 255, 255, 0.9); - position: absolute; - z-index: 99; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: flex; - align-items: center; - justify-content: center; +.wh-100 { + width: 100% !important; + height: 100% !important; } + +.wh-50 { + width: 50% !important; + height: 50% !important; +}
--- a/client/src/assets/tooltip.scss Wed May 29 10:58:45 2019 +0200 +++ b/client/src/assets/tooltip.scss Mon Jun 03 10:19:18 2019 +0200 @@ -17,10 +17,12 @@ } .tooltip .tooltip-inner { - background: black; - color: white; - border-radius: 16px; + background: white; + box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2); + color: #666; + border-radius: 0.25rem; padding: 5px 10px 4px; + font-size: 0.8rem; } .tooltip .tooltip-arrow { @@ -29,7 +31,7 @@ border-style: solid; position: absolute; margin: 5px; - border-color: black; + border-color: white; z-index: 1; } @@ -95,7 +97,7 @@ .tooltip.popover .popover-inner { background: #f9f9f9; - color: black; + color: white; padding: 24px; border-radius: 5px; box-shadow: 0 5px 30px rgba(black, 0.1);
--- a/client/src/components/App.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/App.vue Mon Jun 03 10:19:18 2019 +0200 @@ -3,7 +3,7 @@ <div v-if="isAuthenticated" class="d-flex flex-column userinterface"> <div class="boxes d-flex p-2"> <div class="mr-auto d-flex"> - <Sidebar :routeName="routeName" /> + <Sidebar /> <div :class="searchContainer"> <Search v-if="isMapVisible" /> <Contextbox v-if="isMapVisible" /> @@ -14,6 +14,7 @@ <Profiles v-if="isMapVisible" /> <Gauges v-if="isMapVisible" /> <Pdftool v-if="isMapVisible" /> + <AvailableFairwayDepthDialogue v-if="isMapVisible" /> </div> <div class="d-flex flex-column align-items-end"> <Identify v-if="isMapVisible" /> @@ -22,13 +23,12 @@ <Toolbar v-if="isMapVisible" /> </div> </div> - <Zoom v-if="isMapVisible" /> - <Splitscreen v-if="isMapVisible" /> - <MinimizedSplitscreens v-if="isMapVisible" /> + <MapPopup /> </div> <router-view /> <vue-snotify /> <Popup /> + <KeyboardHandler /> </div> </template> @@ -79,14 +79,10 @@ ...mapState("user", ["isAuthenticated"]), ...mapState("application", ["contextBoxContent", "showSearchbar"]), isMapVisible() { - return /importoverview|stretches|review|bottlenecks|mainview/.test( - this.routeName + return /importconfiguration|importoverview|stretches|sections|review|bottlenecks|mainview/.test( + this.$route.name ); }, - routeName() { - const routeName = this.$route.name; - return routeName; - }, searchContainer() { return [ "ml-2", @@ -100,7 +96,6 @@ Profiles: () => import("./fairway/Profiles"), Gauges: () => import("./gauge/Gauges"), Pdftool: () => import("./Pdftool"), - Zoom: () => import("./Zoom"), Identify: () => import("./identify/Identify"), Layers: () => import("./layers/Layers"), Sidebar: () => import("./Sidebar"), @@ -108,8 +103,10 @@ Contextbox: () => import("./Contextbox"), Toolbar: () => import("./toolbar/Toolbar"), Popup: () => import("./Popup"), - Splitscreen: () => import("./splitscreen/Splitscreen"), - MinimizedSplitscreens: () => import("./splitscreen/MinimizedSplitscreens") + AvailableFairwayDepthDialogue: () => + import("./fairway/AvailableFairwayDepthDialogue.vue"), + MapPopup: () => import("./map/MapPopup"), + KeyboardHandler: () => import("./KeyboardHandler") } }; </script>
--- a/client/src/components/Bottlenecks.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/Bottlenecks.vue Mon Jun 03 10:19:18 2019 +0200 @@ -7,71 +7,55 @@ /> <UITableHeader :columns="[ - { id: 'properties.name', title: `${nameLabel}`, class: 'col-4' }, + { id: 'properties.name', title: `${nameLabel}`, width: '230px' }, + { + id: 'properties.responsible_country', + title: `${countryLabel}`, + width: '100px' + }, { id: 'properties.current', title: `${latestmeasurementLabel}`, - class: 'col-3' + width: '150px' }, - { id: 'properties.from', title: `${chainageLabel}`, class: 'col-3' } + { id: 'properties.from', title: `${chainageLabel}`, width: '135px' } ]" /> <UITableBody :data="filteredBottlenecks() | sortTable(sortColumn, sortDirection)" - :maxHeight="(showSplitscreen ? 18 : 35) + 'rem'" - :active="openBottleneck" - v-slot="{ item: bottleneck }" + maxHeight="35rem" + :isActive="item => item === this.openBottleneck" > - <div class="col-4 py-2 text-left"> - <a href="#" @click="selectBottleneck(bottleneck)">{{ - bottleneck.properties.name - }}</a> - </div> - <div class="col-3 py-2"> - {{ bottleneck.properties.current | surveyDate }} - </div> - <div class="col-3 py-2"> - {{ - displayCurrentChainage( - bottleneck.properties.from, - bottleneck.properties.to - ) - }} - </div> - <div class="col-2 py-2 pr-0 text-right d-flex flex-column"> - <a - class="text-info mt-auto mb-auto mr-2" - @click="loadSurveys(bottleneck)" - v-if="bottleneck.properties.current" - > - <font-awesome-icon - class="pointer" - icon="spinner" - fixed-width - spin - v-if="loading === bottleneck" - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - icon="angle-down" - fixed-width - v-if="loading !== bottleneck && openBottleneck !== bottleneck" - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - icon="angle-up" - fixed-width - v-if="loading !== bottleneck && openBottleneck === bottleneck" - ></font-awesome-icon> - </a> - </div> - <div - :class="[ - 'col-12 p-0', - 'surveys', - { open: openBottleneck === bottleneck } - ]" - > + <template v-slot:row="{ item: bottleneck }"> + <div class="table-cell truncate text-left" style="width: 230px"> + <a href="#" @click="selectBottleneck(bottleneck)">{{ + bottleneck.properties.name + }}</a> + </div> + <div class="table-cell text-center" style="width: 100px"> + {{ bottleneck.properties.responsible_country }} + </div> + <div class="table-cell" style="width: 150px"> + {{ bottleneck.properties.current | surveyDate }} + </div> + <div class="table-cell" style="width: 135px"> + {{ + displayCurrentChainage( + bottleneck.properties.from, + bottleneck.properties.to + ) + }} + </div> + <div class="table-cell center" style="flex-grow: 1"> + <UISpinnerButton + @click="loadSurveys(bottleneck)" + :loading="loading === bottleneck" + :state="bottleneck === openBottleneck" + v-if="bottleneck.properties.current" + /> + </div> + </template> + <template v-slot:expand="{ item: bottleneck }"> <a href="#" class="d-inline-block px-3 py-2" @@ -81,32 +65,11 @@ > {{ survey.date_info | surveyDate }} </a> - </div> + </template> </UITableBody> </div> </template> -<style lang="sass" scoped> -.table-body - .row - > div:not(:last-child) - transition: background-color 0.3s, color 0.3s - &.active - > div:not(:last-child) - background-color: $color-info - color: #fff - a - color: #fff !important - .surveys - border-bottom: solid 1px $color-info - .surveys - overflow: hidden - max-height: 0 - &.open - overflow-y: auto - max-height: 5rem -</style> - <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -138,15 +101,14 @@ }; }, computed: { - ...mapState("application", [ - "searchQuery", - "showSearchbarLastState", - "showSplitscreen" - ]), + ...mapState("application", ["searchQuery", "showSearchbarLastState"]), ...mapState("bottlenecks", ["bottlenecksList"]), bottlenecksLabel() { return this.$gettext("Bottlenecks"); }, + countryLabel() { + return this.$gettext("Country"); + }, nameLabel() { return this.$gettext("Name"); }, @@ -180,7 +142,7 @@ this.$store.commit("bottlenecks/selectedSurvey", survey); }) .then(() => { - this.$store.commit("map/moveToExtent", { + this.$store.dispatch("map/moveToFeauture", { feature: bottleneck, zoom: 17, preventZoomOut: true @@ -194,10 +156,7 @@ bottleneck.properties.name ) .then(() => { - this.$store.commit("bottlenecks/setFirstSurveySelected"); - }) - .then(() => { - this.$store.commit("map/moveToExtent", { + this.$store.dispatch("map/moveToFeauture", { feature: bottleneck, zoom: 17, preventZoomOut: true
--- a/client/src/components/Contextbox.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/Contextbox.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,11 +1,11 @@ <template> <div :class="style"> - <Bottlenecks v-if="contextBoxContent === 'bottlenecks'"></Bottlenecks> - <Staging v-if="contextBoxContent === 'staging'"></Staging> - <Stretches v-if="contextBoxContent === 'stretches'"></Stretches> - <ImportOverview - v-if="contextBoxContent === 'importoverview'" - ></ImportOverview> + <Bottlenecks v-if="contextBoxContent === 'bottlenecks'" /> + <Staging v-if="contextBoxContent === 'staging'" /> + <Stretches v-if="contextBoxContent === 'stretches'" /> + <Sections v-if="contextBoxContent === 'sections'" /> + <ImportOverview v-if="contextBoxContent === 'importoverview'" /> + <ImportConfiguration v-if="contextBoxContent === 'importconfiguration'" /> </div> </template> @@ -29,9 +29,10 @@ name: "contextbox", components: { Bottlenecks: () => import("@/components/Bottlenecks"), - Stretches: () => import("@/components/ImportStretches.vue"), - ImportOverview: () => - import("@/components/importoverview/ImportOverview.vue") + Stretches: () => import("@/components/stretches/Stretches"), + Sections: () => import("@/components/sections/Sections"), + ImportOverview: () => import("@/components/importoverview/ImportOverview"), + ImportConfiguration: () => import("@/components/importconfiguration/Import") }, computed: { ...mapState("application", [ @@ -53,6 +54,7 @@ }, methods: { close() { + this.$store.commit("map/mapPopupEnabled", true); this.$store.commit("application/searchQuery", ""); this.$store.commit("application/showContextBox", false); this.$store.commit( @@ -75,7 +77,7 @@ background: #fff; } .contextbox > div:last-child { - width: 660px; + width: 668px; } .contextboxcollapsed { @@ -84,7 +86,7 @@ } .contextboxextended { - max-width: 660px; + max-width: 668px; } .close-contextbox {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/DiagramLegend.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,75 @@ +<template> + <div :class="['diagram-legend', { collapsed }]"> + <span class="box-control toggle" @click="collapsed = !collapsed"> + <font-awesome-icon icon="angle-left" fixed-width /> + </span> + <div + class="d-flex align-items-center justify-content-center w-100" + style="overflow: hidden" + > + <div class="text-left px-3"> + <slot /> + </div> + </div> + </div> +</template> + +<style lang="sass"> +.diagram-legend + position: relative + width: 180px + font-size: 0.8rem + display: flex + border-right: solid 1px #dee2e6 + .toggle + margin-left: 0 + position: absolute + top: 0.25rem + left: 0.25rem + svg + transition: transform 0.3s + &.collapsed + width: 0 + border-right: none + .toggle + left: 0.25rem + svg + transform: rotateY(180deg) + .legend + margin: 10px 0 + span + vertical-align: middle + display: inline-block + width: 12px + height: 12px + border-radius: 50% +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ + +export default { + data() { + return { + collapsed: false + }; + }, + watch: { + collapsed() { + this.$nextTick(this.$parent.drawDiagram); + } + } +}; +</script>
--- a/client/src/components/ImportApprovedGaugeMeasurement.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,144 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <Spacer></Spacer> - <div class="card sysconfig mt-2 shadow-xs w-100 h-100 mr-2"> - <UIBoxHeader icon="upload" :title="importGaugmeasurmentLabel" /> - <div class="card-body stretches-card"> - <div class="w-95 ml-auto mr-auto mt-4 mb-4"> - <div class="d-flex flex-column text-left w-25"> - <label class="text-nowrap" for="originator"> - <small class="text-muted" - >{{ $options.ORIGINATOR }} / {{ $options.FROM }}</small - > - </label> - <input - type="text" - v-model="originator" - class="form-control" - id="originator" - /> - <span class="text-left text-danger"> - <small v-if="!originator"> - <translate>Please enter an originator</translate> - </small> - </span> - </div> - <div class="mt-4 flex-column w-100"> - <div class="custom-file"> - <input - accept=".csv" - type="file" - @change="fileSelected" - class="custom-file-input" - id="uploadFile" - /> - <label class="pointer custom-file-label" for="uploadFile"> - {{ uploadLabel }} - </label> - </div> - </div> - </div> - <div class="buttons text-right"> - <button - :disabled="disableUploadButton" - @click="submit" - class="btn btn-info mt-4" - type="button" - > - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="play" - ></font-awesome-icon> - <translate>Trigger import</translate> - </button> - </div> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import { HTTP } from "@/lib/http"; -import { displayError, displayInfo } from "@/lib/errors.js"; -import app from "@/main"; - -export default { - name: "importapprovedgaugemeasurements", - data() { - return { - disableUploadButton: false, - uploadLabel: this.$gettext("choose file to upload"), - uploadFile: null, - originator: "viadonau" - }; - }, - computed: { - importGaugmeasurmentLabel() { - return this.$gettext("Import approved gaugemeasurements"); - } - }, - methods: { - initialState() { - this.uploadLabel = this.$gettext("choose file to upload"); - this.uploadFile = null; - this.originator = "viadonau"; - }, - fileSelected(e) { - const files = e.target.files || e.dataTransfer.files; - if (!files) return; - this.uploadLabel = files[0].name; - this.uploadFile = files[0]; - }, - submit() { - if (!this.originator || !this.uploadFile) return; - let formData = new FormData(); - formData.append("agm", this.uploadFile); - formData.append("originator", this.originator); - HTTP.post("/imports/agm", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(() => { - displayInfo({ - title: this.$gettext("Import"), - message: this.$gettext( - "Starting import of Approved Gauge Measurements" - ) - }); - this.initialState(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - components: { - Spacer: () => import("./Spacer") - }, - ORIGINATOR: app.$gettext("originator"), - FROM: app.$gettext("from") -}; -</script> - -<style lang="scss" scoped></style>
--- a/client/src/components/ImportSoundingresults.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,358 +0,0 @@ -<template> - <div class="main d-flex flex-column"> - <div class="d-flex flex-row"> - <Spacer></Spacer> - <div class="card shadow-xs mt-2 mr-2 w-100 h-100"> - <UIBoxHeader icon="upload" :title="importSoundingresultsLabel" /> - <div v-if="editState"> - <div - v-for="(message, index) in messages" - :key="index" - class="alert alert-warning small rounded-0" - > - {{ message }} - </div> - <div class="container"> - <div class="row"> - <div class="col-5"> - <small class="text-muted"> - <translate>Bottleneck</translate> - </small> - <select v-model="bottleneck" class="custom-select"> - <option - v-for="bottleneck in availableBottlenecks" - :value="bottleneck" - :key="bottleneck.properties.objnam" - > - {{ bottleneck.properties.objnam }} - </option> - </select> - <span class="text-danger"> - <small v-if="!bottleneck"> - <translate>Please select a bottleneck</translate> - </small> - </span> - </div> - <div class="col-2"> - <small class="text-muted"> - <translate>Projection</translate> (EPSG) - </small> - <input - class="form-control" - v-model="projection" - value="4326" - placeholder="e.g. 4326" - type="number" - /> - <span class="text-left text-danger"> - <small v-if="!projection"> - <translate>Please enter a projection</translate> - </small> - </span> - </div> - <div class="col-2"> - <small class="text-muted"> - <translate>Depthreference</translate> - </small> - <select - v-model="depthReference" - class="custom-select" - id="depthreference" - > - <option - v-for="option in this.depthReferenceOptions" - :key="option" - >{{ option }}</option - > - </select> - <span class="text-left text-danger"> - <small v-if="!depthReference"> - <translate>Please enter a reference</translate> - </small> - </span> - </div> - <div class="col-3"> - <small class="text-muted"> <translate>Date</translate> </small> - <input - id="importdate" - type="date" - class="form-control" - placeholder="Date of import" - aria-label="bottleneck" - aria-describedby="bottlenecklabel" - v-model="importDate" - /> - <span class="text-left text-danger"> - <small v-if="!importDate"> - <translate>Please enter a date</translate> - </small> - </span> - </div> - </div> - <div class="row"></div> - </div> - </div> - <div class="container py-5"> - <div v-if="uploadState" class="input-group"> - <div class="custom-file"> - <input - accept=".zip" - type="file" - @change="fileSelected" - class="custom-file-input" - id="uploadFile" - /> - <label class="pointer custom-file-label" for="uploadFile"> - {{ uploadLabel }} - </label> - </div> - </div> - <div class="d-flex justify-content-between" v-if="editState"> - <a - download="meta.json" - :href="dataLink" - :class="[ - 'btn btn-outline-info', - { disabled: !bottleneck || !importDate || !depthReference } - ]" - > - <translate>Download Meta.json</translate> - </a> - <span> - <button - @click="deleteTempData" - class="btn btn-danger" - type="button" - > - <translate>Cancel Upload</translate> - </button> - <button - :disabled="disableUploadButton" - @click="confirm" - class="btn btn-info ml-2" - type="button" - > - <translate>Confirm</translate> - </button> - </span> - </div> - </div> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Markus Kottländer <markus.kottlaender@intevation.de> - */ -import { HTTP } from "@/lib/http"; -import { displayError, displayInfo } from "@/lib/errors.js"; -import { mapState } from "vuex"; -import Spacer from "./Spacer"; - -const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; - -export default { - name: "imports", - components: { - Spacer - }, - data() { - return { - importState: IMPORTSTATE.UPLOAD, - depthReference: "", - bottleneck: "", - projection: "", - importDate: "", - uploadLabel: this.$gettext("choose .zip- file"), - uploadFile: null, - disableUpload: false, - token: null, - messages: [] - }; - }, - methods: { - initialState() { - this.importState = IMPORTSTATE.UPLOAD; - this.depthReference = ""; - this.bottleneck = null; - this.projection = ""; - this.importDate = ""; - this.uploadLabel = this.$gettext("choose .zip- file"); - this.uploadFile = null; - this.disableUpload = false; - this.token = null; - this.messages = []; - }, - fileSelected(e) { - const files = e.target.files || e.dataTransfer.files; - if (!files) return; - this.uploadLabel = files[0].name; - this.uploadFile = files[0]; - this.upload(); - }, - deleteTempData() { - HTTP.delete("/imports/sr-upload/" + this.token, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - } - }) - .then(() => { - this.initialState(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - upload() { - let formData = new FormData(); - formData.append("soundingresult", this.uploadFile); - HTTP.post("/imports/sr-upload", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(response => { - if (response.data.meta) { - const { bottleneck, date, epsg } = response.data.meta; - const depthReference = response.data.meta["depth-reference"]; - this.bottleneck = this.bottlenecks.find( - bn => bn.properties.objnam === bottleneck - ); - this.depthReference = depthReference; - this.importDate = new Date(date).toISOString().split("T")[0]; - this.projection = epsg; - } - this.importState = IMPORTSTATE.EDIT; - this.token = response.data.token; - this.messages = response.data.messages; - }) - .catch(error => { - const { status, data } = error.response; - const messages = data.messages ? data.messages.join(", ") : ""; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${messages}` - }); - }); - }, - confirm() { - let formData = new FormData(); - formData.append("token", this.token); - if (this.bottleneck) - formData.append("bottleneck", this.bottleneck.properties.objnam); - if (this.importDate) - formData.append("date", this.importDate.split("T")[0]); - if (this.depthReference) - formData.append("depth-reference", this.depthReference); - if (this.projection) formData.append("", this.projection); - - HTTP.post("/imports/sr", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(() => { - displayInfo({ - title: this.$gettext("Import"), - message: - this.$gettext("Starting import for ") + - this.bottleneck.properties.objnam - }); - this.initialState(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - mounted() { - this.$store.dispatch("bottlenecks/loadBottlenecks"); - }, - watch: { - showContextBox() { - if (!this.showContextBox && this.token) this.deleteTempData(); - } - }, - computed: { - ...mapState("application", ["showContextBox"]), - ...mapState("bottlenecks", ["bottlenecks"]), - importSoundingresultsLabel() { - return this.$gettext("Import Soundingresults"); - }, - disableUploadButton() { - if (this.importState === IMPORTSTATE.UPLOAD) return this.disableUpload; - if ( - !this.bottleneck || - !this.importDate || - !this.depthReference || - !this.projection - ) - return true; - return this.disableUpload; - }, - availableBottlenecks() { - return this.bottlenecks; - }, - editState() { - return this.importState === IMPORTSTATE.EDIT; - }, - uploadState() { - return this.importState === IMPORTSTATE.UPLOAD; - }, - Upload() { - return this.$gettext("Upload"); - }, - Confirm() { - return this.$gettext("Confirm"); - }, - dataLink() { - if (this.bottleneck && this.depthReference && this.import) { - return ( - "data:text/json;charset=utf-8," + - encodeURIComponent( - JSON.stringify({ - depthReference: this.depthReference, - bottleneck: this.bottleneck.properties.objnam, - date: this.importDate - }) - ) - ); - } - }, - depthReferenceOptions() { - if ( - this.bottleneck && - this.bottleneck.properties.reference_water_levels - ) { - return Object.keys( - JSON.parse(this.bottleneck.properties.reference_water_levels) - ); - } - return []; - } - } -}; -</script>
--- a/client/src/components/ImportStretches.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,594 +0,0 @@ -<template> - <div class="d-flex flex-column mb-3"> - <UIBoxHeader - icon="road" - :title="defineStretchesLabel" - :closeCallback="$parent.close" - /> - <div v-if="!edit" class="mb-3"> - <UITableHeader - :columns="[ - { id: 'properties.name', title: `${nameLabel}`, class: 'col-4' }, - { id: 'properties.date_info', title: `${dateLabel}`, class: 'col-2' }, - { - id: 'properties.source_organization', - title: `${sourceorganizationLabel}`, - class: 'col-3' - } - ]" - /> - <UITableBody - :data="filteredStretches() | sortTable(sortColumn, sortDirection)" - v-slot="{ item: stretch }" - > - <div class="py-1 col-4 "> - <a - class="linkto text-info" - v-if="isInStaging(stretch.properties.name)" - @click="gotoStaging(getStagingLink(stretch.properties.name))" - > - {{ stretch.properties.name - }}<font-awesome-icon - class="ml-1 text-danger" - icon="exclamation-triangle" - fixed-width - ></font-awesome-icon - ><small class="ml-1">review</small> - </a> - <a v-else @click="moveMapToStretch(stretch)" href="#">{{ - stretch.properties.name - }}</a> - </div> - <div class="py-1 col-2"> - {{ stretch.properties.date_info | surveyDate }} - </div> - <div class="py-1 col-3"> - {{ stretch.properties.source_organization }} - </div> - <div class="py-1 col text-right"> - <button - class="btn btn-xs btn-dark mr-1" - @click="editStretch(stretch)" - > - <font-awesome-icon icon="pencil-alt" fixed-width /> - </button> - <button class="btn btn-xs btn-dark" @click="deleteStretch(stretch)"> - <font-awesome-icon icon="trash" fixed-width /> - </button> - </div> - </UITableBody> - </div> - <div v-if="edit"> - <div class="ml-3 mr-3"> - <div class="d-flex flex-row justify-content-between"> - <div class="mt-2 w-50 mr-2 text-left"> - <small class="text-muted"> <translate>ID</translate> </small> - <input - id="id" - type="text" - class="form-control" - placeholder="AT_Section_12" - aria-label="id" - v-model="id" - :disabled="editExistingStretch" - /> - <span class="text-left text-danger"> - <small v-if="idError && !id"> - <translate>Please enter an id</translate> - </small> - </span> - </div> - <div class="mt-2 w-50 ml-2 text-left"> - <div> - <small class="text-muted"> - <translate>Countrycode</translate> - </small> - <input - id="countryCode" - type="text" - class="form-control" - placeholder="AT" - aria-label="id" - v-model="countryCode" - /> - <span class="text-left text-danger"> - <small v-if="countryCodeError && !countryCode"> - <translate>Please enter a countrycode </translate> - </small> - </span> - </div> - <div class="w-50 ml-2"></div> - </div> - </div> - <div class="d-flex flex-column justify-content-between"> - <div class="mt-2 text-left"> - <small class="text-muted"> <translate>Start rhm</translate> </small> - <div class="d-flex flex-row"> - <input - id="startrhm" - type="text" - class="form-control" - placeholder="e.g. ATXXX000010000019900" - aria-label="startrhm" - v-model="startrhm" - /> - <span class="input-group-text"> - <font-awesome-icon - @click="togglePipette('start')" - :class="{ 'text-info': pipetteStart }" - icon="bullseye" - ></font-awesome-icon> - </span> - </div> - <span class="text-left text-danger"> - <small v-if="startrhmError && !startrhm"> - <translate>Please enter a start point</translate> - </small> - </span> - </div> - <div class="mt-2 text-left"> - <small class="text-muted"> <translate>End rhm</translate> </small> - <div class="d-flex flex-row"> - <input - id="endrhm" - type="text" - class="form-control" - placeholder="e.g. ATXXX000010000019900" - aria-label="endrhm" - v-model="endrhm" - /> - <span class="input-group-text"> - <font-awesome-icon - @click="togglePipette('end')" - :class="{ 'text-info': pipetteEnd }" - icon="bullseye" - ></font-awesome-icon> - </span> - </div> - <span class="text-left text-danger"> - <small v-if="endrhmError && !endrhm"> - <translate>Please enter an end point</translate> - </small> - </span> - </div> - </div> - <div - v-if="!editExistingStretch" - class="d-flex flex-row justify-content-between" - > - <div class="mt-2 mr-2 w-50 text-left"> - <small class="text-muted"> - <translate>Tolerance for snapping of waterway axis [m]</translate> - </small> - <input - class="form-control" - v-model.number="tolerance" - placeholder="" - type="number" - min="0" - step="any" - aria-label="tolerance" - id="tolerance" - /> - <span class="text-left text-danger"> - <small v-if="toleranceError && !tolerance"> - <translate>Please enter a tolerance value</translate> - </small> - </span> - </div> - </div> - <div class="d-flex flex-row justify-content-between"> - <div class="mt-2 mr-2 w-50 text-left"> - <small class="text-muted"> - <translate>Object name</translate> - </small> - <input - id="objbn" - type="text" - class="form-control" - placeholder="" - aria-label="objbn" - v-model="objbn" - /> - <span class="text-left text-danger"> - <small v-if="objbnError && !objbn"> - <translate>Please enter an objectname</translate> - </small> - </span> - </div> - <div class="mt-2 ml-2 w-50 text-left"> - <small class="text-muted"> - <translate>National Object name</translate> - </small> - <input - id="nobjbn" - type="text" - class="form-control" - placeholder="" - aria-label="nobjbn" - v-model="nobjbn" - /> - </div> - </div> - <div class="d-flex flex-row justify-content-between"> - <div class="mt-2 mr-2 w-50 text-left"> - <small class="text-muted"> <translate>Date info</translate> </small> - <input - id="date_info" - type="date" - class="form-control" - placeholder="date_info" - aria-label="date_info" - v-model="date_info" - /> - <span class="text-left text-danger"> - <small v-if="date_infoError && !date_info"> - <translate>Please enter a date</translate> - </small> - </span> - </div> - <div class="mt-2 ml-2 w-50 text-left"> - <small class="text-muted"> <translate>Source</translate> </small> - <input - id="source" - type="text" - class="form-control" - placeholder="source" - aria-label="source" - v-model="source" - /> - <span class="text-left text-danger"> - <small v-if="sourceError && !source"> - <translate>Please enter a source</translate> - </small> - </span> - </div> - </div> - </div> - <div class="text-right mt-2 mr-3 mb-3"> - <button @click="edit = false" class="btn btn-warning mr-2">Back</button> - <button - @click="save" - type="submit" - class="shadow-sm btn btn-info submit-button" - > - <translate>Submit</translate> - </button> - </div> - </div> - <div class="text-right mr-3"> - <button v-if="!edit" @click="startEdit()" class="btn btn-info"> - <translate>New stretch</translate> - </button> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018, 2019 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Tom Gottfried <tom.gottfried@intevation.de> - */ -import { mapState, mapGetters } from "vuex"; -import { displayError, displayInfo } from "@/lib/errors"; -import { LAYERS } from "@/store/map"; -import { HTTP } from "@/lib/http"; -import { sortTable } from "@/lib/mixins"; - -export default { - name: "importstretches", - mixins: [sortTable], - data() { - return { - staging: [], - edit: false, - editExistingStretch: false, - id: "", - funktion: "", - startrhm: "", - endrhm: "", - tolerance: 5, - objbn: "", - nobjbn: "", - countryCode: "", - date_info: new Date().toISOString().split("T")[0], - source: "", - pipetteStart: false, - pipetteEnd: false, - idError: false, - funktionError: false, - startrhmError: false, - endrhmError: false, - toleranceError: false, - objbnError: false, - nobjbnError: false, - date_infoError: false, - sourceError: false, - countryCodeError: false - }; - }, - computed: { - ...mapState("application", ["searchQuery"]), - ...mapState("map", ["identifiedFeatures", "currentMeasurement"]), - ...mapGetters("user", ["isSysAdmin"]), - ...mapState("imports", ["stretches"]), - defineStretchesLabel() { - return this.$gettext("Define Stretches"); - }, - nameLabel() { - return this.$gettext("Name"); - }, - dateLabel() { - return this.$gettext("Date"); - }, - sourceorganizationLabel() { - return this.$gettext("Source organization"); - }, - stretchesInStaging() { - const result = []; - for (let stretch of this.stretches) { - for (let s of this.staging) { - if (s.kind == "st" && s.summary.stretch == stretch.properties.name) { - result.push({ name: s.summary.stretch, id: s.id }); - } - } - } - return result; - } - }, - watch: { - identifiedFeatures() { - const filterDistanceMarks = x => { - return /^distance_marks/.test(x["id_"]); - }; - const distanceMark = this.identifiedFeatures.filter(filterDistanceMarks); - if (distanceMark.length > 0) { - const value = distanceMark[0].getProperties()["location"]; - this.startrhm = this.pipetteStart ? value : this.startrhm; - this.endrhm = this.pipetteEnd ? value : this.endrhm; - this.pipetteStart = false; - this.pipetteEnd = false; - } - } - }, - methods: { - filteredStretches() { - return this.stretches.filter(s => { - return (s.properties.name + s.properties.source_organization) - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - }); - }, - gotoStaging(id) { - this.$router.push("/review/" + id); - }, - isInStaging(stretchname) { - for (let s of this.stretchesInStaging) { - if (s.name == stretchname) return true; - } - return false; - }, - getStagingLink(stretchname) { - for (let s of this.stretchesInStaging) { - if (s.name == stretchname) return s.id; - } - }, - loadStagingData() { - return new Promise((resolve, reject) => { - HTTP.get("/imports?states=pending", { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - const { imports } = response.data; - this.staging = imports; - resolve(response); - }) - .catch(error => { - reject(error); - }); - }); - }, - editStretch(stretch) { - const properties = stretch.properties; - this.date_info = properties.date_info.split("T")[0]; - this.id = properties.name; - this.nobjbn = properties.nobjnam; - this.objbn = properties.objnam; - this.countryCode = properties.countries; - this.source = properties["source_organization"]; - this.edit = true; - this.startrhm = properties.lower; - this.endrhm = properties.upper; - this.editExistingStretch = true; - }, - deleteStretch(stretch) { - this.$store.commit("application/popup", { - icon: "trash", - title: this.$gettext("Delete Stretch"), - content: - this.$gettext("Do you really want to delete this stretch:") + - `<br> - <b>${stretch.properties.name}, ${ - stretch.properties.source_organization - } (${stretch.properties.countries})</b>`, - confirm: { - label: this.$gettext("Delete"), - icon: "trash", - callback: () => { - displayInfo({ - title: this.$gettext("Not implemented"), - message: this.$gettext("Deleting ") + stretch.id - }); - } - }, - cancel: { - label: this.$gettext("Cancel"), - icon: "times" - } - }); - }, - moveMapToStretch(stretch) { - this.$store.commit("map/setLayerVisible", LAYERS.STRETCHES); - this.$store.commit("map/moveToExtent", { - feature: stretch, - zoom: 17, - preventZoomOut: true - }); - }, - loadStretches() { - return new Promise((resolve, reject) => { - this.$store - .dispatch("imports/loadStretches") - .then(response => { - resolve(response); - }) - .catch(error => { - reject(error); - }); - }); - }, - clean() { - this.id = ""; - this.edit = false; - this.editExistingStretch = false; - this.funktion = ""; - this.startrhm = ""; - this.tolerance = 5; - this.endrhm = ""; - this.objbn = ""; - this.nobjbn = ""; - this.countryCode = ""; - this.date_info = new Date().toISOString().split("T")[0]; - this.source = ""; - this.pipetteStart = false; - this.pipetteEnd = false; - this.idError = false; - this.funktionError = false; - this.startrhmError = false; - this.endrhmError = false; - this.toleranceError = false; - this.objbnError = false; - this.nobjbnError = false; - this.date_infoError = false; - this.sourceError = false; - this.countryCodeError = false; - }, - startEdit() { - this.clean(); - this.edit = true; - }, - togglePipette(t) { - this.$store.commit("map/setLayerVisible", LAYERS.DISTANCEMARKSAXIS); - if (t === "start") { - this.pipetteStart = !this.pipetteStart; - this.pipetteEnd = false; - } else { - this.pipetteEnd = !this.pipetteEnd; - this.pipetteStart = false; - } - }, - validate() { - const fields = [ - "id", - "funktion", - "startrhm", - "tolerance", - "endrhm", - "objbn", - "nobjbn", - "countryCode", - "date_info", - "source" - ]; - fields.forEach(field => { - if (!this[field]) { - this[field + "Error"] = true; - } else { - this[field + "Error"] = false; - } - }); - }, - save() { - this.validate(); - if ( - !this.id || - !this.startrhm || - !this.endrhm || - (!this.tolerance && this.editExistingStretch) || - !this.source || - !this.date_info || - !this.objbn || - !this.countryCode - ) - return; - const data = { - name: this.id, - from: this.startrhm, - to: this.endrhm, - "source-organization": this.source, - "date-info": this.date_info, - objnam: this.objbn, - nobjnam: this.nobjbn, - countries: this.countryCode.split(",").map(x => { - return x.trim(); - }) - }; - if (!this.editExistingStretch) { - data["tolerance"] = this.tolerance; - } - this.$store - .dispatch("imports/saveStretch", data) - .then(() => { - displayInfo({ - title: this.$gettext("Import"), - message: this.$gettext("Starting import of stretch") - }); - this.clean(); - this.loadStretches().then(() => { - this.edit = false; - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - mounted() { - this.edit = false; - this.loadStretches().catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - this.loadStagingData().catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } -}; -</script> - -<style lang="scss" scoped> -.linkto { - cursor: pointer; -} -</style>
--- a/client/src/components/ImportWaterwayProfiles.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,186 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <Spacer></Spacer> - <div class="card sysconfig mt-2 shadow-xs w-100 h-100 mr-2"> - <UIBoxHeader icon="upload" :title="importWaterwayProfilesLabel" /> - <div class="card-body stretches-card"> - <div class="w-95 ml-auto mr-auto mt-4 mb-4"> - <div class="mb-4"> - <div class="d-flex flex-row"> - <div class="flex-column w-100"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>URL</translate> </small> - </div> - <div class="w-100"> - <input class="form-control" type="url" v-model="url" /> - </div> - </div> - </div> - <div v-if="!url" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a URL</translate - ></small - > - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Featuretype</translate> - </small> - </div> - <div class="w-100"> - <input - class="form-control" - type="text" - v-model="featureType" - /> - </div> - <div v-if="!featureType" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a Featuretype</translate - ></small - > - </div> - </div> - <div class="flex-column mt-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>SortBy</translate> - </small> - </div> - <div class="w-100"> - <input class="form-control" type="text" v-model="sortBy" /> - </div> - <div v-if="!sortBy" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter SortBy</translate - ></small - > - </div> - </div> - </div> - </div> - <div class="d-flex flex-row text-left"> - <div class="mt-3 mb-3 flex-column w-100"> - <div class="custom-file"> - <input - accept=".csv" - type="file" - @change="fileSelected" - class="custom-file-input" - id="uploadFile" - /> - <label class="pointer custom-file-label" for="uploadFile"> - {{ uploadLabel }} - </label> - </div> - </div> - </div> - <div class="buttons text-right"> - <button - :disabled="disableUploadButton" - @click="submit" - class="btn btn-info mt-4" - type="button" - > - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="play" - ></font-awesome-icon> - <translate>Trigger import</translate> - </button> - </div> - </div> - </div> - </div> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ - -import { displayError, displayInfo } from "@/lib/errors.js"; -import { HTTP } from "@/lib/http"; - -export default { - name: "importwaterwayprofiles", - data() { - return { - url: "https://service.d4d-portal.info/wamos/wfs/", - sortBy: "hydro_scamin", - featureType: "ws-wamos:ienc_wtwprf", - disableUploadButton: false, - uploadLabel: this.$gettext("choose file to upload"), - uploadFile: null - }; - }, - computed: { - importWaterwayProfilesLabel() { - return this.$gettext("Import Waterway Profiles"); - } - }, - methods: { - fileSelected(e) { - const files = e.target.files || e.dataTransfer.files; - if (!files) return; - this.uploadLabel = files[0].name; - this.uploadFile = files[0]; - }, - submit() { - if (!this.url || !this.featureType || !this.sortBy || !this.uploadFile) - return; - let formData = new FormData(); - formData.append("wp", this.uploadFile); - formData.append("url", this.url); - formData.append("feature-type", this.featureType); - formData.append("sort-by", this.sortBy); - HTTP.post("/imports/wp", formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(() => { - displayInfo({ - title: this.$gettext("Import"), - message: - this.uploadLabel + this.$gettext(" was successfully uploaded.") - }); - this.url = "https://service.d4d-portal.info/wamos/wfs/"; - this.uploadFile = null; - this.uploadLabel = this.$gettext("choose file to upload"); - }) - .catch(error => { - const { status, data } = error.response; - const messages = data.messages ? data.messages.join(", ") : ""; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${messages}` - }); - }); - } - }, - components: { - Spacer: () => import("./Spacer") - } -}; -</script> - -<style lang="scss" scoped></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/KeyboardHandler.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,98 @@ +<template> + <transition name="fade"> + <div class="notice" v-if="showNotice"> + <span>{{ noticeText }}</span> + </div> + </transition> +</template> + +<style lang="sass" scoped> +.notice + position: absolute + top: 0 + width: 100% + text-align: center + z-index: 1 + font-size: 11px + line-height: 11px + > span + opacity: 0.5 + background: white + display: inline-block + border-bottom-right-radius: 0.25rem + border-bottom-left-radius: 0.25rem + padding: 3px 5px +</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 { + computed: { + ...mapState("application", ["paneSetup"]), + ...mapState("map", [ + "openLayersMaps", + "lineToolEnabled", + "polygonToolEnabled", + "cutToolEnabled" + ]), + showNotice() { + return ( + this.lineToolEnabled || + this.polygonToolEnabled || + this.cutToolEnabled || + this.paneSetup.includes("COMPARESURVEYS") + ); + }, + noticeText() { + if ( + this.lineToolEnabled || + this.polygonToolEnabled || + this.cutToolEnabled + ) { + return this.$gettext("Press ESC to stop drawing."); + } else if (this.paneSetup.includes("COMPARESURVEYS")) { + return this.$gettext("Press ESC to close compare view."); + } + } + }, + mounted() { + window.addEventListener("keydown", e => { + // Escape + if (e.keyCode === 27) { + if ( + this.lineToolEnabled || + this.polygonToolEnabled || + this.cutToolEnabled + ) { + this.$store.commit("map/lineToolEnabled", false); + this.$store.commit("map/polygonToolEnabled", false); + this.$store.commit("map/cutToolEnabled", false); + this.$store.commit("map/setCurrentMeasurement", null); + this.openLayersMaps.forEach(m => { + m.getLayer("DRAWTOOL") + .getSource() + .clear(); + }); + } else if (this.paneSetup.includes("COMPARESURVEYS")) { + this.$store.commit("fairwayprofile/additionalSurvey", null); + } + } + }); + } +}; +</script>
--- a/client/src/components/Login.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/Login.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,4 +1,4 @@ -(<template> +<template> <div class="d-flex flex-column login bg-white shadow"> <div class="m-5"> <!-- logo section --> @@ -84,8 +84,8 @@ <img :src="secondaryLogo" /> </div> </div> - </div> </template ->) + </div> +</template> <style lang="scss" scoped> .login { @@ -127,8 +127,8 @@ * Markus Kottländer <markus@intevation.de > */ import { mapState } from "vuex"; -import { HTTP } from "@/lib/http.js"; -import { displayError } from "@/lib/errors.js"; +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors"; const UNAUTHORIZED = 401; @@ -193,7 +193,8 @@ .dispatch("user/login", { user, password }) .then(() => { this.loginFailed = false; - this.$router.push("/"); + this.$router.push(localStorage.getItem("tempRoute") || "/"); + localStorage.removeItem("tempRoute"); }) .catch(error => { this.loginFailed = true;
--- a/client/src/components/Logs.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/Logs.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,7 +1,7 @@ <template> <div class="main d-flex flex-column"> <div class="d-flex flex-row"> - <Spacer></Spacer> + <Spacer /> <div class="card logs shadow-xs mt-2 mr-2"> <UIBoxHeader icon="book" title="Logs" /> <div class="logoutput text-left bg-white"> @@ -14,8 +14,11 @@ <ul class="nav nav-pills"> <li class="nav-item"> <a + id="accesslog" :class="accesslogStyle" - @click="fetch('system/log/apache2/access.log', 'accesslog')" + @click.prevent=" + fetch('system/log/apache2/access.log', 'accesslog') + " href="#" > <translate>Accesslog</translate> @@ -23,8 +26,11 @@ </li> <li class="nav-item"> <a + id="errorlog" :class="errorlogStyle" - @click="fetch('system/log/apache2/error.log', 'errorlog')" + @click.prevent=" + fetch('system/log/apache2/error.log', 'errorlog') + " href="#" > <translate>Errorlog</translate> @@ -65,6 +71,7 @@ .logs { height: 85vh; + width: 100vw; } #code { @@ -113,10 +120,11 @@ * Thomas Junk <thomas.junk@intevation.de> */ import { mapState } from "vuex"; -import { HTTP } from "@/lib/http.js"; +import { HTTP } from "@/lib/http"; import "../../node_modules/highlight.js/styles/paraiso-dark.css"; import Vue from "vue"; import VueHighlightJS from "vue-highlightjs"; +import { displayError } from "@/lib/errors"; Vue.use(VueHighlightJS); const ACCESSLOG = "accesslog"; @@ -149,7 +157,13 @@ this.refreshed = new Date().toLocaleString(); this.currentFile = file; }) - .catch(); + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status} ${data.message || data}` + }); + }); }, disallow(e) { e.target.blur();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Main.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,130 @@ +<template> + <div id="panes" :class="'d-flex position-absolute rotate' + paneRotate"> + <Pane :pane="panes[0]" :key="panes[0].id" :class="paneClasses[0]" /> + <Pane + :pane="panes[1]" + :key="panes[1].id" + :class="paneClasses[1]" + v-if="panes.length >= 2" + /> + <Pane + :pane="panes[2]" + :key="panes[2].id" + :class="paneClasses[2]" + v-if="panes.length >= 3" + /> + <Pane + :pane="panes[3]" + :key="panes[3].id" + :class="paneClasses[3]" + v-if="panes.length === 4" + /> + </div> +</template> + +<style lang="sass"> +#panes + top: -1px + right: -1px + bottom: -1px + left: -1px + z-index: 1 + &.rotate1 + flex-wrap: wrap + flex-direction: row + &.rotate2 + flex-wrap: wrap-reverse + flex-direction: column + &.rotate3 + flex-wrap: wrap-reverse + flex-direction: row-reverse + &.rotate4 + flex-wrap: wrap + flex-direction: column-reverse + .pane + border: solid 1px #dee2e6 + background: #fff +</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"; +import * as paneSetups from "./paneSetups"; + +export default { + components: { + Pane: () => import("./Pane") + }, + computed: { + ...mapState("application", ["paneSetup", "paneRotate"]), + panes() { + return Object.values(paneSetups[this.paneSetup]); + }, + paneClasses() { + if (this.paneSetup === "DEFAULT") { + return ["wh-100"]; + } + + if (this.paneSetup === "COMPARESURVEYS") { + return [2, 4].includes(this.paneRotate) + ? ["w-100 h-50", "w-100 h-50"] + : ["w-50 h-100", "w-50 h-100"]; + } + + if (this.paneSetup === "FAIRWAYPROFILE") { + return [1, 3].includes(this.paneRotate) + ? ["w-100 h-50", "w-100 h-50"] + : ["w-50 h-100", "w-50 h-100"]; + } + + if (this.paneSetup === "AVAILABLEFAIRWAYDEPTH") { + return [1, 3].includes(this.paneRotate) + ? ["w-100 h-50", "w-100 h-50"] + : ["w-50 h-100", "w-50 h-100"]; + } + + if (this.paneSetup === "AVAILABLEFAIRWAYDEPTHLNWL") { + return [1, 3].includes(this.paneRotate) + ? ["w-100 h-50", "w-100 h-50"] + : ["w-50 h-100", "w-50 h-100"]; + } + + if (this.paneSetup === "COMPARESURVEYS_FAIRWAYPROFILE") { + return [1, 3].includes(this.paneRotate) + ? ["wh-50", "wh-50", "w-100 h-50"] + : ["wh-50", "wh-50", "w-50 h-100"]; + } + + if ( + ["GAUGE_WATERLEVEL", "GAUGE_HYDROLOGICALCONDITIONS"].includes( + this.paneSetup + ) + ) { + return [1, 3].includes(this.paneRotate) + ? ["w-100 h-50", "w-100 h-50"] + : ["w-50 h-100", "w-50 h-100"]; + } + + if (this.paneSetup === "GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS") { + return [1, 3].includes(this.paneRotate) + ? ["w-100 h-50", "wh-50", "wh-50"] + : ["h-100 w-50", "wh-50", "wh-50"]; + } + } + } +}; +</script>
--- a/client/src/components/Maplayer.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,439 +0,0 @@ -<template> - <div - id="map" - :class="{ - splitscreen: this.splitscreen, - nocursor: this.hasActiveInteractions - }" - ></div> -</template> - -<style lang="sass" scoped> -#map - height: 100vh - - &.splitscreen - height: 50vh - - &.nocursor - cursor: 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, 2019 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 { equalTo } from "ol/format/filter.js"; -import { Stroke, Style, Fill } from "ol/style.js"; -import { displayError } from "@/lib/errors.js"; -import { LAYERS } from "@/store/map.js"; - -/* for the sake of debugging */ -/* eslint-disable no-console */ -export default { - name: "maplayer", - data() { - return { - projection: "EPSG:3857", - splitscreen: false - }; - }, - computed: { - ...mapGetters("map", ["getLayerByName", "getVSourceByName"]), - ...mapState("map", [ - "initialLoad", - "extent", - "layers", - "openLayersMap", - "lineTool", - "polygonTool", - "cutTool" - ]), - ...mapState("bottlenecks", ["selectedSurvey"]), - ...mapState("application", ["showSplitscreen"]), - hasActiveInteractions() { - return ( - (this.lineTool && this.lineTool.getActive()) || - (this.polygonTool && this.polygonTool.getActive()) || - (this.cutTool && this.cutTool.getActive()) - ); - } - }, - methods: { - buildVectorLoader(featureRequestOptions, endpoint, vectorSource) { - // build a function to be used for VectorSource.setLoader() - // make use of WFS().writeGetFeature to build the request - // and use our HTTP library to actually do it - // NOTE: a) the geometryName has to be given in featureRequestOptions, - // because we want to load depending on the bbox - // b) the VectorSource has to have the option strategy: bbox - featureRequestOptions["outputFormat"] = "application/json"; - var loader = function(extent, resolution, projection) { - featureRequestOptions["bbox"] = extent; - featureRequestOptions["srsName"] = projection.getCode(); - var featureRequest = new WFS().writeGetFeature(featureRequestOptions); - // DEBUG console.log(featureRequest); - HTTP.post( - endpoint, - new XMLSerializer().serializeToString(featureRequest), - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - } - ) - .then(response => { - var features = new GeoJSON().readFeatures( - JSON.stringify(response.data) - ); - vectorSource.addFeatures(features); - // console.log( - // "loaded", - // features.length, - // featureRequestOptions.featureTypes, - // "features" - // ); - // DEBUG console.log("loaded ", features, "for", vectorSource); - // eslint-disable-next-line - }) - .catch(() => { - vectorSource.removeLoadedExtent(extent); - }); - }; - return loader; - }, - updateBottleneckFilter(bottleneck_id, datestr) { - console.log("updating filter with", bottleneck_id, datestr); - const layer = this.getLayerByName(LAYERS.BOTTLENECKISOLINE); - const wmsSrc = layer.data.getSource(); - const exists = bottleneck_id != "does_not_exist"; - - if (exists) { - wmsSrc.updateParams({ - cql_filter: - "date_info='" + - datestr + - "' AND bottleneck_id='" + - bottleneck_id + - "'" - }); - } - layer.isVisible = exists; - layer.data.setVisible(exists); - } - }, - watch: { - showSplitscreen(show) { - if (show) { - setTimeout(() => { - this.splitscreen = true; - }, 350); - } else { - this.splitscreen = false; - } - }, - splitscreen() { - const map = this.openLayersMap; - this.$nextTick(() => { - map && map.updateSize(); - }); - }, - selectedSurvey(newSelectedSurvey) { - if (newSelectedSurvey) { - this.updateBottleneckFilter( - newSelectedSurvey.bottleneck_id, - newSelectedSurvey.date_info - ); - } else { - this.updateBottleneckFilter("does_not_exist", "1999-10-01"); - } - } - }, - mounted() { - let map = new Map({ - layers: [...this.layers.map(x => x.data)], - target: "map", - controls: [], - view: new View({ - center: [this.extent.lon, this.extent.lat], - minZoom: 5, // restrict zooming out to ~size of Europe for width 1000px - zoom: this.extent.zoom, - projection: this.projection - }) - }); - map.on("moveend", event => { - const center = event.map.getView().getCenter(); - this.$store.commit("map/extent", { - lat: center[1], - lon: center[0], - zoom: event.map.getView().getZoom() - }); - }); - this.$store.dispatch("map/openLayersMap", map); - - if (this.initialLoad) { - this.$store.commit("map/initialLoad", false); - var currentUser = this.$store.state.user.user; - HTTP.get("/users/" + currentUser, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - }) - .then(response => { - this.$store.commit("map/moveToBoundingBox", { - boundingBox: [ - response.data.extent.x1, - response.data.extent.y1, - response.data.extent.x2, - response.data.extent.y2 - ], - zoom: 17, - preventZoomOut: true - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - - // TODO make display of layers more dynamic, e.g. from a list - - // load different fairway dimension layers (level of service) - [ - LAYERS.FAIRWAYDIMENSIONSLOS1, - LAYERS.FAIRWAYDIMENSIONSLOS2, - LAYERS.FAIRWAYDIMENSIONSLOS3 - ].forEach((los, i) => { - // loading the full WFS layer without bboxStrategy - var source = this.getVSourceByName(los); - /*eslint-disable no-unused-vars */ - var loader = function(extent, resolution, projection) { - var featureRequest = new WFS().writeGetFeature({ - srsName: "EPSG:3857", - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["fairway_dimensions"], - outputFormat: "application/json", - filter: equalTo("level_of_service", i + 1) - }); - - featureRequest["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 => { - source.addFeatures( - new GeoJSON().readFeatures(JSON.stringify(response.data)) - ); - // would scale to the extend of all resulting features - // this.openLayersMap.getView().fit(vectorSrc.getExtent()); - }); - }; - - layer = this.getLayerByName(los); - layer.data.getSource().setLoader(loader); - layer.data.setVisible(layer.isVisible); - }); - - // load following layers with bboxStrategy (using our request builder) - var layer = null; - - layer = this.getLayerByName(LAYERS.WATERWAYAREA); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["waterway_area"], - geometryName: "area" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName(LAYERS.WATERWAYAXIS); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["waterway_axis"], - geometryName: "wtwaxs" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName(LAYERS.WATERWAYPROFILES); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["waterway_profiles"], - geometryName: "geom" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName(LAYERS.DISTANCEMARKS); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["distance_marks_ashore_geoserver"], - geometryName: "geom" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName(LAYERS.DISTANCEMARKSAXIS); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["distance_marks_geoserver"], - geometryName: "geom" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName(LAYERS.GAUGES); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["gauges_geoserver"], - geometryName: "geom" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName(LAYERS.STRETCHES); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["stretches_geoserver"], - geometryName: "area" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - - layer = this.getLayerByName(LAYERS.BOTTLENECKSTATUS); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["bottlenecks_geoserver"], - geometryName: "area" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - - layer = this.getLayerByName(LAYERS.BOTTLENECKS); - layer.data.getSource().setLoader( - this.buildVectorLoader( - { - featureNS: "gemma", - featurePrefix: "gemma", - featureTypes: ["bottlenecks_geoserver"], - geometryName: "area" - }, - "/internal/wfs", - layer.data.getSource() - ) - ); - layer.data.setVisible(layer.isVisible); - HTTP.get("/system/style/Bottlenecks/stroke", { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - let btlnStrokeC = response.data.code; - HTTP.get("/system/style/Bottlenecks/fill", { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - let btlnFillC = response.data.code; - var newStyle = new Style({ - stroke: new Stroke({ - color: btlnStrokeC, - width: 4 - }), - fill: new Fill({ - color: btlnFillC - }) - }); - layer.data.setStyle(newStyle); - }) - .catch(error => { - console.log(error); - }); - }) - .catch(error => { - console.log(error); - }); - - // so none is shown - this.updateBottleneckFilter("does_not_exist", "1999-10-01"); - this.$store.dispatch("map/disableIdentifyTool"); - this.$store.dispatch("map/enableIdentifyTool"); - } -}; -</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/PageNotFound.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,22 @@ +<template> + <div class="main d-flex flex-row" style="position: relative;"> + <Spacer /> + <div class="my-auto mx-auto"> + <h1> + <font-awesome-icon icon="frown-open" fixed-width /> + We are sorry. The ressource you requested could not be found. + </h1> + </div> + </div> +</template> + +<script> +export default { + name: "pagenotfound", + components: { + Spacer: () => import("@/components/Spacer") + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Pane.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,35 @@ +<template> + <div :id="pane.id" class="pane d-flex position-relative"> + <component :is="pane.component" :key="pane.id" /> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ + +export default { + props: ["pane"], + components: { + // all components that are supposed to be displayed in a pane must be registered here + Map: () => import("./map/Map"), + Fairwayprofile: () => import("./fairway/Fairwayprofile"), + AvailableFairwayDepth: () => import("./fairway/AvailableFairwayDepth"), + AvailableFairwayDepthLNWL: () => + import("./fairway/AvailableFairwayDepthLNWL"), + Waterlevel: () => import("./gauge/Waterlevel"), + HydrologicalConditions: () => import("./gauge/HydrologicalConditions") + } +}; +</script>
--- a/client/src/components/Pdftool.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/Pdftool.vue Mon Jun 03 10:19:18 2019 +0200 @@ -77,6 +77,13 @@ </div> </template> +<style lang="scss" scoped> +input, +select { + font-size: 0.8em; +} +</style> + <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -93,14 +100,13 @@ * * Bernhard E. Reiter <bernhard@intevation.de> * * Fadi Abbud <fadi.abbud@intevation.de> */ -import { mapGetters, mapState } from "vuex"; +import { mapState, mapGetters } from "vuex"; import jsPDF from "jspdf"; -import "@/lib/font-linbiolinum.js"; -import { getPointResolution } from "ol/proj.js"; -import locale2 from "locale2"; -import { HTTP } from "../lib/http"; -import { displayError } from "@/lib/errors.js"; -import { LAYERS } from "@/store/map.js"; +import "@/lib/font-linbiolinum"; +import { getPointResolution } from "ol/proj"; +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors"; +import { pdfgen } from "@/lib/mixins"; var paperSizes = { // in millimeter, landscape [width, height] @@ -109,6 +115,7 @@ }; export default { + mixins: [pdfgen], name: "pdftool", data() { return { @@ -166,9 +173,8 @@ computed: { ...mapState("application", ["showPdfTool", "logoForPDF"]), ...mapState("bottlenecks", ["selectedBottleneck", "selectedSurvey"]), - ...mapState("map", ["openLayersMap", "isolinesLegendImgDataURL"]), - ...mapGetters("map", ["getLayerByName"]), - ...mapState("user", ["user"]), + ...mapState("map", ["isolinesLegendImgDataURL"]), + ...mapGetters("map", ["openLayersMap"]), generatePdfLable() { return this.$gettext("Generate PDF"); }, @@ -205,12 +211,18 @@ // applied to the rest of the form. applyTemplateToForm() { if (this.form.template) { - HTTP.get("/templates/print/" + this.form.template.name, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" + HTTP.get( + "/templates/" + + this.form.template.type + + "/" + + this.form.template.name, + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } } - }) + ) .then(response => { this.templateData = response.data.template_data; this.form.format = this.templateData.properties.format; @@ -265,7 +277,7 @@ // which will generate the pdf and resets the map view // Step 2 which starts rendering a map with the necessary image size - var map = this.openLayersMap; + var map = this.openLayersMap(); this.mapSize = map.getSize(); // size in pixels of the map in the DOM // Calculate the extent for the current view state and the passed size. // The size is the pixel dimensions of the box into which the calculated @@ -437,12 +449,14 @@ map.getView().fit(this.mapExtent, { size: mapSizeForPrint }); }, cancel() { - this.openLayersMap.un( + this.openLayersMap().un( this.rendercompleteListener.type, this.rendercompleteListener.listener ); - this.openLayersMap.setSize(this.mapSize); - this.openLayersMap.getView().fit(this.mapExtent, { size: this.mapSize }); + this.openLayersMap().setSize(this.mapSize); + this.openLayersMap() + .getView() + .fit(this.mapExtent, { size: this.mapSize }); this.readyToGenerate = true; }, // add the used map scale and papersize @@ -459,125 +473,6 @@ ")"; this.addText(position, offset, width, fontSize, color, str); }, - addRoundedBox(x, y, w, h, color, rounding, brcolor) { - // draws a rounded background box at (x,y) width x height - // using jsPDF units - this.pdf.doc.setDrawColor(brcolor); - this.pdf.doc.setFillColor(color); - this.pdf.doc.roundedRect(x, y, w, h, rounding, rounding, "FD"); - }, - // add some text at specific coordinates and determine how many wrolds in single line - addText(position, offset, width, fontSize, color, text) { - text = this.replacePlaceholders(text); - - // split the incoming string to an array, each element is a string of - // words in a single line - this.pdf.doc.setTextColor(color); - this.pdf.doc.setFontSize(fontSize); - var textLines = this.pdf.doc.splitTextToSize(text, width); - - // x/y defaults to offset for topleft corner (normal x/y coordinates) - let x = offset.x; - let y = offset.y; - - // if position is on the right, x needs to be calculate with pdf width and - // the size of the element - if (["topright", "bottomright"].indexOf(position) !== -1) { - x = this.pdf.width - offset.x - width; - } - if (["bottomright", "bottomleft"].indexOf(position) !== -1) { - y = this.pdf.height - offset.y - this.getTextHeight(textLines.length); - } - - this.pdf.doc.text(textLines, x, y, { baseline: "hanging" }); - }, - addBox(position, offset, width, height, rounding, color, brcolor) { - // x/y defaults to offset for topleft corner (normal x/y coordinates) - let x = offset.x; - let y = offset.y; - - // if position is on the right, x needs to be calculate with pdf width and - // the size of the element - if (["topright", "bottomright"].indexOf(position) !== -1) { - x = this.pdf.width - offset.x - width; - } - if (["bottomright", "bottomleft"].indexOf(position) !== -1) { - y = this.pdf.height - offset.y - height; - } - - this.addRoundedBox(x, y, width, height, color, rounding, brcolor); - }, - // add some text at specific coordinates with a background box - addTextBox( - position, - offset, - width, - height, - rounding, - padding, - fontSize, - color, - background, - text, - brcolor - ) { - this.pdf.doc.setFontSize(fontSize); - text = this.replacePlaceholders(text); - - if (!width) { - width = this.pdf.doc.getTextWidth(text) + 2 * padding; - } - let textWidth = width - 2 * padding; - if (!height) { - let textLines = this.pdf.doc.splitTextToSize(text, textWidth); - height = this.getTextHeight(textLines.length) + 2 * padding; - } - - this.addBox( - position, - offset, - width, - height, - rounding, - background, - brcolor - ); - this.addText( - position, - { x: offset.x + padding, y: offset.y + padding }, - textWidth, - fontSize, - color, - text - ); - }, - addImage(url, format, position, offset, width, height) { - // x/y defaults to offset for topleft corner (normal x/y coordinates) - let x = offset.x; - let y = offset.y; - - // if position is on the right, x needs to be calculate with pdf width and - // the size of the element - if (["topright", "bottomright"].indexOf(position) !== -1) { - x = this.pdf.width - offset.x - width; - } - if (["bottomright", "bottomleft"].indexOf(position) !== -1) { - y = this.pdf.height - offset.y - height; - } - - let image = new Image(); - if (url) { - image.src = url; - } else { - if (this.logoForPDF) { - image.src = this.logoForPDF; - } else { - image.src = "/img/gemma-logo-for-pdf.png"; - } - } - - this.pdf.doc.addImage(image, x, y, width, height); - }, addScaleBar(scaleDenominator, position, offset, rounding, brcolor) { // scaleDenominator is the x in 1:x of the map scale @@ -754,7 +649,9 @@ if ( this.selectedBottleneck && this.selectedSurvey && - this.getLayerByName(LAYERS.BOTTLENECKISOLINE).isVisible + this.openLayersMap() + .getLayer("BOTTLENECKISOLINE") + .getVisible() ) { // transforming into an HTMLImageElement only to find out // the width x height of the legend image @@ -793,7 +690,9 @@ if ( this.selectedBottleneck && this.selectedSurvey && - this.getLayerByName(LAYERS.BOTTLENECKISOLINE).isVisible + this.openLayersMap() + .getLayer("BOTTLENECKISOLINE") + .getVisible() ) { let survey = this.selectedSurvey; @@ -896,28 +795,6 @@ ); } }, - replacePlaceholders(text) { - if (text.includes("{date}")) { - text = text.replace("{date}", new Date().toLocaleString(locale2)); - } - //get only day,month and year from the Date object - if (text.includes("{date-minor}")) { - var date = new Date(); - var dt = - (date.getDate() < 10 ? "0" : "") + - date.getDate() + - "." + - (date.getMonth() + 1 < 10 ? "0" : "") + - (date.getMonth() + 1) + - "." + - date.getFullYear(); - text = text.replace("{date-minor}", dt.toLocaleString(locale2)); - } - if (text.includes("{user}")) { - text = text.replace("{user}", this.user); - } - return text; - }, getTextHeight(numberOfLines) { return ( numberOfLines * @@ -929,7 +806,7 @@ mounted() { this.form.template = this.templates[0]; this.templateData = this.form.template; - HTTP.get("/templates/print", { + HTTP.get("/templates/map", { headers: { "X-Gemma-Auth": localStorage.getItem("token"), "Content-type": "text/xml; charset=UTF-8"
--- a/client/src/components/Search.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/Search.vue Mon Jun 03 10:19:18 2019 +0200 @@ -2,7 +2,7 @@ <div :class="searchbarContainerStyle"> <div class="input-group-prepend m-0 d-print-none"> <span @click="toggleSearchbar" :class="searchButtonStyle" for="search"> - <font-awesome-icon icon="search"></font-awesome-icon> + <font-awesome-icon icon="search" /> </span> </div> <div @@ -64,6 +64,12 @@ class="mr-1" fixed-width /> + <font-awesome-icon + icon="object-group" + v-if="entry.type === 'section'" + class="mr-1" + fixed-width + /> {{ entry.name }} </a> </div> @@ -74,6 +80,7 @@ <style lang="scss" scoped> .searchcontainer { opacity: 0.96; + width: 668px; } .searchcontainer .searchbar { @@ -82,8 +89,7 @@ } .searchgroup { - margin-left: -3px; - width: 630px; + width: 635px; overflow: hidden; } @@ -158,7 +164,7 @@ import debounce from "lodash.debounce"; import { mapState, mapGetters } from "vuex"; -import { displayError } from "@/lib/errors.js"; +import { displayError } from "@/lib/errors"; import { HTTP } from "@/lib/http"; import { format } from "date-fns"; @@ -200,7 +206,7 @@ }, searchbarContainerStyle() { return [ - "input-group searchcontainer shadow-xs", + "input-group searchcontainer shadow-xs rounded", { "d-flex": this.contextBoxContent !== "imports", "d-none": this.contextBoxContent === "imports" && this.showContextBox, @@ -299,7 +305,7 @@ if (resultEntry.type === "rhm") zoom = 15; if (resultEntry.type === "city") zoom = 13; if (resultEntry.type === "gauge") zoom = 15; - this.$store.commit("map/moveMap", { + this.$store.dispatch("map/moveMap", { coordinates: resultEntry.geom.coordinates, zoom, preventZoomOut: true
--- a/client/src/components/Sidebar.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/Sidebar.vue Mon Jun 03 10:19:18 2019 +0200 @@ -8,127 +8,73 @@ @click="$store.commit('application/showSidebar', !showSidebar)" class="menubutton ui-element d-print-none p-2 bg-white rounded position-absolute d-flex justify-content-center" > - <font-awesome-icon class="fa-fw" icon="bars"></font-awesome-icon> + <font-awesome-icon class="fa-fw" icon="bars" /> </div> <div class="menu text-nowrap text-left"> <router-link to="/"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="map-marked-alt" - ></font-awesome-icon> + <font-awesome-icon class="mr-2" fixed-width icon="map-marked-alt" /> <span class="fix-trans-space" v-translate>Map</span> </router-link> <router-link to="/bottlenecks"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="ship" - ></font-awesome-icon> + <font-awesome-icon class="mr-2" fixed-width icon="ship" /> <span class="fix-trans-space" v-translate>Bottlenecks</span> </router-link> <div v-if="isWaterwayAdmin"> <router-link to="/imports/overview" class="position-relative"> <font-awesome-icon - class="fa-fw mr-2" + class="mr-2" fixed-width icon="clipboard-check" - ></font-awesome-icon> + /> <span class="fix-trans-space" v-translate>Import review</span> <span class="indicator" v-if="showSidebar && stagingNotifications"> {{ stagingNotifications }} </span> </router-link> + <router-link to="/imports/configuration"> + <font-awesome-icon class="mr-2" fixed-width icon="clock" /> + <translate class="fix-trans-space">Imports</translate> + </router-link> </div> <div v-if="isSysAdmin"> <router-link to="/stretches"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="road" - ></font-awesome-icon> + <font-awesome-icon class="mr-2" fixed-width icon="road" /> <span class="fix-trans-space" v-translate>Define stretches</span> </router-link> </div> <div v-if="isWaterwayAdmin"> - <small class="text-muted pl-3"> <translate>Import</translate> </small> - <hr class="m-0" /> - <router-link to="/importsoundingresults"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="upload" - ></font-awesome-icon> - <span class="fix-trans-space" v-translate>Soundingresults</span> - </router-link> - <router-link to="/importapprovedgaugemeasurement"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="upload" - ></font-awesome-icon> - <span class="fix-trans-space" v-translate - >Approved Gaugemeasurements</span - > + <router-link to="/sections"> + <font-awesome-icon class="mr-2" fixed-width icon="road" /> + <span class="fix-trans-space" v-translate>Define sections</span> </router-link> - <router-link to="/importwaterwayprofiles"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="upload" - ></font-awesome-icon> - <span class="fix-trans-space" v-translate>Waterway Profiles</span> - </router-link> - <router-link to="/importschedule"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="clock" - ></font-awesome-icon> - <translate class="fix-trans-space">Imports</translate> - </router-link> - <small class="text-muted pl-3"> - <translate>Systemadministration</translate> - </small> - <hr class="m-0" /> </div> + <small + class="text-muted pl-2 pb-1 d-block border-bottom" + v-if="isSysAdmin" + > + <translate>Systemadministration</translate> + </small> <div v-if="isSysAdmin"> <router-link to="/usermanagement"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="users-cog" - ></font-awesome-icon> + <font-awesome-icon class="mr-2" fixed-width icon="users-cog" /> <span class="fix-trans-space" v-translate>Users</span> </router-link> </div> <div v-if="isWaterwayAdmin"> <router-link to="/systemconfiguration"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="wrench" - ></font-awesome-icon> + <font-awesome-icon class="mr-2" fixed-width icon="wrench" /> <span class="fix-trans-space" v-translate>Configuration</span> </router-link> </div> <div v-if="isSysAdmin"> <router-link to="/logs"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="book" - ></font-awesome-icon> + <font-awesome-icon class="mr-2" fixed-width icon="book" /> <span class="fix-trans-space" v-translate>Logs</span> </router-link> </div> <hr class="m-0" /> <a @click="logoff" href="#" class="logout"> - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="power-off" - ></font-awesome-icon> + <font-awesome-icon class="mr-2" fixed-width icon="power-off" /> <span class="fix-trans-space" v-translate>Logout</span> {{ user }} </a> </div> @@ -152,13 +98,12 @@ * Markus Kottländer <markus.kottlaender@intevation.de> */ import { mapGetters, mapState } from "vuex"; -import { logOff } from "@/lib/session.js"; +import { logOff } from "@/lib/session"; import { displayError } from "@/lib/errors"; import { HTTP } from "@/lib/http"; export default { name: "sidebar", - props: ["routeName"], data() { return { stagingNotifications: null @@ -198,7 +143,7 @@ return ( this.showContextBox && this.contextBoxContent === item && - this.routeName == "mainview" + this.$route.name == "mainview" ); } },
--- a/client/src/components/Zoom.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -<template> - <div :class="['zoom-buttons shadow-xs', { splitscreen: showSplitscreen }]"> - <button - class="zoom-button border-0 bg-white rounded-left ui-element" - @click="zoomOut" - > - <font-awesome-icon icon="minus"></font-awesome-icon> - </button> - <button - class="zoom-button border-0 bg-white ui-element border-right" - @click="refreshMap" - > - <font-awesome-icon icon="redo"></font-awesome-icon> - </button> - <button - class="zoom-button border-0 bg-white rounded-right ui-element border-right" - @click="zoomIn" - > - <font-awesome-icon icon="plus"></font-awesome-icon> - </button> - </div> -</template> - -<style lang="sass" scoped> -.zoom-buttons - position: absolute - bottom: $small-offset - left: 50% - margin-left: -($icon-width * 1.5) - margin-bottom: 0 - transition: margin-bottom 0.3s - &.splitscreen - margin-bottom: 50vh - - .zoom-button - min-height: $icon-width - min-width: $icon-width - z-index: 1 - outline: none - color: #666 -</style> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Markus Kottländer <markus@intevation.de> - * Thomas Junk <thomas.junk@intevation.de> - */ -import { mapState } from "vuex"; -import { Vector as VectorLayer } from "ol/layer.js"; - -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; - }, - refreshMap() { - let layers = this.openLayersMap.getLayers().getArray(); - for (let i = 0; i < layers.length; i++) { - let layer = layers[i]; - if ( - layer instanceof VectorLayer && - layer.get("source").loader_.name != "VOID" - ) { - layer.getSource().clear(true); - layer.getSource().refresh({ force: true }); - } - } - } - } -}; -</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/AvailableFairwayDepth.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,659 @@ +<template> + <div class="d-flex flex-column flex-fill"> + <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" /> + <UISpinnerOverlay v-if="loading" /> + <div class="d-flex flex-fill"> + <DiagramLegend> + <div v-for="(entry, index) in legend" :key="index" class="legend"> + <span + :style=" + `${legendStyle( + index + )}; border-radius: 0.25rem; width: 40px; height: 20px;` + " + ></span> + {{ entry }} + </div> + <div> + <select + @change="applyChange" + v-model="form.template" + class="form-control d-block custom-select-sm w-100 mt-1" + > + <option + v-for="template in templates" + :value="template" + :key="template.name" + > + {{ template.name }} + </option> + </select> + <button + @click="downloadPDF" + type="button" + class="btn btn-sm btn-info d-block w-100 mt-2" + > + <translate>Export to PDF</translate> + </button> + <a + :href="dataLink" + :download="csvFileName" + class="mt-2 btn btn-sm btn-info w-100" + >Download CSV</a + > + </div> + </DiagramLegend> + <div + ref="diagramContainer" + :id="containerId" + class="mx-auto my-auto diagram-container" + ></div> + </div> + </div> +</template> + +<style></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> + * Fadi Abbud <fadi.abbud@intevation.de> + */ +import * as d3 from "d3"; +import app from "@/main"; +import debounce from "debounce"; +import { diagram } from "@/lib/mixins"; +import { mapState } from "vuex"; +import filters from "@/lib/filters.js"; +import jsPDF from "jspdf"; +import canvg from "canvg"; +import { pdfgen } from "@/lib/mixins"; +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors"; +import { FREQUENCIES } from "@/store/fairwayavailability"; + +const hoursInDays = x => x / 24; + +export default { + mixins: [diagram, pdfgen], + components: { + DiagramLegend: () => import("@/components/DiagramLegend") + }, + data() { + return { + containerId: "availablefairwaydepth", + loading: false, + width: 1000, + height: 600, + paddingRight: 100, + spaceBetween: 80, + labelPaddingTop: 15, + scalePaddingLeft: 50, + paddingTop: 10, + diagram: null, + yScale: null, + barsWidth: 60, + dimensions: null, + ldcoffset: 3, + pdf: { + doc: null, + width: null, + height: null + }, + form: { + template: null + }, + templateData: null, + templates: [], + defaultTemplate: { + name: "Default", + properties: { + paperSize: "a4" + }, + elements: [ + { + type: "diagram", + position: "topleft", + offset: { x: 20, y: 60 }, + width: 290, + height: 100 + }, + { + type: "diagramtitle", + position: "topleft", + offset: { x: 70, y: 20 }, + fontsize: 20, + color: "steelblue" + }, + { + type: "diagramlegend", + position: "topleft", + offset: { x: 30, y: 160 }, + color: "black" + } + ] + } + }; + }, + created() { + window.addEventListener("resize", debounce(this.drawDiagram), 200); + }, + mounted() { + this.drawDiagram(); + this.templates[0] = this.defaultTemplate; + this.form.template = this.templates[0]; + this.templateData = this.form.template; + HTTP.get("/templates/diagram", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + if (response.data.length) { + this.templates = response.data; + this.form.template = this.templates[0]; + this.templates[this.templates.length] = this.defaultTemplate; + this.applyChange(); + } + }) + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + computed: { + ...mapState("fairwayavailability", [ + "selectedFairwayAvailabilityFeature", + "fwData", + "from", + "to", + "frequency", + "csv", + "depthlimit1", + "depthlimit2", + "widthlimit1", + "widthlimit2" + ]), + legend() { + const d = [this.depthlimit1, this.depthlimit2].sort(); + const w = [this.widthlimit1, this.widthlimit2].sort(); + const lowerBound = [d[0], w[0]].filter(x => x).join(", "); + const upperBound = [d[1], w[1]].filter(x => x).join(", "); + return [ + `> LDC`, + `< ${lowerBound}`, + `< ${upperBound}`, + `>= ${upperBound}` + ]; + }, + dataLink() { + return `data:text/csv;charset=utf-8, ${encodeURIComponent(this.csv)}`; + }, + csvFileName() { + return `${this.$gettext("fairwayavailability")}-${ + this.featureName + }-${filters.surveyDate(this.fromDate)}-${filters.surveyDate( + this.toDate + )}-${this.$gettext(this.frequency)}-.csv`; + }, + frequencyToRange() { + const frequencies = { + [FREQUENCIES.MONTHLY]: [-33, 33], + [FREQUENCIES.QUARTERLY]: [-93, 93], + [FREQUENCIES.YEARLY]: [-370, 370] + }; + return frequencies[this.frequency]; + }, + fromDate() { + return this.from; + }, + toDate() { + return this.to; + }, + availability() { + return this.plainAvailability; + }, + title() { + return `Available Fairway Depth: ${ + this.featureName + } (${filters.surveyDate(this.fromDate)} - ${filters.surveyDate( + this.toDate + )}) ${this.$gettext(this.frequency)}`; + }, + featureName() { + if (this.selectedFairwayAvailabilityFeature == null) return ""; + return this.selectedFairwayAvailabilityFeature.properties.name; + } + }, + methods: { + applyChange() { + if (this.form.template.hasOwnProperty("properties")) { + this.templateData = this.defaultTemplate; + return; + } + if (this.form.template) { + HTTP.get("/templates/diagram/" + this.form.template.name, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + this.templateData = response.data.template_data; + }) + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + downloadPDF() { + this.pdf.doc = new jsPDF( + "l", + "mm", + this.templateData.properties.paperSize + ); + // pdf width and height in millimeter (landscape) + this.pdf.width = + this.templateData.properties.paperSize === "a3" ? 420 : 297; + this.pdf.height = + this.templateData.properties.paperSize === "a3" ? 297 : 210; + if (this.templateData) { + // default values if some are missing in template + let defaultFontSize = 11, + defaultColor = "black", + defaultWidth = 70, + defaultTextColor = "black", + defaultBorderColor = "white", + defaultBgColor = "white", + defaultRounding = 2, + defaultPadding = 2, + defaultOffset = { x: 0, y: 0 }; + this.templateData.elements.forEach(e => { + switch (e.type) { + case "diagram": { + this.addDiagram( + e.position, + e.offset || defaultOffset, + e.width, + e.height + ); + break; + } + case "diagramtitle": { + let title = `Available Fairway Depth: ${this.featureName}`; + this.addDiagramTitle( + e.position, + e.offset || defaultOffset, + e.fontsize || defaultFontSize, + e.color || defaultColor, + title + ); + break; + } + case "diagramlegend": { + this.addDiagramLegend( + e.position, + e.offset || defaultOffset, + e.color || defaultColor + ); + break; + } + case "text": { + this.addText( + e.position, + e.offset || defaultOffset, + e.width || defaultWidth, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + e.text || "" + ); + break; + } + case "image": { + this.addImage( + e.url, + e.format || "", + e.position, + e.offset || defaultOffset, + e.width || 90, + e.height || 60 + ); + break; + } + case "box": { + this.addBox( + e.position, + e.offset || defaultOffset, + e.width || 90, + e.height || 60, + e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, + e.color || defaultBgColor, + e.brcolor || defaultBorderColor + ); + break; + } + case "textbox": { + this.addTextBox( + e.position, + e.offset || defaultOffset, + e.width, + e.height, + e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, + e.padding || defaultPadding, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + e.background || defaultBgColor, + e.text || "", + e.brcolor || defaultBorderColor + ); + break; + } + } + }); + } + this.pdf.doc.save(`Available Fairway Depth: ${this.featureName}`); + }, + addDiagram(position, offset, width, height) { + let x = offset.x, + y = offset.y; + var svg = this.$refs.diagramContainer.innerHTML; + if (svg) { + svg = svg.replace(/\r?\n|\r/g, "").trim(); + } + // use default width,height if they are missing in the template definition + if (!width) { + width = this.templateData.properties.paperSize === "a3" ? 380 : 290; + } + if (!height) { + height = this.templateData.properties.paperSize === "a3" ? 130 : 100; + } + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - height; + } + var canvas = document.createElement("canvas"); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight / 2; + canvg(canvas, svg, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true + }); + var imgData = canvas.toDataURL("image/png"); + this.pdf.doc.addImage(imgData, "PNG", x, y, width, height); + }, + addDiagramLegend(position, offset, color) { + let x = offset.x, + y = offset.y; + this.pdf.doc.setFontSize(10); + let width = + (this.pdf.doc.getStringUnitWidth(">= LDC") * 10) / (72 / 25.6) + 15; + // if position is on the right, x needs to be calculate with pdf width and + // the size of the element + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - this.getTextHeight(6); + } + + this.pdf.doc.setTextColor(color); + this.pdf.doc.setDrawColor(this.$options.COLORS.LDC); + this.pdf.doc.setFillColor(this.$options.COLORS.LDC); + this.pdf.doc.roundedRect(x, y, 10, 4, 1.5, 1.5, "FD"); + this.pdf.doc.text(this.legend[0], x + 12, y + 3); + + this.pdf.doc.setDrawColor(this.$options.COLORS.REST[0]); + this.pdf.doc.setFillColor(this.$options.COLORS.REST[0]); + this.pdf.doc.roundedRect(x, y + 5, 10, 4, 1.5, 1.5, "FD"); + this.pdf.doc.text(this.legend[1], x + 12, y + 8); + + this.pdf.doc.setDrawColor(this.$options.COLORS.REST[1]); + this.pdf.doc.setFillColor(this.$options.COLORS.REST[1]); + this.pdf.doc.roundedRect(x, y + 10, 10, 4, 1.5, 1.5, "FD"); + this.pdf.doc.text(this.legend[2], x + 12, y + 13); + + this.pdf.doc.setDrawColor(this.$options.COLORS.HIGHEST); + this.pdf.doc.setFillColor(this.$options.COLORS.HIGHEST); + this.pdf.doc.roundedRect(x, y + 15, 10, 4, 1.5, 1.5, "FD"); + this.pdf.doc.text(this.legend[3], x + 12, y + 18); + }, + legendStyle(index) { + const style = { + 0: `background-color: ${this.$options.COLORS.LDC};`, + 1: `background-color: ${this.$options.COLORS.REST[0]};`, + 2: `background-color: ${this.$options.COLORS.REST[1]};`, + 3: `background-color: ${this.$options.COLORS.HIGHEST};` + }; + return style[index]; + }, + close() { + this.$store.commit("application/paneSetup", "DEFAULT"); + }, + drawDiagram() { + this.dimensions = this.getDimensions({ + main: { top: 20, right: 20, bottom: 110, left: 200 } + }); + this.yScale = d3 + .scaleLinear() + .domain(this.frequencyToRange) + .range([this.dimensions.mainHeight - 30, 0]); + d3.select(".diagram-container svg").remove(); + this.generateDiagramContainer(); + this.drawBars(); + this.drawScaleLabel(); + this.drawScale(); + this.drawTooltip(); + }, + generateDiagramContainer() { + const diagram = d3 + .select(".diagram-container") + .append("svg") + .attr("width", this.dimensions.width) + .attr("height", this.dimensions.mainHeight); + this.diagram = diagram + .append("g") + .attr("transform", `translate(0 ${this.paddingTop})`); + }, + drawTooltip() { + this.diagram + .append("text") + .text("banane") + .attr("font-size", "0.8em") + .attr("opacity", 0) + .attr("id", "tooltip"); + }, + drawBars() { + const everyBar = this.diagram + .selectAll("g") + .data(this.fwData) + .enter() + .append("g") + .attr("transform", (d, i) => { + const dx = this.paddingRight + i * this.spaceBetween; + return `translate(${dx})`; + }); + this.drawSingleBars(everyBar); + this.drawLabelPerBar(everyBar); + }, + drawSingleBars(everyBar) { + this.drawLDC(everyBar); + this.drawHighestLevel(everyBar); + this.drawLowerLevels(everyBar); + }, + drawLowerLevels(everyBar) { + everyBar + .selectAll("g") + .data(d => d.lowerLevels) + .enter() + .append("rect") + .on("mouseover", function() { + d3.select(this).attr("opacity", "0.8"); + d3.select("#tooltip").attr("opacity", 1); + }) + .on("mouseout", function() { + d3.select(this).attr("opacity", 1); + d3.select("#tooltip").attr("opacity", 0); + }) + .on("mousemove", function(d) { + let y = d3.mouse(this)[1]; + const dy = document + .querySelector(".diagram-container") + .getBoundingClientRect().left; + const value = Number.parseFloat(hoursInDays(d.height)).toFixed(2); + d3.select("#tooltip") + .text(value) + .attr("y", y - 10) + .attr("x", d3.event.pageX - dy); + //d3.event.pageX gives coordinates relative to SVG + //dy gives offset of svg on page + }) + .attr("y", d => { + return 2 * this.yScale(0) - this.yScale(hoursInDays(d.translateY)); + }) + .attr("height", d => { + return this.yScale(0) - this.yScale(hoursInDays(d.height)); + }) + .attr("x", this.ldcoffset) + .attr("width", this.barsWidth - this.ldcoffset) + .attr("fill", (d, i) => { + return this.$options.COLORS.REST[i]; + }); + }, + fnheight(name) { + return d => this.yScale(0) - this.yScale(hoursInDays(d[name])); + }, + drawLDC(everyBar) { + const height = this.fnheight("ldc"); + everyBar + .append("rect") + .on("mouseover", function() { + d3.select(this).attr("opacity", "0.8"); + d3.select("#tooltip").attr("opacity", 1); + }) + .on("mouseout", function() { + d3.select(this).attr("opacity", 1); + d3.select("#tooltip").attr("opacity", 0); + }) + .on("mousemove", function(d) { + let y = d3.mouse(this)[1]; + const dy = document + .querySelector(".diagram-container") + .getBoundingClientRect().left; + const value = Number.parseFloat(hoursInDays(d.ldc)).toFixed(2); + d3.select("#tooltip") + .text(value) + .attr("y", y - 50) + .attr("x", d3.event.pageX - dy); + //d3.event.pageX gives coordinates relative to SVG + //dy gives offset of svg on page + }) + .attr("y", this.yScale(0)) + .attr("height", height) + .attr("x", -this.ldcoffset) + .attr("width", this.barsWidth - this.ldcoffset) + .attr("transform", d => `translate(0 ${-1 * height(d)})`) + .attr("fill", this.$options.COLORS.LDC) + .attr("id", "ldc"); + }, + drawHighestLevel(everyBar) { + const height = this.fnheight("highestLevel"); + everyBar + .append("rect") + .on("mouseover", function() { + d3.select(this).attr("opacity", "0.8"); + d3.select("#tooltip").attr("opacity", 1); + }) + .on("mouseout", function() { + d3.select(this).attr("opacity", 1); + d3.select("#tooltip").attr("opacity", 0); + }) + .on("mousemove", function(d) { + let y = d3.mouse(this)[1]; + const dy = document + .querySelector(".diagram-container") + .getBoundingClientRect().left; + const value = Number.parseFloat(hoursInDays(d.highestLevel)).toFixed( + 2 + ); + d3.select("#tooltip") + .text(value) + .attr("y", y - 50) + .attr("x", d3.event.pageX - dy); + //d3.event.pageX gives coordinates relative to SVG + //dy gives offset of svg on page + }) + .attr("y", this.yScale(0)) + .attr("x", this.ldcoffset) + .attr("height", height) + .attr("width", this.barsWidth - this.ldcoffset) + .attr("transform", d => `translate(0 ${-1 * height(d)})`) + .attr("fill", this.$options.COLORS.HIGHEST); + }, + drawLabelPerBar(everyBar) { + everyBar + .append("text") + .text(d => d.label) + .attr("y", this.labelPaddingTop); + }, + drawScaleLabel() { + const center = this.dimensions.mainHeight / 2; + this.diagram + .append("text") + .text(this.$options.LEGEND) + .attr("text-anchor", "middle") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "1em") + .attr("transform", `translate(0, ${center}), rotate(-90)`); + }, + drawScale() { + const yAxis = d3.axisLeft().scale(this.yScale); + this.diagram + .append("g") + .attr("transform", `translate(${this.scalePaddingLeft})`) + .call(yAxis) + .selectAll(".tick text") + .attr("fill", "black") + .select(function() { + return this.parentNode; + }) + .selectAll(".tick line") + .attr("stroke", "black"); + this.diagram.selectAll(".domain").attr("stroke", "black"); + } + }, + watch: { + fwData() { + this.drawDiagram(); + } + }, + LEGEND: app.$gettext("Sum of days"), + COLORS: { + LDC: "#cdcdcd", + HIGHEST: "#3675ff", + REST: ["#782121", "#ff6c6c", "#ffaaaa"] + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/AvailableFairwayDepthDialogue.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,591 @@ +<template> + <div + :class="[ + 'box ui-element rounded bg-white text-nowrap', + { expanded: showFairwayDepth } + ]" + > + <div style="width: 18rem"> + <UIBoxHeader icon="chart-line" :title="label" :closeCallback="close" /> + <div class="box-body"> + <UISpinnerOverlay v-if="loading" /> + <div class="mb-2 d-flex justify-content-between align-items-center"> + <div> + <input :value="$options.BOTTLENECK" type="radio" v-model="type" /> + <small class="ml-1 text-muted"> + <translate>Bottlenecks</translate> + </small> + </div> + <div> + <input :value="$options.STRETCH" type="radio" v-model="type" /> + <small class="ml-1 text-muted"> + <translate>Stretches</translate> + </small> + </div> + <div> + <input :value="$options.SECTION" type="radio" v-model="type" /> + <small class="ml-1 text-muted"> + <translate>Sections</translate> + </small> + </div> + </div> + <select + v-if="type === $options.BOTTLENECK" + @change="entrySelected" + class="form-control font-weight-bold" + v-model="selectedEntry" + > + <option :value="null">{{ placeholder }}</option> + <optgroup + v-for="(bottlenecksForCountry, cc) in orderedBottlenecks" + :key="cc" + :label="cc" + > + <option + v-for="bn in bottlenecksForCountry" + :key="bn.properties.name" + :value="bn" + > + {{ bn.properties.name }} + </option> + </optgroup> + </select> + <select + v-else-if="type === $options.STRETCH" + @change="entrySelected" + class="form-control font-weight-bold" + v-model="selectedEntry" + > + <option :value="null">{{ placeholder }}</option> + <option + v-for="stretch in stretches" + :value="stretch" + :key="stretch.id" + > + {{ stretch.properties.name }} + </option> + </select> + <select + v-else-if="type === $options.SECTION" + @change="entrySelected" + class="form-control font-weight-bold" + v-model="selectedEntry" + > + <option :value="null">{{ placeholder }}</option> + <option + v-for="section in sections" + :value="section" + :key="section.id" + > + {{ section.properties.name }} + </option> + </select> + <div class="d-flex mt-2"> + <div class="d-flex flex-column w-50 mr-1"> + <small class="my-auto text-muted"> + <translate>Type</translate> + </small> + <select + v-model="selectedFrequency" + class="form-control form-control-sm" + > + <option + v-for="(option, index) in $options.FREQUENCIES" + :value="option" + :key="index" + > + <translate>{{ option }}</translate> + </option> + </select> + </div> + <div class="d-flex flex-column w-50 ml-1"> + <small class="my-auto text-muted"><translate>LOS</translate></small> + <select v-model="los" class="form-control form-control-sm"> + <option value="1">1</option> + <option value="2">2</option> + <option value="3">3</option> + </select> + </div> + </div> + <div class="d-flex mt-2"> + <div class="d-flex flex-column w-50 mr-1"> + <small for="from" class="my-auto text-muted"> + <translate>Date from</translate> + </small> + <input + id="from" + v-model="fromDate" + class="form-control form-control-sm" + type="date" + /> + </div> + <div class="d-flex flex-column w-50 ml-1"> + <small for="to" class="my-auto text-muted"> + <translate>Date to</translate> + </small> + <input + id="to" + v-model="toDate" + class="form-control form-control-sm" + type="date" + /> + </div> + </div> + + <div v-if="depthLimitVisible" class="d-flex mt-2" :key="1"> + <div class="d-flex flex-column w-50 mr-1"> + <small for="from" class="my-auto text-muted"> + <translate>Depthlimit 1 (in cm)</translate> + </small> + <input + id="depthlimit1" + v-model.number="depthLimit1" + class="form-control form-control-sm" + type="number" + min="0" + /> + </div> + <div + v-if="depthLimitVisible" + class="d-flex flex-column w-50 ml-1" + :key="2" + > + <small for="to" class="my-auto text-muted"> + <translate>Depthlimit 2 ( in cm)</translate> + </small> + <input + id="depthlimit2" + v-model.number="depthLimit2" + class="form-control form-control-sm" + type="number" + min="0" + /> + </div> + </div> + <div v-if="widthLimitVisible" class="d-flex mt-2" :key="3"> + <div class="d-flex flex-column w-50 mr-1"> + <small for="from" class="my-auto text-muted"> + <translate>Widthlimit 1</translate> + </small> + <input + id="widthLimit" + v-model.number="widthLimit1" + class="form-control form-control-sm" + type="number" + min="0" + /> + </div> + <div + v-if="widthLimitVisible" + class="d-flex flex-column w-50 mr-1" + :key="4" + > + <small for="from" class="my-auto text-muted"> + <translate>Widthlimit 2</translate> + </small> + <input + id="widthLimit" + v-model.number="widthLimit2" + class="form-control form-control-sm" + type="number" + min="0" + /> + </div> + </div> + + <div class="mt-3"> + <button + @click="openFairwaydepthDiagram" + :disabled="!isComplete" + class="btn btn-info btn-sm d-block w-100" + > + <translate>Available fairway depth</translate> + </button> + <button + @click="openFairwaydepthLNWLDiagram" + :disabled="!isComplete" + class="btn btn-info btn-sm d-block w-100 mt-2" + > + <translate>Available fairway depth vs LNWL</translate> + </button> + </div> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +input, +select { + font-size: 0.8em; +} +</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> + * Thomas Junk <thomas.junk@intevation.de> + */ + +import app from "@/main"; +import { displayError } from "@/lib/errors"; +import { mapState, mapGetters } from "vuex"; +import { FREQUENCIES, LIMITINGFACTORS } from "@/store/fairwayavailability"; + +export default { + data() { + return { + loading: false + }; + }, + computed: { + ...mapState("application", [ + "showFairwayDepth", + "paneSetup", + "showProfiles" + ]), + ...mapState("fairwayavailability", [ + "selectedFairwayAvailabilityFeature", + "from", + "to", + "frequency", + "LOS", + "depthlimit1", + "depthlimit2", + "widthlimit1", + "widthlimit2" + ]), + ...mapState("imports", [ + "stretches", + "sections", + "selectedStretchId", + "selectedSectionId" + ]), + ...mapState("bottlenecks", ["bottlenecksList", "selectedBottleneck"]), + ...mapGetters("map", ["openLayersMap"]), + ...mapGetters("bottlenecks", [ + "orderedBottlenecks", + "limitingFactorsPerBottleneck" + ]), + depthLimitVisible() { + if (this.type !== this.$options.BOTTLENECK) return true; + if ( + this.selectedEntry && + this.limitingFactorsPerBottleneck[this.selectedEntry.properties.name] == + this.$options.LIMITINGFACTORS.DEPTH + ) + return true; + return false; + }, + widthLimitVisible() { + if (this.type !== this.$options.BOTTLENECK) return true; + if ( + this.selectedEntry && + this.limitingFactorsPerBottleneck[this.selectedEntry.properties.name] == + this.$options.LIMITINGFACTORS.WIDTH + ) + return true; + }, + limitingFactor() { + if (this.type !== this.$options.BOTTLENECK) return; + if (this.selectedEntry) + return this.limitingFactorsPerBottleneck[ + this.selectedEntry.properties.name + ]; + }, + isComplete() { + return ( + this.from !== null && + this.to !== null && + this.frequency !== null && + this.los !== null && + this.selectedFairwayAvailabilityFeature !== null + ); + }, + type: { + get() { + return this.$store.state.fairwayavailability.type; + }, + set(type) { + this.$store.commit("fairwayavailability/type", type); + } + }, + los: { + get() { + return this.LOS; + }, + set(value) { + this.$store.commit("fairwayavailability/setLOS", value); + } + }, + fromDate: { + get() { + return this.from; + }, + set(value) { + this.$store.commit("fairwayavailability/setFrom", value); + } + }, + toDate: { + get() { + return this.to; + }, + set(value) { + this.$store.commit("fairwayavailability/setTo", value); + } + }, + depthLimit1: { + get() { + return this.depthlimit1; + }, + set(value) { + this.$store.commit("fairwayavailability/setDepthlimit1", value); + } + }, + depthLimit2: { + get() { + return this.depthlimit2; + }, + set(value) { + this.$store.commit("fairwayavailability/setDepthlimit2", value); + } + }, + widthLimit1: { + get() { + return this.widthlimit1; + }, + set(value) { + this.$store.commit("fairwayavailability/setWidthlimit1", value); + } + }, + widthLimit2: { + get() { + return this.widthlimit2; + }, + set(value) { + this.$store.commit("fairwayavailability/setWidthlimit2", value); + } + }, + selectedFrequency: { + get() { + return this.frequency; + }, + set(value) { + this.$store.commit("fairwayavailability/setFrequency", value); + } + }, + selectedEntry: { + get() { + return this.selectedFairwayAvailabilityFeature; + }, + set(feature) { + this.$store.commit( + "fairwayavailability/setSelectedFairwayAvailability", + feature + ); + } + }, + label() { + return this.$gettext("Available fairway depth"); + }, + placeholder() { + if (this.type === this.$options.BOTTLENECK) + return this.$gettext("Select bottleneck"); + if (this.type === this.$options.STRETCH) + return this.$gettext("Select stretch"); + return this.$gettext("Select section"); + } + }, + watch: { + selectedBottleneck() { + this.type = this.$options.BOTTLENECK; + this.setSelectedBottleneck(); + }, + selectedStretchId() { + this.type = this.$options.STRETCH; + this.setSelectedStretch(); + }, + selectedSectionId() { + this.type = this.$options.SECTION; + this.setSelectedSection(); + }, + type(type) { + if (type === this.$options.BOTTLENECK && this.selectedBottleneck) { + this.openLayersMap() + .getLayer("BOTTLENECKS") + .setVisible(true); + this.setSelectedBottleneck(); + } else if (type === this.$options.STRETCH && this.selectedStretchId) { + this.openLayersMap() + .getLayer("STRETCHES") + .setVisible(true); + this.setSelectedStretch(); + } else if (type === this.$options.SECTION && this.selectedSectionId) { + this.openLayersMap() + .getLayer("SECTIONS") + .setVisible(true); + this.setSelectedSection(); + } else { + this.$store.commit( + "fairwayavailability/setSelectedFairwayAvailability", + null + ); + } + }, + showFairwayDepth() { + if (this.showFairwayDepth) { + this.loading = true; + Promise.all([ + this.$store.dispatch("bottlenecks/loadBottlenecks"), + this.$store.dispatch("bottlenecks/loadBottlenecksList"), + this.$store.dispatch("imports/loadStretches"), + this.$store.dispatch("imports/loadSections") + ]) + .then(() => { + if (this.selectedBottleneck) this.setSelectedBottleneck(); + }) + .finally(() => (this.loading = false)); + } + } + }, + methods: { + openFairwaydepthLNWLDiagram() { + this.loading = true; + this.$store + .dispatch("fairwayavailability/loadAvailableFairwayDepthLNWLDiagram", { + feature: this.selectedFairwayAvailabilityFeature, + from: this.from, + to: this.to, + frequency: this.frequency, + LOS: this.los, + type: this.type, + depthLimit1: this.depthLimit1, + depthLimit2: this.depthLimit2, + widthLimit1: this.widthLimit1, + widthLimit2: this.widthLimit2, + limitingFactor: this.limitingFactor + }) + .then(() => { + this.$store.commit( + "application/paneSetup", + "AVAILABLEFAIRWAYDEPTHLNWL" + ); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => { + this.loading = false; + }); + }, + openFairwaydepthDiagram() { + this.loading = true; + this.$store + .dispatch("fairwayavailability/loadAvailableFairwayDepth", { + feature: this.selectedFairwayAvailabilityFeature, + from: this.from, + to: this.to, + frequency: this.frequency, + LOS: this.los, + type: this.type, + depthLimit1: this.depthLimit1, + depthLimit2: this.depthLimit2, + widthLimit1: this.widthLimit1, + widthLimit2: this.widthLimit2, + limitingFactor: this.limitingFactor + }) + .then(() => { + this.$store.commit("application/paneSetup", "AVAILABLEFAIRWAYDEPTH"); + }) + .catch(error => { + console.log(error); + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => { + this.loading = false; + }); + }, + close() { + this.$store.commit("application/showFairwayDepth", false); + this.$store.commit("application/showFairwayDepthLNWL", false); + }, + entrySelected() { + if (this.type === this.$options.BOTTLENECK) { + this.openLayersMap() + .getLayer("BOTTLENECKS") + .setVisible(true); + if (this.showProfiles) { + this.$store.dispatch( + "bottlenecks/setSelectedBottleneck", + this.selectedFairwayAvailabilityFeature.properties.name + ); + } + } + if (this.type === this.$options.STRETCH) { + this.openLayersMap() + .getLayer("STRETCHES") + .setVisible(true); + } + if (this.type === this.$options.SECTION) { + this.openLayersMap() + .getLayer("SECTIONS") + .setVisible(true); + } + if (this.selectedFairwayAvailabilityFeature) { + this.$store.dispatch("map/moveToFeauture", { + feature: this.selectedFairwayAvailabilityFeature, + zoom: 17, + preventZoomOut: true + }); + } + }, + setSelectedBottleneck() { + const bn = this.bottlenecksList.filter( + x => x.properties.name === this.selectedBottleneck + )[0]; + this.$store.commit( + "fairwayavailability/setSelectedFairwayAvailability", + bn + ); + }, + setSelectedStretch() { + const stretch = this.stretches.find(x => x.id === this.selectedStretchId); + this.$store.commit( + "fairwayavailability/setSelectedFairwayAvailability", + stretch + ); + }, + setSelectedSection() { + const section = this.sections.find(x => x.id === this.selectedSectionId); + this.$store.commit( + "fairwayavailability/setSelectedFairwayAvailability", + section + ); + } + }, + BOTTLENECK: "bottleneck", + SECTION: "section", + STRETCH: "stretch", + AVAILABLEFAIRWAYDEPTH: app.$gettext("Available Fairway Depth"), + FREQUENCIES: FREQUENCIES, + LIMITINGFACTORS: LIMITINGFACTORS +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/fairway/AvailableFairwayDepthLNWL.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,644 @@ +<template> + <div class="d-flex flex-column flex-fill"> + <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" /> + <UISpinnerOverlay v-if="loading" /> + <div class="d-flex flex-fill"> + <DiagramLegend> + <div v-for="(entry, index) in legendLNWL" :key="index" class="legend"> + <span + :style=" + `${legendStyle( + index + )}; border-radius: 0.25rem; width: 40px; height: 20px;` + " + ></span> + {{ entry }} + </div> + <div> + <select + @change="applyChange" + v-model="form.template" + class="form-control d-block custom-select-sm w-100 mt-2" + > + <option + v-for="template in templates" + :value="template" + :key="template.name" + > + {{ template.name }} + </option> + </select> + <button + @click="downloadPDF" + type="button" + class="btn btn-sm btn-info d-block w-100 mt-2" + > + <translate>Export to PDF</translate> + </button> + <a + :href="dataLink" + :download="csvFileName" + class="mt-2 btn btn-sm btn-info w-100" + >Download CSV</a + > + </div> + </DiagramLegend> + <div + ref="diagramContainer" + :id="containerId" + class="mx-auto my-auto diagram-container" + ></div> + </div> + </div> +</template> + +<style></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> + * Fadi Abbud <fadi.abbud@intevation.de> + */ +import * as d3 from "d3"; +import app from "@/main"; +import debounce from "debounce"; +import { diagram } from "@/lib/mixins"; +import { mapState } from "vuex"; +import filters from "@/lib/filters.js"; +import jsPDF from "jspdf"; +import canvg from "canvg"; +import { pdfgen } from "@/lib/mixins"; +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors"; + +export default { + mixins: [diagram, pdfgen], + components: { + DiagramLegend: () => import("@/components/DiagramLegend") + }, + data() { + return { + containerId: "availablefairwaydepthlnwl", + loading: false, + labelPaddingTop: 15, + scalePaddingLeft: 50, + paddingTop: 10, + diagram: null, + yScale: null, + dimensions: null, + pdf: { + doc: null, + width: null, + height: null + }, + form: { + template: null + }, + templateData: null, + templates: [], + defaultTemplate: { + name: "Default", + properties: { + paperSize: "a4" + }, + elements: [ + { + type: "diagram", + position: "topleft", + offset: { x: 20, y: 60 }, + width: 290, + height: 100 + }, + { + type: "diagramtitle", + position: "topleft", + offset: { x: 70, y: 20 }, + fontsize: 20, + color: "steelblue" + }, + { + type: "diagramlegend", + position: "topleft", + offset: { x: 30, y: 160 }, + color: "black" + } + ] + } + }; + }, + created() { + window.addEventListener("resize", debounce(this.drawDiagram), 200); + }, + mounted() { + this.drawDiagram(); + this.templates[0] = this.defaultTemplate; + this.form.template = this.templates[0]; + this.templateData = this.form.template; + HTTP.get("/templates/diagram", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + if (response.data.length) { + this.templates = response.data; + this.form.template = this.templates[0]; + this.templates[this.templates.length] = this.defaultTemplate; + this.applyChange(); + } + }) + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + computed: { + ...mapState("fairwayavailability", [ + "selectedFairwayAvailabilityFeature", + "fwLNWLData", + "from", + "to", + "frequency", + "csv", + "depthlimit1", + "depthlimit2", + "widthlimit1", + "widthlimit2" + ]), + legendLNWL() { + const d = [this.depthlimit1, this.depthlimit2].sort(); + const w = [this.widthlimit1, this.widthlimit2].sort(); + const lowerBound = [d[0], w[0]].filter(x => x).join(", "); + const upperBound = [d[1], w[1]].filter(x => x).join(", "); + return [ + `> LDC`, + `< ${lowerBound}`, + `< ${upperBound}`, + `>= ${upperBound}` + ]; + }, + dataLink() { + return `data:text/csv;charset=utf-8, ${encodeURIComponent(this.csv)}`; + }, + csvFileName() { + return `${this.$gettext("fairwayavailabilityLNWL")}-${ + this.featureName + }-${filters.surveyDate(this.fromDate)}-${filters.surveyDate( + this.toDate + )}-${this.$gettext(this.frequency)}-.csv`; + }, + fromDate() { + return this.from; + }, + toDate() { + return this.to; + }, + availability() { + return this.plainAvailability; + }, + title() { + return `Available Fairway Depth vs LNWL: ${ + this.featureName + } (${filters.surveyDate(this.fromDate)} - ${filters.surveyDate( + this.toDate + )}) ${this.$gettext(this.frequency)}`; + }, + featureName() { + if (this.selectedFairwayAvailabilityFeature == null) return ""; + return this.selectedFairwayAvailabilityFeature.properties.name; + }, + widthPerItem() { + return Math.min( + (this.dimensions.width - this.scalePaddingLeft) / + this.fwLNWLData.length, + 180 + ); + }, + ldcWidth() { + return this.widthPerItem * 0.3; + }, + afdWidth() { + return this.widthPerItem * 0.5; + }, + spaceBetween() { + return this.widthPerItem * 0.2; + } + }, + methods: { + legendStyle(index) { + const style = { + 0: `background-color: ${this.$options.LWNLCOLORS.LDC};`, + 1: `background-color: ${this.$options.AFDCOLORS[2]};`, + 2: `background-color: ${this.$options.AFDCOLORS[1]};`, + 3: `background-color: ${this.$options.AFDCOLORS[0]};` + }; + return style[index]; + }, + applyChange() { + if (this.form.template.hasOwnProperty("properties")) { + this.templateData = this.defaultTemplate; + return; + } + if (this.form.template) { + HTTP.get("/templates/diagram/" + this.form.template.name, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + this.templateData = response.data.template_data; + }) + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + downloadPDF() { + this.pdf.doc = new jsPDF( + "l", + "mm", + this.templateData.properties.paperSize + ); + // pdf width and height in millimeter (landscape) + this.pdf.width = + this.templateData.properties.paperSize === "a3" ? 420 : 297; + this.pdf.height = + this.templateData.properties.paperSize === "a3" ? 297 : 210; + if (this.templateData) { + // default values if some are missing in template + let defaultFontSize = 11, + defaultColor = "black", + defaultWidth = 70, + defaultTextColor = "black", + defaultBorderColor = "white", + defaultBgColor = "white", + defaultRounding = 2, + defaultPadding = 2, + defaultOffset = { x: 0, y: 0 }; + this.templateData.elements.forEach(e => { + switch (e.type) { + case "diagram": { + this.addDiagram( + e.position, + e.offset || defaultOffset, + e.width, + e.height + ); + break; + } + case "diagramtitle": { + let title = `Available Fairway Depth vs LNWL: ${ + this.featureName + }`; + this.addDiagramTitle( + e.position, + e.offset || defaultOffset, + e.fontsize || defaultFontSize, + e.color || defaultColor, + title + ); + break; + } + case "diagramlegend": { + this.addDiagramLegend( + e.position, + e.offset || defaultOffset, + e.color || defaultColor + ); + break; + } + case "text": { + this.addText( + e.position, + e.offset || defaultOffset, + e.width || defaultWidth, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + e.text || "" + ); + break; + } + case "image": { + this.addImage( + e.url, + e.format || "", + e.position, + e.offset || defaultOffset, + e.width || 90, + e.height || 60 + ); + break; + } + case "box": { + this.addBox( + e.position, + e.offset || defaultOffset, + e.width || 90, + e.height || 60, + e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, + e.color || defaultBgColor, + e.brcolor || defaultBorderColor + ); + break; + } + case "textbox": { + this.addTextBox( + e.position, + e.offset || defaultOffset, + e.width, + e.height, + e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, + e.padding || defaultPadding, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + e.background || defaultBgColor, + e.text || "", + e.brcolor || defaultBorderColor + ); + break; + } + } + }); + } + this.pdf.doc.save(`Available Fairway Depth LNWL: ${this.featureName}`); + }, + addDiagram(position, offset, width, height) { + let x = offset.x, + y = offset.y; + var svg = this.$refs.diagramContainer.innerHTML; + if (svg) { + svg = svg.replace(/\r?\n|\r/g, "").trim(); + } + // use default width,height if they are missing in the template definition + if (!width) { + width = this.templateData.properties.paperSize === "a3" ? 380 : 290; + } + if (!height) { + height = this.templateData.properties.paperSize === "a3" ? 130 : 100; + } + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - height; + } + var canvas = document.createElement("canvas"); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight / 2; + canvg(canvas, svg, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true + }); + var imgData = canvas.toDataURL("image/png"); + this.pdf.doc.addImage(imgData, "PNG", x, y, width, height); + }, + addDiagramLegend(position, offset, color) { + let x = offset.x, + y = offset.y; + this.pdf.doc.setFontSize(10); + let width = + (this.pdf.doc.getStringUnitWidth(">= LDC") * 10) / (72 / 25.6) + 15; + // if position is on the right, x needs to be calculate with pdf width and + // the size of the element + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - this.getTextHeight(6); + } + this.pdf.doc.setTextColor(color); + this.pdf.doc.setDrawColor(this.$options.LWNLCOLORS.LDC); + this.pdf.doc.setFillColor(this.$options.LWNLCOLORS.LDC); + this.pdf.doc.roundedRect(x, y, 10, 4, 1.5, 1.5, "FD"); + this.pdf.doc.text(this.legendLNWL[0], x + 12, y + 3); + + this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[2]); + this.pdf.doc.setFillColor(this.$options.AFDCOLORS[2]); + this.pdf.doc.roundedRect(x, y + 5, 10, 4, 1.5, 1.5, "FD"); + this.pdf.doc.text(this.legendLNWL[1], x + 12, y + 8); + + this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[1]); + this.pdf.doc.setFillColor(this.$options.AFDCOLORS[1]); + this.pdf.doc.roundedRect(x, y + 10, 10, 4, 1.5, 1.5, "FD"); + this.pdf.doc.text(this.legendLNWL[2], x + 12, y + 13); + + this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[0]); + this.pdf.doc.setFillColor(this.$options.AFDCOLORS[0]); + this.pdf.doc.roundedRect(x, y + 15, 10, 4, 1.5, 1.5, "FD"); + this.pdf.doc.text(this.legendLNWL[3], x + 12, y + 18); + }, + close() { + this.$store.commit("application/paneSetup", "DEFAULT"); + }, + drawDiagram() { + this.dimensions = this.getDimensions({ + main: { top: 20, right: 20, bottom: 110, left: 200 } + }); + this.yScale = d3 + .scaleLinear() + .domain([0, 100]) + .range([this.dimensions.mainHeight - 30, 0]); + d3.select(".diagram-container svg").remove(); + this.generateDiagramContainer(); + this.drawBars(); + this.drawScaleLabel(); + this.drawScale(); + this.drawTooltip(); + }, + drawTooltip() { + this.diagram + .append("text") + .text("") + .attr("font-size", "0.8em") + .attr("opacity", 0) + .attr("id", "tooltip"); + }, + generateDiagramContainer() { + const diagram = d3 + .select(".diagram-container") + .append("svg") + .attr("width", this.dimensions.width) + .attr("height", this.dimensions.mainHeight); + this.diagram = diagram + .append("g") + .attr("transform", `translate(0 ${this.paddingTop})`); + }, + drawBars() { + if (this.fwLNWLData) { + this.fwLNWLData.forEach((data, i) => { + this.drawLNWL(data, i); + this.drawAFD(data, i); + this.drawLabel(data.date, i); + }); + } + }, + drawLabel(date, i) { + this.diagram + .append("text") + .text(date) + .attr("text-anchor", "middle") + .attr( + "transform", + `translate(${this.scalePaddingLeft + + this.widthPerItem * i + + this.widthPerItem / 2} ${this.dimensions.mainHeight - 15})` + ); + }, + drawAFD(data, i) { + let afd = this.diagram + .append("g") + .attr( + "transform", + `translate(${this.scalePaddingLeft + + this.spaceBetween / 2 + + this.widthPerItem * i + + this.ldcWidth})` + ); + afd + .selectAll("rect") + .data([data.above, data.between, data.below]) + .enter() + .append("rect") + .on("mouseover", function() { + d3.select(this).attr("opacity", "0.8"); + d3.select("#tooltip").attr("opacity", 1); + }) + .on("mouseout", function() { + d3.select(this).attr("opacity", 1); + d3.select("#tooltip").attr("opacity", 0); + }) + .on("mousemove", function(d) { + let y = d3.mouse(this)[1]; + const dy = document + .querySelector(".diagram-container") + .getBoundingClientRect().left; + d3.select("#tooltip") + .text(d.toFixed(2)) + .attr("y", y - 10) + .attr("x", d3.event.pageX - dy); + //d3.event.pageX gives coordinates relative to SVG + //dy gives offset of svg on page + }) + .attr("height", d => { + return this.yScale(0) - this.yScale(d); + }) + .attr("y", (d, i) => { + if (i === 0) { + return this.yScale(d); + } + if (i === 1) { + return this.yScale(data.above + d); + } + if (i === 2) { + return this.yScale(data.above + data.between + d); + } + }) + .attr("width", this.afdWidth) + .attr("fill", (d, i) => { + return this.$options.AFDCOLORS[i]; + }); + }, + drawLNWL(data, i) { + let lnwl = this.diagram + .append("g") + .attr( + "transform", + `translate(${this.scalePaddingLeft + + this.spaceBetween / 2 + + this.widthPerItem * i})` + ); + lnwl + .append("rect") + .datum([data.ldc]) + .on("mouseover", function() { + d3.select(this).attr("opacity", "0.8"); + d3.select("#tooltip").attr("opacity", 1); + }) + .on("mouseout", function() { + d3.select(this).attr("opacity", 1); + d3.select("#tooltip").attr("opacity", 0); + }) + .on("mousemove", function(d) { + let y = d3.mouse(this)[1]; + const dy = document + .querySelector(".diagram-container") + .getBoundingClientRect().left; + d3.select("#tooltip") + .text(d[0].toFixed(2)) + .attr("y", y - 10) + .attr("x", d3.event.pageX - dy); + //d3.event.pageX gives coordinates relative to SVG + //dy gives offset of svg on page + }) + .attr("height", d => { + return this.yScale(0) - this.yScale(d); + }) + .attr("y", d => { + return this.yScale(d); + }) + .attr("width", this.ldcWidth) + .attr("fill", () => { + return this.$options.LWNLCOLORS.LDC; + }); + }, + drawScaleLabel() { + const center = this.dimensions.mainHeight / 2; + this.diagram + .append("text") + .text(this.$options.LEGEND) + .attr("text-anchor", "middle") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "1em") + .attr("transform", `translate(0, ${center}), rotate(-90)`); + }, + drawScale() { + const yAxis = d3.axisLeft().scale(this.yScale); + this.diagram + .append("g") + .attr("transform", `translate(${this.scalePaddingLeft})`) + .call(yAxis) + .selectAll(".tick text") + .attr("fill", "black") + .select(function() { + return this.parentNode; + }) + .selectAll(".tick line") + .attr("stroke", "black"); + this.diagram.selectAll(".domain").attr("stroke", "black"); + } + }, + watch: { + fwLNWLData() { + this.drawDiagram(); + } + }, + LEGEND: app.$gettext("Percent"), + AFDCOLORS: ["#3636ff", "#f49b7f", "#e15472"], + LWNLCOLORS: { + LDC: "#97ddf3", + HDC: "#43FFE1" + } +}; +</script>
--- a/client/src/components/fairway/Fairwayprofile.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/fairway/Fairwayprofile.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,5 +1,65 @@ <template> - <div class="fairwayprofile m-3 mt-0 bg-white flex-grow-1"></div> + <div class="d-flex flex-column flex-fill"> + <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" /> + <div class="d-flex flex-fill"> + <DiagramLegend> + <div class="legend"> + <span + style="background-color: #5995ff; width: 20px; height: 20px;" + ></span> + Water + </div> + <div class="legend"> + <span + style="background-color: #1f4fff; width: 20px; height: 20px;" + ></span> + Fairway + </div> + <div class="legend"> + <span + style="width: 14px; height: 14px; background-color: #4a2f06; border: solid 3px black; background-clip: padding-box; box-sizing: content-box;" + ></span> + Sediment + </div> + <div class="legend"> + <span + style="width: 14px; height: 14px; background-color: rgba(74, 47, 6, 0.6); border: solid 3px #943007; background-clip: padding-box; box-sizing: content-box;" + ></span> + Sediment (Compare) + </div> + <div> + <select + v-model="form.template" + @change="applyChange" + class="form-control d-block custom-select-sm w-100" + > + <option + v-for="template in templates" + :value="template" + :key="template.name" + > + {{ template.name }} + </option> + </select> + <button + @click="downloadPDF" + type="button" + class="btn btn-sm btn-info d-block w-100 mt-2" + > + <translate>Export to PDF</translate> + </button> + </div> + </DiagramLegend> + <div + ref="diagramContainer" + class="d-flex flex-fill justify-content-center align-items-center diagram-container" + > + <div v-if="!fairwayData"> + <translate>No data available.</translate> + </div> + </div> + </div> + </div> </template> <script> @@ -16,32 +76,78 @@ * Author(s): * Thomas Junk <thomas.junk@intevation.de> * Markus Kottländer <markus.kottlaender@intevation.de> + * Fadi Abbud <fadi.abbud@intevation.de> */ import * as d3 from "d3"; import { mapState, mapGetters } from "vuex"; import debounce from "debounce"; +import jsPDF from "jspdf"; +import canvg from "canvg"; +import { pdfgen } from "@/lib/mixins"; +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors"; const GROUND_COLOR = "#4A2F06"; +const WATER_COLOR = "#005DFF"; export default { + mixins: [pdfgen], name: "fairwayprofile", + components: { + DiagramLegend: () => import("@/components/DiagramLegend") + }, data() { return { - coordinatesInput: "", - coordinatesSelect: null, - cutLabel: "", - showLabelInput: false, width: null, height: null, margin: { top: 20, - right: 40, - bottom: 30, - left: 40 - } + right: 80, + bottom: 60, + left: 80 + }, + form: { + template: null + }, + templates: [], + defaultTemplate: { + name: "default", + properties: { + paperSize: "a4" + }, + elements: [ + { + type: "diagram", + position: "topleft", + offset: { x: 20, y: 60 }, + width: 290, + height: 100 + }, + { + type: "diagramtitle", + position: "topleft", + offset: { x: 90, y: 30 }, + fontsize: 22, + color: "steelblue" + }, + { + type: "diagramlegend", + position: "topleft", + offset: { x: 30, y: 160 }, + color: "black" + } + ] + }, + pdf: { + doc: null, + width: 32, + height: 297 + }, + templateData: null }; }, computed: { + ...mapGetters("map", ["openLayersMap"]), ...mapGetters("fairwayprofile", ["totalLength"]), ...mapState("fairwayprofile", [ "additionalSurvey", @@ -50,13 +156,21 @@ "endPoint", "fairwayData", "maxAlt", - "referenceWaterLevel", - "selectedWaterLevel", - "waterLevels" + "selectedWaterLevel" ]), - ...mapState("bottlenecks", ["selectedSurvey"]), - relativeWaterLevelDelta() { - return this.selectedWaterLevel.value - this.referenceWaterLevel; + ...mapState("bottlenecks", ["selectedSurvey", "selectedBottleneck"]), + ...mapState("application", ["paneSetup"]), + title() { + let dates = [this.selectedSurvey.date_info]; + let waterlevelLabel = + this.selectedWaterLevel === "ref" + ? this.selectedSurvey.depth_reference + : "Current"; + if (this.additionalSurvey) dates.push(this.additionalSurvey.date_info); + dates.map(d => this.$options.filters.dateTime(d, true)); + return `${this.$gettext("Fairwayprofile")}: ${ + this.selectedBottleneck + } (${dates.join(", ")}) WL: ${waterlevelLabel} (${this.waterlevel} cm)`; }, currentData() { if ( @@ -74,18 +188,20 @@ return []; return this.currentProfile[this.additionalSurvey.date_info].points; }, - waterColor() { - return "#005DFF"; - }, - xScale() { - return [0, this.totalLength]; + bottleneck() { + return this.openLayersMap() + .getLayer("BOTTLENECKS") + .getSource() + .getFeatures() + .find(f => f.get("objnam") === this.selectedBottleneck); }, - yScaleRight() { - //ToDO calcReleativeDepth(this.maxAlt) to get the - // maximum depth according to the actual waterlevel - // additionally: take the one which is higher reference or current waterlevel - const DELTA = this.maxAlt * 1.1 - this.maxAlt; - return [this.maxAlt * 1 + DELTA, -DELTA]; + waterlevel() { + return this.selectedWaterLevel === "ref" + ? this.refWaterlevel + : this.bottleneck.get("gm_waterlevel"); + }, + refWaterlevel() { + return this.selectedSurvey.waterlevel_value; } }, watch: { @@ -109,41 +225,222 @@ }, fairwayData() { this.drawDiagram(); + }, + selectedBottleneck() { + this.$store.commit("application/paneSetup", "DEFAULT"); } }, methods: { - calcRelativeDepth(depth) { - /* takes a depth value and substracts the delta of the relative waterlevel - * say the reference level is above the current level, the ground is nearer, - * thus, the depth is lower. - * - * E.g.: - * - * Reference waterlevel 5m, current 4m => delta = -1m - * If the distance to the ground was 3m from the 5m mark - * it is now only 2m from the current waterlevel. - * - * Vice versa: - * - * If the reference level is 5m and the current 6m => delta = +1m - * The ground is one meter farer away from the current waterlevel - * - */ - return depth - this.relativeWaterLevelDelta; + close() { + this.$store.commit( + "application/paneSetup", + this.paneSetup === "COMPARESURVEYS_FAIRWAYPROFILE" + ? "COMPARESURVEYS" + : "DEFAULT" + ); + this.$store.dispatch("fairwayprofile/clearCurrentProfile"); + }, + applyChange() { + if (this.form.template.hasOwnProperty("properties")) { + this.templateData = this.defaultTemplate; + return; + } + if (this.form.template) { + HTTP.get("/templates/diagram/" + this.form.template.name, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + this.templateData = response.data.template_data; + }) + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + downloadPDF() { + if (this.templateData) { + this.pdf.doc = new jsPDF( + "l", + "mm", + this.templateData.properties.paperSize + ); + // pdf width and height in millimeter (landscape) + this.pdf.width = + this.templateData.properties.paperSize === "a3" ? 420 : 297; + this.pdf.height = + this.templateData.properties.paperSize === "a3" ? 297 : 210; + // default values if some are missing in template + let defaultFontSize = 11, + defaultWidth = 70, + defaultTextColor = "black", + defaultBorderColor = "white", + defaultBgColor = "white", + defaultRounding = 2, + defaultPadding = 2, + defaultOffset = { x: 0, y: 0 }; + this.templateData.elements.forEach(e => { + switch (e.type) { + case "diagram": { + this.addDiagram( + e.position, + e.offset || defaultOffset, + e.width, + e.height + ); + break; + } + case "diagramlegend": { + this.addDiagramLegend( + e.position, + e.offset || defaultOffset, + e.color || defaultTextColor + ); + break; + } + case "diagramtitle": { + let fairwayInfo = + this.selectedBottleneck + + " (" + + this.selectedSurvey.date_info + + ")"; + this.addDiagramTitle( + e.position, + e.offset || defaultOffset, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + fairwayInfo + ); + break; + } + case "image": { + this.addImage( + e.url, + e.format || "", + e.position, + e.offset || defaultOffset, + e.width || 90, + e.height || 60 + ); + break; + } + case "text": { + this.addText( + e.position, + e.offset || defaultOffset, + e.width || defaultWidth, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + e.text || "" + ); + break; + } + case "box": { + this.addBox( + e.position, + e.offset || defaultOffset, + e.width || 90, + e.height || 60, + e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, + e.color || defaultBgColor, + e.brcolor || defaultBorderColor + ); + break; + } + case "textbox": { + this.addTextBox( + e.position, + e.offset || defaultOffset, + e.width, + e.height, + e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, + e.padding || defaultPadding, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + e.background || defaultBgColor, + e.text || "", + e.brcolor || defaultBorderColor + ); + break; + } + } + }); + } + this.pdf.doc.save(this.title.replace(/\s/g, "_").replace(/[():,]/g, "")); + }, + addDiagram(position, offset, width, height) { + let x = offset.x, + y = offset.y; + var svg = this.$refs.diagramContainer.innerHTML; + if (svg) { + svg = svg.replace(/\r?\n|\r/g, "").trim(); + } + // use default width,height if they are missing in the template definition + if (!width) { + width = this.templateData.properties.paperSize === "a3" ? 380 : 290; + } + if (!height) { + height = this.templateData.properties.paperSize === "a3" ? 130 : 100; + } + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - height; + } + var canvas = document.createElement("canvas"); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight / 2; + canvg(canvas, svg, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true + }); + var imgData = canvas.toDataURL("image/png"); + this.pdf.doc.addImage(imgData, "PNG", x, y, width, height); + }, + // Diagram legend + addDiagramLegend(position, offset, color) { + let x = offset.x, + y = offset.y; + let width = + (this.pdf.doc.getStringUnitWidth("Ground") * 10) / (72 / 25.6) + 5; + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - this.getTextHeight(3); + } + this.pdf.doc.setFontSize(10); + this.pdf.doc.setTextColor(color); + this.pdf.doc.setDrawColor("white"); + this.pdf.doc.setFillColor("#5995ff"); + this.pdf.doc.circle(x, y, 2, "FD"); + this.pdf.doc.text(x + 3, y + 1, "Water"); + this.pdf.doc.setFillColor("#1f4fff"); + this.pdf.doc.circle(x, y + 5, 2, "FD"); + this.pdf.doc.text(x + 3, y + 6, "Fairway"); + this.pdf.doc.setFillColor("#4a2f06"); + this.pdf.doc.circle(x, y + 10, 2, "FD"); + this.pdf.doc.text(x + 3, y + 11, "Ground"); }, drawDiagram() { - this.coordinatesSelect = null; - const chartDiv = document.querySelector(".fairwayprofile"); - d3.select(".fairwayprofile svg").remove(); + d3.select(".diagram-container svg").remove(); this.scaleFairwayProfile(); - let svg = d3.select(chartDiv).append("svg"); - svg.attr("width", this.width); - svg.attr("height", this.height); - const width = this.width - this.margin.right - 1.5 * this.margin.left; - const height = this.height - this.margin.top - 2 * this.margin.bottom; + let svg = d3.select(".diagram-container").append("svg"); + svg.attr("width", "100%"); + svg.attr("height", "100%"); + const width = this.width - this.margin.right - this.margin.left; + const height = this.height - this.margin.top - this.margin.bottom; const currentData = this.currentData; const additionalData = this.additionalData; - const { xScale, yScaleRight, graph } = this.generateCoordinates( + const { xScale, yScaleRight, graph } = this.generateScalesAndGraph( svg, height, width @@ -183,7 +480,6 @@ } for (let data of this.fairwayData) { const [startPoint, endPoint, depth] = data.coordinates[0]; - const style = data.style(); let fairwayArea = d3 .area() .x(function(d) { @@ -198,7 +494,8 @@ .datum([{ x: startPoint, y: depth }, { x: endPoint, y: depth }]) .attr("fill", "#002AFF") .attr("fill-opacity", 0.65) - .attr("stroke", style[0].getStroke().getColor()) + .attr("stroke", "#002AFF") + .attr("stroke-width", 2) .attr("d", fairwayArea); } }, @@ -206,16 +503,23 @@ graph .append("text") .attr("transform", ["rotate(-90)"]) - .attr("y", this.width - 60) + .attr("y", this.width - 100) .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("transform", ["rotate(-90)"]) + .attr("y", -50) + .attr("x", -(this.height - this.margin.top - this.margin.bottom) / 2) + .attr("fill", "black") + .style("text-anchor", "middle") + .text("Waterlevel [cm]"); + graph + .append("text") + .attr("y", -50) + .attr("x", -(height / 4)) .attr("dy", "1em") .attr("fill", "black") .style("text-anchor", "middle") @@ -225,21 +529,35 @@ ]) .text("Width [m]"); }, - generateCoordinates(svg, height, width) { + generateScalesAndGraph(svg, height, width) { let xScale = d3 .scaleLinear() - .domain(this.xScale) + .domain([0, this.totalLength]) .rangeRound([0, width]); - xScale.ticks(5); - let yScaleRight = d3 .scaleLinear() - .domain(this.yScaleRight) + .domain([ + this.maxAlt * 1.1 + + Math.abs(this.waterlevel - this.refWaterlevel) / 100, + -(this.maxAlt * 0.1) + ]) .rangeRound([height, 0]); - let xAxis = d3.axisBottom(xScale); - let yAxis2 = d3.axisRight(yScaleRight); + let yScaleLeft = d3 + .scaleLinear() + .domain([ + this.waterlevel - + (this.maxAlt * 100 + + Math.abs(this.waterlevel - this.refWaterlevel)), + this.waterlevel + this.maxAlt * 0.1 * 100 + ]) + .rangeRound([height, 0]); + + let xAxis = d3.axisBottom(xScale).ticks(5); + let yAxisRight = d3.axisRight(yScaleRight); + let yAxisLeft = d3.axisLeft(yScaleLeft); + let graph = svg .append("g") .attr( @@ -249,11 +567,38 @@ graph .append("g") .attr("transform", "translate(0," + height + ")") - .call(xAxis.ticks(5)); + .call(xAxis) + .selectAll(".tick text") + .attr("fill", "black") + .select(function() { + return this.parentNode; + }) + .selectAll(".tick line") + .attr("stroke", "black"); graph .append("g") .attr("transform", "translate(" + width + ",0)") - .call(yAxis2); + .call(yAxisRight) + .selectAll(".tick text") + .attr("fill", "black") + .select(function() { + return this.parentNode; + }) + .selectAll(".tick line") + .attr("stroke", "black"); + graph + .append("g") + .attr("transform", "translate(0 0)") + .call(yAxisLeft) + .selectAll(".tick text") + .attr("fill", "black") + .select(function() { + return this.parentNode; + }) + .selectAll(".tick line") + .attr("stroke", "black"); + + graph.selectAll(".domain").attr("stroke", "black"); return { xScale, yScaleRight, graph }; }, drawWaterlevel({ graph, xScale, yScaleRight, height }) { @@ -270,8 +615,8 @@ .append("path") .datum([{ x: 0, y: 0 }, { x: this.totalLength, y: 0 }]) .attr("fill-opacity", 0.65) - .attr("fill", this.waterColor) - .attr("stroke", this.waterColor) + .attr("fill", WATER_COLOR) + .attr("stroke", "transparent") .attr("d", waterArea); }, drawProfile({ @@ -290,25 +635,27 @@ .x(d => { return xScale(d.x); }) - .y(d => { - return yScaleRight(d.y); - }); + .y(d => + yScaleRight( + d.y + Math.abs(this.waterlevel - this.refWaterlevel) / 100 + ) + ); let profileArea = d3 .area() .x(function(d) { return xScale(d.x); }) .y0(height) - .y1(function(d) { - return yScaleRight(d.y); - }); + .y1(d => + yScaleRight( + d.y + Math.abs(this.waterlevel - this.refWaterlevel) / 100 + ) + ); graph .append("path") .datum(part) .attr("fill", color) - .attr("stroke", color) - .attr("stroke-width", 3) - .attr("stroke-opacity", opacity) + .attr("stroke", "transparent") .attr("fill-opacity", opacity) .attr("d", profileArea); graph @@ -320,15 +667,16 @@ .attr("stroke-linecap", "round") .attr("stroke-width", 3) .attr("stroke-opacity", opacity) - .attr("fill-opacity", opacity) + .attr("fill-opacity", 0) .attr("d", profileLine); } }, scaleFairwayProfile() { - if (!document.querySelector(".fairwayprofile")) return; - const clientHeight = document.querySelector(".fairwayprofile") + if (!document.querySelector(".diagram-container")) return; + const clientHeight = document.querySelector(".diagram-container") .clientHeight; - const clientWidth = document.querySelector(".fairwayprofile").clientWidth; + const clientWidth = document.querySelector(".diagram-container") + .clientWidth; if (!clientHeight || !clientWidth) return; this.height = clientHeight; this.width = clientWidth; @@ -339,9 +687,33 @@ }, mounted() { this.drawDiagram(); + this.templates[0] = this.defaultTemplate; + this.form.template = this.templates[0]; + this.templateData = this.form.template; + HTTP.get("/templates/diagram", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + if (response.data.length) { + this.templates = response.data; + this.form.template = this.templates[0]; + this.templates[this.templates.length] = this.defaultTemplate; + this.applyChange(); + } + }) + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); }, updated() { - this.scaleFairwayProfile(); + this.drawDiagram(); }, destroyed() { window.removeEventListener("resize", debounce(this.drawDiagram));
--- a/client/src/components/fairway/Profiles.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/fairway/Profiles.vue Mon Jun 03 10:19:18 2019 +0200 @@ -12,11 +12,7 @@ :closeCallback="close" /> <div class="box-body"> - <transition name="fade"> - <div class="loading" v-if="surveysLoading || profileLoading"> - <font-awesome-icon icon="spinner" spin /> - </div> - </transition> + <UISpinnerOverlay v-if="surveysLoading || profileLoading" /> <select @change="moveToBottleneck" v-model="selectedBottleneck" @@ -25,16 +21,23 @@ <option :value="null"> <translate>Select Bottleneck</translate> </option> - <option - v-for="bn in bottlenecksList" - :key="bn.properties.name" - :value="bn.properties.name" - >{{ bn.properties.name }}</option + <optgroup + v-for="(bottlenecksForCountry, cc) in orderedBottlenecks" + :key="cc" + :label="cc" > + <option + v-for="bn in bottlenecksForCountry" + :key="bn.properties.name" + :value="bn.properties.name" + > + {{ bn.properties.name }} + </option> + </optgroup> </select> <div v-if="selectedBottleneck"> <div class="d-flex mt-2"> - <div class="flex-fill"> + <div class="flex-fill" style="max-width: 75px;"> <small class="text-muted"> <translate>Waterlevel</translate>: </small> @@ -42,15 +45,11 @@ v-model="selectedWaterLevel" class="form-control form-control-sm small" > - <option value="" v-if="Object.keys(waterLevels).length === 0"> - <translate>Current</translate> + <option value="ref"> + <translate>Depth Reference</translate> </option> - <option - v-for="wl in Object.keys(waterLevels)" - :key="wl" - :value="wl" - > - {{ wl | surveyDate }} + <option value="current"> + <translate>Current Waterlevel</translate> </option> </select> </div> @@ -89,6 +88,46 @@ </select> </div> </div> + <div class="mt-2 d-flex" v-if="additionalSurvey"> + <button + v-if="differencesLoading" + class="btn btn-info btn-xs flex-fill" + disabled + > + <font-awesome-icon icon="spinner" spin class="mr-1" /> + <translate>Calculating differences</translate> + </button> + <button + class="btn btn-info btn-xs flex-fill" + @click="differencesVisible ? showSurvey() : showDifferences()" + v-else + > + <translate v-if="differencesVisible" key="showsurvey" + >Show survey</translate + > + <translate v-else key="showdifferences" + >Show differences</translate + > + </button> + <button + v-if="!paneSetup.includes('FAIRWAYPROFILE')" + class="btn btn-info btn-xs ml-2" + @click="$store.commit('application/paneRotate')" + v-tooltip="rotatePanesTooltip" + > + <font-awesome-icon icon="redo" fixed-width /> + </button> + <button + class="btn btn-info btn-xs ml-2" + @click="toggleSyncMaps()" + v-tooltip="syncMapsTooltip" + > + <font-awesome-icon + :icon="mapsAreSynced ? 'unlink' : 'link'" + fixed-width + /> + </button> + </div> <hr class="w-100 mb-0" /> <small class="text-muted d-block mt-2"> <translate>Saved cross profiles</translate>: @@ -171,10 +210,8 @@ :class="startPoint && endPoint && !selectedCut ? 'w-50' : 'w-100'" > <button class="btn btn-info btn-sm w-100" @click="toggleCutTool"> - <font-awesome-icon - :icon="cutTool && cutTool.getActive() ? 'times' : 'plus'" - ></font-awesome-icon> - {{ cutTool && cutTool.getActive() ? "Cancel" : "New" }} + <font-awesome-icon :icon="cutToolEnabled ? 'times' : 'plus'" /> + {{ cutToolEnabled ? "Cancel" : "New" }} </button> </div> </div> @@ -217,6 +254,11 @@ border-top-left-radius: $border-radius; border-bottom-left-radius: $border-radius; } + +input, +select { + font-size: 0.8em; +} </style> <script> @@ -236,8 +278,9 @@ import { mapState, mapGetters } from "vuex"; import Feature from "ol/Feature"; import LineString from "ol/geom/LineString"; -import { displayError, displayInfo } from "@/lib/errors.js"; -import { LAYERS } from "@/store/map.js"; +import { displayError, displayInfo } from "@/lib/errors"; +import { HTTP } from "@/lib/http"; +import { COMPARESURVEYS } from "@/components/paneSetups"; export default { name: "profiles", @@ -249,9 +292,8 @@ }; }, computed: { - ...mapGetters("map", ["getVSourceByName"]), - ...mapState("application", ["showProfiles"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), + ...mapState("application", ["showProfiles", "paneSetup"]), + ...mapState("map", ["openLayersMaps", "syncedMaps", "cutToolEnabled"]), ...mapState("bottlenecks", [ "bottlenecksList", "surveys", @@ -262,26 +304,26 @@ "startPoint", "endPoint", "profileLoading", - "waterLevels" + "differencesLoading", + "waterLevels", + "currentProfile" ]), + ...mapGetters("map", ["openLayersMap"]), + ...mapGetters("bottlenecks", ["orderedBottlenecks"]), profilesLable() { - return this.$gettext("Profiles"); + return this.$gettext("Bottleneck Surveys"); }, selectedBottleneck: { get() { return this.$store.state.bottlenecks.selectedBottleneck; }, set(name) { - this.$store - .dispatch("bottlenecks/setSelectedBottleneck", name) - .then(() => { - this.$store.commit("bottlenecks/setFirstSurveySelected"); - }); + this.$store.dispatch("bottlenecks/setSelectedBottleneck", name); } }, selectedWaterLevel: { get() { - return this.$store.state.fairwayprofile.selectedWaterLevel.date || ""; + return this.$store.state.fairwayprofile.selectedWaterLevel; }, set(value) { this.$store.commit("fairwayprofile/setSelectedWaterLevel", value); @@ -312,8 +354,11 @@ this.$store.commit("fairwayprofile/selectedCut", cut); if (!cut) { this.$store.commit("fairwayprofile/clearCurrentProfile"); - this.$store.commit("application/showSplitscreen", false); - this.getVSourceByName(LAYERS.CUTTOOL).clear(); + this.openLayersMaps.forEach(m => { + m.getLayer("CUTTOOL") + .getSource() + .clear(); + }); } } }, @@ -337,6 +382,28 @@ .map(coord => parseFloat(coord.trim())) .filter(c => Number(c) === c); return coordinates.length === 4; + }, + differencesVisible() { + return ( + this.openLayersMap(COMPARESURVEYS.compare.id) && + !this.openLayersMap(COMPARESURVEYS.compare.id) + .getLayer("BOTTLENECKISOLINE") + .getVisible() && + this.openLayersMap(COMPARESURVEYS.compare.id) + .getLayer("DIFFERENCES") + .getVisible() + ); + }, + rotatePanesTooltip() { + return this.$gettext("Rotate Maps"); + }, + syncMapsTooltip() { + return this.$gettext( + this.mapsAreSynced ? "Unsynchronize Maps" : "Synchronize Maps" + ); + }, + mapsAreSynced() { + return this.syncedMaps.includes(COMPARESURVEYS.compare.id); } }, watch: { @@ -349,6 +416,22 @@ this.loadProfile(survey); }, additionalSurvey(survey) { + if (survey) { + this.loadDifferences(); + this.$store.commit( + "application/paneSetup", + Object.keys(this.currentProfile).length + ? "COMPARESURVEYS_FAIRWAYPROFILE" + : "COMPARESURVEYS" + ); + this.$store.commit("map/syncedMaps", [COMPARESURVEYS.compare.id]); + } else { + this.$store.commit( + "application/paneSetup", + Object.keys(this.currentProfile).length ? "FAIRWAYPROFILE" : "DEFAULT" + ); + this.$store.commit("map/syncedMaps", []); + } this.loadProfile(survey); }, selectedCut(cut) { @@ -358,25 +441,75 @@ } }, methods: { + toggleSyncMaps() { + if (this.mapsAreSynced) { + this.$store.commit( + "map/syncedMaps", + this.syncedMaps.filter(m => m !== COMPARESURVEYS.compare.id) + ); + } else { + this.$store.commit("map/syncedMaps", [COMPARESURVEYS.compare.id]); + } + }, + loadDifferences() { + this.$store.commit("fairwayprofile/setDifferencesLoading", true); + HTTP.post( + "/diff", + { + bottleneck: this.selectedSurvey.bottleneck_id, + minuend: this.selectedSurvey.date_info, + subtrahend: this.additionalSurvey.date_info + }, + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + } + } + ) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => { + this.$store.commit("fairwayprofile/setDifferencesLoading", false); + }); + }, + showDifferences() { + this.openLayersMap(COMPARESURVEYS.compare.id) + .getLayer("BOTTLENECKISOLINE") + .setVisible(false); + this.openLayersMap(COMPARESURVEYS.compare.id) + .getLayer("DIFFERENCES") + .setVisible(true); + }, + showSurvey() { + this.openLayersMap(COMPARESURVEYS.compare.id) + .getLayer("BOTTLENECKISOLINE") + .setVisible(true); + this.openLayersMap(COMPARESURVEYS.compare.id) + .getLayer("DIFFERENCES") + .setVisible(false); + }, close() { this.$store.commit("application/showProfiles", false); }, loadProfile(survey) { if (survey) { this.$store.commit("fairwayprofile/profileLoading", true); - this.$store.commit("application/splitscreenLoading", true); this.$store .dispatch("fairwayprofile/loadProfile", survey) .finally(() => { this.$store.commit("fairwayprofile/profileLoading", false); - this.$store.commit("application/splitscreenLoading", false); }); } }, toggleCutTool() { - this.cutTool.setActive(!this.cutTool.getActive()); - this.lineTool.setActive(false); - this.polygonTool.setActive(false); + this.$store.commit("map/cutToolEnabled", !this.cutToolEnabled); + this.$store.commit("map/lineToolEnabled", false); + this.$store.commit("map/polygonToolEnabled", false); this.$store.commit("map/setCurrentMeasurement", null); }, onCopyCoordinates() { @@ -402,14 +535,22 @@ coordinates = coordinates.filter(c => Number(c) === c); if (coordinates.length === 4) { // draw line on map - this.getVSourceByName(LAYERS.CUTTOOL).clear(); + this.openLayersMaps.forEach(m => { + m.getLayer("CUTTOOL") + .getSource() + .clear(); + }); const cut = new Feature({ geometry: new LineString([ [coordinates[0], coordinates[1]], [coordinates[2], coordinates[3]] ]).transform("EPSG:4326", "EPSG:3857") }); - this.getVSourceByName(LAYERS.CUTTOOL).addFeature(cut); + this.openLayersMaps.forEach(m => { + m.getLayer("CUTTOOL") + .getSource() + .addFeature(cut); + }); // draw diagram this.$store.dispatch("fairwayprofile/cut", cut); @@ -481,7 +622,7 @@ bn => bn.properties.name === this.selectedBottleneck ); if (!bottleneck) return; - this.$store.commit("map/moveToExtent", { + this.$store.dispatch("map/moveToFeauture", { feature: bottleneck, zoom: 17, preventZoomOut: true
--- a/client/src/components/gauge/Gauges.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/gauge/Gauges.vue Mon Jun 03 10:19:18 2019 +0200 @@ -12,13 +12,8 @@ :closeCallback="close" /> <div class="box-body"> - <transition name="fade"> - <div class="loading" v-if="loading"> - <font-awesome-icon icon="spinner" spin /> - </div> - </transition> + <UISpinnerOverlay v-if="loading" /> <select - @change="moveToGauge" v-model="selectedGaugeISRS" class="form-control font-weight-bold" > @@ -67,11 +62,23 @@ > <translate>Show Waterlevels</translate> </button> - <hr /> + <hr class="mb-1" /> + <div class="row no-gutters mb-2"> + <small class="text-muted"> + <translate>Compare to</translate>: + </small> + <input + type="number" + step="1" + min="1900" + :max="new Date().getFullYear()" + class="form-control form-control-sm small" + v-model="yearCompare" + /> + </div> <button @click="showHydrologicalConditionsDiagram()" class="btn btn-sm btn-info d-block w-100 mt-2" - disabled > <translate>Show Hydrological Conditions</translate> </button> @@ -81,6 +88,13 @@ </div> </template> +<style lang="scss" scoped> +input, +select { + font-size: 0.8em; +} +</style> + <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -96,7 +110,7 @@ * Markus Kottländer <markus.kottlaender@intevation.de> */ import { mapState, mapGetters } from "vuex"; -import { displayError } from "@/lib/errors.js"; +import { displayError } from "@/lib/errors"; export default { data() { @@ -105,8 +119,8 @@ }; }, computed: { - ...mapState("application", ["showGauges", "activeSplitscreenId"]), - ...mapState("gauges", ["gauges"]), + ...mapState("application", ["showGauges", "paneSetup"]), + ...mapState("gauges", ["gauges", "longtermInterval"]), ...mapGetters("gauges", ["selectedGauge"]), gaugesLabel() { return this.$gettext("Gauges"); @@ -146,15 +160,27 @@ set(date) { this.$store.commit("gauges/dateTo", date); } + }, + yearCompare: { + get() { + return this.$store.state.gauges.yearCompare; + }, + set(year) { + this.$store.commit("gauges/yearCompare", year); + } } }, watch: { - selectedGaugeISRS() { - if (this.activeSplitscreenId === "gauge-waterlevel") { - this.showWaterlevelDiagram(); - } - if (this.activeSplitscreenId === "gauge-hydrological-conditions") { - this.showHydrologicalConditionsDiagram(); + selectedGaugeISRS(gauge) { + if (gauge) { + let coordinates = this.selectedGauge.geometry.coordinates; + this.$store.dispatch("map/moveMap", { + coordinates, + zoom: 15, + preventZoomOut: true + }); + } else { + this.$store.commit("application/paneSetup", "DEFAULT"); } } }, @@ -162,47 +188,8 @@ close() { this.$store.commit("application/showGauges", false); }, - moveToGauge() { - if (!this.selectedGauge) return; - this.$store.commit("map/moveToExtent", { - feature: this.selectedGauge, - zoom: null, - preventZoomOut: true - }); - }, showWaterlevelDiagram() { - // for panning the map to the gauge on opening the diagram: needs to be - // set outside of the expandCallback to not always refer to the currently - // selectedGauge - let coordinates = this.selectedGauge.geometry.coordinates; - - // configure splitscreen - let splitscreenConf = { - id: "gauge-waterlevel", - component: "waterlevel", - title: - this.$gettext("Waterlevel") + - ": " + - this.gaugeLabel(this.selectedGauge), - icon: "ruler-vertical", - closeCallback: () => { - this.$store.commit("gauges/selectedGaugeISRS", null); - this.$store.commit("gauges/waterlevels", []); - }, - expandCallback: () => { - this.$store.commit("map/moveMap", { - coordinates, - zoom: 17, - preventZoomOut: true - }); - } - }; - this.$store.commit("application/addSplitscreen", splitscreenConf); - this.$store.commit("application/activeSplitscreenId", splitscreenConf.id); - this.$store.commit("application/splitscreenLoading", true); this.loading = true; - this.$store.commit("application/showSplitscreen", true); - Promise.all([ this.$store.dispatch("gauges/loadWaterlevels"), this.$store.dispatch("gauges/loadNashSutcliffe") @@ -215,45 +202,25 @@ }); }) .finally(() => { - this.$store.commit("application/splitscreenLoading", false); + this.$store.commit( + "application/paneSetup", + [ + "GAUGE_HYDROLOGICALCONDITIONS", + "GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS" + ].includes(this.paneSetup) + ? "GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS" + : "GAUGE_WATERLEVEL" + ); this.loading = false; }); }, showHydrologicalConditionsDiagram() { - // for panning the map to the gauge on opening the diagram: needs to be - // set outside of the expandCallback to not always refer to the currently - // selectedGauge - let coordinates = this.selectedGauge.geometry.coordinates; + this.loading = true; - // configure splitscreen - let splitscreenConf = { - id: "gauge-hydrological-conditions", - component: "hydrological-conditions", - title: - this.$gettext("Hydrological Conditions") + - ": " + - this.gaugeLabel(this.selectedGauge), - icon: "ruler-vertical", - closeCallback: () => { - this.$store.commit("gauges/selectedGaugeISRS", null); - this.$store.commit("gauges/meanWaterlevels", []); - }, - expandCallback: () => { - this.$store.commit("map/moveMap", { - coordinates, - zoom: 17, - preventZoomOut: true - }); - } - }; - this.$store.commit("application/addSplitscreen", splitscreenConf); - this.$store.commit("application/activeSplitscreenId", splitscreenConf.id); - this.$store.commit("application/splitscreenLoading", true); - this.loading = true; - this.$store.commit("application/showSplitscreen", true); - - this.$store - .dispatch("gauges/loadMeanWaterlevels") + Promise.all([ + this.$store.dispatch("gauges/loadLongtermWaterlevels"), + this.$store.dispatch("gauges/loadYearWaterlevels") + ]) .catch(error => { const { status, data } = error.response; displayError({ @@ -262,7 +229,15 @@ }); }) .finally(() => { - this.$store.commit("application/splitscreenLoading", false); + this.$store.commit( + "application/paneSetup", + [ + "GAUGE_WATERLEVEL", + "GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS" + ].includes(this.paneSetup) + ? "GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS" + : "GAUGE_HYDROLOGICALCONDITIONS" + ); this.loading = false; }); },
--- a/client/src/components/gauge/HydrologicalConditions.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/gauge/HydrologicalConditions.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,53 +1,93 @@ <template> - <div - class="d-flex flex-fill justify-content-center align-items-center diagram-container" - > - <div v-if="!meanWaterlevels.length"> - <translate>No data available.</translate> + <div class="d-flex flex-column flex-fill"> + <UIBoxHeader + icon="ruler-vertical" + :title="title" + :closeCallback="close" + class="rounded-0" + /> + <div class="d-flex flex-fill"> + <DiagramLegend id="diagramlegendId"> + <div class="legend"> + <span + style="background-color: red; width: 20px; height: 20px;" + ></span> + {{ yearCompare }} + </div> + <div class="legend"> + <span + style="background-color: orange; width: 20px; height: 20px;" + ></span> + Q25% + </div> + <div class="legend"> + <span + style="background-color: black; width: 20px; height: 20px;" + ></span> + Median + </div> + <div class="legend"> + <span + style="background-color: purple; width: 20px; height: 20px;" + ></span> + Q75% + </div> + <div class="legend"> + <span + style="background-color: lightsteelblue; width: 20px; height: 20px;" + ></span> + Long-term Amplitude + </div> + <select + @change="applyChange" + v-model="form.template" + class="form-control d-block custom-select-sm w-100" + > + <option + v-for="template in templates" + :value="template" + :key="template.name" + > + {{ template.name }} + </option> + </select> + <div> + <button + @click="downloadPDF" + type="button" + class="btn btn-sm btn-info d-block w-100 mt-2" + :disabled="!longtermWaterlevels.length" + > + <translate>Export to PDF</translate> + </button> + </div> + <a + :class="[ + 'btn btn-sm btn-info d-block w-100 mt-2', + { disabled: !longtermWaterlevels.length } + ]" + :href="csvLink" + :download="csvFileName" + > + <translate>Export as CSV</translate> + </a> + </DiagramLegend> + <div + class="d-flex flex-fill justify-content-center align-items-center" + :id="containerId" + > + <div v-if="!longtermWaterlevels.length"> + <translate>No data available.</translate> + </div> + </div> </div> + <div + id="tmpContainer" + style="position: absolute; z-index: -1; top: 600px;" + ></div> </div> </template> -<style lang="sass" scoped> -.diagram-container - /deep/ - .line - clip-path: url(#clip) - - .hdc-line, - .ldc-line, - .mw-line - stroke-width: 1 - fill: none - clip-path: url(#clip) - .hdc-line - stroke: red - .ldc-line - stroke: green - .mw-line - stroke: grey - .ref-waterlevel-label - font-size: 11px - fill: #999 - - .tick - line - stroke-dasharray: 5 - stroke: #ccc - - .zoom - cursor: move - fill: none - pointer-events: all - .brush - .selection - stroke: none - fill-opacity: 0.2 - .handle - stroke: rgba($color-info, 0.5) - fill: rgba($color-info, 0.5) -</style> - <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -55,330 +95,1019 @@ * SPDX-License-Identifier: AGPL-3.0-or-later * License-Filename: LICENSES/AGPL-3.0.txt * - * Copyright (C) 2018 by via donau + * Copyright (C) 2019 by via donau * – Österreichische Wasserstraßen-Gesellschaft mbH * Software engineering by Intevation GmbH * * Author(s): * Markus Kottländer <markus.kottlaender@intevation.de> + * Fadi Abbud <fadi.abbud@intevation.de> */ import { mapState, mapGetters } from "vuex"; -import * as d3Base from "d3"; +import * as d3 from "d3"; import debounce from "debounce"; -import { lineChunked } from "d3-line-chunked"; import { startOfYear, endOfYear } from "date-fns"; - -// we should load only d3 modules we need but for now we'll go with the lazy way -// https://www.giacomodebidda.com/how-to-import-d3-plugins-with-webpack/ -const d3 = Object.assign(d3Base, { lineChunked }); +import jsPDF from "jspdf"; +import canvg from "canvg"; +import { pdfgen } from "@/lib/mixins"; +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors"; export default { + mixins: [pdfgen], + components: { + DiagramLegend: () => import("@/components/DiagramLegend") + }, + data() { + return { + containerId: "hydrologicalconditions-diagram-container", + svg: null, + diagram: null, + navigation: null, + dimensions: null, + extent: null, + scale: null, + axes: null, + templateData: null, + form: { + template: null, + form: null + }, + templates: [], + defaultTemplate: { + name: "Default", + properties: { + paperSize: "a4" + }, + elements: [ + { + type: "diagram", + position: "topleft", + offset: { x: 15, y: 50 }, + width: 290, + height: 100 + }, + { + type: "diagramlegend", + position: "topleft", + offset: { x: 30, y: 150 }, + colot: "black" + }, + { + type: "diagramtitle", + position: "topleft", + offset: { x: 50, y: 26 }, + fontsize: 22, + color: "steelblue" + } + ] + }, + pdf: { + doc: null, + width: 420, + height: 297 + } + }; + }, computed: { - ...mapState("gauges", ["meanWaterlevels"]), - ...mapGetters("gauges", ["selectedGauge", "minMaxWaterlevelForDay"]) + ...mapState("application", ["paneSetup"]), + ...mapState("gauges", [ + "longtermWaterlevels", + "yearWaterlevels", + "yearCompare", + "longtermInterval" + ]), + ...mapGetters("gauges", ["selectedGauge"]), + title() { + return `${this.selectedGauge.properties.objname}: ${this.$gettext( + "Hydrological Conditions" + )} (${this.longtermInterval.join(" - ")})`; + }, + csvLink() { + return "data:text/csv;charset=utf-8," + encodeURIComponent(this.csvData); + }, + csvFileName() { + return `${this.$gettext("hydrological-conditions")}-${ + this.selectedGauge.properties.objname + }-${this.longtermInterval.join(" - ")}.csv`; + }, + csvData() { + // We cannot directly use the csv data provided by the backend because the + // diagram uses data from two endpoints, longterm- and yearWaterlevels. + // So we need to merge them here to have them in one csv export. + let merged = this.longtermWaterlevels + .filter(d => d) // copy array, don't mutate original + .map(d => { + let yearData = this.yearWaterlevels.find(y => { + return d.date.getTime() === y.date.getTime(); + }); + d[this.yearCompare] = yearData ? yearData.mean : ""; + return `${d.date.getMonth() + 1}-${d.date.getDate()};${d.min};${ + d.max + };${d.mean};${d.median};${d.q25};${d.q75};${d[this.yearCompare]}`; + }) + .join("\n"); + return `#Interval: ${this.longtermInterval.join( + " - " + )}\n#date;#min;#max;#mean;#median;#q25;#q75;#${ + this.yearCompare + }\n${merged}`; + } }, watch: { - meanWaterlevels() { + paneSetup() { + this.$nextTick(() => this.drawDiagram()); + }, + longtermWaterlevels() { + this.drawDiagram(); + }, + yearWaterlevels() { this.drawDiagram(); } }, methods: { + close() { + this.$store.commit( + "application/paneSetup", + this.paneSetup === "GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS" + ? "GAUGE_WATERLEVEL" + : "DEFAULT" + ); + }, + downloadPDF() { + if (this.templateData) { + this.pdf.doc = new jsPDF( + "l", + "mm", + this.templateData.properties.paperSize + ); + // pdf width and height in millimeter (landscape) + this.pdf.width = + this.templateData.properties.paperSize === "a3" ? 420 : 297; + this.pdf.height = + this.templateData.properties.paperSize === "a3" ? 297 : 210; + // default values if some are missing in template + let defaultFontSize = 11, + defaultColor = "black", + defaultWidth = 70, + defaultTextColor = "black", + defaultBorderColor = "white", + defaultBgColor = "white", + defaultRounding = 2, + defaultPadding = 2, + defaultOffset = { x: 0, y: 0 }; + this.templateData.elements.forEach(e => { + switch (e.type) { + case "diagram": { + this.addDiagram( + e.position, + e.offset || defaultOffset, + e.width, + e.height + ); + break; + } + case "diagramtitle": { + let gaugeInfo = + this.selectedGauge.properties.objname + + " (" + + this.selectedGauge.id + .split(".")[1] + .replace(/[()]/g, "") + .split(",")[3] + + "): Hydrological Conditions " + + this.longtermInterval.join(" - "); + this.addDiagramTitle( + e.position, + e.offset || defaultOffset, + e.fontsize || defaultFontSize, + e.color || defaultColor, + gaugeInfo + ); + break; + } + case "diagramlegend": { + this.addDiagramLegend( + e.position, + e.offset || defaultOffset, + e.color || defaultColor + ); + break; + } + case "image": { + this.addImage( + e.url, + e.format || "", + e.position, + e.offset || defaultOffset, + e.width || 90, + e.height || 60 + ); + break; + } + case "text": { + this.addText( + e.position, + e.offset || defaultOffset, + e.width || defaultWidth, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + e.text || "" + ); + break; + } + case "box": { + this.addBox( + e.position, + e.offset || defaultOffset, + e.width || 90, + e.height || 60, + e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, + e.color || defaultBgColor, + e.brcolor || defaultBorderColor + ); + + break; + } + case "textbox": { + this.addTextBox( + e.position, + e.offset || defaultOffset, + e.width, + e.height, + e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, + e.padding || defaultPadding, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + e.background || defaultBgColor, + e.text || "", + e.brcolor || defaultBorderColor + ); + break; + } + } + }); + } + this.pdf.doc.save( + this.selectedGauge.properties.objname + + " Hydrological-condition Diagram.pdf" + ); + }, + addDiagram(position, offset, width, height) { + let x = offset.x, + y = offset.y; + // check if there are tow diagrams on the screen + // draw the diagram in a separated html element to get the full size + if ( + ["GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS"].indexOf(this.paneSetup) !== + -1 + ) { + this.containerId = "tmpContainer"; + // set width and height + document.querySelector("#tmpContainer").style.width = + document.querySelector("#hydrologicalconditions-diagram-container") + .clientWidth * + 2 + + document.querySelector("#diagramlegendId").clientWidth + + "px"; + document.querySelector("#tmpContainer").style.height = + document.querySelector("#hydrologicalconditions-diagram-container") + .clientHeight + "px"; + this.drawDiagram(); + } + var svg = document.getElementById(this.containerId).innerHTML; + if (svg) { + svg = svg.replace(/\r?\n|\r/g, "").trim(); + } + this.containerId = "hydrologicalconditions-diagram-container"; + var canvas = document.createElement("canvas"); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight / 2; + canvg(canvas, svg, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true + }); + var imgData = canvas.toDataURL("image/png"); + // use default width,height if they are missing in the template definition + if (!width) { + width = this.templateData.properties.paperSize === "a3" ? 380 : 290; + } + if (!height) { + height = this.templateData.properties.paperSize === "a3" ? 130 : 100; + } + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - height; + } + this.pdf.doc.addImage(imgData, "PNG", x, y, width, height); + }, + applyChange() { + if (this.form.template.hasOwnProperty("properties")) { + this.templateData = this.defaultTemplate; + return; + } + if (this.form.template) { + HTTP.get("/templates/diagram/" + this.form.template.name, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + this.templateData = response.data.template_data; + }) + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + // Diagram legend + addDiagramLegend(position, offset, color) { + let x = offset.x, + y = offset.y; + let width = + (this.pdf.doc.getStringUnitWidth("Long-term Amplitude") * 10) / + (72 / 25.6) + + 5; + // if position is on the right, x needs to be calculate with pdf width and + // the size of the element + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - this.getTextHeight(4); + } + this.pdf.doc.setFontSize(10); + this.pdf.doc.setTextColor(color); + this.pdf.doc.setDrawColor("white"); + this.pdf.doc.setFillColor("red"); + this.pdf.doc.circle(x, y, 2, "FD"); + this.pdf.doc.text(x + 3, y + 1, "" + this.yearCompare); + this.pdf.doc.setFillColor("orange"); + this.pdf.doc.circle(x, y + 5, 2, "FD"); + this.pdf.doc.text(x + 3, y + 6, "Q25%"); + this.pdf.doc.setFillColor("black"); + this.pdf.doc.circle(x, y + 10, 2, "FD"); + this.pdf.doc.text(x + 3, y + 11, "Median "); + this.pdf.doc.setFillColor("purple"); + this.pdf.doc.circle(x, y + 15, 2, "FD"); + this.pdf.doc.text(x + 3, y + 16, "Q75%"); + this.pdf.doc.setFillColor("lightsteelblue"); + this.pdf.doc.circle(x, y + 20, 2, "FD"); + this.pdf.doc.text(x + 3, y + 21, "Long-term Amplitude"); + }, drawDiagram() { - // TODO: Optimize code. I'm pretty sure all of this can be done in a much - // more elegant way and with less lines of code. - // remove old diagram - d3.select(".diagram-container svg").remove(); + d3.select("#" + this.containerId + " svg").remove(); + if (!this.selectedGauge || !this.longtermWaterlevels.length) return; + // PREPARE HELPERS - if (!this.selectedGauge || !this.meanWaterlevels.length) return; - - // get HDC/LDC/MW of the gauge - let refWaterLevels = JSON.parse( + // HDC/LDC/MW for the selected gauge + const refWaterLevels = JSON.parse( this.selectedGauge.properties.reference_water_levels ); - // CREATE SVG AND SET DIMENSIONS/MARGINS + // dimensions (widths, heights, margins) + this.dimensions = this.getDimensions(); + + // get min/max values for date and waterlevel axis + this.extent = this.getExtent(refWaterLevels); + + // scaling helpers + this.scale = this.getScale(); - let svgWidth = document.querySelector(".diagram-container").clientWidth; - let svgHeight = document.querySelector(".diagram-container").clientHeight; - let svg = d3 - .select(".diagram-container") + // creating the axes based on the scales + this.axes = this.getAxes(); + + // DRAW DIAGRAM/NAVIGATION AREAS + + // create svg + this.svg = d3 + .select("#" + this.containerId) .append("svg") .attr("width", "100%") .attr("height", "100%"); - let mainMargin = { top: 20, right: 20, bottom: 110, left: 80 }, - navMargin = { - top: svgHeight - mainMargin.top - 65, - right: 20, - bottom: 30, - left: 80 - }, - width = +svgWidth - mainMargin.left - mainMargin.right, - mainHeight = +svgHeight - mainMargin.top - mainMargin.bottom, - navHeight = +svgHeight - navMargin.top - navMargin.bottom; + + // create container for main diagram + this.diagram = this.svg + .append("g") + .attr("class", "main") + .attr( + "transform", + `translate(${this.dimensions.mainMargin.left}, ${ + this.dimensions.mainMargin.top + })` + ); + + // create container for navigation diagram + this.navigation = this.svg + .append("g") + .attr("class", "nav") + .attr( + "transform", + `translate(${this.dimensions.navMargin.left}, ${ + this.dimensions.navMargin.top + })` + ); + + // define visible area, everything outside this area will be hidden + this.svg + .append("defs") + .append("clipPath") + .attr("id", "hydrocond-clip") + .append("rect") + .attr("width", this.dimensions.width) + .attr("height", this.dimensions.mainHeight); - // PREPARING AXES/SCALING + // DRAW DIAGRAM PARTS + + // Each drawSomething function (with the exception of drawRefLines) + // returns a fuction to update the respective chart/area/etc. These + // updater functions are used by the zoom feature to rescale all elements. + const updaters = []; + + // draw + updaters.push(this.drawAxes()); + updaters.push(this.drawWaterlevelMinMaxAreaChart()); + updaters.push(this.drawWaterlevelLineChart("median")); + updaters.push(this.drawWaterlevelLineChart("q25")); + updaters.push(this.drawWaterlevelLineChart("q75")); + updaters.push(this.drawWaterlevelLineChart("mean", this.yearWaterlevels)); + updaters.push(this.drawNowLines()); + this.drawRefLines(refWaterLevels); // static, doesn't need an updater + + // INTERACTIONS + + // create rectanlge on the main chart area to capture mouse events + const eventRect = this.svg + .append("rect") + .attr("id", "zoom-hydrocond") + .attr("class", "zoom") + .attr("width", this.dimensions.width) + .attr("height", this.dimensions.mainHeight) + .attr( + "transform", + `translate(${this.dimensions.mainMargin.left}, ${ + this.dimensions.mainMargin.top + })` + ); - // scaling helpers to convert real values to pixels - // based on the diagrams dimensions - let x = d3.scaleTime().range([0, width]), - x2 = d3.scaleTime().range([0, width]), - y = d3.scaleLinear().range([mainHeight, 0]), - y2 = d3.scaleLinear().range([navHeight, 0]); - // find min/max values for the waterlevel axis - // including HDC/LDC (+/- 1/8 HDC-LDC) - let WaterlevelMinMax = d3.extent( - [ - ...this.meanWaterlevels, - { - waterlevel: - refWaterLevels.HDC + (refWaterLevels.HDC - refWaterLevels.LDC) / 8 - }, - { - waterlevel: Math.max( - refWaterLevels.LDC - - (refWaterLevels.HDC - refWaterLevels.LDC) / 8, - 0 - ) - } - ], - d => d.waterlevel + this.createZoom(updaters, eventRect); + this.createTooltips(eventRect); + this.setInlineStyles(); + }, + setInlineStyles() { + this.svg.selectAll(".hide").attr("fill-opacity", 0); + this.svg + .selectAll(".line") + .attr("clip-path", "url(#hydrocond-clip)") + .attr("stroke-width", 2) + .attr("fill", "none"); + this.svg.selectAll(".line.mean").attr("stroke", "red"); + this.svg.selectAll(".line.median").attr("stroke", "black"); + this.svg.selectAll(".line.q25").attr("stroke", "orange"); + this.svg.selectAll(".line.q75").attr("stroke", "purple"); + this.svg + .selectAll(".area") + .attr("clip-path", "url(#hydrocond-clip)") + .attr("stroke", "none") + .attr("fill", "lightsteelblue"); + this.svg + .selectAll(".hdc-line, .ldc-line, .mw-line, .rn-line") + .attr("stroke-width", 1) + .attr("fill", "none") + .attr("clip-path", "url(#hydrocond-clip)"); + this.svg.selectAll(".hdc-line").attr("stroke", "red"); + this.svg.selectAll(".ldc-line").attr("stroke", "green"); + this.svg.selectAll(".mw-line").attr("stroke", "grey"); + this.svg.selectAll(".rn-line").attr("stroke", "grey"); + this.svg + .selectAll(".ref-waterlevel-label") + .style("font-size", "10px") + .attr("fill", "black"); + this.svg + .selectAll(".ref-waterlevel-label-background") + .attr("fill", "rgba(255, 255, 255, 0.6)"); + this.svg + .selectAll(".now-line") + .attr("stroke", "#999") + .attr("stroke-width", 1) + .attr("stroke-dasharray", "5, 5") + .attr("clip-path", "url(#hydrocond-clip)"); + this.svg + .selectAll(".now-line-label") + .attr("fill", "#999") + .style("font-size", "11px"); + this.svg + .selectAll(".tick line") + .attr("stroke-dasharray", 5) + .attr("stroke", " #ccc"); + this.svg.selectAll(".tick text").attr("fill", "black"); + this.svg.selectAll(".domain").attr("stroke", "black"); + + this.svg + .selectAll(".zoom") + .attr("cursor", "move") + .attr("fill", "none") + .attr("pointer-events", "all"); + this.svg + .selectAll(".brush .selection") + .attr("stroke", "none") + .attr("fill-opacity", 0.2); + this.svg + .selectAll(".brush .handle") + .attr("stroke", "rgba(23, 162, 184, 0.5)") + .attr("fill", "rgba(23, 162, 184, 0.5)"); + this.svg + .selectAll(".chart-dots") + .attr("clip-path", "url(#hydrocond-clip)"); + this.svg + .selectAll(".chart-dots .chart-dot") + .attr("fill", "black") + .attr("stroke", "black") + .attr("stroke-opacity", 0) + .style("pointer-events", "none") + .attr("fill-opacity", 0) + .transition() + .attr("fill-opacity", "0.1s"); + this.svg + .selectAll(".chart-tooltip") + .attr("fill-opacity", 0) + .transition() + .attr("fill-opacity", "0.3s"); + this.svg + .selectAll(".chart-tooltip rect") + .attr("fill", "#fff") + .attr("stroke", "#ccc"); + this.svg + .selectAll(".chart-tooltip text") + .attr("fill", "666") + .style("font-size", "0.8em"); + }, + getDimensions() { + // dimensions and margins + const svgWidth = document.querySelector("#" + this.containerId) + .clientWidth; + const svgHeight = document.querySelector("#" + this.containerId) + .clientHeight; + const mainMargin = { top: 20, right: 20, bottom: 110, left: 80 }; + const navMargin = { + top: svgHeight - mainMargin.top - 65, + right: 20, + bottom: 30, + left: 80 + }; + const width = +svgWidth - mainMargin.left - mainMargin.right; + const mainHeight = +svgHeight - mainMargin.top - mainMargin.bottom; + const navHeight = +svgHeight - navMargin.top - navMargin.bottom; + + return { width, mainHeight, navHeight, mainMargin, navMargin }; + }, + getExtent(refWaterLevels) { + const waterlevelsRelevantForExtent = []; + this.longtermWaterlevels.forEach(wl => { + waterlevelsRelevantForExtent.push(wl.min, wl.max); + }); + waterlevelsRelevantForExtent.push( + refWaterLevels.HDC + (refWaterLevels.HDC - refWaterLevels.LDC) / 8, + Math.max( + refWaterLevels.LDC - (refWaterLevels.HDC - refWaterLevels.LDC) / 4, + 0 + ) ); + return { + // set min/max values for the date axis + date: [startOfYear(new Date()), endOfYear(new Date())], + // set min/max values for the waterlevel axis + // including HDC (+ 1/8 HDC-LDC) and LDC (- 1/4 HDC-LDC) + waterlevel: d3.extent(waterlevelsRelevantForExtent) + }; + }, + getScale() { + // scaling helpers to convert real world values into pixels + const x = d3.scaleTime().range([0, this.dimensions.width]); + const y = d3.scaleLinear().range([this.dimensions.mainHeight, 0]); + const x2 = d3.scaleTime().range([0, this.dimensions.width]); + const y2 = d3.scaleLinear().range([this.dimensions.navHeight, 0]); + // setting the min and max values for the diagram axes - let yearStart = startOfYear(new Date()); - let yearEnd = endOfYear(new Date()); - x.domain(d3.extent([yearStart, yearEnd])); - y.domain(WaterlevelMinMax); + x.domain(d3.extent(this.extent.date)); + y.domain(this.extent.waterlevel); x2.domain(x.domain()); y2.domain(y.domain()); - // creating the axes based on these scales - let xAxis = d3 - .axisTop(x) - .tickSizeInner(mainHeight) - .tickSizeOuter(0) - .tickFormat(date => { - return (d3.timeSecond(date) < date - ? d3.timeFormat(".%L") - : d3.timeMinute(date) < date - ? d3.timeFormat(":%S") - : d3.timeHour(date) < date - ? d3.timeFormat("%I:%M") - : d3.timeDay(date) < date - ? d3.timeFormat("%I %p") - : d3.timeMonth(date) < date - ? d3.timeWeek(date) < date - ? d3.timeFormat("%a %d") - : d3.timeFormat("%b %d") - : d3.timeFormat("%B"))(date); - }); - let xAxis2 = d3.axisBottom(x2).tickFormat(d3.timeFormat("%B")); - let yAxis = d3 - .axisRight(y) - .tickSizeInner(width) - .tickSizeOuter(0); - // PREPARING CHART FUNCTIONS - - // points are "next to each other" when they are exactly 1 day apart - const isNext = (prev, current) => - current.date - prev.date === 24 * 60 * 60 * 1000; + return { x, y, x2, y2 }; + }, + getAxes() { + return { + x: d3 + .axisTop(this.scale.x) + .tickSizeInner(this.dimensions.mainHeight) + .tickSizeOuter(0) + .tickFormat(date => { + // make the x-axis label formats dynamic, based on zoom + // but never display year numbers since they don't make any sense in + // this diagram + return (d3.timeSecond(date) < date + ? d3.timeFormat(".%L") + : d3.timeMinute(date) < date + ? d3.timeFormat(":%S") + : d3.timeHour(date) < date + ? d3.timeFormat("%I:%M") + : d3.timeDay(date) < date + ? d3.timeFormat("%I %p") + : d3.timeMonth(date) < date + ? d3.timeWeek(date) < date + ? d3.timeFormat("%a %d") + : d3.timeFormat("%b %d") + : d3.timeFormat("%B"))(date); + }), + y: d3 + .axisRight(this.scale.y) + .tickSizeInner(this.dimensions.width) + .tickSizeOuter(0), + x2: d3.axisBottom(this.scale.x2) + }; + }, + drawNowLines() { + const nowLine = d3 + .line() + .x(d => this.scale.x(d.x)) + .y(d => this.scale.y(d.y)); - // waterlevel line in big chart - // d3-line-chunked plugin: https://github.com/pbeshai/d3-line-chunked - var mainLineChart = d3 - .lineChunked() - .x(d => x(d.date)) - .y(d => y(d.waterlevel)) - .curve(d3.curveLinear) - .isNext(isNext) - .pointAttrs({ r: 2.2 }); - // waterlevel line in small chart - let navLineChart = d3 - .lineChunked() - .x(d => x2(d.date)) - .y(d => y2(d.waterlevel)) - .curve(d3.curveMonotoneX) - .isNext(isNext) - .pointAttrs({ r: 1.7 }); - // hdc/ldc/mw - let refWaterlevelLine = d3 - .line() - .x(d => x(d.x)) - .y(d => y(d.y)); + const nowLabel = selection => { + selection.attr( + "transform", + `translate(${this.scale.x(new Date())}, ${this.scale.y( + this.extent.waterlevel[1] - 16 + )})` + ); + }; - // DRAWING MAINCHART + // draw in main + this.diagram + .append("path") + .datum([ + { x: new Date(), y: this.extent.waterlevel[0] }, + { x: new Date(), y: this.extent.waterlevel[1] - 20 } + ]) + .attr("class", "now-line") + .attr("d", nowLine); + this.diagram // label + .append("text") + .text(this.$gettext("Now")) + .attr("class", "now-line-label") + .attr("text-anchor", "middle") + .call(nowLabel); - // define visible chart area - // everything outside this area will be hidden (clipped) - svg - .append("defs") - .append("clipPath") - .attr("id", "clip") - .append("rect") - .attr("width", width) - .attr("height", mainHeight); + // draw in nav + this.navigation + .append("path") + .datum([ + { x: new Date(), y: this.extent.waterlevel[0] }, + { x: new Date(), y: this.extent.waterlevel[1] - 20 } + ]) + .attr("class", "now-line") + .attr( + "d", + d3 + .line() + .x(d => this.scale.x2(d.x)) + .y(d => this.scale.y2(d.y)) + ); - let mainChart = svg - .append("g") - .attr("class", "main") - .attr("transform", `translate(${mainMargin.left}, ${mainMargin.top})`); - - // axes - mainChart + return () => { + this.diagram.select(".now-line").attr("d", nowLine); + this.diagram.select(".now-line-label").call(nowLabel); + }; + }, + drawAxes() { + this.diagram .append("g") .attr("class", "axis--x") - .attr("transform", `translate(0, ${mainHeight})`) - .call(xAxis) + .attr("transform", `translate(0, ${this.dimensions.mainHeight})`) + .call(this.axes.x) .selectAll(".tick text") .attr("y", 15); - mainChart // label + this.diagram // label .append("text") .text(this.$gettext("Waterlevel [cm]")) .attr("text-anchor", "middle") - .attr("transform", `translate(-45, ${mainHeight / 2}) rotate(-90)`); - mainChart + .attr( + "transform", + `translate(-45, ${this.dimensions.mainHeight / 2}) rotate(-90)` + ); + this.diagram .append("g") - .call(yAxis) + .call(this.axes.y) .selectAll(".tick text") .attr("x", -25); - // reference waterlevels - // HDC - mainChart - .append("path") - .datum([ - { x: 0, y: refWaterLevels.HDC }, - { x: yearEnd, y: refWaterLevels.HDC } - ]) - .attr("class", "hdc-line") - .attr("d", refWaterlevelLine); - mainChart // label - .append("text") - .text("HDC") - .attr("class", "ref-waterlevel-label") - .attr("x", x(yearEnd) - 20) - .attr("y", y(refWaterLevels.HDC) - 3); - // LDC - mainChart - .append("path") - .datum([ - { x: 0, y: refWaterLevels.LDC }, - { x: yearEnd, y: refWaterLevels.LDC } - ]) - .attr("class", "ldc-line") - .attr("d", refWaterlevelLine); - mainChart // label - .append("text") - .text("LDC") - .attr("class", "ref-waterlevel-label") - .attr("x", x(yearEnd) - 20) - .attr("y", y(refWaterLevels.LDC) - 3); - // MW - mainChart - .append("path") - .datum([ - { x: 0, y: refWaterLevels.MW }, - { x: yearEnd, y: refWaterLevels.MW } - ]) - .attr("class", "mw-line") - .attr("d", refWaterlevelLine); - mainChart // label - .append("text") - .text("MW") - .attr("class", "ref-waterlevel-label") - .attr("x", x(yearEnd) - 20) - .attr("y", y(refWaterLevels.MW) - 3); - - // waterlevel chart - mainChart - .append("g") - .attr("class", "line") - .datum([]) - .call(mainLineChart); - - // DRAWING NAVCHART - - let navChart = svg - .append("g") - .attr("class", "nav") - .attr("transform", `translate(${navMargin.left}, ${navMargin.top})`); - - // axis (nav chart only has x-axis) - navChart + this.navigation .append("g") .attr("class", "axis axis--x") - .attr("transform", `translate(0, ${navHeight})`) - .call(xAxis2); + .attr("transform", `translate(0, ${this.dimensions.navHeight})`) + .call(this.axes.x2); + + return () => { + this.diagram + .select(".axis--x") + .call(this.axes.x) + .selectAll(".tick text") + .attr("y", 15); + }; + }, + drawWaterlevelMinMaxAreaChart() { + const areaChart = isNav => + d3 + .area() + .x(d => this.scale[isNav ? "x2" : "x"](d.date)) + .y0(d => this.scale[isNav ? "y2" : "y"](d.min)) + .y1(d => this.scale[isNav ? "y2" : "y"](d.max)); + + this.diagram + .append("path") + .datum(this.longtermWaterlevels) + .attr("class", "area") + .attr("d", areaChart()); + + this.navigation + .append("path") + .datum(this.longtermWaterlevels) + .attr("class", "area") + .attr("d", areaChart(true)); - // waterlevel chart - navChart - .append("g") - .attr("class", "line") - .datum([]) - .call(navLineChart); + return () => { + this.diagram.select(".area").attr("d", areaChart()); + }; + }, + drawWaterlevelLineChart(type, data) { + const lineChart = type => + d3 + .line() + .x(d => this.scale.x(d.date)) + .y(d => this.scale.y(d[type])) + .curve(d3.curveLinear); + this.diagram + .append("path") + .attr("class", "line " + type) + .datum(data || this.longtermWaterlevels) + .attr("d", lineChart(type)); + + return () => { + this.diagram.select(".line." + type).attr("d", lineChart(type)); + }; + }, + drawRefLines(refWaterLevels) { + const refWaterlevelLine = d3 + .line() + .x(d => this.scale.x(d.x)) + .y(d => this.scale.y(d.y)); - // INTERACTIVITY - - // selecting time period in nav chart - let brush = d3 + for (let ref in refWaterLevels) { + if (refWaterLevels[ref]) { + this.diagram + .append("path") + .datum([ + { x: 0, y: refWaterLevels[ref] }, + { x: this.extent.date[1], y: refWaterLevels[ref] } + ]) + .attr("class", ref.toLowerCase() + "-line") + .attr("d", refWaterlevelLine); + this.diagram // label + .append("rect") + .attr("class", "ref-waterlevel-label-background") + .attr("x", 1) + .attr("y", this.scale.y(refWaterLevels[ref]) - 13) + .attr("width", 55) + .attr("height", 12); + this.diagram + .append("text") + .text(`${ref} (${refWaterLevels[ref]})`) + .attr("class", "ref-waterlevel-label") + .attr("x", 5) + .attr("y", this.scale.y(refWaterLevels[ref]) - 3); + } + } + }, + createZoom(updaters, eventRect) { + const brush = d3 .brushX() .handleSize(4) - .extent([[0, 0], [width, navHeight]]) - .on("brush end", () => { - if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") - return; // ignore brush-by-zoom - let s = d3.event.selection || x2.range(); - x.domain(s.map(x2.invert, x2)); - mainChart.select(".line").call(mainLineChart); - mainChart - .select(".axis--x") - .call(xAxis) - .selectAll(".tick text") - .attr("y", 15); - svg - .select(".zoom") - .call( - zoom.transform, - d3.zoomIdentity.scale(width / (s[1] - s[0])).translate(-s[0], 0) - ); - }); + .extent([[0, 0], [this.dimensions.width, this.dimensions.navHeight]]); - // zooming with mousewheel in main chart - let zoom = d3 + const zoom = d3 .zoom() .scaleExtent([1, Infinity]) - .translateExtent([[0, 0], [width, mainHeight]]) - .extent([[0, 0], [width, mainHeight]]) - .on("zoom", () => { - if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") - return; // ignore zoom-by-brush - let t = d3.event.transform; - x.domain(t.rescaleX(x2).domain()); - mainChart.select(".line").call(mainLineChart); - mainChart - .select(".axis--x") - .call(xAxis) - .selectAll(".tick text") - .attr("y", 15); - navChart - .select(".brush") - .call(brush.move, x.range().map(t.invertX, t)); - }); + .translateExtent([ + [0, 0], + [this.dimensions.width, this.dimensions.mainHeight] + ]) + .extent([[0, 0], [this.dimensions.width, this.dimensions.mainHeight]]); - navChart + brush.on("brush end", () => { + if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") + return; // ignore brush-by-zoom + let s = d3.event.selection || this.scale.x2.range(); + this.scale.x.domain(s.map(this.scale.x2.invert, this.scale.x2)); + updaters.forEach(u => u && u()); + this.setInlineStyles(); + this.svg + .select(".zoom") + .call( + zoom.transform, + d3.zoomIdentity + .scale(this.dimensions.width / (s[1] - s[0])) + .translate(-s[0], 0) + ); + }); + + zoom.on("zoom", () => { + if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") + return; // ignore zoom-by-brush + let t = d3.event.transform; + this.scale.x.domain(t.rescaleX(this.scale.x2).domain()); + updaters.forEach(u => u && u()); + this.setInlineStyles(); + this.navigation + .select(".brush") + .call(brush.move, this.scale.x.range().map(t.invertX, t)); + }); + zoom.on("start", () => { + this.svg.select(".chart-dot").style("opacity", 0); + this.svg.select(".chart-tooltip").style("opacity", 0); + }); + + this.navigation .append("g") .attr("class", "brush") .call(brush) - .call(brush.move, x.range()); + .call(brush.move, this.scale.x.range()); - svg + eventRect.call(zoom); + }, + createTooltips(eventRect) { + // create clippable container for the dot + this.diagram + .append("g") + .attr("class", "chart-dots") + .append("circle") + .attr("class", "chart-dot") + .attr("r", 4); + + // create container for the tooltip + const tooltip = this.diagram.append("g").attr("class", "chart-tooltip"); + tooltip .append("rect") - .attr("class", "zoom") - .attr("width", width) - .attr("height", mainHeight) - .attr("transform", `translate(${mainMargin.left}, ${mainMargin.top})`) - .call(zoom); + .attr("rx", "0.25em") + .attr("ry", "0.25em"); + + // create container for multiple text rows + const tooltipText = tooltip.append("text").attr("text-anchor", "middle"); + + // padding inside the tooltip box and diagram padding to determine left + // and right offset from the diagram boundaries for the tooltip position. + const tooltipPadding = 10; + const diagramPadding = 5; + + eventRect + .on("mouseover", () => { + this.diagram.select(".chart-dot").style("opacity", 1); + this.diagram.select(".chart-tooltip").style("opacity", 1); + }) + .on("mouseout", () => { + this.diagram.select(".chart-dot").style("opacity", 0); + this.diagram.select(".chart-tooltip").style("opacity", 0); + }) + .on("mousemove", () => { + // find data point closest to mouse + const x0 = this.scale.x.invert( + d3.mouse(document.getElementById("zoom-hydrocond"))[0] + ), + i = d3.bisector(d => d.date).left(this.longtermWaterlevels, x0, 1), + d0 = this.longtermWaterlevels[i - 1], + d1 = this.longtermWaterlevels[i] || d0, + d = x0 - d0.date > d1.date - x0 ? d1 : d0; + + const coords = { + x: this.scale.x(d.date), + y: this.scale.y(d.median) + }; + + // position the dot + this.diagram + .select(".chart-dot") + .style("opacity", 1) + .attr("transform", `translate(${coords.x}, ${coords.y})`); + + // remove current texts + tooltipText.selectAll("tspan").remove(); + + // write date + tooltipText + .append("tspan") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .text( + d.date.toLocaleString([], { + year: "2-digit", + month: "2-digit", + day: "2-digit" + }) + ); + + tooltipText + .append("tspan") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "1.4em") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .text(`Q75%: ${d.q75} cm`); + tooltipText + .append("tspan") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "2.6em") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .text(`Median: ${d.median} cm`); + tooltipText + .append("tspan") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "3.8em") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .text(`Q25%: ${d.q25} cm`); + tooltipText + .append("tspan") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "5em") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .text(`Max: ${d.max} cm`); + tooltipText + .append("tspan") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "6.2em") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .text(`Min: ${d.min} cm`); + + const dYear = this.yearWaterlevels.find( + ywl => ywl.date.getTime() === d.date.getTime() + ); + if (dYear) { + tooltipText + .append("tspan") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "7.4em") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .text(`${this.yearCompare}: ${dYear.mean.toFixed(1)} cm`); + } + + // get text dimensions + const textBBox = tooltipText.node().getBBox(); + + this.diagram + .selectAll(".chart-tooltip text tspan") + .attr("x", textBBox.width / 2 + tooltipPadding) + .attr("y", tooltipPadding); + + // position and scale tooltip box + const xMax = + this.dimensions.width - + (textBBox.width + diagramPadding + tooltipPadding * 2); + const tooltipX = Math.max( + diagramPadding, + Math.min(coords.x - (textBBox.width + tooltipPadding * 2) / 2, xMax) + ); + let tooltipY = coords.y - (textBBox.height + tooltipPadding * 2) - 10; + if (coords.y < textBBox.height + tooltipPadding * 2) { + tooltipY = coords.y + 10; + } + + this.diagram + .select(".chart-tooltip") + .style("opacity", 1) + .attr("transform", `translate(${tooltipX}, ${tooltipY})`) + .select("rect") + .attr("width", textBBox.width + tooltipPadding * 2) + .attr("height", textBBox.height + tooltipPadding * 2); + }); } }, created() { @@ -386,9 +1115,36 @@ }, mounted() { this.drawDiagram(); + this.templates[0] = this.defaultTemplate; + this.form.template = this.templates[0]; + this.templateData = this.form.template; + HTTP.get("/templates/diagram", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + if (response.data.length) { + this.templates = response.data; + this.form.template = this.templates[0]; + this.templates[this.templates.length] = this.defaultTemplate; + this.applyChange(); + } + }) + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); }, updated() { this.drawDiagram(); + }, + destroyed() { + window.removeEventListener("resize", debounce(this.drawDiagram)); } }; </script>
--- a/client/src/components/gauge/Waterlevel.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/gauge/Waterlevel.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,112 +1,90 @@ <template> - <div - class="d-flex flex-fill justify-content-center align-items-center diagram-container" - > - <div v-if="!waterlevels.length"> - <translate>No data available.</translate> + <div class="d-flex flex-column flex-fill"> + <UIBoxHeader + icon="ruler-vertical" + :title="title" + :closeCallback="close" + class="rounded-0" + /> + <div class="d-flex flex-fill"> + <DiagramLegend id="diagramlegendId"> + <div class="legend"> + <span + style="background-color: steelblue; width: 20px; height: 20px;" + ></span> + Waterlevel + </div> + <div class="legend"> + <span + style="width: 8px; height: 8px; background-color: rgba(70, 130, 180, 0.6); border: solid 7px rgba(70, 130, 180, 0.2); background-clip: padding-box; box-sizing: content-box;" + ></span> + Prediction + </div> + <div class="legend"> + <span + style="background-color: rgba(0, 255, 0, 0.1); width: 20px; height: 20px;" + ></span> + Navigable Range + </div> + <div> + <select + @change="applyChange" + v-model="form.template" + class="form-control d-block custom-select-sm w-100" + > + <option + v-for="template in templates" + :value="template" + :key="template.name" + > + {{ template.name }} + </option> + </select> + <button + @click="downloadPDF" + type="button" + class="btn btn-sm btn-info d-block w-100 mt-2" + :disabled="!waterlevels.length" + > + <translate>Export to PDF</translate> + </button> + <a + :class="[ + 'btn btn-sm btn-info d-block w-100 mt-2', + { disabled: !waterlevels.length } + ]" + :href="csvLink" + :download="csvFileName" + > + <translate>Export as CSV</translate> + </a> + + <button + @click="downloadSVG" + type="button" + class="btn btn-sm btn-info d-block w-100 mt-2" + :disabled="!waterlevels.length" + > + <translate>Export as SVG</translate> + </button> + </div> + </DiagramLegend> + <div + class="d-flex flex-fill justify-content-center align-items-center" + :id="containerId" + > + <div v-if="!waterlevels.length"> + <translate>No data available.</translate> + </div> + </div> </div> + <div + id="pdfContainer" + style="position: absolute; z-index: -1; top: -9999px;" + ></div> </div> </template> -<style lang="sass" scoped> -.diagram-container - /deep/ - .line - clip-path: url(#clip) - path - stroke: steelblue - stroke-width: 2 - fill: none - &.d3-line-chunked-chunk-gap - stroke-width: 1 - stroke-dasharray: 4, 4 - stroke-opacity: 1 - circle - stroke-width: 0 - fill: steelblue - - .hdc-line, - .ldc-line, - .mw-line - stroke-width: 1 - fill: none - clip-path: url(#clip) - .hdc-line - stroke: red - .ldc-line - stroke: green - .mw-line - stroke: grey - .ref-waterlevel-label - font-size: 11px - fill: #999 - .hdc-ldc-area - fill: rgba(0, 255, 0, 0.1) - .now-line - stroke: #999 - stroke-width: 1 - stroke-dasharray: 5, 5 - clip-path: url(#clip) - .now-line-label - font-size: 11px - fill: #999 - .prediction-area - fill: steelblue - fill-opacity: 0.2 - clip-path: url(#clip) - - path.nash-sutcliffe - fill: none - stroke: black - stroke-width: 1 - clip-path: url(#clip) - &.ns72 - fill: rgba(0, 0, 0, 0.05) - text.nash-sutcliffe - font-size: 10px - clip-path: url(#clip) - tspan:last-child - font-size: 9px - fill: #777 - - .tick - line - stroke-dasharray: 5 - stroke: #ccc - - .zoom - cursor: move - fill: none - pointer-events: all - .brush - .selection - stroke: none - fill-opacity: 0.2 - .handle - stroke: rgba($color-info, 0.5) - fill: rgba($color-info, 0.5) - - .chart-dots - clip-path: url(#clip) - .chart-dot - fill: steelblue - stroke: steelblue - pointer-events: none - opacity: 0 - transition: opacity 0.1s - .chart-tooltip - opacity: 0 - transition: opacity 0.3s - rect - fill: #fff - stroke: #ccc - text - fill: #666 - font-size: 12px - tspan:last-child - font-weight: bold -</style> - <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -114,624 +92,1200 @@ * SPDX-License-Identifier: AGPL-3.0-or-later * License-Filename: LICENSES/AGPL-3.0.txt * - * Copyright (C) 2018 by via donau + * Copyright (C) 2019 by via donau * – Österreichische Wasserstraßen-Gesellschaft mbH * Software engineering by Intevation GmbH * * Author(s): - * Markus Kottländer <markus.kottlaender@intevation.de> + * * Bernhard Reiter <bernhard@intevation.de> + * * Markus Kottländer <markus.kottlaender@intevation.de> + * * Fadi Abbud <fadi.abbud@intevation.de> */ import { mapState, mapGetters } from "vuex"; import * as d3Base from "d3"; import { lineChunked } from "d3-line-chunked"; +import { endOfDay } from "date-fns"; import debounce from "debounce"; - +import jsPDF from "jspdf"; +import canvg from "canvg"; +import { saveAs } from "file-saver"; +import { pdfgen } from "@/lib/mixins"; +import { HTTP } from "@/lib/http"; +import { displayError } from "@/lib/errors"; // we should load only d3 modules we need but for now we'll go with the lazy way // https://www.giacomodebidda.com/how-to-import-d3-plugins-with-webpack/ +// d3-line-chunked plugin: https://github.com/pbeshai/d3-line-chunked const d3 = Object.assign(d3Base, { lineChunked }); export default { + mixins: [pdfgen], + components: { + DiagramLegend: () => import("@/components/DiagramLegend") + }, + data() { + return { + containerId: "waterlevel-diagram-container", + svg: null, + diagram: null, + navigation: null, + dimensions: null, + extent: null, + scale: null, + axes: null, + form: { + template: null + }, + templates: [], + defaultTemplate: { + name: "Default", + properties: { + paperSize: "a4" + }, + elements: [ + { + type: "diagram", + position: "topleft", + offset: { x: 15, y: 50 }, + width: 290, + height: 100 + }, + { + type: "diagramlegend", + position: "topleft", + offset: { x: 30, y: 150 }, + color: "black" + }, + { + type: "diagramtitle", + position: "topleft", + offset: { x: 50, y: 26 }, + fontsize: 22, + color: "steelblue" + }, + { + type: "text", + position: "topleft", + offset: { x: 3, y: 3 }, + fontsize: 8, + width: 90, + color: "gray", + text: this.$gettext("Generated by") + " " + "{user}, {date}" + } + ] + }, + pdf: { + doc: null, + width: 420, + height: 297 + }, + templateData: null + }; + }, computed: { + ...mapState("application", ["paneSetup"]), ...mapState("gauges", [ + "dateFrom", "waterlevels", - "dateFrom", - "dateTo", + "waterlevelsCSV", "nashSutcliffe" ]), - ...mapGetters("gauges", ["selectedGauge"]) + ...mapGetters("gauges", ["selectedGauge"]), + title() { + return `${this.selectedGauge.properties.objname}: ${this.$gettext( + "Waterlevel" + )} (${this.dateFrom.toLocaleDateString()} - ${this.dateTo.toLocaleDateString()})`; + }, + dateFrom: { + get() { + return this.$store.state.gauges.dateFrom; + } + }, + dateTo: { + get() { + return this.$store.state.gauges.dateTo; + } + }, + csvLink() { + return ( + "data:text/csv;charset=utf-8," + encodeURIComponent(this.waterlevelsCSV) + ); + }, + csvFileName() { + return `${this.$gettext("waterlevels")}-${ + this.selectedGauge.properties.objname + }-${this.dateFrom.toISOString().split("T")[0]}-${ + this.dateTo.toISOString().split("T")[0] + }.csv`; + } }, watch: { + paneSetup() { + this.$nextTick(() => this.drawDiagram()); + }, waterlevels() { this.drawDiagram(); } }, methods: { + close() { + this.$store.commit( + "application/paneSetup", + this.paneSetup === "GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS" + ? "GAUGE_HYDROLOGICALCONDITIONS" + : "DEFAULT" + ); + }, + downloadSVG() { + let svg = document.getElementById(this.containerId).firstElementChild; + let svgXML = new XMLSerializer().serializeToString(svg); + let blog = new Blob([svgXML], { type: "image/svg+xml;charset=utf-8" }); + let filename = + this.selectedGauge.properties.objname + "-waterlevel-diagram.svg"; + saveAs(blog, filename); + }, + downloadPDF() { + this.pdf.doc = new jsPDF( + "l", + "mm", + this.templateData.properties.paperSize + ); + // pdf width and height in millimeter (landscape) + this.pdf.width = + this.templateData.properties.paperSize === "a3" ? 420 : 297; + this.pdf.height = + this.templateData.properties.paperSize === "a3" ? 297 : 210; + // check the template elements + if (this.templateData) { + let defaultFontSize = 11, + defaultColor = "black", + defaultWidth = 70, + defaultTextColor = "black", + defaultBorderColor = "white", + defaultBgColor = "white", + defaultRounding = 2, + defaultPadding = 2, + defaultOffset = { x: 0, y: 0 }; + this.templateData.elements.forEach(e => { + switch (e.type) { + case "diagram": { + this.addDiagram( + e.position, + e.offset || defaultOffset, + e.width, + e.height + ); + break; + } + case "diagramlegend": { + this.addDiagramLegend( + e.position, + e.offset || defaultOffset, + e.color || defaultColor + ); + break; + } + case "diagramtitle": { + let gaugeInfo = + this.selectedGauge.properties.objname + + " (" + + this.selectedGauge.id + .split(".")[1] + .replace(/[()]/g, "") + .split(",")[3] + + "):" + + " Waterlevel " + + this.dateFrom.toLocaleDateString() + + " - " + + this.dateTo.toLocaleDateString(); + this.addDiagramTitle( + e.position, + e.offset || defaultOffset, + e.fontsize || defaultFontSize, + e.color || defaultColor, + gaugeInfo + ); + break; + } + case "text": { + this.addText( + e.position, + e.offset || defaultOffset, + e.width || defaultWidth, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + e.text || "" + ); + break; + } + case "image": { + this.addImage( + e.url, + e.format || "", + e.position, + e.offset || defaultOffset, + e.width || 90, + e.height || 60 + ); + break; + } + case "box": { + this.addBox( + e.position, + e.offset || defaultOffset, + e.width || 90, + e.height || 60, + e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, + e.color || defaultBgColor, + e.brcolor || defaultBorderColor + ); + break; + } + case "textbox": { + this.addTextBox( + e.position, + e.offset || defaultOffset, + e.width, + e.height, + e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, + e.padding || defaultPadding, + e.fontsize || defaultFontSize, + e.color || defaultTextColor, + e.background || defaultBgColor, + e.text || "", + e.brcolor || defaultBorderColor + ); + break; + } + } + }); + } + this.pdf.doc.save( + this.selectedGauge.properties.objname + " Waterlevel-Diagram.pdf" + ); + }, + applyChange() { + if (this.form.template.hasOwnProperty("properties")) { + this.templateData = this.defaultTemplate; + return; + } + if (this.form.template) { + HTTP.get("/templates/diagram/" + this.form.template.name, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + this.templateData = response.data.template_data; + }) + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + addDiagram(position, offset, width, height) { + let x = offset.x, + y = offset.y; + // check if there are tow diagrams on screen + if ( + ["GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS"].indexOf(this.paneSetup) !== + -1 + ) { + this.containerId = "pdfContainer"; + // set width and height + document.querySelector("#pdfContainer").style.width = + document.querySelector("#waterlevel-diagram-container").clientWidth * + 2 + + document.querySelector("#diagramlegendId").clientWidth + + "px"; + document.querySelector("#pdfContainer").style.height = + document.querySelector("#waterlevel-diagram-container").clientHeight + + "px"; + this.drawDiagram(); + } + var svg = document.getElementById(this.containerId).innerHTML; + if (svg) { + svg = svg.replace(/\r?\n|\r/g, "").trim(); + } + this.containerId = "waterlevel-diagram-container"; + var canvas = document.createElement("canvas"); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight / 2; + canvg(canvas, svg, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true + }); + var imgData = canvas.toDataURL("image/png"); + // use default width,height if they are missing in the template definition + if (!width) { + width = this.templateData.properties.paperSize === "a3" ? 380 : 290; + } + if (!height) { + height = this.templateData.properties.paperSize === "a3" ? 130 : 100; + } + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - height; + } + this.pdf.doc.addImage(imgData, "PNG", x, y, width, height); + }, + // Diagram legend + addDiagramLegend(position, offset, color) { + let x = offset.x; + let y = offset.y; + this.pdf.doc.setFontSize(10); + let width = + (this.pdf.doc.getStringUnitWidth("Navigable Range") * 10) / + (72 / 25.6) + + 5; + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - this.getTextHeight(4); + } + this.pdf.doc.setTextColor(color); + this.pdf.doc.setDrawColor("white"); + this.pdf.doc.setFillColor("steelblue"); + this.pdf.doc.circle(x, y, 2, "FD"); + this.pdf.doc.text(x + 3, y + 1, "Waterlevel"); + this.pdf.doc.setFillColor("#dae6f0"); + this.pdf.doc.circle(x, y + 5, 2, "FD"); + this.pdf.doc.setFillColor("#e5ffe5"); + this.pdf.doc.circle(x, y + 10, 2, "FD"); + this.pdf.doc.text(x + 3, y + 11, "Navigable Range"); + this.pdf.doc.setDrawColor("#90b4d2"); + this.pdf.doc.setFillColor("#90b4d2"); + this.pdf.doc.circle(x, y + 5, 0.6, "FD"); + this.pdf.doc.text(x + 3, y + 6, "Prediction"); + }, drawDiagram() { - // TODO: Optimize code. I'm pretty sure all of this can be done in a much - // more elegant way and with less lines of code. - - // remove old diagram - d3.select(".diagram-container svg").remove(); - + // remove old diagram and exit if necessary data is missing + d3.select("#" + this.containerId + " svg").remove(); if (!this.selectedGauge || !this.waterlevels.length) return; - // get HDC/LDC/MW of the gauge - let refWaterLevels = JSON.parse( + // PREPARE HELPERS + + // HDC/LDC/MW for the selected gauge + const refWaterLevels = JSON.parse( this.selectedGauge.properties.reference_water_levels ); - // CREATE SVG AND SET DIMENSIONS/MARGINS + // dimensions (widths, heights, margins) + this.dimensions = this.getDimensions(); + + // get min/max values for date and waterlevel axis + this.extent = this.getExtent(refWaterLevels); + + // scaling helpers + this.scale = this.getScale(); - let svgWidth = document.querySelector(".diagram-container").clientWidth; - let svgHeight = document.querySelector(".diagram-container").clientHeight; - let svg = d3 - .select(".diagram-container") + // creating the axes based on the scales + this.axes = { + x: d3 + .axisTop(this.scale.x) + .tickSizeInner(this.dimensions.mainHeight) + .tickSizeOuter(0), + y: d3 + .axisRight(this.scale.y) + .tickSizeInner(this.dimensions.width) + .tickSizeOuter(0), + x2: d3.axisBottom(this.scale.x2) + }; + + // DRAW DIAGRAM/NAVIGATION AREAS + + // create svg + this.svg = d3 + .select("#" + this.containerId) .append("svg") .attr("width", "100%") .attr("height", "100%"); - let mainMargin = { top: 20, right: 20, bottom: 110, left: 80 }, - navMargin = { - top: svgHeight - mainMargin.top - 65, - right: 20, - bottom: 30, - left: 80 - }, - width = +svgWidth - mainMargin.left - mainMargin.right, - mainHeight = +svgHeight - mainMargin.top - mainMargin.bottom, - navHeight = +svgHeight - navMargin.top - navMargin.bottom; + + // create container for main diagram + this.diagram = this.svg + .append("g") + .attr("class", "main") + .attr( + "transform", + `translate(${this.dimensions.mainMargin.left}, ${ + this.dimensions.mainMargin.top + })` + ); + + // create container for navigation diagram + this.navigation = this.svg + .append("g") + .attr("class", "nav") + .attr( + "transform", + `translate(${this.dimensions.navMargin.left}, ${ + this.dimensions.navMargin.top + })` + ); + + // define visible area, everything outside this area will be hidden + this.svg + .append("defs") + .append("clipPath") + .attr("id", "waterlevel-clip") + .append("rect") + .attr("width", this.dimensions.width) + .attr("height", this.dimensions.mainHeight); + + // DRAW DIAGRAM PARTS + + // Each drawSomething function (with the exception of drawRefLines) + // returns a fuction to update the respective chart/area/etc. These + // updater functions are used by the zoom feature to rescale all elements. + const updaters = []; + + // draw (order matters) + updaters.push(this.drawAxes()); + updaters.push(this.drawWaterlevelChart()); + updaters.push(this.drawPredictionAreas()); + updaters.push(this.drawNowLines()); - // PREPARING AXES/SCALING + // static, don't need updater + this.drawNavigationChart(); + this.drawRefLines(refWaterLevels); + + updaters.push(this.drawNashSutcliffe(72)); + updaters.push(this.drawNashSutcliffe(48)); + updaters.push(this.drawNashSutcliffe(24)); + + // INTERACTIONS + + // create rectanlge on the main chart area to capture mouse events + const eventRect = this.svg + .append("rect") + .attr("id", "zoom-waterlevels") + .attr("class", "zoom") + .attr("width", this.dimensions.width) + .attr("height", this.dimensions.mainHeight) + .attr( + "transform", + `translate(${this.dimensions.mainMargin.left}, ${ + this.dimensions.mainMargin.top + })` + ); + + this.createZoom(updaters, eventRect); + this.createTooltips(eventRect); + this.setInlineStyles(); + }, + //set the styles of the diagrams to include them in the pdf + setInlineStyles() { + this.svg + .selectAll(".line") + .attr("clip-path", "url(#waterlevel-clip)") + .selectAll("path") + .attr("stroke", "steelblue") + .attr("stroke-width", 2) + .attr("fill", "none"); + this.svg + .selectAll(".line") + .selectAll("path.d3-line-chunked-chunk-gap") + .attr("stroke-opacity", 0); + this.svg + .selectAll(".line") + .selectAll("circle") + .attr("fill", "steelblue") + .attr("stroke-width", 0); + this.svg + .selectAll(".line") + .selectAll("circle.d3-line-chunked-chunk-predicted-point") + .attr("fill-opacity", 0.6); - // scaling helpers to convert real values to pixels - // based on the diagrams dimensions - let x = d3.scaleTime().range([0, width]), - x2 = d3.scaleTime().range([0, width]), - y = d3.scaleLinear().range([mainHeight, 0]), - y2 = d3.scaleLinear().range([navHeight, 0]); - // find min/max values for the waterlevel axis - // including HDC (+ 1/8 HDC-LDC) and LDC (- 1/4 HDC-LDC) - let WaterlevelMinMax = d3.extent( - [ - ...this.waterlevels, - { - waterlevel: - refWaterLevels.HDC + (refWaterLevels.HDC - refWaterLevels.LDC) / 8 - }, - { - waterlevel: Math.max( - refWaterLevels.LDC - - (refWaterLevels.HDC - refWaterLevels.LDC) / 4, - 0 - ) - } + this.svg + .selectAll(".hdc-line, .mw-line, .ldc-line, .rn-line") + .attr("stroke-width", 1) + .attr("fill", "none") + .attr("clip-path", "url(#waterlevel-clip)"); + this.svg.selectAll(".hdc-line").attr("stroke", "red"); + this.svg.selectAll(".ldc-line").attr("stroke", "green"); + this.svg.selectAll(".mw-line").attr("stroke", "grey"); + this.svg.selectAll(".rn-line").attr("stroke", "grey"); + this.svg + .selectAll(".ref-waterlevel-label") + .style("font-size", "10px") + .attr("fill", "black"); + this.svg + .selectAll(".ref-waterlevel-label-background") + .attr("fill", "rgb(255, 255, 255)") + .attr("fill-opacity", 0.6); + this.svg + .selectAll(".hdc-ldc-area") + .attr("fill", "rgb(0, 255, 0)") + .attr("fill-opacity", 0.1); + this.svg + .selectAll(".now-line") + .attr("stroke", "#999") + .attr("stroke-width", 1) + .attr("stroke-dasharray", "5, 5") + .attr("clip-path", "url(#waterlevel-clip)"); + this.svg + .selectAll(".now-line-label") + .attr("font-size", "11px") + .attr("fill", "#999"); + this.svg + .selectAll(".prediction-area") + .attr("fill", "steelblue") + .attr("fill-opacity", 0.2) + .attr("clip-path", "url(#waterlevel-clip)"); + this.svg + .selectAll("path.nash-sutcliffe") + .attr("fill", "none") + .attr("stroke", "darkgrey") + .attr("stroke-width", 1) + .attr("clip-path", "url(#waterlevel-clip)"); + this.svg + .selectAll("path.nash-sutcliffe.ns72") + .attr("fill", "rgb(255, 255, 255)") + .attr("fill-opacity", 0.5); + this.svg + .selectAll("text.nash-sutcliffe") + .style("font-size", "10px") + .attr("clip-path", "url(#waterlevel-clip)") + .selectAll("tspan:last-child, tspan:first-child") + .attr("fill", "#555"); + this.svg + .selectAll(".tick line") + .attr("stroke-dasharray", 5) + .attr("stroke", "#ccc"); + this.svg.selectAll(".tick text").attr("fill", "black"); + this.svg.selectAll(".domain").attr("stroke", "black"); + this.svg + .selectAll(".zoom") + .attr("cursor", "move") + .attr("fill", "none") + .attr("pointer-events", "all"); + this.svg + .selectAll(".brush .selection") + .attr("stroke", "none") + .attr("fill-opacity", 0.2); + this.svg + .selectAll(".brush .handle") + .attr("stroke", "rgba(23, 162, 184, 0.5)") + .attr("fill", "rgba(23, 162, 184, 0.5)"); + this.svg + .selectAll(".chart-dots") + .attr("clip-path", "url(#waterlevel-clip)"); + this.svg + .selectAll(".chart-dots .chart-dot") + .attr("fill", "steelblue") + .attr("stroke", "steelblue") + .attr("stroke-opacity", 0) + .style("pointer-events", "none") + .transition() + .attr("fill-opacity", "0.1s"); + this.svg + .selectAll(".chart-tooltip") + .attr("fill-opacity", 0) + .transition() + .attr("fill-opacity", "0.3s"); + this.svg + .selectAll(".chart-tooltip rect") + .attr("fill", "#fff") + .attr("stroke", "#ccc"); + this.svg + .selectAll(".chart-tooltip text") + .attr("fill", "666") + .style("font-size", "0.8em"); + }, + getDimensions() { + // dimensions and margins + const svgWidth = document.querySelector("#" + this.containerId) + .clientWidth; + const svgHeight = document.querySelector("#" + this.containerId) + .clientHeight; + const mainMargin = { top: 20, right: 20, bottom: 110, left: 80 }; + const navMargin = { + top: svgHeight - mainMargin.top - 65, + right: 20, + bottom: 30, + left: 80 + }; + const width = +svgWidth - mainMargin.left - mainMargin.right; + const mainHeight = +svgHeight - mainMargin.top - mainMargin.bottom; + const navHeight = +svgHeight - navMargin.top - navMargin.bottom; + + return { width, mainHeight, navHeight, mainMargin, navMargin }; + }, + getExtent(refWaterLevels) { + return { + // set min/max values for the date axis + date: [ + this.waterlevels[0].date, + endOfDay(this.waterlevels[this.waterlevels.length - 1].date) ], - d => d.waterlevel - ); + // set min/max values for the waterlevel axis + // including HDC (+ 1/8 HDC-LDC) and LDC (- 1/4 HDC-LDC) + waterlevel: d3.extent( + [ + ...this.waterlevels, + { + waterlevel: + refWaterLevels.HDC + + (refWaterLevels.HDC - refWaterLevels.LDC) / 8 + }, + { + waterlevel: Math.max( + refWaterLevels.LDC - + (refWaterLevels.HDC - refWaterLevels.LDC) / 4, + 0 + ) + } + ], + d => d.waterlevel + ) + }; + }, + getScale() { + // scaling helpers to convert real world values into pixels + const x = d3.scaleTime().range([0, this.dimensions.width]); + const y = d3.scaleLinear().range([this.dimensions.mainHeight, 0]); + const x2 = d3.scaleTime().range([0, this.dimensions.width]); + const y2 = d3.scaleLinear().range([this.dimensions.navHeight, 0]); + // setting the min and max values for the diagram axes - let dateTo = new Date(this.dateTo.getTime() + 86400); - x.domain(d3.extent([this.dateFrom, dateTo])); - y.domain(WaterlevelMinMax); + x.domain(d3.extent(this.extent.date)); + y.domain(this.extent.waterlevel); x2.domain(x.domain()); y2.domain(y.domain()); - // creating the axes based on these scales - let xAxis = d3 - .axisTop(x) - .tickSizeInner(mainHeight) - .tickSizeOuter(0); - let xAxis2 = d3.axisBottom(x2); - let yAxis = d3 - .axisRight(y) - .tickSizeInner(width) - .tickSizeOuter(0); - // PREPARING CHART FUNCTIONS - - // points are "next to each other" when they are exactly 15 minutes apart - const isNext = (prev, current) => - current.date - prev.date === 15 * 60 * 1000; - - const preditionStyle = { - predicted: { - pointStyles: { - fill: "steelblue", - "fill-opacity": 0.6 - } - } - }; - - // waterlevel line in big chart - // d3-line-chunked plugin: https://github.com/pbeshai/d3-line-chunked - var mainLineChart = d3 - .lineChunked() - .x(d => x(d.date)) - .y(d => y(d.waterlevel)) - .curve(d3.curveLinear) - .isNext(isNext) - .pointAttrs({ r: 2.2 }) - .chunk(d => (d.predicted ? "predicted" : "line")) - .chunkDefinitions(preditionStyle); - // waterlevel line in small chart - let navLineChart = d3 - .lineChunked() - .x(d => x2(d.date)) - .y(d => y2(d.waterlevel)) - .curve(d3.curveMonotoneX) - .isNext(isNext) - .pointAttrs({ r: 1.7 }) - .chunk(d => (d.predicted ? "predicted" : "line")) - .chunkDefinitions(preditionStyle); - // hdc/ldc/mw - let refWaterlevelLine = d3 - .line() - .x(d => x(d.x)) - .y(d => y(d.y)); - // now - let nowLine = d3 - .line() - .x(d => x(d.x)) - .y(d => y(d.y)); - let nowLineNav = d3 - .line() - .x(d => x2(d.x)) - .y(d => y2(d.y)); - let nowLineLabel = selection => { - selection.attr( - "transform", - `translate(${x(new Date())}, ${y(WaterlevelMinMax[1] - 16)})` - ); - }; - // prediction area - let predictionArea = d3 - .area() - .defined(d => d.predicted && d.min && d.max) - .x(d => x(d.date)) - .y0(d => y(d.min)) - .y1(d => y(d.max)); - let predictionAreaNav = d3 - .area() - .defined(d => d.predicted && d.min && d.max) - .x(d => x2(d.date)) - .y0(d => y2(d.min)) - .y1(d => y2(d.max)); - - // DRAWING MAINCHART - - // define visible chart area - // everything outside this area will be hidden (clipped) - svg - .append("defs") - .append("clipPath") - .attr("id", "clip") - .append("rect") - .attr("width", width) - .attr("height", mainHeight); - - let mainChart = svg - .append("g") - .attr("class", "main") - .attr("transform", `translate(${mainMargin.left}, ${mainMargin.top})`); - - // axes - mainChart + return { x, y, x2, y2 }; + }, + drawAxes() { + this.diagram .append("g") .attr("class", "axis--x") - .attr("transform", `translate(0, ${mainHeight})`) - .call(xAxis) + .attr("transform", `translate(0, ${this.dimensions.mainHeight})`) + .call(this.axes.x) .selectAll(".tick text") .attr("y", 15); - mainChart // label + this.diagram // label .append("text") .text(this.$gettext("Waterlevel [cm]")) .attr("text-anchor", "middle") - .attr("transform", `translate(-45, ${mainHeight / 2}) rotate(-90)`); - mainChart + .attr( + "transform", + `translate(-45, ${this.dimensions.mainHeight / 2}) rotate(-90)` + ); + this.diagram .append("g") - .call(yAxis) + .call(this.axes.y) .selectAll(".tick text") .attr("x", -25); - // reference waterlevels - // filling area between HDC and LDC - mainChart - .append("rect") - .attr("class", "hdc-ldc-area") - .attr("x", 0) - .attr("y", y(refWaterLevels.HDC)) - .attr("width", width) - .attr("height", y(refWaterLevels.LDC) - y(refWaterLevels.HDC)); + this.navigation + .append("g") + .attr("class", "axis axis--x") + .attr("transform", `translate(0, ${this.dimensions.navHeight})`) + .call(this.axes.x2); + + return () => { + this.diagram + .select(".axis--x") + .call(this.axes.x) + .selectAll(".tick text") + .attr("y", 15); + }; + }, + drawWaterlevelChart() { + const waterlevelChartDrawer = () => { + let domainLeft = new Date(this.scale.x.domain()[0].getTime()); + let domainRight = new Date(this.scale.x.domain()[1].getTime()); + domainLeft.setDate(domainLeft.getDate() - 1); + domainRight.setDate(domainRight.getDate() + 1); + + return ( + d3 + .lineChunked() + // render only data points that are visible in the current scale + .defined(d => d.date > domainLeft && d.date < domainRight) + .x(d => this.scale.x(d.date)) + .y(d => this.scale.y(d.waterlevel)) + .curve(d3.curveLinear) + .isNext(this.isNext()) + .pointAttrs({ r: 1.7 }) + .chunk(d => (d.predicted ? "predicted" : "line")) + .chunkDefinitions({ predicted: {} }) + ); + }; - // HDC - mainChart - .append("path") - .datum([ - { x: 0, y: refWaterLevels.HDC }, - { x: dateTo, y: refWaterLevels.HDC } - ]) - .attr("class", "hdc-line") - .attr("d", refWaterlevelLine); - mainChart // label - .append("text") - .text("HDC") - .attr("class", "ref-waterlevel-label") - .attr("x", x(dateTo) - 20) - .attr("y", y(refWaterLevels.HDC) - 3); - // LDC - mainChart + this.diagram + .append("g") + .attr("class", "line") + .datum(this.waterlevels) + .call(waterlevelChartDrawer()); + + return () => { + this.diagram.select(".line").call(waterlevelChartDrawer()); + }; + }, + drawNavigationChart() { + this.navigation + .append("g") + .attr("class", "line") + .datum(this.waterlevels) + .call( + d3 + .lineChunked() + .x(d => this.scale.x2(d.date)) + .y(d => this.scale.y2(d.waterlevel)) + .curve(d3.curveLinear) + .isNext(this.isNext()) + .pointAttrs({ r: 1.7 }) + .chunk(d => (d.predicted ? "predicted" : "line")) + .chunkDefinitions({ predicted: {} }) + ); + }, + drawNowLines() { + const nowLine = d3 + .line() + .x(d => this.scale.x(d.x)) + .y(d => this.scale.y(d.y)); + + const nowLabel = selection => { + selection.attr( + "transform", + `translate(${this.scale.x(new Date())}, ${this.scale.y( + this.extent.waterlevel[1] - 16 + )})` + ); + }; + + // draw in main + this.diagram .append("path") .datum([ - { x: 0, y: refWaterLevels.LDC }, - { x: dateTo, y: refWaterLevels.LDC } - ]) - .attr("class", "ldc-line") - .attr("d", refWaterlevelLine); - mainChart // label - .append("text") - .text("LDC") - .attr("class", "ref-waterlevel-label") - .attr("x", x(dateTo) - 20) - .attr("y", y(refWaterLevels.LDC) - 3); - // MW - mainChart - .append("path") - .datum([ - { x: 0, y: refWaterLevels.MW }, - { x: dateTo, y: refWaterLevels.MW } - ]) - .attr("class", "mw-line") - .attr("d", refWaterlevelLine); - mainChart // label - .append("text") - .text("MW") - .attr("class", "ref-waterlevel-label") - .attr("x", x(dateTo) - 20) - .attr("y", y(refWaterLevels.MW) - 3); - - // now - mainChart - .append("path") - .datum([ - { x: new Date(), y: WaterlevelMinMax[0] }, - { x: new Date(), y: WaterlevelMinMax[1] - 20 } + { x: new Date(), y: this.extent.waterlevel[0] }, + { x: new Date(), y: this.extent.waterlevel[1] - 20 } ]) .attr("class", "now-line") .attr("d", nowLine); - mainChart // label + this.diagram // label .append("text") .text(this.$gettext("Now")) .attr("class", "now-line-label") .attr("text-anchor", "middle") - .call(nowLineLabel); + .call(nowLabel); + + // draw in nav + this.navigation + .append("path") + .datum([ + { x: new Date(), y: this.extent.waterlevel[0] }, + { x: new Date(), y: this.extent.waterlevel[1] - 20 } + ]) + .attr("class", "now-line") + .attr( + "d", + d3 + .line() + .x(d => this.scale.x2(d.x)) + .y(d => this.scale.y2(d.y)) + ); - // prediction area - mainChart + return () => { + this.diagram.select(".now-line").attr("d", nowLine); + this.diagram.select(".now-line-label").call(nowLabel); + }; + }, + drawPredictionAreas() { + const predictionArea = isNav => + d3 + .area() + .defined(d => d.predicted && d.min && d.max) + .x(d => this.scale[isNav ? "x2" : "x"](d.date)) + .y0(d => this.scale[isNav ? "y2" : "y"](d.min)) + .y1(d => this.scale[isNav ? "y2" : "y"](d.max)); + + this.diagram + .append("path") + .datum(this.waterlevels) + .attr("class", "prediction-area") + .attr("d", predictionArea()); + + this.navigation .append("path") .datum(this.waterlevels) .attr("class", "prediction-area") - .attr("d", predictionArea); - - // waterlevel chart - mainChart - .append("g") - .attr("class", "line") - .datum(this.waterlevels) - .call(mainLineChart); - - // DRAWING NAVCHART + .attr("d", predictionArea(true)); - let navChart = svg - .append("g") - .attr("class", "nav") - .attr("transform", `translate(${navMargin.left}, ${navMargin.top})`); + return () => { + this.diagram.select(".prediction-area").attr("d", predictionArea()); + }; + }, + drawRefLines(refWaterLevels) { + // filling area between HDC and LDC + this.diagram + .append("rect") + .attr("class", "hdc-ldc-area") + .attr("x", 0) + .attr("y", this.scale.y(refWaterLevels.HDC)) + .attr("width", this.dimensions.width) + .attr( + "height", + this.scale.y(refWaterLevels.LDC) - this.scale.y(refWaterLevels.HDC) + ); - // axis (nav chart only has x-axis) - navChart - .append("g") - .attr("class", "axis axis--x") - .attr("transform", `translate(0, ${navHeight})`) - .call(xAxis2); + const refWaterlevelLine = d3 + .line() + .x(d => this.scale.x(d.x)) + .y(d => this.scale.y(d.y)); - // now - navChart - .append("path") - .datum([ - { x: new Date(), y: WaterlevelMinMax[0] }, - { x: new Date(), y: WaterlevelMinMax[1] - 20 } - ]) - .attr("class", "now-line") - .attr("d", nowLineNav); - - // prediction area - navChart - .append("path") - .datum(this.waterlevels) - .attr("class", "prediction-area") - .attr("d", predictionAreaNav); - - // waterlevel chart - navChart - .append("g") - .attr("class", "line") - .datum(this.waterlevels) - .call(navLineChart); - - // NASH SUTCLIFFE - - let nashSut24 = this.nashSutcliffe.coeffs.find(c => c.hours === 24); - let nashSut48 = this.nashSutcliffe.coeffs.find(c => c.hours === 48); - let nashSut72 = this.nashSutcliffe.coeffs.find(c => c.hours === 72); - - let nashSutDateNow = new Date(this.nashSutcliffe.when); - let nashSutDate24 = new Date(this.nashSutcliffe.when); - let nashSutDate48 = new Date(this.nashSutcliffe.when); - let nashSutDate72 = new Date(this.nashSutcliffe.when); - nashSutDate24.setDate(nashSutDate24.getDate() - 1); - nashSutDate48.setDate(nashSutDate48.getDate() - 2); - nashSutDate72.setDate(nashSutDate72.getDate() - 3); + for (let ref in refWaterLevels) { + if (refWaterLevels[ref]) { + this.diagram + .append("path") + .datum([ + { x: 0, y: refWaterLevels[ref] }, + { x: this.extent.date[1], y: refWaterLevels[ref] } + ]) + .attr("class", ref.toLowerCase() + "-line") + .attr("d", refWaterlevelLine); + this.diagram // label + .append("rect") + .attr("class", "ref-waterlevel-label-background") + .attr("x", 1) + .attr("y", this.scale.y(refWaterLevels[ref]) - 13) + .attr("width", 55) + .attr("height", 12); + this.diagram + .append("text") + .text(`${ref} (${refWaterLevels[ref]})`) + .attr("class", "ref-waterlevel-label") + .attr("x", 5) + .attr("y", this.scale.y(refWaterLevels[ref]) - 3); + } + } + }, + drawNashSutcliffe(hours) { + const coeff = this.nashSutcliffe.coeffs.find(c => c.hours === hours); + const dateNow = new Date(this.nashSutcliffe.when); + const dateStart = new Date(dateNow.getTime() - hours * 60 * 60 * 1000); const nashSutcliffeBox = hours => { + // show/hide boxes depending on scale of chart (hide if > 90 days) + this.diagram + .selectAll("path.nash-sutcliffe") + .attr( + "stroke-opacity", + this.scale.x.domain()[1] - this.scale.x.domain()[0] > 90 * 86400000 + ? 0 + : 1 + ); + return d3 .area() - .x(d => x(d)) - .y0(() => mainHeight + 0.5) - .y1(() => mainHeight - 15 * (hours / 24)); + .x(d => this.scale.x(d)) + .y0(() => this.dimensions.mainHeight + 0.5) + .y1(() => this.dimensions.mainHeight - 15 * (hours / 24)); }; const nashSutcliffeLabel = (label, date, hours) => { let days = hours / 24; label - .attr("x", x(date) + 3) - .attr("y", mainHeight - (15 * days + 0.5) + 12); + .attr("x", Math.min(this.scale.x(date), this.dimensions.width) - 4) + .attr("y", this.dimensions.mainHeight - (15 * days + 0.5) + 12); }; - // Show nash-sutcliffe only when x-axis extent is smaller than 35 days - // (3024000000 ms). Since it shows squares representing 1, 2 and 3 days - // it does not make sense to show them on a x-axis with hundres of days. - if (this.nashSutcliffe && x.domain()[1] - x.domain()[0] < 3024000000) { - if (nashSut24.samples) { - mainChart - .append("path") - .datum([nashSutDate24, nashSutDateNow]) - .attr("class", "nash-sutcliffe ns24") - .attr("d", nashSutcliffeBox(24)); - mainChart - .append("text") - .attr("class", "nash-sutcliffe ns24") - .call(nashSutcliffeLabel, nashSutDate24, 24) - .append("tspan") - .text(nashSut24.value.toFixed(2)) - .select(function() { - return this.parentNode; - }) - .append("tspan") - .text(` (${nashSut24.samples})`) - .attr("dy", -1); - } - if (nashSut48.samples) { - mainChart - .append("path") - .datum([nashSutDate48, nashSutDateNow]) - .attr("class", "nash-sutcliffe ns48") - .attr("d", nashSutcliffeBox(48)); - mainChart - .append("text") - .attr("class", "nash-sutcliffe ns48") - .call(nashSutcliffeLabel, nashSutDate48, 48) - .append("tspan") - .text(nashSut48.value.toFixed(2)) - .select(function() { - return this.parentNode; - }) - .append("tspan") - .text(` (${nashSut48.samples})`) - .attr("dy", -1); - } - if (nashSut72.samples) { - mainChart - .append("path") - .datum([nashSutDate72, nashSutDateNow]) - .attr("class", "nash-sutcliffe ns72") - .attr("d", nashSutcliffeBox(72)); - mainChart - .append("text") - .attr("class", "nash-sutcliffe ns72") - .call(nashSutcliffeLabel, nashSutDate72, 72) - .append("tspan") - .text(nashSut72.value.toFixed(2)) - .select(function() { - return this.parentNode; - }) - .append("tspan") - .text(` (${nashSut72.samples})`) - .attr("dy", -1); - } + if (coeff.samples) { + this.diagram + .append("path") + .datum([dateStart, dateNow]) + .attr("class", "nash-sutcliffe ns" + hours) + .attr("d", nashSutcliffeBox(hours)); + this.diagram + .append("text") + .attr("class", "nash-sutcliffe ns" + hours) + .attr("text-anchor", "end") + .call(nashSutcliffeLabel, dateNow, hours) + .append("tspan") + .text(hours + "h: ") + .select(function() { + return this.parentNode; + }) + .append("tspan") + .text(coeff.value.toFixed(2)) + .select(function() { + return this.parentNode; + }) + .append("tspan") + .text(` (${coeff.samples})`); } - // INTERACTIVITY - - // selecting time period in nav chart - let brush = d3 + return () => { + this.diagram + .select("path.nash-sutcliffe.ns" + hours) + .attr("d", nashSutcliffeBox(hours)); + this.diagram + .select("text.nash-sutcliffe.ns" + hours) + .call(nashSutcliffeLabel, dateNow, hours); + }; + }, + createZoom(updaters, eventRect) { + const brush = d3 .brushX() .handleSize(4) - .extent([[0, 0], [width, navHeight]]) - .on("brush end", () => { - if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") - return; // ignore brush-by-zoom - let s = d3.event.selection || x2.range(); - x.domain(s.map(x2.invert, x2)); - mainChart.select(".line").call(mainLineChart); - mainChart.select(".now-line").attr("d", nowLine); - mainChart.select(".now-line-label").call(nowLineLabel); - mainChart.select(".prediction-area").attr("d", predictionArea); - mainChart - .select("path.nash-sutcliffe.ns24") - .attr("d", nashSutcliffeBox(24)); - mainChart - .select("text.nash-sutcliffe.ns24") - .call(nashSutcliffeLabel, nashSutDate24, 24); - mainChart - .select("path.nash-sutcliffe.ns48") - .attr("d", nashSutcliffeBox(48)); - mainChart - .select("text.nash-sutcliffe.ns48") - .call(nashSutcliffeLabel, nashSutDate48, 48); - mainChart - .select("path.nash-sutcliffe.ns72") - .attr("d", nashSutcliffeBox(72)); - mainChart - .select("text.nash-sutcliffe.ns72") - .call(nashSutcliffeLabel, nashSutDate72, 72); - mainChart - .select(".axis--x") - .call(xAxis) - .selectAll(".tick text") - .attr("y", 15); - svg - .select(".zoom") - .call( - zoom.transform, - d3.zoomIdentity.scale(width / (s[1] - s[0])).translate(-s[0], 0) - ); - }); + .extent([[0, 0], [this.dimensions.width, this.dimensions.navHeight]]); - // zooming with mousewheel in main chart - let zoom = d3 + const zoom = d3 .zoom() .scaleExtent([1, Infinity]) - .translateExtent([[0, 0], [width, mainHeight]]) - .extent([[0, 0], [width, mainHeight]]) - .on("zoom", () => { - if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") - return; // ignore zoom-by-brush - let t = d3.event.transform; - x.domain(t.rescaleX(x2).domain()); - mainChart.select(".line").call(mainLineChart); - mainChart.select(".now-line").attr("d", nowLine); - mainChart.select(".now-line-label").call(nowLineLabel); - mainChart.select(".prediction-area").attr("d", predictionArea); - mainChart - .select("path.nash-sutcliffe.ns24") - .attr("d", nashSutcliffeBox(24)); - mainChart - .select("text.nash-sutcliffe.ns24") - .call(nashSutcliffeLabel, nashSutDate24, 24); - mainChart - .select("path.nash-sutcliffe.ns48") - .attr("d", nashSutcliffeBox(48)); - mainChart - .select("text.nash-sutcliffe.ns48") - .call(nashSutcliffeLabel, nashSutDate48, 48); - mainChart - .select("path.nash-sutcliffe.ns72") - .attr("d", nashSutcliffeBox(72)); - mainChart - .select("text.nash-sutcliffe.ns72") - .call(nashSutcliffeLabel, nashSutDate72, 72); - mainChart - .select(".axis--x") - .call(xAxis) - .selectAll(".tick text") - .attr("y", 15); - navChart - .select(".brush") - .call(brush.move, x.range().map(t.invertX, t)); - }) - .on("start", () => { - svg.select(".chart-dot").style("opacity", 0); - svg.select(".chart-tooltip").style("opacity", 0); - }); + .translateExtent([ + [0, 0], + [this.dimensions.width, this.dimensions.mainHeight] + ]) + .extent([[0, 0], [this.dimensions.width, this.dimensions.mainHeight]]); - navChart + brush.on("brush end", () => { + if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") + return; // ignore brush-by-zoom + let s = d3.event.selection || this.scale.x2.range(); + this.scale.x.domain(s.map(this.scale.x2.invert, this.scale.x2)); + updaters.forEach(u => u && u()); + this.setInlineStyles(); + this.svg + .select(".zoom") + .call( + zoom.transform, + d3.zoomIdentity + .scale(this.dimensions.width / (s[1] - s[0])) + .translate(-s[0], 0) + ); + }); + + zoom.on("zoom", () => { + if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") + return; // ignore zoom-by-brush + let t = d3.event.transform; + this.scale.x.domain(t.rescaleX(this.scale.x2).domain()); + updaters.forEach(u => u && u()); + this.setInlineStyles(); + this.navigation + .select(".brush") + .call(brush.move, this.scale.x.range().map(t.invertX, t)); + }); + zoom.on("start", () => { + this.svg.select(".chart-dot").style("opacity", 0); + this.svg.select(".chart-tooltip").style("opacity", 0); + }); + + this.navigation .append("g") .attr("class", "brush") .call(brush) - .call(brush.move, x.range()); + .call(brush.move, this.scale.x.range()); - let zoomRect = svg - .append("rect") - .attr("class", "zoom") - .attr("width", width) - .attr("height", mainHeight) - .attr("transform", `translate(${mainMargin.left}, ${mainMargin.top})`) - .call(zoom); - - // TOOLTIPS - - let dots = mainChart.append("g").attr("class", "chart-dots"); - dots + eventRect.call(zoom); + }, + createTooltips(eventRect) { + // create clippable container for the dot + this.diagram + .append("g") + .attr("class", "chart-dots") .append("circle") .attr("class", "chart-dot") .attr("r", 4); - let tooltips = mainChart.append("g").attr("class", "chart-tooltip"); - tooltips + + // create container for the tooltip + const tooltip = this.diagram.append("g").attr("class", "chart-tooltip"); + tooltip .append("rect") - .attr("x", -25) - .attr("y", -25) - .attr("rx", 4) - .attr("ry", 4) - .attr("width", 105) - .attr("height", 40); - let tooltipText = tooltips.append("text"); - tooltipText - .append("tspan") - .attr("x", -15) - .attr("y", -8); - tooltipText - .append("tspan") - .attr("x", 8) - .attr("y", 8); + .attr("rx", "0.25em") + .attr("ry", "0.25em"); + + // create container for multiple text rows + const tooltipText = tooltip.append("text").attr("text-anchor", "middle"); - let bisectDate = d3.bisector(d => d.date).left; - zoomRect + // padding inside the tooltip box and diagram padding to determine left + // and right offset from the diagram boundaries for the tooltip position. + const tooltipPadding = 10; + const diagramPadding = 5; + + eventRect .on("mouseover", () => { - svg.select(".chart-dot").style("opacity", 1); - svg.select(".chart-tooltip").style("opacity", 1); + this.diagram.select(".chart-dot").style("opacity", 1); + this.diagram.select(".chart-tooltip").style("opacity", 1); }) .on("mouseout", () => { - svg.select(".chart-dot").style("opacity", 0); - svg.select(".chart-tooltip").style("opacity", 0); + this.diagram.select(".chart-dot").style("opacity", 0); + this.diagram.select(".chart-tooltip").style("opacity", 0); }) .on("mousemove", () => { - let x0 = x.invert(d3.mouse(document.querySelector(".zoom"))[0]), - i = bisectDate(this.waterlevels, x0, 1), + // find data point closest to mouse + const x0 = this.scale.x.invert( + d3.mouse(document.getElementById("zoom-waterlevels"))[0] + ), + i = d3.bisector(d => d.date).left(this.waterlevels, x0, 1), d0 = this.waterlevels[i - 1], d1 = this.waterlevels[i] || d0, d = x0 - d0.date > d1.date - x0 ? d1 : d0; - svg + const coords = { + x: this.scale.x(d.date), + y: this.scale.y(d.waterlevel) + }; + + // position the dot + this.diagram .select(".chart-dot") .style("opacity", 1) - .attr("transform", `translate(${x(d.date)}, ${y(d.waterlevel)})`); - svg + .attr("transform", `translate(${coords.x}, ${coords.y})`); + + // remove current texts + tooltipText.selectAll("tspan").remove(); + + // write date + tooltipText + .append("tspan") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .text( + d.date.toLocaleString([], { + year: "2-digit", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit" + }) + ); + + if (d.predicted) { + tooltipText + .append("tspan") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "1.4em") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .text(d.max + " cm"); + tooltipText + .append("tspan") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "2.6em") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .attr("class", "font-weight-bold") + .text(d.waterlevel + " cm"); + tooltipText + .append("tspan") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "3.8em") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .text(d.min + " cm"); + } else { + tooltipText + .append("tspan") + .attr("x", 0) + .attr("y", 0) + .attr("dy", "1.4em") + .attr("dominant-baseline", "hanging") + .attr("text-anchor", "middle") + .attr("class", "font-weight-bold") + .text(d.waterlevel + " cm"); + } + + // get text dimensions + const textBBox = tooltipText.node().getBBox(); + + this.diagram + .selectAll(".chart-tooltip text tspan") + .attr("x", textBBox.width / 2 + tooltipPadding) + .attr("y", tooltipPadding); + + // position and scale tooltip box + const xMax = + this.dimensions.width - + (textBBox.width + diagramPadding + tooltipPadding * 2); + const tooltipX = Math.max( + diagramPadding, + Math.min(coords.x - (textBBox.width + tooltipPadding * 2) / 2, xMax) + ); + let tooltipY = coords.y - (textBBox.height + tooltipPadding * 2) - 10; + if (coords.y < textBBox.height + tooltipPadding * 2) { + tooltipY = coords.y + 10; + } + + this.diagram .select(".chart-tooltip") .style("opacity", 1) - .attr( - "transform", - `translate(${x(d.date) - 25}, ${y(d.waterlevel) - 25})` - ); - svg.select(".chart-tooltip text tspan:first-child").text( - d.date.toLocaleString([], { - year: "2-digit", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit" - }) - ); - svg - .select(".chart-tooltip text tspan:last-child") - .text(d.waterlevel + " cm"); + .attr("transform", `translate(${tooltipX}, ${tooltipY})`) + .select("rect") + .attr("width", textBBox.width + tooltipPadding * 2) + .attr("height", textBBox.height + tooltipPadding * 2); }); + }, + isNext() { + // Check whether points in the chart can be considered "next to each other". + // For that they need to be exactly 15 minutes apart (for automatically + // imported gauge measurements). If the chart shows more than 15 days then + // 1 hour is also valid (for approved gauge measurements). + return (prev, current) => { + let difference = (current.date - prev.date) / 1000; + if ( + (this.scale.x.domain()[1] - this.scale.x.domain()[0]) / 86400000 > + 15 + ) + return [900, 3600].includes(difference); + return difference === 900; + }; } }, created() { window.addEventListener("resize", debounce(this.drawDiagram), 100); }, mounted() { - this.drawDiagram(); + // Nasty but necessary if we don't want to use the updated hook to re-draw + // the diagram because this would re-draw it also for irrelevant reasons. + // In this case we need to wait for the child component (DiagramLegend) to + // render. According to the docs (https://vuejs.org/v2/api/#mounted) this + // should be possible with $nextTick() but it doesn't work because it does + // not guarantee that the DOM is not only updated but also re-painted on the + // screen. + setTimeout(this.drawDiagram, 50); + + this.templates[0] = this.defaultTemplate; + this.form.template = this.templates[0]; + this.templateData = this.form.template; + HTTP.get("/templates/diagram", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + if (response.data.length) { + this.templates = response.data; + this.form.template = this.templates[0]; + this.templates[this.templates.length] = this.defaultTemplate; + this.applyChange(); + } + }) + .catch(e => { + const { status, data } = e.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); }, - updated() { - this.drawDiagram(); + destroyed() { + window.removeEventListener("resize", debounce(this.drawDiagram)); } }; </script>
--- a/client/src/components/identify/Identify.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/identify/Identify.vue Mon Jun 03 10:19:18 2019 +0200 @@ -13,7 +13,7 @@ /> <div class="features"> <div v-if="currentMeasurement"> - <small class="d-block bg-dark text-light text-center px-2 py-1"> + <small class="d-block bg-secondary text-light px-2 py-1"> {{ $gettext("Measurement") }} </small> <small class="d-flex justify-content-between px-2"> @@ -28,7 +28,7 @@ :key="feature.getId()" > <small - class="d-flex justify-content-between bg-dark text-light px-2 py-1" + class="d-flex justify-content-between bg-secondary text-light px-2 py-1" > {{ $gettext(featureLabel(feature)) }} <a @@ -56,6 +56,18 @@ <translate>No features identified.</translate> </div> </div> + <div + v-if="userManualUrl" + class="border-top text-left pl-2" + style="font-size: 90%;" + > + <translate>Download</translate> + <a + :href="userManualUrl ? userManualUrl : '#'" + :download="usermanualFilename" + ><translate> User Manual</translate></a + > + </div> <div class="versioninfo border-top box-body"> <span v-translate="{ license: 'AGPL-3.0-or-later' }"> This app uses <i>gemma</i>, which is Free Software under <br /> @@ -138,16 +150,19 @@ name: "identify", computed: { ...mapGetters("application", ["versionStr"]), - ...mapState("application", ["showIdentify"]), + ...mapState("application", ["showIdentify", "userManualUrl"]), ...mapGetters("map", ["filteredIdentifiedFeatures"]), ...mapState("map", ["currentMeasurement"]), identifiedLabel() { - return this.$gettext("Identified"); + return this.$gettext("Identified Features"); + }, + usermanualFilename() { + return this.$gettext("User Manual"); } }, methods: { zoomTo(feature) { - this.$store.commit("map/moveMap", { + this.$store.dispatch("map/moveMap", { coordinates: getCenter( feature .getGeometry() @@ -164,7 +179,14 @@ }, featureId(feature) { // cut away everything from the last . to the end - return feature.getId().replace(/[.][^.]*$/, ""); + let id = ""; + if (feature.getId) { + id = feature.getId(); + } + if (feature.id) { + id = feature.id; + } + return id.replace(/[.][^.]*$/, ""); }, featureLabel(feature) { if (formatter.hasOwnProperty(this.featureId(feature))) {
--- a/client/src/components/identify/formatter.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/identify/formatter.js Mon Jun 03 10:19:18 2019 +0200 @@ -4,7 +4,11 @@ if (p.key === "objname") p.key = "Name"; if (p.key === "staging_done" || p.key === "fa_critical") p.val = p.val ? "yes" : "no"; - if (p.key === "date_info" || p.key === "fa_date_info") { + if ( + p.key === "date_info" || + p.key === "fa_date_info" || + p.key === "gm_measuredate" + ) { p.val = new Date(p.val).toLocaleString(); } @@ -21,9 +25,14 @@ if (p.key === "responsible_country") p.key = "Responsible Country"; if (p.key === "fa_date_info") p.key = "Fairway Date"; if (p.key === "fa_critical") p.key = "Fairway Critical"; + if (p.key === "gauge_objname") p.key = "Reference Gauge"; + if (p.key === "source_organization") p.key = "Source Organization"; + if (p.key === "gm_measuredate") p.key = "Gauge Waterlevel Date"; + if (p.key === "gm_waterlevel") p.key = "Gauge Waterlevel"; + if (p.key === "gm_n_14d") p.key = "G.W. Count in Last 14 Days"; // remove certain props - let propsToRemove = ["nobjnm", "reference_water_levels"]; + let propsToRemove = ["nobjnm", "reference_water_levels", "fa_data"]; if (propsToRemove.indexOf(p.key) !== -1) return null; return p; @@ -38,6 +47,9 @@ distance_marks_geoserver: { label: "Distance Mark" }, + distance_marks_ashore_geoserver: { + label: "Distance Mark ashore" + }, waterway_axis: { label: "Waterway Axis" }, @@ -47,8 +59,22 @@ stretches_geoserver: { label: "Stretch" }, + sections_geoserver: { + label: "Section" + }, gauges_geoserver: { - label: "Gauge" + label: "Gauge", + props: p => { + if (p.key === "gm_measuredate") p.key = "Latest Waterlevel Date"; + if (p.key === "gm_waterlevel") p.key = "Latest Waterlevel"; + if (p.key === "gm_n_14d") p.key = "Measurement Count in Last 14 Days"; + + // remove certain props + let propsToRemove = ["nsc_data"]; + if (propsToRemove.indexOf(p.key) !== -1) return null; + + return p; + } } };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/Import.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,280 @@ +<template> + <div> + <UIBoxHeader icon="clock" :title="title" :closeCallback="$parent.close" /> + <div v-if="mode === $options.MODES.LIST"> + <UISpinnerOverlay v-if="loading" /> + <UITableHeader + :columns="[ + { id: 'id', title: `${idLabel}`, class: 'col-1' }, + { id: 'kind', title: `${typeLabel}`, class: 'col-1' }, + { id: 'user', title: `${ownerLabel}`, class: 'col-2' }, + { id: 'country', title: `${countryLabel}`, class: 'col-1' }, + { id: 'config.cron', title: `${scheduleLabel}`, class: 'col-2' }, + { id: 'config.send-email', title: `${emailLabel}`, class: 'col-2' } + ]" + /> + <UITableBody + :data="filteredSchedules | sortTable(sortColumn, sortDirection)" + :isActive="item => currentSchedule && item.id === currentSchedule.id" + > + <template v-slot:row="{ item: schedule }"> + <div class="table-cell py-1 col-1">{{ schedule.id }}</div> + <div class="table-cell py-1 col-1"> + {{ schedule.kind.toUpperCase() }} + </div> + <div style="width:115px;" class="table-cell py-1"> + {{ schedule.user }} + </div> + <div style="width:55px;" class="table-cell py-1"> + {{ userCountries[schedule.user] }} + </div> + <div class="table-cell py-1 col-2">{{ schedule.config.cron }}</div> + <div class="table-cell py-1 col-2"> + <font-awesome-icon + v-if="schedule.config['send-email']" + class="fa-fw mr-2" + fixed-width + icon="check" + /> + </div> + <div class="table-cell py-1 col justify-content-end"> + <button + @click="triggerManualImport(schedule.id)" + class="btn btn-xs btn-dark mr-1" + :disabled="importScheduleDetailVisible" + > + <font-awesome-icon icon="play" fixed-width /> + </button> + <button + @click="editSchedule(schedule.id)" + class="btn btn-xs btn-dark mr-1" + :disabled="importScheduleDetailVisible" + > + <font-awesome-icon icon="pencil-alt" fixed-width /> + </button> + <button + @click="deleteSchedule(schedule)" + class="btn btn-xs btn-dark" + :disabled="importScheduleDetailVisible" + > + <font-awesome-icon icon="trash" fixed-width /> + </button> + </div> + </template> + </UITableBody> + </div> + <ImportDetails v-if="mode === $options.MODES.EDIT"></ImportDetails> + <div + class="text-right border-top p-2" + v-if="mode === $options.MODES.LIST && !isOnetime" + > + <button :key="3" @click="newConfiguration()" class="btn btn-sm btn-info"> + <translate>New import</translate> + </button> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; +import { displayInfo, displayError } from "@/lib/errors"; +import { HTTP } from "@/lib/http"; +import { + IMPORTTYPES, + MODES + // IMPORTTYPEKIND, + // initializeCurrentSchedule +} from "@/store/importschedule"; +import { sortTable } from "@/lib/mixins"; + +export default { + mixins: [sortTable], + components: { + ImportDetails: () => import("./ImportDetails") + }, + data() { + return { + loading: false, + sortColumn: "", + sortDirection: "" + }; + }, + methods: { + back() { + this.$store.commit("importschedule/setListMode"); + }, + newConfiguration() { + this.$store.commit("importschedule/setEditMode"); + }, + getSchedules() { + this.loading = true; + this.$store + .dispatch("importschedule/loadSchedules") + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => { + this.loading = false; + }); + }, + editSchedule(id) { + this.$store + .dispatch("importschedule/loadSchedule", id) + .then(() => { + this.$store.commit("importschedule/setEditMode"); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + triggerManualImport(id) { + HTTP.get("/imports/config/" + id + "/run", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + const { id } = response.data; + displayInfo({ + title: this.$gettext("Imports"), + message: this.$gettext("Manually triggered import: #") + id + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + deleteSchedule(schedule) { + console.log(schedule); + this.$store.commit("application/popup", { + icon: "trash", + title: this.$gettext("Delete Import"), + content: + this.$gettext("Do you really want to delete the import with ID") + + `<b>${schedule.id}</b>` + + this.$gettext("of type") + + `<b>${schedule.kind.toUpperCase()}</b>?`, + confirm: { + label: this.$gettext("Delete"), + icon: "trash", + callback: () => { + this.$store + .dispatch("importschedule/deleteSchedule", schedule.id) + .then(() => { + this.getSchedules(); + displayInfo({ + title: this.$gettext("Imports"), + message: this.$gettext("Deleted import: #") + schedule.id + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + cancel: { + label: this.$gettext("Cancel"), + icon: "times" + } + }); + } + }, + computed: { + ...mapState("application", ["searchQuery"]), + ...mapState("importschedule", [ + "mode", + "schedules", + "currentSchedule", + "importScheduleDetailVisible" + ]), + ...mapGetters("usermanagement", ["userCountries"]), + countryLabel() { + return this.$gettext("Country"); + }, + isOnetime() { + for (let kind of [ + this.$options.IMPORTTYPES.SOUNDINGRESULTS, + this.$options.IMPORTTYPES.APPROVEDGAUGEMEASUREMENTS, + this.$options.IMPORTTYPES.WATERWAYPROFILES + ]) { + if (kind === this.currentSchedule.importType) return true; + } + return false; + }, + title() { + return this.$gettext("Imports"); + }, + filteredSchedules() { + return this.schedules.filter(s => { + return (s.id + s.kind) + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + }); + }, + importScheduleLabel() { + return this.$gettext("Import Schedule"); + }, + idLabel() { + return this.$gettext("ID"); + }, + typeLabel() { + return this.$gettext("Type"); + }, + ownerLabel() { + return this.$gettext("Owner"); + }, + scheduleLabel() { + return this.$gettext("Schedule"); + }, + emailLabel() { + return this.$gettext("Email"); + } + }, + mounted() { + this.$store + .dispatch("usermanagement/loadUsers") + .then(() => { + this.$store.commit("importschedule/setListMode"); + this.$store.commit("importschedule/clearCurrentSchedule"); + this.getSchedules(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + IMPORTTYPES: IMPORTTYPES, + MODES: MODES +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/ImportDetails.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,143 @@ +<template> + <div class="text-left"> + <div> + <div class="p-2 pb-3 border-bottom"> + <small class="text-muted"> + <translate>Import type</translate> + </small> + <select + v-model="Import" + class="custom-select custom-select-sm" + id="importtype" + > + <optgroup :label="regularLabel"> + <option :value="$options.IMPORTTYPES.WATERWAYAREA"> + <translate>Waterway area</translate> + </option> + <option :value="$options.IMPORTTYPES.WATERWAYAXIS"> + <translate>Waterway axis</translate> + </option> + <option :value="$options.IMPORTTYPES.FAIRWAYDIMENSION"> + <translate>Fairway dimension</translate> + </option> + <option :value="$options.IMPORTTYPES.DISTANCEMARKSVIRTUAL"> + <translate>Distance marks virtual</translate> + </option> + <option :value="$options.IMPORTTYPES.DISTANCEMARKSASHORE"> + <translate>Distance marks ashore</translate> + </option> + <option :value="$options.IMPORTTYPES.WATERWAYGAUGES"> + <translate>Waterway gauges</translate> + </option> + <option :value="$options.IMPORTTYPES.BOTTLENECK"> + <translate>Bottlenecks</translate> + </option> + <option :value="$options.IMPORTTYPES.FAIRWAYAVAILABILITY"> + <translate>Available fairway depths</translate> + </option> + <option :value="$options.IMPORTTYPES.GAUGEMEASUREMENT"> + <translate>Gauge measurement</translate> + </option> + </optgroup> + <optgroup :label="onetimeLabel"> + <option :value="$options.IMPORTTYPES.SOUNDINGRESULTS"> + <translate>Soundingresults</translate> + </option> + <option :value="$options.IMPORTTYPES.APPROVEDGAUGEMEASUREMENTS"> + <translate>Approved Gaugemeasurements</translate> + </option> + <option :value="$options.IMPORTTYPES.WATERWAYPROFILES"> + <translate>Waterway Profiles</translate> + </option> + </optgroup> + </select> + </div> + <ApprovedGaugeMeasurement + v-if="Import === $options.IMPORTTYPES.APPROVEDGAUGEMEASUREMENTS" + class="mt-1" + /> + <WaterwayProfiles + class="mt-1" + v-if="Import === $options.IMPORTTYPES.WATERWAYPROFILES" + /> + <SoundingResults + class="mt-1" + v-if="Import === $options.IMPORTTYPES.SOUNDINGRESULTS" + /> + <ScheduledImports + class="mt-1" + v-if="Import && !isOnetime" + ></ScheduledImports> + </div> + <div v-if="!Import" class="p-2"> + <button :key="1" @click="back()" class="btn btn-sm btn-warning"> + Back + </button> + </div> + </div> +</template> + +<style lang="scss" scoped></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, 2019 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Tom Gottfried <tom.gottfried@intevation.de> + */ +import { IMPORTTYPES } from "@/store/importschedule"; +import { mapState } from "vuex"; +export default { + components: { + ApprovedGaugeMeasurement: () => import("./types/ApprovedGaugeMeasurement"), + WaterwayProfiles: () => import("./types/WaterwayProfiles"), + SoundingResults: () => import("./types/Soundingresults"), + ScheduledImports: () => import("./ScheduledImports") + }, + data() { + return {}; + }, + computed: { + ...mapState("importschedule", ["currentSchedule"]), + isOnetime() { + for (let kind of [ + this.$options.IMPORTTYPES.SOUNDINGRESULTS, + this.$options.IMPORTTYPES.APPROVEDGAUGEMEASUREMENTS, + this.$options.IMPORTTYPES.WATERWAYPROFILES + ]) { + if (kind === this.currentSchedule.importType) return true; + } + return false; + }, + Import: { + get() { + return this.currentSchedule.importType; + }, + set(value) { + this.$store.commit("importschedule/setImportType", value); + } + }, + onetimeLabel() { + return this.$gettext("Onetime Imports"); + }, + regularLabel() { + return this.$gettext("Regular Imports"); + } + }, + methods: { + back() { + this.$store.commit("importschedule/setListMode"); + } + }, + IMPORTTYPES: IMPORTTYPES +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/ScheduledImports.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,1029 @@ +<template> + <form @submit.prevent="save" class="w-100"> + <div class="d-flex px-2"> + <div :key="1" class="flex-column"> + <small class="text-muted"> + <translate>Email Notification</translate> + </small> + <div class="flex-flex-row text-left"> + <toggle-button + :value="eMailNotification" + v-model="eMailNotification" + :sync="true" + class="mt-2" + :speed="100" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="60" + :height="30" + /> + </div> + </div> + <div + :key="2" + v-if="directImportAvailable" + class="flex-column text-left ml-3" + > + <small class="text-muted"> + <translate>Import via</translate> + </small> + <div> + <!-- '#75c791' is the DEFAULT_COLOR_CHECKED + from vue-js-toggle-button as here both states are active --> + <toggle-button + :color="{ unchecked: '#75c791' }" + v-model="directImport" + class="mt-2" + :speed="100" + :labels="{ + checked: this.$options.FILE, + unchecked: this.$options.URL + }" + :width="60" + :height="30" + /> + </div> + </div> + </div> + <Availablefairwaydepth + v-if=" + import_ == $options.IMPORTTYPES.FAIRWAYAVAILABILITY && !directImport + " + @urlChanged="setUrl" + :url="url" + /> + <Bottleneck + v-if="import_ == $options.IMPORTTYPES.BOTTLENECK" + @urlChanged="setUrl" + @toleranceChanged="setTolerance" + :url="url" + :tolerance="tolerance" + :directImport="directImport" + /> + <Distancemarksvirtual + v-if="import_ == $options.IMPORTTYPES.DISTANCEMARKSVIRTUAL" + @urlChanged="setUrl" + @usernameChanged="setUsername" + @passwordChanged="setPassword" + :url="url" + :username="username" + :password="password" + /> + <Distancemarksashore + v-if="import_ == $options.IMPORTTYPES.DISTANCEMARKSASHORE" + @urlChanged="setUrl" + @featureTypeChanged="setFeatureType" + @sortByChanged="setSortBy" + :url="url" + :featureType="featureType" + :sortBy="sortBy" + /> + <Faiwaydimensions + v-if="import_ == $options.IMPORTTYPES.FAIRWAYDIMENSION" + @urlChanged="setUrl" + @featureTypeChanged="setFeatureType" + @sortByChanged="setSortBy" + @LOSChanged="setLOS" + @depthChanged="setDepth" + @minWidthChanged="setMinWidth" + @maxWidthChanged="setMaxWidth" + @sourceOrganizationChanged="setSourceOrganization" + :url="url" + :featureType="featureType" + :sortBy="sortBy" + :LOS="LOS" + :minWidth="minWidth" + :maxWidth="maxWidth" + :sourceOrganization="sourceOrganization" + :depth="depth" + /> + <Gaugemeasurement + v-if="import_ == $options.IMPORTTYPES.GAUGEMEASUREMENT && !directImport" + @urlChanged="setUrl" + :url="url" + /> + <Waterwayarea + v-if="import_ == $options.IMPORTTYPES.WATERWAYAREA" + @urlChanged="setUrl" + @featureTypeChanged="setFeatureType" + @sortByChanged="setSortBy" + :url="url" + :featureType="featureType" + :sortBy="sortBy" + /> + <Waterwaygauges + v-if="import_ == $options.IMPORTTYPES.WATERWAYGAUGES" + @urlChanged="setUrl" + @usernameChanged="setUsername" + @passwordChanged="setPassword" + :url="url" + :username="username" + :password="password" + /> + <Waterwayaxis + v-if="import_ == $options.IMPORTTYPES.WATERWAYAXIS" + @urlChanged="setUrl" + @featureTypeChanged="setFeatureType" + @sortByChanged="setSortBy" + :url="url" + :featureType="featureType" + :sortBy="sortBy" + /> + + <div class="d-flex p-2"> + <template v-if="!directImport || !directImportAvailable"> + <div class="flex-column mr-4"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Scheduled</translate>? + </small> + </div> + <div class="flex-flex-row text-left"> + <toggle-button + :value="scheduled" + v-model="scheduled" + :sync="true" + class="mt-2" + :speed="100" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="60" + :height="30" + /> + </div> + </div> + <div class="flex-column mr-2"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Simple</translate> + </small> + </div> + <div class="flex-flex-row text-left"> + <toggle-button + :disabled="!scheduled" + :value="easyCron" + v-model="easyCron" + :sync="true" + class="mt-2" + :speed="100" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="60" + :height="30" + /> + </div> + </div> + <div class="ml-auto flex-column"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Tries</translate> + </small> + </div> + <div> + <input + style="width:120px;" + v-model="trys" + class="mr-1 form-control form-control-sm" + type="number" + /> + </div> + </div> + <div class="flex-column"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Wait to retry</translate> + </small> + </div> + <div> + <input + style="width:120px;" + v-model="waitRetry" + class="ml-1 form-control form-control-sm" + /> + </div> + </div> + </template> + </div> + <template v-if="!directImport || !directImportAvailable"> + <div class="flex-column w-100 px-2 pb-3"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Schedule</translate> + </small> + </div> + <div v-if="easyCron" class="text-left w-50"> + <select + :disabled="!scheduled" + v-model="simple" + class="form-control form-control-sm" + ><option value="weekly"><translate>Weekly</translate></option> + <option value="monthly"><translate>Monthly</translate> </option> + </select> + </div> + <div v-if="!easyCron" class="text-left w-100"> + <div class="d-flex flex-row"> + <div class="my-auto mr-2">{{ $options.EVERY }}</div> + <select + :disabled="!scheduled" + style="width: 130px;" + v-model="cronMode" + class="form-control form-control-sm" + @change="clearInputs" + > + <option :value="null"></option> + <option + v-for="(option, key) in $options.CRONMODE" + :value="key" + :key="key" + >{{ option }}</option + > + </select> + <div v-if="cronMode == 'hour'" class="ml-1 d-flex flex-row"> + <div class="mt-auto mb-auto">{{ $options.ON }}</div> + <input + :disabled="!scheduled" + v-model="minutes" + class="cronfield ml-1 mr-1 form-control form-control-sm" + type="number" + /> + <div class="mt-auto mb-auto">{{ $options.MINUTESPAST }}</div> + </div> + <div v-if="cronMode == 'day'" class="ml-1 d-flex flex-row"> + <div class="mt-auto mb-auto">{{ $options.AT }}</div> + <input + :disabled="!scheduled" + v-model="hour" + class="cronfield ml-1 mr-1 form-control form-control-sm" + type="number" + /> + <input + :disabled="!scheduled" + v-model="minutes" + class="cronfield ml-1 mr-1 form-control form-control-sm" + type="number" + /> + <div class="mt-auto mb-auto">{{ $options.OCLOCK }}</div> + </div> + <div v-if="cronMode == 'week'" class="ml-1 d-flex flex-row"> + <div class="ml-1 mr-1 mt-auto mb-auto">{{ $options.ON }}</div> + <select + :disabled="!scheduled" + v-model="day" + class="form-control form-control-sm" + > + <option + v-for="(option, key) in $options.DAYSOFWEEK" + :key="key" + :value="key" + >{{ option }}</option + > + </select> + <div class="ml-1 mt-auto mb-auto">{{ $options.AT }}</div> + <input + :disabled="!scheduled" + v-model="hour" + class="cronfield ml-1 mr-1 form-control form-control-sm" + type="number" + /> + <input + :disabled="!scheduled" + v-model="minutes" + class="cronfield ml-1 mr-1 form-control form-control-sm" + type="number" + /> + </div> + <div v-if="cronMode == 'month'" class="ml-1 d-flex flex-row"> + <div class="ml-1 mt-auto mb-auto">{{ $options.ON }}</div> + <input + :disabled="!scheduled" + v-model="dayOfMonth" + class="cronfield ml-1 mr-1 form-control form-control-sm" + type="number" + /> + <div class="mt-auto mb-auto">{{ $options.AT }}</div> + <input + :disabled="!scheduled" + v-model="hour" + class="cronfield ml-1 mr-2 form-control form-control-sm" + type="number" + /> + <input + :disabled="!scheduled" + v-model="minutes" + class="cronfield ml-1 mr-2 form-control form-control-sm" + type="number" + /> + <div class="mt-auto mb-auto">{{ $options.OCLOCK }}</div> + </div> + <div v-if="cronMode == 'year'" class="ml-1 d-flex flex-row"> + <div class="ml-1 mt-auto mb-auto">{{ $options.ON }}</div> + <input + :disabled="!scheduled" + v-model="dayOfMonth" + class="cronfield ml-1 mr-1 form-control form-control-sm" + type="number" + /> + <div class="mt-auto mb-auto">{{ $options.OF }}</div> + <select + :disabled="!scheduled" + v-model="month" + class="ml-1 mr-1 form-control form-control-sm" + > + <option + v-for="(option, key) in $options.MONTHS" + :value="key" + :key="key" + >{{ option }}</option + > + </select> + <div class="mt-auto mb-auto">{{ $options.ON }}</div> + <input + :disabled="!scheduled" + v-model="hour" + class="cronfield ml-1 mr-1 form-control form-control-sm" + type="number" + /> + <input + :disabled="!scheduled" + v-model="minutes" + class="cronfield ml-1 mr-1 form-control form-control-sm" + type="number" + /> + </div> + </div> + <div class="mt-3 w-50 d-flex"> + <div class="my-auto mr-2"> + <translate>Cronstring</translate> + </div> + <input + :disabled="!scheduled" + class="form-control form-control-sm" + v-model="cronString" + type="text" + /> + </div> + </div> + </div> + </template> + <div v-else class="d-flex text-left px-2 pb-3"> + <div class="flex-column w-100"> + <div class="custom-file"> + <input + accept=".xml" + type="file" + @change="fileSelected" + class="custom-file-input custom-file-input-sm" + id="uploadFile" + /> + <label class="pointer custom-file-label" for="uploadFile"> + {{ uploadLabel }} + </label> + </div> + </div> + </div> + <div class="d-flex justify-content-between p-2 border-top"> + <button :key="1" @click="back()" class="btn btn-sm btn-warning"> + Back + </button> + <div> + <button + v-if="!currentSchedule.id" + @click="triggerManualImport" + type="button" + class="btn btn-sm btn-outline-info" + :disabled="!triggerActive || !isValid" + > + <font-awesome-icon fixed-width icon="play" /> + <translate>Trigger import</translate> + </button> + <button + v-if="!directImport || !directImportAvailable" + :disabled="!isValid" + type="submit" + class="btn btn-sm btn-info ml-3" + > + <translate>Save</translate> + </button> + </div> + </div> + </form> +</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, 2019 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Tom Gottfried <tom.gottfried@intevation.de> + */ +import { + IMPORTTYPES, + IMPORTTYPEKIND, + initializeCurrentSchedule +} from "@/store/importschedule"; +import { mapState } from "vuex"; +import { displayInfo, displayError } from "@/lib/errors"; +import app from "@/main"; +import { HTTP } from "@/lib/http"; + +export default { + name: "importscheduledetail", + components: { + Availablefairwaydepth: () => import("./types/Availablefairwaydepth"), + Bottleneck: () => import("./types/Bottleneck"), + Distancemarksvirtual: () => import("./types/Distancemarksvirtual"), + Distancemarksashore: () => import("./types/Distancemarksashore"), + Faiwaydimensions: () => import("./types/Fairwaydimensions"), + Gaugemeasurement: () => import("./types/Gaugemeasurement"), + Waterwayarea: () => import("./types/Waterwayarea"), + Waterwaygauges: () => import("./types/Waterwaygauges"), + Waterwayaxis: () => import("./types/Waterwayaxis") + }, + data() { + return { + directImport: false, + passwordVisible: false, + uploadLabel: this.$gettext("choose file to upload"), + uploadFile: null, + ...initializeCurrentSchedule() + }; + }, + mounted() { + this.initialize(); + }, + watch: { + cronMode() { + this.cronString = this.calcCronString(); + }, + minutes() { + this.cronString = this.calcCronString(); + }, + hour() { + this.cronString = this.calcCronString(); + }, + month() { + this.cronString = this.calcCronString(); + }, + day() { + this.cronString = this.calcCronString(); + }, + dayOfMonth() { + this.cronString = this.calcCronString(); + }, + importScheduleDetailVisible() { + this.initialize(); + }, + cronString() { + if (this.isWeekly(this.cronString)) { + this.simple = "weekly"; + } + if (this.isMonthly(this.cronString)) { + this.simple = "monthly"; + } + } + }, + computed: { + ...mapState("importschedule", [ + "importScheduleDetailVisible", + "currentSchedule" + ]), + import_() { + return this.currentSchedule.importType; + }, + directImportAvailable() { + switch (this.import_) { + case this.$options.IMPORTTYPES.BOTTLENECK: + case this.$options.IMPORTTYPES.FAIRWAYAVAILABILITY: + case this.$options.IMPORTTYPES.GAUGEMEASUREMENT: + return true; + default: + return false; + } + }, + isCredentialsRequired() { + switch (this.import_) { + case this.$options.IMPORTTYPES.WATERWAYGAUGES: + case this.$options.IMPORTTYPES.DISTANCEMARKSVIRTUAL: + return true; + default: + return false; + } + }, + isURLRequired() { + switch (this.import_) { + case this.$options.IMPORTTYPES.BOTTLENECK: + case this.$options.IMPORTTYPES.WATERWAYAXIS: + case this.$options.IMPORTTYPES.GAUGEMEASUREMENT: + case this.$options.IMPORTTYPES.FAIRWAYAVAILABILITY: + case this.$options.IMPORTTYPES.WATERWAYAREA: + case this.$options.IMPORTTYPES.FAIRWAYDIMENSION: + case this.$options.IMPORTTYPES.WATERWAYGAUGES: + case this.$options.IMPORTTYPES.DISTANCEMARKSVIRTUAL: + case this.$options.IMPORTTYPES.DISTANCEMARKSASHORE: + return true; + default: + return false; + } + }, + isFeatureTypeRequired() { + switch (this.import_) { + case this.$options.IMPORTTYPES.WATERWAYAXIS: + case this.$options.IMPORTTYPES.WATERWAYAREA: + case this.$options.IMPORTTYPES.FAIRWAYDIMENSION: + case this.$options.IMPORTTYPES.DISTANCEMARKSASHORE: + return true; + default: + return false; + } + }, + isSortbyRequired() { + switch (this.import_) { + case this.$options.IMPORTTYPES.WATERWAYAXIS: + case this.$options.IMPORTTYPES.WATERWAYAREA: + case this.$options.IMPORTTYPES.FAIRWAYDIMENSION: + case this.$options.IMPORTTYPES.DISTANCEMARKSASHORE: + return true; + default: + return false; + } + }, + isToleranceRequired() { + switch (this.import_) { + case this.$options.IMPORTTYPES.BOTTLENECK: + return true; + default: + return false; + } + }, + usernamePasswordFilled() { + if ( + this.isCredentialsRequired && + this.currentSchedule.id && + this.username + ) + return true; + if ( + this.isCredentialsRequired && + !this.currentSchedule.id && + this.username && + this.password + ) + return true; + return false; + }, + isValid() { + if (!this.import_) return false; + if (this.isToleranceRequired && !this.tolerance) return false; + if (this.directImport && !this.uploadFile) return false; + else if (!this.directImport) { + if (this.isURLRequired && !this.url) return false; + if (this.isSortbyRequired && !this.sortBy) return false; + if (this.isFeatureTypeRequired && !this.featureType) return false; + if (this.isCredentialsRequired && !this.usernamePasswordFilled) + return false; + if (this.import_ == this.$options.IMPORTTYPES.FAIRWAYDIMENSION) { + if ( + !this.LOS || + !this.minWidth || + !this.maxWidth || + !this.depth || + !this.sourceOrganization + ) + return false; + } + } + return true; + } + }, + methods: { + back() { + this.$store.commit("importschedule/setListMode"); + }, + fileSelected(e) { + const files = e.target.files || e.dataTransfer.files; + if (!files) return; + this.uploadLabel = files[0].name; + this.uploadFile = files[0]; + }, + setUrl(value) { + this.url = value; + }, + setFeatureType(value) { + this.featureType = value; + }, + setSortBy(value) { + this.sortBy = value; + }, + setTolerance(value) { + this.tolerance = value; + }, + setUsername(value) { + this.username = value; + }, + setPassword(value) { + this.password = value; + }, + setLOS(value) { + this.LOS = value; + }, + setMinWidth(value) { + this.minWidth = value; + }, + setMaxWidth(value) { + this.maxWidth = value; + }, + setDepth(value) { + this.depth = value; + }, + setSourceOrganization(value) { + this.sourceOrganization = value; + }, + calcCronString() { + let getValue = value => { + return this[value] !== null ? this[value] : "*"; + }; + + const min = getValue("minutes"); + const h = getValue("hour"); + const dm = getValue("dayOfMonth"); + const m = getValue("month"); + const wd = getValue("day"); + + if (this.cronMode === "15minutes") return "0 */15 * * * *"; + if (this.cronMode === "hour") return `0 ${min} * * * *`; + if (this.cronMode === "day") return `0 ${min} ${h} * * *`; + if (this.cronMode === "week") return `0 ${min} ${h} * * ${wd}`; + if (this.cronMode === "month") return `0 ${min} ${h} ${dm} * *`; + if (this.cronMode === "year") return `0 ${min} ${h} ${dm} ${m} *`; + return this.cronString; + }, + validateBottleneckfields() { + return !!this.url; + }, + initialize() { + this.id = this.currentSchedule.id; + this.importType = this.currentSchedule.importType; + this.schedule = this.currentSchedule.schedule; + this.scheduled = this.currentSchedule.scheduled; + this.importSource = this.currentSchedule.importSource; + this.eMailNotification = this.currentSchedule.eMailNotification; + this.easyCron = this.currentSchedule.easyCron; + this.cronMode = this.currentSchedule.cronMode; + this.minutes = this.currentSchedule.minutes; + this.month = this.currentSchedule.month; + this.hour = this.currentSchedule.hour; + this.day = this.currentSchedule.day; + this.dayOfMonth = this.currentSchedule.dayOfMonth; + this.simple = this.currentSchedule.simple; + this.url = this.currentSchedule.url; + this.insecure = this.currentSchedule.insecure; + this.cronString = this.currentSchedule.cronString; + this.featureType = this.currentSchedule.featureType; + this.sortBy = this.currentSchedule.sortBy; + this.tolerance = this.currentSchedule.tolerance; + this.username = this.currentSchedule.username; + this.password = this.currentSchedule.password; + this.LOS = this.currentSchedule.LOS; + this.minWidth = this.currentSchedule.minWidth; + this.maxWidth = this.currentSchedule.maxWidth; + this.depth = this.currentSchedule.depth; + this.sourceOrganization = this.currentSchedule.sourceOrganization; + this.directImport = false; + this.trys = this.currentSchedule.trys; + this.waitRetry = this.currentSchedule.waitRetry; + }, + isWeekly(cron) { + return /0 \d{1,2} \d{1,2} \* \* \d{1}/.test(cron); + }, + isMonthly(cron) { + return /0 \d{1,2} \d{1,2} \d{1,2} \* \*/.test(cron); + }, + clearInputs() { + this.minutes = this.currentSchedule.minutes; + this.month = this.currentSchedule.month; + this.hour = this.currentSchedule.hour; + this.day = this.currentSchedule.day; + this.dayOfMonth = this.currentSchedule.dayOfMonth; + }, + triggerFileUpload() { + if (!this.uploadFile) return; + let formData = new FormData(); + let routeParam = ""; + switch (this.import_) { + case this.$options.IMPORTTYPES.BOTTLENECK: + formData.append("tolerance", this.tolerance); + routeParam = "ubn"; + break; + case this.$options.IMPORTTYPES.FAIRWAYAVAILABILITY: + routeParam = "ufa"; + break; + case this.$options.IMPORTTYPES.GAUGEMEASUREMENT: + routeParam = "ugm"; + break; + default: + throw new Error("invalid importroute"); + } + + formData.append(routeParam, this.uploadFile); + if (this.eMailNotification) { + formData.append("send-email", this.eMailNotification); + } + HTTP.post("/imports/" + routeParam, formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(response => { + const { id } = response.data; + displayInfo({ + title: this.$gettext("File Import"), + message: this.$gettext("Import import: #") + id + }); + this.closeDetailview(); + this.$store.dispatch("importschedule/loadSchedules").catch(error => { + const { status, data } = error.response; + displayError({ + title: this.gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + triggerManualImport() { + if (!this.triggerActive) return; + if (!this.import_) return; + if (this.directImport) { + if (!this.uploadFile) return; + this.triggerFileUpload(); + return; + } + let data = {}; + if (this.isURLRequired) { + if (!this.url) return; + data["url"] = this.url; + data["insecure"] = this.insecure; + } + if (this.isFeatureTypeRequired) { + if (!this.featureType) return; + data["feature-type"] = this.featureType; + } + if (this.isSortbyRequired) { + if (!this.sortBy) return; + data["sort-by"] = this.sortBy; + } + if (this.isToleranceRequired) { + if (!this.tolerance) return; + data["tolerance"] = parseFloat(this.tolerance); + } + if (this.isCredentialsRequired) { + if (!this.username || !this.password) return; + data["user"] = this.username; + data["password"] = this.password; + } + if (this.import_ == this.$options.IMPORTTYPES.FAIRWAYDIMENSION) { + if ( + !this.LOS || + !this.minWidth || + !this.maxWidth || + !this.depth || + !this.sourceOrganization + ) + return; + data["feature-type"] = this.featureType; + data["sort-by"] = this.sortBy; + data["los"] = this.LOS * 1; + data["min-width"] = this.minWidth * 1; + data["max-width"] = this.maxWidth * 1; + data["depth"] = this.depth * 1; + data["source-organization"] = this.sourceOrganization; + } + data["send-email"] = this.eMailNotification; + this.triggerActive = false; + this.$store + .dispatch("importschedule/triggerImport", { + type: IMPORTTYPEKIND[this.import_], + data + }) + .then(response => { + const { id } = response.data; + displayInfo({ + title: this.$gettext("Import"), + message: this.$gettext("Manually triggered import: #") + id + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => { + this.triggerActive = true; + }); + }, + save() { + if (!this.import_) return; + let cron = this.cronString; + if (this.easyCron) { + if (this.simple === "weekly") cron = "0 0 0 * * 0"; + if (this.simple === "monthly") cron = "0 0 0 1 * *"; + } + let data = {}; + let config = {}; + data["kind"] = IMPORTTYPEKIND[this.import_]; + + if (this.isURLRequired) { + if (!this.url) return; + config["url"] = this.url; + config["insecure"] = this.insecure; + } + if (this.isSortbyRequired) { + if (!this.sortBy) return; + config["sort-by"] = this.sortBy; + } + if (this.isFeatureTypeRequired) { + if (!this.featureType) return; + config["feature-type"] = this.featureType; + } + if (this.isToleranceRequired) { + if (!this.tolerance) return; + config["tolerance"] = parseFloat(this.tolerance); + } + if (this.isCredentialsRequired) { + if (!this.usernamePasswordFilled) return; + config = { + ...config, + user: this.username + }; + if (this.password) { + config["password"] = this.password; + } + } + if (this.import_ == this.$options.IMPORTTYPES.FAIRWAYDIMENSION) { + if ( + !this.LOS || + !this.minWidth || + !this.maxWidth || + !this.depth || + !this.sourceOrganization + ) + return; + config = { ...config, los: this.LOS, depth: this.depth }; + config["min-width"] = this.minWidth; + config["max-width"] = this.maxWidth; + config["source-organization"] = this.sourceOrganization; + } + if (this.scheduled) { + config["cron"] = cron; + } + if (this.waitRetry) config["wait-retry"] = this.waitRetry; + if (this.trys) config["trys"] = Number(this.trys); + config["send-email"] = this.eMailNotification; + if (!this.id) { + data["config"] = config; + this.$store + .dispatch("importschedule/saveCurrentSchedule", data) + .then(response => { + const { id } = response.data; + displayInfo({ + title: this.$gettext("Import"), + message: this.$gettext("Saved import: #") + id + }); + this.closeDetailview(); + this.$store + .dispatch("importschedule/loadSchedules") + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } else { + this.$store + .dispatch("importschedule/updateCurrentSchedule", { + data: config, + id: this.id + }) + .then(response => { + const { id } = response.data; + displayInfo({ + title: this.$gettext("Import"), + message: this.$gettext("update import: #") + id + }); + this.closeDetailview(); + this.$store + .dispatch("importschedule/loadSchedules") + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + closeDetailview() { + this.$store.commit("importschedule/clearCurrentSchedule"); + this.$store.commit("importschedule/setListMode"); + } + }, + IMPORTTYPES: IMPORTTYPES, + on: "on", + off: "off", + FILE: app.$gettext("File"), + URL: app.$gettext("URL"), + EVERY: app.$gettext("Every"), + MINUTESPAST: app.$gettext("minutes past"), + ON: app.$gettext("on"), + OF: app.$gettext("of"), + AT: app.$gettext("at"), + OCLOCK: app.$gettext("o' clock"), + CRONMODE: { + "15minutes": app.$gettext("15 minutes"), + hour: app.$gettext("hour"), + day: app.$gettext("day"), + week: app.$gettext("week"), + month: app.$gettext("month"), + year: app.$gettext("year") + }, + DAYSOFWEEK: { + 1: app.$gettext("Monday"), + 2: app.$gettext("Tuesday"), + 3: app.$gettext("Wednesday"), + 4: app.$gettext("Thursday"), + 5: app.$gettext("Friday"), + 6: app.$gettext("Saturday"), + 0: app.$gettext("Sunday") + }, + MONTHS: { + 1: app.$gettext("January"), + 2: app.$gettext("February"), + 3: app.$gettext("March"), + 4: app.$gettext("April"), + 5: app.$gettext("May"), + 6: app.$gettext("June"), + 7: app.$gettext("July"), + 8: app.$gettext("August"), + 9: app.$gettext("September"), + 10: app.$gettext("October"), + 11: app.$gettext("November"), + 12: app.$gettext("December") + } +}; +</script> + +<style lang="scss" scoped> +.cronfield { + width: 55px; +} + +.importscheduledetailscard { + min-height: 550px; +} + +.importscheduledetails { + width: 100%; + margin-top: $offset; + margin-right: $offset; +} +</style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/ApprovedGaugeMeasurement.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,160 @@ +<template> + <div> + <div class="d-flex px-2"> + <div :key="1" class="flex-column mr-4"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Email Notification</translate> + </small> + </div> + <div class="flex-flex-row text-left"> + <toggle-button + v-model="eMailNotification" + class="mt-2" + :speed="100" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="60" + :height="30" + /> + </div> + </div> + <div class="d-flex flex-column text-left w-25"> + <label class="text-nowrap" for="originator"> + <small class="text-muted" + >{{ $options.ORIGINATOR }} / {{ $options.FROM }}</small + > + </label> + <input + type="text" + v-model="originator" + class="form-control form-control-sm" + id="originator" + /> + <span class="text-left text-danger"> + <small v-if="!originator"> + <translate>Please enter an originator</translate> + </small> + </span> + </div> + </div> + <div class="mt-4 flex-column px-2 w-100"> + <div class="custom-file"> + <input + accept=".csv" + type="file" + @change="fileSelected" + class="custom-file-input" + id="uploadFile" + /> + <label class="pointer custom-file-label" for="uploadFile"> + {{ uploadLabel }} + </label> + </div> + </div> + <div class="d-flex w-100 mt-3 border-top justify-content-between p-2"> + <button :key="1" @click="back()" class="btn btn-sm btn-warning"> + Back + </button> + <button + :key="2" + type="submit" + @click="submit" + class="btn btn-sm btn-info" + > + <translate>Submit</translate> + </button> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { HTTP } from "@/lib/http"; +import { displayError, displayInfo } from "@/lib/errors"; +import app from "@/main"; + +export default { + name: "importapprovedgaugemeasurements", + data() { + return { + disableUploadButton: false, + uploadLabel: this.$gettext("choose file to upload"), + uploadFile: null, + originator: "viadonau", + eMailNotification: false + }; + }, + computed: { + importGaugmeasurmentLabel() { + return this.$gettext("Import approved gaugemeasurements"); + } + }, + methods: { + back() { + this.uploadLabel = this.$gettext("choose file to upload"); + this.uploadFile = null; + this.originator = "viadonau"; + this.$store.commit("importschedule/setListMode"); + }, + fileSelected(e) { + const files = e.target.files || e.dataTransfer.files; + if (!files) return; + this.uploadLabel = files[0].name; + this.uploadFile = files[0]; + }, + submit() { + if (!this.originator || !this.uploadFile) return; + let formData = new FormData(); + formData.append("agm", this.uploadFile); + formData.append("originator", this.originator); + if (this.eMailNotification) { + formData.append("send-email", this.eMailNotification); + } + HTTP.post("/imports/agm", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(() => { + displayInfo({ + title: this.$gettext("Import"), + message: this.$gettext( + "Starting import of Approved Gauge Measurements" + ) + }); + this.back(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + ORIGINATOR: app.$gettext("originator"), + FROM: app.$gettext("from"), + on: "on", + off: "off" +}; +</script> + +<style lang="scss" scoped></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/Availablefairwaydepth.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,51 @@ +<template> + <div> + <div class="d-flex px-2"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div class="w-100"> + <input + @input="urlChanged" + class="url form-control form-control-sm" + type="url" + :value="url" + /> + </div> + </div> + </div> + <div v-if="!url" class="d-flex px-2"> + <small + ><translate class="text-danger">Please enter a URL</translate></small + > + </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> + */ +export default { + name: "availablefairwaydepth", + props: ["url"], + methods: { + urlChanged(e) { + this.$emit("urlChanged", e.target.value); + } + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/Bottleneck.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,111 @@ +<template> + <div class="p-2"> + <div class="d-flex"> + <div class="flex-column w-100"> + <template v-if="!directImport"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>URL</translate> + </small> + </div> + <div class="w-100"> + <input + @input="urlChanged" + class="url form-control form-control-sm" + type="url" + :value="url" + /> + </div> + </template> + </div> + <div v-if="false" class="flex-column mt-2 text-left"> + <div class="d-flex flex-row"> + <small class="text-muted mr-2" + ><translate>Insecure</translate> + </small> + </div> + <div class="d-flex flex-row"> + <toggle-button + v-model="insecure" + class="mt-2" + :speed="100" + :color="{ + checked: '#FF0000', + unchecked: '#E9ECEF', + disabled: '#CCCCCC' + }" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="60" + :height="30" + /> + </div> + </div> + </div> + <div v-if="!directImport && !url" class="d-flex"> + <small + ><translate class="text-danger">Please enter a URL</translate></small + > + </div> + <div class="d-flex"> + <div class="flex-column mt-2 mr-3 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Tolerance for snapping of waterway axis [m]</translate> + </small> + </div> + <div class="w-100"> + <input + @input="toleranceChanged" + class="tolerance form-control form-control-sm" + type="number" + min="0" + :value="tolerance" + /> + </div> + <div v-if="!tolerance" class="d-flex"> + <small + ><translate class="text-danger" + >Please enter a tolerance value</translate + ></small + > + </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, 2019 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Tom Gottfried <tom.gottfried@intevation.de> + */ +export default { + name: "bottleneckimport", + props: ["url", "tolerance", "directImport"], + methods: { + urlChanged(e) { + this.$emit("urlChanged", e.target.value); + }, + toleranceChanged(e) { + this.$emit("toleranceChanged", e.target.value); + } + }, + on: "on", + off: "off" +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/Distancemarksashore.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,99 @@ +<template> + <div> + <div class="d-flex px-2"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div class="w-100"> + <input + @input="urlChanged" + class="url form-control form-control-sm" + type="url" + :value="url" + /> + </div> + </div> + </div> + <div v-if="!url" class="d-flex px-2"> + <small + ><translate class="text-danger">Please enter a URL</translate></small + > + </div> + <div class="d-flex px-2"> + <div class="flex-column mt-2 mr-3 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>Featuretype</translate> </small> + </div> + <div class="w-100"> + <input + @input="featureTypeChanged" + class="featuretype form-control form-control-sm" + type="text" + :value="featureType" + /> + </div> + <div v-if="!featureType" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a Featuretype</translate + ></small + > + </div> + </div> + <div class="flex-column mt-2 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>SortBy</translate> </small> + </div> + <div class="w-100"> + <input + @input="sortByChanged" + class="sortby form-control form-control-sm" + type="text" + :value="sortBy" + /> + </div> + <div v-if="!sortBy" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter SortBy</translate + ></small + > + </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> + */ +export default { + name: "distancemarksashore", + props: ["url", "featureType", "sortBy"], + methods: { + urlChanged(e) { + this.$emit("urlChanged", e.target.value); + }, + featureTypeChanged(e) { + this.$emit("featureTypeChanged", e.target.value); + }, + sortByChanged(e) { + this.$emit("sortByChanged", e.target.value); + } + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/Distancemarksvirtual.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,125 @@ +<template> + <div> + <div class="d-flex px-2"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div class="w-100"> + <input + @input="urlChanged" + class="url form-control form-control-sm" + type="url" + :value="url" + /> + </div> + </div> + </div> + <div v-if="!url" class="d-flex px-2"> + <small + ><translate class="text-danger">Please enter a URL</translate></small + > + </div> + <div class="d-flex px-2"> + <div class="flex-column mt-2 mr-3 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>Username</translate> </small> + </div> + <div class="w-100"> + <input + @input="usernameChanged" + class="username form-control form-control-sm" + type="text" + :value="username" + /> + </div> + <div v-if="!username" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a Username</translate + ></small + > + </div> + </div> + <div class="flex-column mt-2 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>Password</translate> </small> + </div> + <div class="w-100 d-flex flex-row"> + <input + @input="passwordChanged" + class="pasword form-control form-control-sm" + :type="showPassword" + :value="password" + /> + <span + class="input-group-text ml-2" + @click="passwordVisible = !passwordVisible" + > + <font-awesome-icon :icon="passwordVisible ? 'eye-slash' : 'eye'" /> + </span> + </div> + <div + v-if="!password && !this.currentSchedule.id" + class="d-flex flex-row" + > + <small + ><translate class="text-danger" + >Please enter a Password</translate + ></small + > + </div> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { mapState } from "vuex"; +export default { + name: "distancemarksvirtual", + props: ["url", "username", "password"], + data() { + return { + passwordVisible: false + }; + }, + computed: { + ...mapState("importschedule", [ + "importScheduleDetailVisible", + "currentSchedule" + ]), + showPassword() { + if (this.passwordVisible) return "text"; + return "password"; + } + }, + methods: { + urlChanged(e) { + this.$emit("urlChanged", e.target.value); + }, + usernameChanged(e) { + this.$emit("usernameChanged", e.target.value); + }, + passwordChanged(e) { + this.$emit("passwordChanged", e.target.value); + } + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/Fairwaydimensions.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,243 @@ +<template> + <div> + <div class="d-flex px-2"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div class="w-100"> + <input + @input="urlChanged" + class="url form-control form-control-sm" + type="url" + :value="url" + /> + </div> + </div> + </div> + <div v-if="!url" class="d-flex px-2"> + <small + ><translate class="text-danger">Please enter a URL</translate></small + > + </div> + <div class="d-flex px-2"> + <div class="flex-column mt-2 mr-3 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>Featuretype</translate> </small> + </div> + <div class="w-100"> + <input + @input="featureTypeChanged" + class="featuretype form-control form-control-sm" + type="text" + :value="featureType" + /> + </div> + <div v-if="!featureType" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a Featuretype</translate + ></small + > + </div> + </div> + <div class="flex-column mt-2 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>SortBy</translate> </small> + </div> + <div class="w-100"> + <input + @input="sortByChanged" + class="sortby form-control form-control-sm" + type="text" + :value="sortBy" + /> + </div> + <div v-if="!sortBy" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter SortBy</translate + ></small + > + </div> + </div> + </div> + <div class="d-flex px-2"> + <div class="flex-column mt-2 mr-3 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>LOS</translate> </small> + </div> + <div class="w-100"> + <select v-model="los" class="form-control form-control-sm"> + <option>1</option> + <option>2</option> + <option>3</option> + </select> + </div> + <div v-if="!LOS" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a level of service</translate + ></small + > + </div> + </div> + <div class="flex-column mt-2 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>Depth</translate> </small> + </div> + <div class="d-flex flex-row"> + <input + @input="depthChanged" + class="depth form-control form-control-sm" + type="number" + :value="depth" + /> + <div class="ml-2 my-auto">cm</div> + </div> + <div v-if="!depth" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a depth</translate + ></small + > + </div> + </div> + </div> + <div class="d-flex px-2"> + <div class="flex-column mt-2 mr-3 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>MinWidth</translate> </small> + </div> + <div class="d-flex flex-row"> + <input + @input="minWidthChanged" + class="minwidth form-control form-control-sm" + type="number" + :value="minWidth" + /> + <div class="ml-2 my-auto"> m</div> + </div> + <div v-if="!minWidth" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a minimum width</translate + ></small + > + </div> + </div> + <div class="flex-column mt-2 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>MaxWidth</translate> </small> + </div> + <div class="d-flex flex-row"> + <input + @input="maxWidthChanged" + class="maxwidth form-control form-control-sm" + type="number" + :value="maxWidth" + /> + <div class="ml-2 my-auto"> m</div> + </div> + <div v-if="!maxWidth" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a maximum width</translate + ></small + > + </div> + </div> + </div> + <div class="d-flex px-2"> + <div class="flex-column mt-2 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Source orgranization</translate> + </small> + </div> + <div class="w-100"> + <input + @input="sourceOrganizationChanged" + class="sourceorganization form-control form-control-sm" + type="text" + :value="sourceOrganization" + /> + </div> + <div v-if="!sourceOrganization" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a source orgranization</translate + ></small + > + </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> + */ +export default { + name: "fairwaydimensions", + props: [ + "url", + "featureType", + "sortBy", + "depth", + "LOS", + "minWidth", + "maxWidth", + "sourceOrganization" + ], + methods: { + urlChanged(e) { + this.$emit("urlChanged", e.target.value); + }, + featureTypeChanged(e) { + this.$emit("featureTypeChanged", e.target.value); + }, + sortByChanged(e) { + this.$emit("sortByChanged", e.target.value); + }, + depthChanged(e) { + this.$emit("depthChanged", e.target.value * 1); + }, + LOSChanged(e) { + this.$emit("LOSChanged", e.target.value * 1); + }, + minWidthChanged(e) { + this.$emit("minWidthChanged", e.target.value * 1); + }, + maxWidthChanged(e) { + this.$emit("maxWidthChanged", e.target.value * 1); + }, + sourceOrganizationChanged(e) { + this.$emit("sourceOrganizationChanged", e.target.value); + } + }, + computed: { + los: { + get() { + return this.LOS; + }, + set(value) { + this.$emit("LOSChanged", value * 1); + } + } + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/Gaugemeasurement.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,51 @@ +<template> + <div> + <div class="d-flex px-2"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div class="w-100"> + <input + @input="urlChanged" + class="url form-control form-control-sm" + type="url" + :value="url" + /> + </div> + </div> + </div> + <div v-if="!url" class="d-flex px-2"> + <small + ><translate class="text-danger">Please enter a URL</translate></small + > + </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> + */ +export default { + name: "gaugemeasurement", + props: ["url"], + methods: { + urlChanged(e) { + this.$emit("urlChanged", e.target.value); + } + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/Soundingresults.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,382 @@ +<template> + <div> + <div v-if="editState" class="mb-2 p-2"> + <div + v-for="(message, index) in messages" + :key="index" + class="alert alert-warning small" + > + {{ message }} + </div> + <div class="d-flex w-100"> + <div class="w-50 mr-2 text-left"> + <small class="text-muted"> + <translate>Bottleneck</translate> + </small> + <select v-model="bottleneck" class="custom-select custom-select-sm"> + <option + v-for="bottleneck in availableBottlenecks" + :value="bottleneck" + :key="bottleneck.properties.objnam" + > + {{ bottleneck.properties.objnam }} + </option> + </select> + <span class="text-danger"> + <small v-if="!bottleneck"> + <translate>Please select a bottleneck</translate> + </small> + </span> + </div> + <div class="w-50 ml-3 text-left"> + <small class="text-muted"> + <translate>Projection</translate> (EPSG) + </small> + <input + class="form-control form-control-sm" + v-model="projection" + value="4326" + placeholder="e.g. 4326" + type="number" + /> + <span class="text-left text-danger"> + <small v-if="!projection"> + <translate>Please enter a projection</translate> + </small> + </span> + </div> + </div> + <div class="d-flex flex-row w-100 mt-3"> + <div class="w-50 mr-2 text-left"> + <small class="text-muted"> + <translate>Depthreference</translate> + </small> + <select + v-model="depthReference" + class="custom-select custom-select-sm" + id="depthreference" + > + <option + v-for="option in this.depthReferenceOptions" + :key="option" + >{{ option }}</option + > + </select> + <span class="text-left text-danger"> + <small v-if="!depthReference"> + <translate>Please enter a reference</translate> + </small> + </span> + </div> + <div class="w-50 ml-3 text-left"> + <small class="text-muted"> <translate>Date</translate> </small> + <input + id="importdate" + type="date" + class="form-control form-control-sm" + placeholder="Date of import" + v-model="importDate" + /> + <span class="text-left text-danger"> + <small v-if="!importDate"> + <translate>Please enter a date</translate> + </small> + </span> + </div> + </div> + </div> + <div class="mt-2"> + <div v-if="uploadState" class="input-group px-2"> + <div :key="1" class="flex-column mr-4"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Email Notification</translate> + </small> + </div> + <div class="flex-flex-row text-left"> + <toggle-button + v-model="eMailNotification" + class="mt-2" + :speed="100" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="60" + :height="30" + /> + </div> + </div> + <div class="custom-file mt-4"> + <input + accept=".zip" + type="file" + @change="fileSelected" + class="custom-file-input" + id="uploadFile" + /> + <label class="pointer custom-file-label" for="uploadFile"> + {{ uploadLabel }} + </label> + </div> + </div> + <div + class="d-flex justify-content-between mt-2 p-2 border-top" + v-if="editState" + > + <button + :key="1" + @click="deleteTempData()" + class="btn btn-sm btn-warning" + > + Back + </button> + <span> + <a + download="meta.json" + :href="dataLink" + :class="[ + 'btn btn-sm btn-outline-info', + { disabled: !bottleneck || !importDate || !depthReference } + ]" + > + <translate>Download Meta.json</translate> + </a> + <button + :disabled="disableUploadButton" + @click="confirm" + class="btn btn-sm btn-info ml-2" + type="button" + > + <translate>Confirm</translate> + </button> + </span> + </div> + <div v-if="uploadState" class="d-flex mt-2 p-2 border-top"> + <button :key="2" @click="back()" class="btn btn-sm btn-warning"> + Back + </button> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { HTTP } from "@/lib/http"; +import { displayError, displayInfo } from "@/lib/errors"; +import { mapState } from "vuex"; + +const IMPORTSTATE = { UPLOAD: "UPLOAD", EDIT: "EDIT" }; + +export default { + data() { + return { + importState: IMPORTSTATE.UPLOAD, + depthReference: "", + bottleneck: "", + projection: "", + importDate: "", + uploadLabel: this.$gettext("choose .zip- file"), + uploadFile: null, + disableUpload: false, + token: null, + messages: [], + eMailNotification: false + }; + }, + methods: { + back() { + this.$store.commit("importschedule/setListMode"); + }, + initialState() { + this.importState = IMPORTSTATE.UPLOAD; + this.depthReference = ""; + this.bottleneck = null; + this.projection = ""; + this.importDate = ""; + this.uploadLabel = this.$gettext("choose .zip- file"); + this.uploadFile = null; + this.disableUpload = false; + this.token = null; + this.eMailNotification = false; + this.messages = []; + }, + fileSelected(e) { + const files = e.target.files || e.dataTransfer.files; + if (!files) return; + this.uploadLabel = files[0].name; + this.uploadFile = files[0]; + this.upload(); + }, + deleteTempData() { + HTTP.delete("/imports/sr-upload/" + this.token, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + } + }) + .then(() => { + this.initialState(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + upload() { + let formData = new FormData(); + formData.append("soundingresult", this.uploadFile); + if (this.eMailNotification) { + formData.append("send-email", this.eMailNotification); + } + HTTP.post("/imports/sr-upload", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(response => { + if (response.data.meta) { + const { bottleneck, date, epsg } = response.data.meta; + const depthReference = response.data.meta["depth-reference"]; + this.bottleneck = this.bottlenecks.find( + bn => bn.properties.objnam === bottleneck + ); + this.depthReference = depthReference; + this.importDate = new Date(date).toISOString().split("T")[0]; + this.projection = epsg; + } + this.importState = IMPORTSTATE.EDIT; + this.token = response.data.token; + this.messages = response.data.messages; + }) + .catch(error => { + const { status, data } = error.response; + const messages = data.messages ? data.messages.join(", ") : ""; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${messages}` + }); + }); + }, + confirm() { + let formData = new FormData(); + formData.append("token", this.token); + if (this.bottleneck) + formData.append("bottleneck", this.bottleneck.properties.objnam); + if (this.importDate) + formData.append("date", this.importDate.split("T")[0]); + if (this.depthReference) + formData.append("depth-reference", this.depthReference); + if (this.projection) formData.append("", this.projection); + + HTTP.post("/imports/sr", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(() => { + displayInfo({ + title: this.$gettext("Import"), + message: + this.$gettext("Starting import for ") + + this.bottleneck.properties.objnam + }); + this.initialState(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + mounted() { + this.$store.dispatch("bottlenecks/loadBottlenecks"); + }, + watch: { + showContextBox() { + if (!this.showContextBox && this.token) this.deleteTempData(); + } + }, + computed: { + ...mapState("application", ["showContextBox"]), + ...mapState("bottlenecks", ["bottlenecks"]), + importSoundingresultsLabel() { + return this.$gettext("Import Soundingresults"); + }, + disableUploadButton() { + if (this.importState === IMPORTSTATE.UPLOAD) return this.disableUpload; + if ( + !this.bottleneck || + !this.importDate || + !this.depthReference || + !this.projection + ) + return true; + return this.disableUpload; + }, + availableBottlenecks() { + return this.bottlenecks; + }, + editState() { + return this.importState === IMPORTSTATE.EDIT; + }, + uploadState() { + return this.importState === IMPORTSTATE.UPLOAD; + }, + Upload() { + return this.$gettext("Upload"); + }, + Confirm() { + return this.$gettext("Confirm"); + }, + dataLink() { + if (this.bottleneck && this.depthReference && this.import) { + return ( + "data:text/json;charset=utf-8," + + encodeURIComponent( + JSON.stringify({ + depthReference: this.depthReference, + bottleneck: this.bottleneck.properties.objnam, + date: this.importDate + }) + ) + ); + } + }, + depthReferenceOptions() { + if (this.bottleneck) { + const referenceLevels = JSON.parse( + this.bottleneck.properties.reference_water_levels + ); + const result = Object.keys(referenceLevels); + if (!referenceLevels["ZPG"]) result.push("ZPG"); // ZPG should always be available + return result; + } + return []; + } + }, + on: "on", + off: "off" +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/WaterwayProfiles.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,208 @@ +<template> + <div> + <div class="mb-2 px-2"> + <div :key="1" class="flex-column mr-4"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Email Notification</translate> + </small> + </div> + <div class="flex-flex-row text-left"> + <toggle-button + v-model="eMailNotification" + class="mt-2" + :speed="100" + :labels="{ + checked: this.$options.on, + unchecked: this.$options.off + }" + :width="60" + :height="30" + /> + </div> + </div> + <div class="d-flex flex-row"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div class="w-100"> + <input + class="form-control form-control-sm" + type="url" + v-model="url" + /> + </div> + </div> + </div> + <div v-if="!url" class="d-flex flex-row"> + <small + ><translate class="text-danger">Please enter a URL</translate></small + > + </div> + <div class="d-flex flex-row"> + <div class="flex-column mt-2 mr-3 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>Featuretype</translate> + </small> + </div> + <div class="w-100"> + <input + class="form-control form-control-sm" + type="text" + v-model="featureType" + /> + </div> + <div v-if="!featureType" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a Featuretype</translate + ></small + > + </div> + </div> + <div class="flex-column mt-2 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> + <translate>SortBy</translate> + </small> + </div> + <div class="w-100"> + <input + class="form-control form-control-sm" + type="text" + v-model="sortBy" + /> + </div> + <div v-if="!sortBy" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter SortBy</translate + ></small + > + </div> + </div> + </div> + </div> + <div class="d-flex text-left px-2"> + <div class="mt-3 mb-3 flex-column w-100"> + <div class="custom-file"> + <input + accept=".csv" + type="file" + @change="fileSelected" + class="custom-file-input" + id="uploadFile" + /> + <label class="pointer custom-file-label" for="uploadFile"> + {{ uploadLabel }} + </label> + </div> + </div> + </div> + <div class="d-flex justify-content-between w-100 p-2 border-top"> + <button :key="1" @click="back()" class="btn btn-sm btn-warning"> + Back + </button> + <button + :key="2" + type="submit" + @click="submit" + class="btn btn-sm btn-info submit-button" + > + <translate>Submit</translate> + </button> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { displayError, displayInfo } from "@/lib/errors"; +import { HTTP } from "@/lib/http"; + +export default { + data() { + return { + url: "https://service.d4d-portal.info/wamos/wfs/", + sortBy: "hydro_scamin", + featureType: "ws-wamos:ienc_wtwprf", + disableUploadButton: false, + uploadLabel: this.$gettext("choose file to upload"), + uploadFile: null, + eMailNotification: false + }; + }, + computed: { + importWaterwayProfilesLabel() { + return this.$gettext("Import Waterway Profiles"); + } + }, + methods: { + back() { + this.url = "https://service.d4d-portal.info/wamos/wfs/"; + this.uploadFile = null; + this.uploadLabel = this.$gettext("choose file to upload"); + this.$store.commit("importschedule/setListMode"); + }, + fileSelected(e) { + const files = e.target.files || e.dataTransfer.files; + if (!files) return; + this.uploadLabel = files[0].name; + this.uploadFile = files[0]; + }, + submit() { + if (!this.url || !this.featureType || !this.sortBy || !this.uploadFile) + return; + let formData = new FormData(); + formData.append("wp", this.uploadFile); + formData.append("url", this.url); + formData.append("feature-type", this.featureType); + formData.append("sort-by", this.sortBy); + if (this.eMailNotification) { + formData.append("send-email", this.eMailNotification); + } + HTTP.post("/imports/wp", formData, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-Type": "multipart/form-data" + } + }) + .then(() => { + displayInfo({ + title: this.$gettext("Import"), + message: + this.uploadLabel + this.$gettext(" was successfully uploaded.") + }); + this.back(); + }) + .catch(error => { + const { status, data } = error.response; + const messages = data.messages ? data.messages.join(", ") : ""; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${messages}` + }); + }); + } + }, + on: "on", + off: "off" +}; +</script> + +<style lang="scss" scoped></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/Waterwayarea.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,99 @@ +<template> + <div> + <div class="d-flex px-2"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div class="w-100"> + <input + @input="urlChanged" + class="url form-control form-control-sm" + type="url" + :value="url" + /> + </div> + </div> + </div> + <div v-if="!url" class="d-flex px-2"> + <small + ><translate class="text-danger">Please enter a URL</translate></small + > + </div> + <div class="d-flex px-2"> + <div class="flex-column mt-2 mr-3 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>Featuretype</translate> </small> + </div> + <div class="w-100"> + <input + @input="featureTypeChanged" + class="featuretype form-control form-control-sm" + type="text" + :value="featureType" + /> + </div> + <div v-if="!featureType" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a Featuretype</translate + ></small + > + </div> + </div> + <div class="flex-column mt-2 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>SortBy</translate> </small> + </div> + <div class="w-100"> + <input + @input="sortByChanged" + class="sortby form-control form-control-sm" + type="text" + :value="sortBy" + /> + </div> + <div v-if="!sortBy" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter SortBy</translate + ></small + > + </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> + */ +export default { + name: "waterwayarea", + props: ["url", "featureType", "sortBy"], + methods: { + urlChanged(e) { + this.$emit("urlChanged", e.target.value); + }, + featureTypeChanged(e) { + this.$emit("featureTypeChanged", e.target.value); + }, + sortByChanged(e) { + this.$emit("sortByChanged", e.target.value); + } + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/Waterwayaxis.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,99 @@ +<template> + <div> + <div class="d-flex px-2"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div class="w-100"> + <input + @input="urlChanged" + class="url form-control form-control-sm" + type="url" + :value="url" + /> + </div> + </div> + </div> + <div v-if="!url" class="d-flex px-2"> + <small + ><translate class="text-danger">Please enter a URL</translate></small + > + </div> + <div class="d-flex px-2"> + <div class="flex-column mt-2 mr-3 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>Featuretype</translate> </small> + </div> + <div class="w-100"> + <input + @input="featureTypeChanged" + class="featuretype form-control form-control-sm" + type="text" + :value="featureType" + /> + </div> + <div v-if="!featureType" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a Featuretype</translate + ></small + > + </div> + </div> + <div class="flex-column mt-2 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>SortBy</translate> </small> + </div> + <div class="w-100"> + <input + @input="sortByChanged" + class="sortby form-control form-control-sm" + type="text" + :value="sortBy" + /> + </div> + <div v-if="!sortBy" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter SortBy</translate + ></small + > + </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> + */ +export default { + name: "waterwayaxis", + props: ["url", "featureType", "sortBy"], + methods: { + urlChanged(e) { + this.$emit("urlChanged", e.target.value); + }, + featureTypeChanged(e) { + this.$emit("featureTypeChanged", e.target.value); + }, + sortByChanged(e) { + this.$emit("sortByChanged", e.target.value); + } + } +}; +</script> + +<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importconfiguration/types/Waterwaygauges.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,126 @@ +<template> + <div> + <div class="d-flex px-2"> + <div class="flex-column w-100"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>URL</translate> </small> + </div> + <div class="w-100"> + <input + @input="urlChanged" + class="url form-control form-control-sm" + type="url" + :value="url" + /> + </div> + </div> + </div> + <div v-if="!url" class="d-flex px-2"> + <small + ><translate class="text-danger">Please enter a URL</translate></small + > + </div> + <div class="d-flex px-2"> + <div class="flex-column mt-2 mr-3 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>Username</translate> </small> + </div> + <div class="w-100"> + <input + @input="usernameChanged" + class="username form-control form-control-sm" + type="text" + :value="username" + /> + </div> + <div v-if="!username" class="d-flex flex-row"> + <small + ><translate class="text-danger" + >Please enter a Username</translate + ></small + > + </div> + </div> + <div class="flex-column mt-2 w-50"> + <div class="flex-row text-left"> + <small class="text-muted"> <translate>Password</translate> </small> + </div> + <div class="w-100 d-flex flex-row"> + <input + @input="passwordChanged" + class="password form-control form-control-sm" + :type="showPassword" + :value="password" + /> + <span + class="input-group-text ml-2" + @click="passwordVisible = !passwordVisible" + > + <font-awesome-icon :icon="passwordVisible ? 'eye-slash' : 'eye'" /> + </span> + </div> + <div + v-if="!password && !this.currentSchedule.id" + class="d-flex flex-row" + > + <small + ><translate class="text-danger" + >Please enter a Password</translate + ></small + > + </div> + </div> + </div> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ + +import { mapState } from "vuex"; + +export default { + name: "waterwaygauges", + props: ["username", "password", "url"], + data() { + return { + passwordVisible: false + }; + }, + computed: { + ...mapState("importschedule", [ + "importScheduleDetailVisible", + "currentSchedule" + ]), + showPassword() { + if (this.passwordVisible) return "text"; + return "password"; + } + }, + methods: { + urlChanged(e) { + this.$emit("urlChanged", e.target.value); + }, + usernameChanged(e) { + this.$emit("usernameChanged", e.target.value); + }, + passwordChanged(e) { + this.$emit("passwordChanged", e.target.value); + } + } +}; +</script> + +<style></style>
--- a/client/src/components/importoverview/AdditionalDetail.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -<template> - <div> - <FairwayDimensionDetail - :entry="entry" - :details="details" - v-if="isFairwayDimension" - ></FairwayDimensionDetail> - <ApprovedGaugeMeasurementDetail - :entry="entry" - :details="details" - v-if="isApprovedGaugeMeasurement" - ></ApprovedGaugeMeasurementDetail> - <BottleneckDetail - :details="details" - :entry="entry" - v-if="isBottleneck" - ></BottleneckDetail> - </div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -import { mapState } from "vuex"; - -export default { - name: "additionaldetail", - props: ["entry"], - components: { - BottleneckDetail: () => import("./BottleneckDetail.vue"), - ApprovedGaugeMeasurementDetail: () => - import("./ApprovedGaugeMeasurementDetail.vue"), - FairwayDimensionDetail: () => import("./FairwayDimension.vue") - }, - computed: { - ...mapState("imports", ["showLogs", "details"]), - kind() { - return this.entry.kind.toUpperCase(); - }, - isFairwayDimension() { - return this.kind === "FD"; - }, - isApprovedGaugeMeasurement() { - return this.kind === "AGM"; - }, - isBottleneck() { - return this.kind === "BN" || this.kind === "UBN"; - } - } -}; -</script> - -<style lang="scss" scoped></style>
--- a/client/src/components/importoverview/AdditionalLog.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/importoverview/AdditionalLog.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,18 +1,12 @@ <template> <div :class="[ - 'additionallog', - 'd-flex', - 'flex-column', - 'text-left', - { - full: showAdditional === $options.NODETAILS, - split: showAdditional !== $options.NODETAILS - } + 'additionallog d-flex flex-column text-left', + { split: showAdditional } ]" > <div - class="d-flex flex-row" + class="d-flex flex-row px-2 border-top" v-for="(line, index) in details.entries" :key="index" > @@ -36,7 +30,7 @@ 'font-weight-bold': /warn|error/.test(line.kind) } ]" - >{{ line.time }}</span + >{{ line.time | dateTime }}</span > <span :class="[ @@ -53,6 +47,30 @@ </div> </template> +<style lang="sass" scoped> +.additionallog + overflow-y: auto + &.split + max-height: 35vh + + > div + &:not(:first-child) + border-top-style: dashed !important + + &:hover + background-color: #fcfcfc + + .kind + width: 9% + + .time + width: 26% + + .message + width: 65% + word-wrap: break-word +</style> + <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -73,32 +91,6 @@ name: "additionallogs", computed: { ...mapState("imports", ["showAdditional", "details"]) - }, - NODETAILS: -1 + } }; </script> - -<style lang="scss" scoped> -.additionallog { - overflow-y: auto; -} - -.split { - max-height: 35vh; -} - -.full { - max-height: 70vh; -} - -.kind { - width: 9%; -} -.time { - width: 26%; -} -.message { - width: 65%; - word-wrap: break-word; -} -</style>
--- a/client/src/components/importoverview/ApprovedGaugeMeasurementDetail.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/importoverview/ApprovedGaugeMeasurementDetail.vue Mon Jun 03 10:19:18 2019 +0200 @@ -2,89 +2,58 @@ <div :class="{ diffs: true, - full: showLogs === $options.NODETAILS, - split: showLogs !== $options.NODETAILS + full: !showLogs, + split: showLogs }" > <div v-for="(result, index) in details.summary" :key="index"> - <div class="pl-2 d-flex flex-row"> + <div class="px-2 d-flex justify-content-between"> + <div class="d-flex"> + <div @click="toggleDiff(index)" class="my-auto text-left"> + <UISpinnerButton + :state="showDiff === index" + :icons="['angle-right', 'angle-down']" + classes="text-info" + /> + </div> + <div> + {{ result["fk-gauge-id"] }} + <sup v-if="isNew(result)" class="text-success"> + (<translate>New</translate>) + </sup> + </div> + </div> + <div>{{ result["measure-date"] | dateTime }}</div> + </div> + <div v-if="showDiff === index" class="compare-table"> + <div class="row no-gutters px-4 text-left font-weight-bold"> + <div :class="isNew(result) ? 'col-6' : 'col-4'"> + <translate>Value</translate> + </div> + <div v-if="isOld(result)" class="col-4"> + <translate>Old</translate> + </div> + <div :class="isNew(result) ? 'col-6' : 'col-4'"> + <translate>New</translate> + </div> + </div> <div - @click="toggleDiff(index)" - class="small mt-auto mb-auto text-info text-left" - > - <font-awesome-icon - class="pointer" - v-if="showDiff == index" - icon="angle-down" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="showDiff != index" - icon="angle-right" - fixed-width - ></font-awesome-icon> - </div> - <span v-if="result.versions.length == 1" class="agmcode text-left" - ><div> - {{ result["fk-gauge-id"] }} <translate>( New )</translate> - </div></span + class="row no-gutters px-4 text-left" + v-for="(entry, index) in Object.keys(result.versions[0])" + :key="index" + v-if="isNew(result) || isDifferent(result, entry)" > - <span v-if="result.versions.length == 2" class="agmcode text-left" - ><div>{{ result["fk-gauge-id"] }}</div></span - > - <span class="text-left" - ><div>{{ result["measure-date"] | dateTime }}</div></span - > - </div> - <div v-if="showDiff == index" class="pl-3 d-flex flex-row"> - <div class="w-100"> - <div class="d-flex flex-row pl-3 text-left"> - <div class="header border-bottom agmdetailskeys"> - <small><translate>Value</translate></small> - </div> - <div - v-if="result.versions.length == 2" - class="header border-bottom agmdetailsvalues" - > - <small><translate>Old</translate></small> - </div> - <div class="header border-bottom agmdetailsvalues"> - <small><translate>New</translate></small> - </div> + <div :class="isNew(result) ? 'col-6' : 'col-4'"> + {{ entry }} + </div> + <div :class="isNew(result) ? 'col-6' : 'col-4'"> + {{ result.versions[0][entry] }} </div> <div - class="d-flex flex-row pl-3 text-left" - v-for="(entry, index) in Object.keys(result.versions[0])" - :key="index" + v-if="isOld(result) && isDifferent(result, entry)" + :class="isNew(result) ? 'col-6' : 'col-4'" > - <div - v-if=" - result.versions.length == 1 || - result.versions[0][entry] != result.versions[1][entry] - " - class="agmdetailskeys" - > - <small>{{ entry }}</small> - </div> - <div - v-if=" - result.versions.length == 1 || - result.versions[0][entry] != result.versions[1][entry] - " - class="agmdetailsvalues" - > - <small>{{ result.versions[0][entry] }}</small> - </div> - <div - v-if=" - result.versions.length == 2 && - result.versions[0][entry] != result.versions[1][entry] - " - class="agmdetailsvalues" - > - <small>{{ result.versions[1][entry] }}</small> - </div> + {{ result.versions[1][entry] }} </div> </div> </div> @@ -92,6 +61,37 @@ </div> </template> +<style lang="sass" scoped> +.diffs + width: 100% + overflow-y: auto + > div + border-top: dashed 1px #dee2e6 + &:first-child + border-top: none + .compare-table + position: relative + overflow: hidden + &::after + content: '' + position: absolute + top: 0 + right: -5px + bottom: 0 + left: -5px + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.4) + > div + font-size: 0.7rem + &:nth-child(odd) + background-color: #f8f9fa + +.split + max-height: 35vh + +.full + max-height: 70vh +</style> + <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -108,14 +108,10 @@ */ import { mapState } from "vuex"; -const NODIFF = -1; - export default { - name: "agmdetails", - props: ["entry"], data() { return { - showDiff: NODIFF + showDiff: 0 // open first item by default }; }, computed: { @@ -123,41 +119,24 @@ }, methods: { toggleDiff(number) { - if (this.showDiff !== number || this.showDiff == NODIFF) { + if (this.showDiff !== number) { this.showDiff = number; } else { - this.showDiff = NODIFF; + this.showDiff = false; } + }, + isNew(result) { + return result && result.versions && result.versions.length === 1; + }, + isOld(result) { + return !this.isNew(result); + }, + isDifferent(result, entry) { + return ( + this.isOld(result) && + result.versions[0][entry] != result.versions[1][entry] + ); } - }, - NODETAILS: -1 + } }; </script> - -<style lang="scss" scoped> -.diffs { - width: 615px; - max-height: 20vh; - overflow-y: auto; -} - -.agmcode { - width: 35%; -} - -.agmdetailskeys { - width: 33%; -} - -.agmdetailsvalues { - width: 33%; -} - -.split { - max-height: 35vh; -} - -.full { - max-height: 70vh; -} -</style>
--- a/client/src/components/importoverview/BottleneckDetail.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/importoverview/BottleneckDetail.vue Mon Jun 03 10:19:18 2019 +0200 @@ -2,58 +2,72 @@ <div :class="{ bottleneckdetails: true, - full: showLogs === $options.NODETAILS, - split: showLogs !== $options.NODETAILS + full: !showLogs, + split: showLogs }" > - <div - v-for="(bottleneck, index) in bottlenecks" - :key="index" - class="d-flex flex-row" - > - <div class="d-flex flex-column"> - <div class="d-flex flex-row"> - <div - @click="showBottleneckDetails(index)" - class="mt-auto mb-auto text-info text-left" + <div v-for="(bottleneck, index) in bottlenecks" :key="index"> + <div class="d-flex pl-2"> + <div + @click="showBottleneckDetails(index)" + class="mt-auto mb-auto text-info text-left" + > + <UISpinnerButton + :state="showBottleneckDetail === index" + :icons="['angle-right', 'angle-down']" + class="text-info" + /> + </div> + <a @click="moveToBottleneck(index)" href="#"> + {{ bottleneck.properties.objnam }} + </a> + </div> + + <div class="d-flex properties" v-if="showBottleneckDetail === index"> + <table class="w-100"> + <tr + v-for="(info, index) in Object.keys(bottleneck.properties)" + :key="index" > - <font-awesome-icon - class="pointer" - v-if="showBottleneckDetail === index" - icon="angle-down" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - v-if="!(showBottleneckDetail === index)" - icon="angle-right" - fixed-width - ></font-awesome-icon> - </div> - <a @click="moveToBottleneck(index)" class="" href="#">{{ - bottleneck.properties.objnam - }}</a> - </div> - - <div class="ml-3 d-flex flex-row" v-if="showBottleneckDetail === index"> - <table> - <tr - v-for="(info, index) in Object.keys(bottleneck.properties)" - :key="index" - class="mr-1 condensed text-muted" - > - <td class="text-left">{{ info }}</td> - <td class="pl-3 text-left"> - {{ bottleneck.properties[info] }} - </td> - </tr> - </table> - </div> + <td class="pl-4">{{ info }}</td> + <td> + {{ bottleneck.properties[info] }} + </td> + </tr> + </table> </div> </div> </div> </template> +<style lang="sass" scoped> +.bottleneckdetails + width: 100% + overflow-y: auto + > div + border-top: dashed 1px #dee2e6 + &:first-child + border-top: none + .properties + position: relative + overflow: hidden + &::after + content: '' + position: absolute + top: 0 + right: -5px + bottom: 0 + left: -5px + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.4) + tr + font-size: 0.7rem + &:nth-child(odd) + background-color: #f8f9fa + +.split + max-height: 35vh +</style> + <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -69,29 +83,25 @@ * Thomas Junk <thomas.junk@intevation.de> */ -import { LAYERS } from "@/store/map.js"; import { HTTP } from "@/lib/http"; -import { WFS } from "ol/format.js"; -import { or as orFilter, equalTo as equalToFilter } from "ol/format/filter.js"; -import { displayError } from "@/lib/errors.js"; -import { mapState } from "vuex"; - -const NO_BOTTLENECK = -1; +import { WFS } from "ol/format"; +import { or as orFilter, equalTo as equalToFilter } from "ol/format/filter"; +import { displayError } from "@/lib/errors"; +import { mapState, mapGetters } from "vuex"; export default { - name: "bottleneckdetails", - props: ["entry"], data() { return { bottlenecks: [], - showBottleneckDetail: NO_BOTTLENECK + showBottleneckDetail: null }; }, mounted() { this.loadBottlenecks(); }, computed: { - ...mapState("imports", ["showLogs", "details"]) + ...mapState("imports", ["showLogs", "details"]), + ...mapGetters("map", ["openLayersMap"]) }, methods: { loadBottlenecks() { @@ -137,39 +147,22 @@ }); }, moveToBottleneck(index) { - this.$store.commit("map/setLayerVisible", LAYERS.BOTTLENECKS); - this.moveToExtent(this.bottlenecks[index]); - }, - moveToExtent(feature) { - this.$store.commit("map/moveToExtent", { - feature: feature, + this.openLayersMap() + .getLayer("BOTTLENECKS") + .setVisible(true); + this.$store.dispatch("map/moveToFeauture", { + feature: this.bottlenecks[index], zoom: 17, preventZoomOut: true }); }, showBottleneckDetails(index) { - if (index == this.showBottleneckDetail) { - this.showBottleneckDetail = NO_BOTTLENECK; - return; + if (index === this.showBottleneckDetail) { + this.showBottleneckDetail = null; + } else { + this.showBottleneckDetail = index; } - this.showBottleneckDetail = index; } - }, - NODETAILS: -1 + } }; </script> - -<style lang="scss" scoped> -.bottleneckdetails { - width: 615px; - overflow-y: auto; -} - -.split { - max-height: 35vh; -} - -.full { - max-height: 70vh; -} -</style>
--- a/client/src/components/importoverview/FairwayDimension.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,24 +0,0 @@ -<template> - <div>Fairwaydimension</div> -</template> - -<script> -/* This is Free Software under GNU Affero General Public License v >= 3.0 - * without warranty, see README.md and license for details. - * - * SPDX-License-Identifier: AGPL-3.0-or-later - * License-Filename: LICENSES/AGPL-3.0.txt - * - * Copyright (C) 2018 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - */ -export default { - name: "fairwaydimensiondetails" -}; -</script> - -<style></style>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importoverview/FairwayDimensionDetail.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,20 @@ +<template> + <div>Fairwaydimension</div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + */ +export default {}; +</script>
--- a/client/src/components/importoverview/Filters.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/importoverview/Filters.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,18 +1,33 @@ <template> <div> - <button @click="setFilter('pending')" :class="pendingStyle"> + <button + @click="setFilter('pending')" + :class="'mr-1 btn btn-xs btn-' + (this.pending ? 'secondary' : 'light')" + > <translate>pending</translate> </button> - <button @click="setFilter('failed')" :class="failedStyle"> + <button + @click="setFilter('failed')" + :class="'mr-1 btn btn-xs btn-' + (this.failed ? 'secondary' : 'light')" + > <translate>failed</translate> </button> - <button @click="setFilter('accepted')" :class="acceptedStyle"> + <button + @click="setFilter('accepted')" + :class="'mr-1 btn btn-xs btn-' + (this.accepted ? 'secondary' : 'light')" + > <translate>accepted</translate> </button> - <button @click="setFilter('declined')" :class="declinedStyle"> + <button + @click="setFilter('declined')" + :class="'mr-1 btn btn-xs btn-' + (this.declined ? 'secondary' : 'light')" + > <translate>declined</translate> </button> - <button @click="setFilter('warning')" :class="warningStyle"> + <button + @click="setFilter('warning')" + :class="'btn btn-xs btn-' + (this.warning ? 'secondary' : 'light')" + > <translate>warning</translate> </button> </div> @@ -49,53 +64,7 @@ "accepted", "warning", "declined" - ]), - pendingStyle() { - return { - btn: true, - "btn-sm": true, - "btn-light": !this.pending, - "btn-secondary": this.pending - }; - }, - failedStyle() { - return { - "ml-2": true, - btn: true, - "btn-sm": true, - "btn-light": !this.failed, - "btn-secondary": this.failed - }; - }, - declinedStyle() { - return { - "ml-2": true, - btn: true, - "btn-sm": true, - "btn-light": !this.declined, - "btn-secondary": this.declined - }; - }, - acceptedStyle() { - return { - "ml-2": true, - btn: true, - "btn-sm": true, - "btn-light": !this.accepted, - "btn-secondary": this.accepted - }; - }, - warningStyle() { - return { - "ml-2": true, - btn: true, - "btn-sm": true, - "btn-light": !this.warning, - "btn-secondary": this.warning - }; - } + ]) } }; </script> - -<style lang="scss" scoped></style>
--- a/client/src/components/importoverview/ImportOverview.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/importoverview/ImportOverview.vue Mon Jun 03 10:19:18 2019 +0200 @@ -4,90 +4,82 @@ icon="clipboard-check" :title="importReviewLabel" :closeCallback="$parent.close" - :actions="[{ callback: loadLogs, icon: 'redo' }]" + :actions="[{ callback: loadUpdatedLogs, icon: 'sync' }]" /> <div class="position-relative"> - <transition name="fade"> - <div class="loading" v-if="loading"> - <font-awesome-icon icon="spinner" spin /> - </div> - </transition> - <div class="p-2 mb-1 d-flex flex-row flex-fill justify-content-between"> + <UISpinnerOverlay v-if="loading" /> + <div class="border-bottom p-2 d-flex justify-content-between"> <Filters></Filters> - <div> - <button - class="btn btn-sm btn-info" - :disabled="!reviewed.length" - @click="save" - > - <translate>Commit</translate> {{ reviewed.length }} - </button> - </div> + <button + class="btn btn-xs btn-info" + :disabled="!reviewed.length" + @click="save" + > + <translate>Commit</translate> {{ reviewed.length }} + </button> </div> <div - class="ml-2 mr-2 mb-2 datefilter d-flex flex-row justify-content-between" + class="p-2 d-flex align-items-center justify-content-between border-bottom" > - <div class="mr-3 my-auto pointer"> - <button - :disabled="!this.prev" - @click="earlier" - class="btn btn-sm btn-outline-light text-dark" + <button + :disabled="!this.prev" + @click="earlier" + class="btn btn-xs btn-outline-secondary" + > + <font-awesome-icon icon="angle-left" fixed-width /> + <translate>Earlier</translate> + </button> + <div class="d-flex align-items-center small"> + {{ interval[0] | dateTime(selectedInterval !== $options.LAST_HOUR) }} + <template v-if="selectedInterval !== $options.TODAY"> + <span class="mx-2">–</span> + {{ + interval[1] | dateTime(selectedInterval !== $options.LAST_HOUR) + }} + </template> + <select + style="width: 75px; height: 24px" + class="form-control form-control-sm small ml-2" + v-model="selectedInterval" > - <translate>Earlier</translate> - <font-awesome-icon class="ml-2" icon="angle-left" /> - </button> + <option :value="$options.LAST_HOUR"> + <translate>Hour</translate> + </option> + <option :value="$options.TODAY"><translate>Day</translate></option> + <option :value="$options.LAST_7_DAYS"> + <translate>7 days</translate> + </option> + <option :value="$options.LAST_30_DAYS"> + <translate>30 Days</translate> + </option> + </select> </div> - <div class="selected-interval my-auto"> - <span class="date">{{ interval[0] | dateTime }}</span> - <span class="ml-3 mr-3">-</span> - <span class="date">{{ interval[1] | dateTime }}</span> - </div> - <div class="ml-3 my-auto pointer"> + <div class="btn-group"> <button :disabled="!this.next" @click="later" - class="btn btn-sm btn-outline-light text-dark" - > - <font-awesome-icon class="mr-2" icon="angle-right" /><translate - >Later</translate - > - </button> - </div> - <div class="d-flex flex-row"> - <select - class="my-auto btn btn-outline-light text-dark form-control interval-select" - v-model="selectedInterval" + class="btn btn-xs btn-outline-secondary" > - <option - :selected="selectedInterval === $options.LAST_HOUR" - :value="$options.LAST_HOUR" - ><translate>Hour</translate></option - > - <option - :selected="selectedInterval === $options.TODAY" - :value="$options.TODAY" - ><translate>Day</translate></option - > - <option - :selected="selectedInterval === $options.LAST_7_DAYS" - :value="$options.LAST_7_DAYS" - ><translate>7 days</translate></option - > - <option - :selected="selectedInterval === $options.LAST_30_DAYS" - :value="$options.LAST_30_DAYS" - ><translate>30 Days</translate></option - > - </select> + <translate>Later</translate> + <font-awesome-icon icon="angle-right" fixed-width /> + </button> + <button + :disabled="!this.next" + @click="now" + class="btn btn-xs btn-outline-secondary" + > + <font-awesome-icon icon="angle-double-right" fixed-width /> + </button> </div> </div> <UITableHeader :columns="[ - { id: 'id', title: `${idLabel}`, width: '79px' }, + { id: 'id', title: `${idLabel}`, width: '75px' }, { id: 'kind', title: `${kindLabel}`, width: '53px' }, { id: 'enqueued', title: `${enqueuedLabel}`, width: '138px' }, - { id: 'user', title: `${userLabel}`, width: '105px' }, - { id: 'signer', title: `${signerLabel}`, width: '105px' }, + { id: 'user', title: `${ownerLabel}`, width: '80px' }, + { id: 'country', title: `${countryLabel}`, width: '55px' }, + { id: 'signer', title: `${signerLabel}`, width: '80px' }, { id: 'state', title: `${statusLabel}`, width: '72px' }, { id: 'warnings', icon: 'exclamation-triangle', width: '44px' } ]" @@ -99,15 +91,25 @@ --> <UITableBody :data="filteredImports() | sortTable(sortColumn, sortDirection)" - maxHeight="80vh" - v-slot="{ item: entry }" + :isActive="item => item.id === this.show" + maxHeight="70vh" > - <LogEntry :entry="entry"></LogEntry> + <template v-slot:row="{ item: entry }"> + <LogEntry :entry="entry"></LogEntry> + </template> + <template v-slot:expand="{ item: entry }"> + <LogDetail :entry="entry"></LogDetail> + </template> </UITableBody> </div> </div> </template> +<style lang="sass" scoped> +.spinner-overlay + top: 110px +</style> + <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -128,8 +130,7 @@ import { displayError, displayInfo } from "@/lib/errors"; import { STATES } from "@/store/imports"; import { sortTable } from "@/lib/mixins"; -import { HTTP } from "@/lib/http.js"; - +import { HTTP } from "@/lib/http"; import { startOfDay, startOfHour, @@ -141,22 +142,22 @@ } from "date-fns"; export default { - name: "importoverview", + components: { + Filters: () => import("./Filters"), + LogEntry: () => import("./LogEntry"), + LogDetail: () => import("./LogDetail") + }, mixins: [sortTable], - components: { - Filters: () => import("./Filters.vue"), - LogEntry: () => import("./LogEntry.vue") - }, + LAST_HOUR: "lasthour", + TODAY: "today", + LAST_7_DAYS: "lastsevendays", + LAST_30_DAYS: "lastthirtydays", data() { return { loading: false, selectedInterval: this.$options.LAST_HOUR }; }, - LAST_HOUR: "lasthour", - TODAY: "today", - LAST_7_DAYS: "lastsevendays", - LAST_30_DAYS: "lastthirtydays", computed: { ...mapState("application", ["searchQuery"]), ...mapState("imports", [ @@ -169,6 +170,9 @@ "next" ]), ...mapGetters("imports", ["filters"]), + countryLabel() { + return this.$gettext("Country"); + }, importReviewLabel() { return this.$gettext("Import review"); }, @@ -181,8 +185,8 @@ enqueuedLabel() { return this.$gettext("Enqueued"); }, - userLabel() { - return this.$gettext("User"); + ownerLabel() { + return this.$gettext("Owner"); }, signerLabel() { return this.$gettext("Signer"); @@ -194,7 +198,68 @@ return [this.startDate, this.endDate]; } }, + watch: { + $route() { + const { id } = this.$route.params; + if (id) this.showSingleRessource(id); + }, + selectedInterval() { + this.loadUpdatedLogs(); + }, + imports() { + if (this.imports.length == 0) { + if (this.next) { + const [start, end] = this.determineInterval(this.next); + this.$store.commit("imports/setStartDate", start); + this.$store.commit("imports/setEndDate", end); + this.loadLogs(); + } else if (this.prev) { + const [start, end] = this.determineInterval(this.prev); + this.$store.commit("imports/setStartDate", start); + this.$store.commit("imports/setEndDate", end); + this.loadLogs(); + } + } + }, + filters() { + this.loadLogs(); + } + }, methods: { + showSingleRessource(id) { + id = id * 1; + this.loadDetails(id) + .then(response => { + this.$store.commit("imports/setCurrentDetails", response.data); + const { enqueued } = response.data; + this.$store.commit("imports/setStartDate", startOfHour(enqueued)); + this.$store.commit("imports/setEndDate", endOfHour(enqueued)); + this.$store.commit("imports/showDetailsFor", id); + this.loadLogs(); + }) + .catch(error => { + this.loading = false; + this.$store.commit("imports/setCurrentDetails", {}); + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + loadDetails(id) { + return new Promise((resolve, reject) => { + HTTP.get("/imports/" + id, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + }, determineInterval(pointInTime) { let start, end; switch (this.selectedInterval) { @@ -208,11 +273,11 @@ break; case this.$options.LAST_7_DAYS: start = startOfDay(pointInTime); - end = endOfDay(addDays(7, start)); + end = endOfDay(addDays(start, 7)); break; case this.$options.LAST_30_DAYS: start = startOfDay(pointInTime); - end = endOfDay(addDays(30, start)); + end = endOfDay(addDays(start, 30)); break; } return [start, end]; @@ -231,9 +296,44 @@ this.$store.commit("imports/setEndDate", end); this.loadLogs(); }, + now() { + if (!this.next) return; + const [start, end] = this.determineInterval(new Date()); + this.$store.commit("imports/setStartDate", start); + this.$store.commit("imports/setEndDate", end); + this.loadLogs(); + }, filteredImports() { return this.imports; }, + loadUpdatedLogs() { + const now = new Date(); + switch (this.selectedInterval) { + case this.$options.LAST_HOUR: + this.$store.commit("imports/setStartDate", startOfHour(now)); + this.$store.commit("imports/setEndDate", now); + break; + case this.$options.TODAY: + this.$store.commit("imports/setStartDate", startOfDay(now)); + this.$store.commit("imports/setEndDate", now); + break; + case this.$options.LAST_7_DAYS: + this.$store.commit( + "imports/setStartDate", + subDays(startOfDay(now), 7) + ); + this.$store.commit("imports/setEndDate", now); + break; + case this.$options.LAST_30_DAYS: + this.$store.commit( + "imports/setStartDate", + subDays(startOfDay(now), 30) + ); + this.$store.commit("imports/setEndDate", now); + break; + } + this.loadLogs(); + }, loadLogs() { this.loading = true; this.$store @@ -244,15 +344,15 @@ query: this.searchQuery }) .then(() => { - if (this.show != -1) { - HTTP.get("/imports/" + this.show, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) + if (this.show) { + this.loadDetails(this.show) .then(response => { this.$store.commit("imports/setCurrentDetails", response.data); this.loading = false; }) .catch(error => { + this.loading = false; + this.$store.commit("imports/setCurrentDetails", {}); const { status, data } = error.response; displayError({ title: this.$gettext("Backend Error"), @@ -265,6 +365,7 @@ }) .catch(error => { const { status, data } = error.response; + this.loading = false; displayError({ title: this.$gettext("Backend Error"), message: `${status}: ${data.message || data}` @@ -307,6 +408,7 @@ .then(response => { this.loadLogs(); this.$store.commit("imports/setReviewed", []); + this.$store.dispatch("map/refreshLayers"); const messages = response.data .map(x => { if (x.message) return x.message; @@ -338,68 +440,21 @@ }); } }, - watch: { - selectedInterval() { - const now = new Date(); - switch (this.selectedInterval) { - case this.$options.LAST_HOUR: - this.$store.commit("imports/setStartDate", startOfHour(now)); - this.$store.commit("imports/setEndDate", now); - break; - case this.$options.TODAY: - this.$store.commit("imports/setStartDate", startOfDay(now)); - this.$store.commit("imports/setEndDate", now); - break; - case this.$options.LAST_7_DAYS: - this.$store.commit( - "imports/setStartDate", - subDays(startOfDay(now), 7) - ); - this.$store.commit("imports/setEndDate", now); - break; - case this.$options.LAST_30_DAYS: - this.$store.commit( - "imports/setStartDate", - subDays(startOfDay(now), 30) - ); - this.$store.commit("imports/setEndDate", now); - break; - } - this.loadLogs(); - }, - imports() { - if (this.imports.length == 0) { - if (this.next) { - const [start, end] = this.determineInterval(this.next); - this.$store.commit("imports/setStartDate", start); - this.$store.commit("imports/setEndDate", end); - this.loadLogs(); - } else if (this.prev) { - const [start, end] = this.determineInterval(this.prev); - this.$store.commit("imports/setStartDate", start); - this.$store.commit("imports/setEndDate", end); - this.loadLogs(); - } - } - }, - filters() { + mounted() { + this.$store.dispatch("usermanagement/loadUsers").catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + const { id } = this.$route.params; + if (id) { + this.showSingleRessource(id); + } else { + this.$store.commit("application/searchQuery", ""); this.loadLogs(); } - }, - mounted() { - this.$store.commit("application/searchQuery", ""); - this.loadLogs(); } }; </script> - -<style lang="scss" scoped> -.date { - font-stretch: condensed; -} -.interval-select { - padding: 0px; - margin: 0px; - font-size: 80%; -} -</style>
--- a/client/src/components/importoverview/LogDetail.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/importoverview/LogDetail.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,78 +1,54 @@ <template> - <div class="border-top"> + <div> <div - class="d-flex fex-row" + class="d-flex border-bottom" style="padding-left: 3px;" - v-if="hasAdditionalInfo || isStretch || isSoundingResult" + v-if="hasAdditionalInfo || isST || isSEC || isSR" > <div v-if="hasAdditionalInfo"> - <font-awesome-icon - v-if="entry.id === showAdditional" - @click="toggleAdditionalInfo" - class="my-auto mr-1 text-info pointer" - icon="angle-down" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - v-if="entry.id !== showAdditional" + <UISpinnerButton @click="toggleAdditionalInfo" - class="my-auto mr-1 text-info pointer" - icon="angle-right" - fixed-width - ></font-awesome-icon> + :state="entry.id === showAdditional" + :icons="['angle-right', 'angle-down']" + class="text-info d-inline-block" + /> <span class="text-info"><translate>Additional Info</translate></span> + <span class="text-info" v-if="isAGM && details.summary"> + ({{ details.summary.length }}) + </span> <span - class="text-info" - v-if="isApprovedGaugeMeasurement && details.summary" - > - ({{ details.summary.length }})</span - > - <span - v-if="isBottleneck && details.summary && details.summary.bottlenecks" + v-if="isBN && details.summary && details.summary.bottlenecks" class="text-info text-left" > - ({{ details.summary.bottlenecks.length }})</span - > - <span class="text-left" v-if="isFairwayDimension" - >{{ details.summary["source-organization"] }} (LOS: - {{ details.summary.los }})</span - > + ({{ details.summary.bottlenecks.length }}) + </span> + <span class="text-left" v-if="isFD"> + {{ details.summary["source-organization"] }} + (LOS: {{ details.summary.los }}) + </span> </div> - <StretchDetail - v-if="isStretch && isPending" - :entry="entry" - ></StretchDetail> - <SoundingResultDetail - :entry="entry" - v-if="isSoundingResult && isPending" - ></SoundingResultDetail> + <SectionDetails v-if="isSEC && isPending" :entry="entry" /> + <StretchDetails v-if="isST && isPending" :entry="entry" /> + <SoundingResultDetail :entry="entry" v-if="isSR && isPending" /> </div> - <AdditionalDetail - v-if="entry.id === showAdditional && isPending" - class="ml-2 d-flex flex-row" - :entry="entry" - ></AdditionalDetail> - <div class="d-flex fex-row" style="padding-left: 3px;"> - <font-awesome-icon - v-if="entry.id === showLogs" + <div + v-if="entry.id === showAdditional && isPending && (isFD || isAGM || isBN)" + class="d-flex border-bottom" + > + <FairwayDimensionDetail v-if="isFD" /> + <ApprovedGaugeMeasurementDetail v-if="isAGM" /> + <BottleneckDetail :entry="entry" v-if="isBN" /> + </div> + <div class="d-flex" style="padding-left: 3px;"> + <UISpinnerButton @click="toggleAdditionalLogging" - class="my-auto mr-1 text-info pointer" - icon="angle-down" - fixed-width - ></font-awesome-icon> - <font-awesome-icon - v-if="entry.id !== showLogs" - @click="toggleAdditionalLogging" - class="my-auto mr-1 text-info pointer" - icon="angle-right" - fixed-width - ></font-awesome-icon> + :state="entry.id === showLogs" + :icons="['angle-right', 'angle-down']" + classes="text-info" + /> <span class="text-info"><translate>Logs</translate></span> </div> - <AdditionalLog - v-if="entry.id === showLogs" - class="mx-4 pb-1 d-flex flex-row" - ></AdditionalLog> + <AdditionalLog v-if="entry.id === showLogs" class="d-flex flex-row" /> </div> </template> @@ -92,36 +68,48 @@ */ import { mapState } from "vuex"; -import { displayError } from "@/lib/errors.js"; -import { HTTP } from "@/lib/http.js"; export default { - name: "logdetail", - props: ["entry"], components: { - SoundingResultDetail: () => import("./SoundingResultDetail.vue"), - StretchDetail: () => import("./StretchDetails.vue"), - AdditionalDetail: () => import("./AdditionalDetail.vue"), - AdditionalLog: () => import("./AdditionalLog.vue") + SoundingResultDetail: () => import("./SoundingResultDetail"), + StretchDetails: () => import("./StretchDetails"), + SectionDetails: () => import("./SectionDetails"), + FairwayDimensionDetail: () => import("./FairwayDimensionDetail"), + ApprovedGaugeMeasurementDetail: () => + import("./ApprovedGaugeMeasurementDetail"), + BottleneckDetail: () => import("./BottleneckDetail"), + AdditionalLog: () => import("./AdditionalLog") }, - mounted() { - HTTP.get("/imports/" + this.entry.id, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - this.$store.commit("imports/setCurrentDetails", response.data); - if (this.entry.state === "pending") { - this.$store.commit("imports/showAdditionalInfoFor", this.entry.id); - } - this.$store.commit("imports/showAdditionalLogsFor", this.entry.id); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); + props: ["entry"], + computed: { + ...mapState("imports", ["showAdditional", "showLogs", "details"]), + kind() { + return this.entry.kind.toUpperCase(); + }, + isPending() { + return this.entry.state == "pending"; + }, + hasAdditionalInfo() { + return this.isPending && (this.isAGM || this.isBN); + }, + isFD() { + return this.kind === "FD"; + }, + isAGM() { + return this.kind === "AGM"; + }, + isBN() { + return this.kind === "BN" || this.kind === "UBN"; + }, + isST() { + return this.kind === "ST"; + }, + isSEC() { + return this.kind === "SEC"; + }, + isSR() { + return this.kind === "SR"; + } }, methods: { toggleAdditionalInfo() { @@ -139,34 +127,11 @@ } } }, - computed: { - ...mapState("imports", ["showAdditional", "showLogs", "details"]), - kind() { - return this.entry.kind.toUpperCase(); - }, - isPending() { - return this.entry.state == "pending"; - }, - hasAdditionalInfo() { - return ( - this.isPending && (this.isApprovedGaugeMeasurement || this.isBottleneck) - ); - }, - isFairwayDimension() { - return this.kind === "FD"; - }, - isApprovedGaugeMeasurement() { - return this.kind === "AGM"; - }, - isBottleneck() { - return this.kind === "BN" || this.kind === "UBN"; - }, - isStretch() { - return this.kind === "ST"; - }, - isSoundingResult() { - return this.kind === "SR"; + mounted() { + if (this.entry.state === "pending") { + this.$store.commit("imports/showAdditionalInfoFor", this.entry.id); } + this.$store.commit("imports/showAdditionalLogsFor", this.entry.id); } }; </script>
--- a/client/src/components/importoverview/LogEntry.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/importoverview/LogEntry.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,104 +1,84 @@ <template> - <div class="logentry"> - <div class="row no-gutters text-left"> - <div - style="width: 79px;" - class="table-cell d-flex justify-content-between" + <div class="row w-100 no-gutters text-left"> + <div style="width: 75px;" class="table-cell d-flex justify-content-between"> + <UISpinnerButton + @click="toggleDetails" + :loading="loading" + :state="entry.id === show" + :icons="['angle-right', 'angle-down']" + /> + {{ entry.id }} + </div> + <div style="width: 53px;" class="table-cell center"> + {{ entry.kind.toUpperCase() }} + </div> + <div style="width: 138px;" class="table-cell center"> + {{ entry.enqueued | dateTime }} + </div> + <div style="width: 80px;" class="table-cell truncate"> + {{ entry.user }} + </div> + <div style="width: 55px;" class="table-cell center"> + {{ userCountries[entry.user] }} + </div> + <div style="width: 80px;" class="table-cell truncate"> + {{ entry.signer }} + </div> + <div style="width: 72px;" :class="stateStyle"> + {{ entry.state }} + </div> + <div style="width: 44px;" class="table-cell center"> + <font-awesome-icon + v-if="entry.warnings" + class="text-warning" + icon="exclamation-triangle" + fixed-width + /> + </div> + <div style="flex-grow: 1; padding: 0;" class="table-cell text-right"> + <button + :class="['action approved', { active: isApproved }]" + @click="toggleApproval($options.STATES.APPROVED)" + v-if="entry.state === 'pending'" > - <font-awesome-icon - @click="toggleDetails" - class="my-auto text-info pointer" - :icon="entry.id === show ? 'angle-down' : 'angle-right'" - fixed-width - ></font-awesome-icon> - {{ entry.id }} - </div> - <div style="width: 53px;" class="table-cell text-center"> - {{ entry.kind.toUpperCase() }} - </div> - <div style="width: 138px;" class="table-cell text-center"> - {{ entry.enqueued | dateTime }} - </div> - <div style="width: 105px;" class="table-cell truncate"> - {{ entry.user }} - </div> - <div style="width: 105px;" class="table-cell truncate"> - {{ entry.signer }} - </div> - <div style="width: 72px;" class="table-cell text-center"> - <span v-if="entry.state === 'failed'" class="text-danger">{{ - entry.state - }}</span> - <span v-else>{{ entry.state }}</span> - </div> - <div style="width: 44px;" class="table-cell text-center"> - <font-awesome-icon - v-if="entry.warnings" - class="text-warning" - icon="exclamation-triangle" - fixed-width - ></font-awesome-icon> - </div> - <div style="flex-grow: 1; padding: 0;" class="table-cell text-right"> - <div v-if="entry.state === 'pending'"> - <button - :class="['actions approved', { active: isApproved }]" - @click="toggleApproval($options.STATES.APPROVED)" - > - <font-awesome-icon - class="small pointer" - icon="check" - ></font-awesome-icon> - </button> - <button - :class="['actions rejected', { active: isRejected }]" - @click="toggleApproval($options.STATES.REJECTED)" - > - <font-awesome-icon - icon="times" - class="small pointer" - ></font-awesome-icon> - </button> - </div> - </div> + <font-awesome-icon class="small pointer" icon="check" /> + </button> + <button + :class="['action rejected', { active: isRejected }]" + @click="toggleApproval($options.STATES.REJECTED)" + v-if="entry.state === 'pending'" + > + <font-awesome-icon icon="times" class="small pointer" /> + </button> </div> - <LogDetail - :entry="entry" - :details="details" - v-if="show === entry.id" - ></LogDetail> </div> </template> <style lang="sass" scoped> -.logentry - width: 100% - &:hover - background: #fafafa - .actions - height: 100% - width: 50% - border: 0 - background: transparent - outline: none - &.approved - color: green - &.active, - &:hover - color: white - background: green +.action + height: 100% + width: 50% + border: 0 + background: white + outline: none + &.approved + color: green + &.active, + &:hover + color: white + background: green + &.rejected + border-left: 1px solid #dee2e6 + color: red + &.active, + &:hover + color: white + background: red +.active + .action + background-color: #d2eaee &.rejected - border-left: 1px solid #dee2e6 - color: red - &.active, - &:hover - color: white - background: red -.table-cell - padding: 0 3px - border-right: solid 1px #dee2e6 - &:last-child - border-right: none + border-left: solid 1px rgba(255, 255, 255, 0.3) </style> <script> @@ -108,26 +88,49 @@ * SPDX-License-Identifier: AGPL-3.0-or-later * License-Filename: LICENSES/AGPL-3.0.txt * - * Copyright (C) 2018 by via donau + * Copyright (C) 2018, 2019 by via donau * – Österreichische Wasserstraßen-Gesellschaft mbH * Software engineering by Intevation GmbH * * Author(s): - * Thomas Junk <thomas.junk@intevation.de> + * * Thomas Junk <thomas.junk@intevation.de> + * * Markus Kottländer <markus.kottlaender@intevation.de> */ -import { mapState } from "vuex"; -import { STATES } from "@/store/imports.js"; +import { mapState, mapGetters } from "vuex"; +import { STATES } from "@/store/imports"; +import { displayError } from "@/lib/errors"; +import { HTTP } from "@/lib/http"; export default { - name: "importlogentry", + STATES, props: ["entry"], data() { return { - details: null + loading: false }; }, - components: { - LogDetail: () => import("./LogDetail.vue") + computed: { + ...mapState("imports", ["show"]), + ...mapGetters("usermanagement", ["userCountries"]), + stateStyle() { + return [ + "table-cell", + "center", + { + "text-danger": this.entry.state === "failed", + "font-weight-bolder": this.entry.state === "running" + } + ]; + }, + needsApproval() { + return this.entry.status === STATES.NEEDSAPPROVAL; + }, + isRejected() { + return this.entry.status === STATES.REJECTED; + }, + isApproved() { + return this.entry.status === STATES.APPROVED; + } }, methods: { toggleApproval(state) { @@ -143,22 +146,24 @@ this.$store.commit("imports/hideAdditionalInfo"); this.$store.commit("imports/hideAdditionalLogs"); } else { - this.$store.commit("imports/showDetailsFor", id); + this.loading = true; + HTTP.get("/imports/" + this.entry.id, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.$store.commit("imports/showDetailsFor", id); + this.$store.commit("imports/setCurrentDetails", response.data); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => (this.loading = false)); } } - }, - computed: { - ...mapState("imports", ["show"]), - needsApproval() { - return this.entry.status === STATES.NEEDSAPPROVAL; - }, - isRejected() { - return this.entry.status === STATES.REJECTED; - }, - isApproved() { - return this.entry.status === STATES.APPROVED; - } - }, - STATES: STATES + } }; </script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/importoverview/SectionDetails.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,116 @@ +<template> + <div + :class="{ + full: !showLogs, + split: showLogs + }" + > + <div v-if="!details.summary.section" class="d-flex"> + <UISpinnerButton + @click="showDetails = !showDetails" + :state="showDetails" + :icons="['angle-right', 'angle-down']" + classes="text-info" + /> + <a @click="zoomToSection()" class="text-info pointer">{{ + details.summary.objnam + }}</a> + </div> + <div> + <div v-if="showDetails"> + <div + v-for="(entry, index) in Object.keys(details.summary)" + :key="index" + class="comparison row no-gutters px-4 text-left" + > + <span class="col-4">{{ entry }}</span> + <span class="col-4">{{ details.summary[entry] }}</span> + </div> + </div> + </div> + </div> +</template> + +<style lang="scss" scoped> +.comparison { + width: 668px; + border-top: dashed 1px #dee2e6; +} + +.comparison:nth-child(odd) { + background-color: #f8f9fa; +} + +.split { + max-height: 35vh; +} + +.full { + max-height: 70vh; +} +</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 { displayError } from "@/lib/errors"; +import { mapState, mapGetters } from "vuex"; + +export default { + props: ["entry"], + data() { + return { + showDetails: true + }; + }, + mounted() { + this.$store.commit("imports/hideAdditionalInfo"); + }, + computed: { + ...mapState("imports", ["showAdditional", "showLogs", "details"]), + ...mapGetters("map", ["openLayersMap"]) + }, + methods: { + zoomToSection() { + const { name } = this.details.summary; + this.openLayersMap() + .getLayer("SECTIONS") + .setVisible(true); + this.$store + .dispatch("imports/loadSection", name) + .then(response => { + if (response.data.features.length < 1) + throw new Error("no features found for: " + name); + this.$store.commit( + "imports/selectedSectionId", + response.data.features[0].id + ); + this.$store.dispatch("map/moveToFeauture", { + feature: response.data.features[0], + zoom: 17, + preventZoomOut: true + }); + }) + .catch(error => { + console.log(error); + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + } +}; +</script>
--- a/client/src/components/importoverview/SoundingResultDetail.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/importoverview/SoundingResultDetail.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,6 +1,5 @@ <template> <div> - <span class="empty"></span> <a @click="zoomTo()" class="text-info pointer"> {{ details.summary.bottleneck }} </a> @@ -33,17 +32,13 @@ ...mapState("imports", ["showAdditional", "details"]) }, methods: { - moveMap(coordinates) { - this.$store.commit("map/moveMap", { - coordinates: coordinates, + zoomTo() { + const { lat, lon, bottleneck, date } = this.details.summary; + this.$store.dispatch("map/moveMap", { + coordinates: [lat, lon], zoom: 17, preventZoomOut: true }); - }, - zoomTo() { - const { lat, lon, bottleneck, date } = this.details.summary; - const coordinates = [lat, lon]; - this.moveMap(coordinates); this.$store .dispatch("bottlenecks/setSelectedBottleneck", bottleneck) .then(() => {
--- a/client/src/components/importoverview/StretchDetails.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/importoverview/StretchDetails.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,12 +1,59 @@ <template> - <div> - <span class="empty"> </span> - <a @click="zoomToStretch()" class="text-info pointer">{{ - details.summary.stretch - }}</a> + <div + :class="{ + full: !showLogs, + split: showLogs + }" + > + <div v-if="!details.summary.stretch" class="d-flex"> + <UISpinnerButton + @click="showDetails = !showDetails" + :state="showDetails" + :icons="['angle-right', 'angle-down']" + classes="text-info" + /> + <a @click="zoomToStretch()" class="text-info pointer" + >{{ details.summary.objnam }} ( + {{ details.summary.countries.join(", ") }} )</a + > + </div> + <div> + <div v-if="showDetails"> + <div + v-for="(entry, index) in Object.keys(details.summary)" + :key="index" + class="comparison row no-gutters px-4 text-left" + > + <span class="col-4">{{ entry }}</span> + <span v-if="entry === 'countries'" class="col-4">{{ + details.summary[entry].join(", ") + }}</span> + <span v-else class="col-4">{{ details.summary[entry] }}</span> + </div> + </div> + </div> </div> </template> +<style lang="scss" scoped> +.comparison { + width: 668px; + border-top: dashed 1px #dee2e6; +} + +.comparison:nth-child(odd) { + background-color: #f8f9fa; +} + +.split { + max-height: 35vh; +} + +.full { + max-height: 70vh; +} +</style> + <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -21,36 +68,43 @@ * Author(s): * Thomas Junk <thomas.junk@intevation.de> */ -import { displayError } from "@/lib/errors.js"; -import { LAYERS } from "@/store/map.js"; -import { mapState } from "vuex"; +import { displayError } from "@/lib/errors"; +import { mapState, mapGetters } from "vuex"; export default { - name: "stretchdetails", + data() { + return { + showDetails: true + }; + }, props: ["entry"], mounted() { this.$store.commit("imports/hideAdditionalInfo"); }, computed: { - ...mapState("imports", ["showAdditional", "details"]) + ...mapState("imports", ["showAdditional", "showLogs", "details"]), + ...mapGetters("map", ["openLayersMap"]) }, methods: { - moveToExtent(feature) { - this.$store.commit("map/moveToExtent", { - feature: feature, - zoom: 17, - preventZoomOut: true - }); - }, zoomToStretch() { - const name = this.details.summary.stretch; - this.$store.commit("map/setLayerVisible", LAYERS.STRETCHES); + const { name } = this.details.summary; + this.openLayersMap() + .getLayer("STRETCHES") + .setVisible(true); this.$store .dispatch("imports/loadStretch", name) .then(response => { if (response.data.features.length < 1) throw new Error("no feaures found for: " + name); - this.moveToExtent(response.data.features[0]); + this.$store.commit( + "imports/selectedStretchId", + response.data.features[0].id + ); + this.$store.dispatch("map/moveToFeauture", { + feature: response.data.features[0], + zoom: 17, + preventZoomOut: true + }); }) .catch(error => { console.log(error); @@ -64,5 +118,3 @@ } }; </script> - -<style></style>
--- a/client/src/components/importschedule/Importschedule.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,268 +0,0 @@ -<template> - <div class="d-flex flex-row"> - <Spacer></Spacer> - <div class="mt-2 w-100"> - <div class="card flex-grow-1 schedulecard shadow-xs"> - <UIBoxHeader icon="clock" :title="importScheduleLabel" /> - <div class="searchandfilter p-3 w-50 mx-auto"> - <div class="searchgroup input-group"> - <div class="input-group-prepend"> - <span class="input-group-text" id="search"> - <font-awesome-icon icon="search"></font-awesome-icon> - </span> - </div> - <input - v-model="searchQuery" - type="text" - class="form-control" - placeholder - aria-label="Search" - aria-describedby="search" - /> - </div> - </div> - <UITableHeader - :columns="[ - { id: 'id', title: `${idLabel}`, class: 'col-1' }, - { id: 'kind', title: `${typeLabel}`, class: 'col-2' }, - { id: 'user', title: `${authorLabel}`, class: 'col-2' }, - { id: 'config.cron', title: `${scheduleLabel}`, class: 'col-2' }, - { id: 'config.send-email', title: `${emailLabel}`, class: 'col-2' } - ]" - /> - <UITableBody - :data="filteredSchedules() | sortTable(sortColumn, sortDirection)" - v-slot="{ item: schedule }" - > - <div class="py-1 col-1">{{ schedule.id }}</div> - <div class="py-1 col-2">{{ schedule.kind.toUpperCase() }}</div> - <div class="py-1 col-2">{{ schedule.user }}</div> - <div class="py-1 col-2">{{ schedule.config.cron }}</div> - <div class="py-1 col-2 text-center"> - <font-awesome-icon - v-if="schedule.config['send-email']" - class="fa-fw mr-2" - fixed-width - icon="check" - ></font-awesome-icon> - </div> - <div class="py-1 col text-right"> - <button - @click="editSchedule(schedule.id)" - class="btn btn-xs btn-dark mr-1" - :disabled="importScheduleDetailVisible" - > - <font-awesome-icon - icon="pencil-alt" - fixed-width - ></font-awesome-icon> - </button> - <button - @click="deleteSchedule(schedule)" - class="btn btn-xs btn-dark mr-1" - :disabled="importScheduleDetailVisible" - > - <font-awesome-icon icon="trash" fixed-width></font-awesome-icon> - </button> - <button - @click="triggerManualImport(schedule.id)" - class="btn btn-xs btn-dark" - :disabled="importScheduleDetailVisible" - > - <font-awesome-icon icon="play" fixed-width></font-awesome-icon> - </button> - </div> - </UITableBody> - <div class="p-3 text-right"> - <button - :disabled="importScheduleDetailVisible" - @click="newImport" - class="btn btn-info newbutton" - > - <translate>New Import</translate> - </button> - </div> - </div> - </div> - <Importscheduledetail></Importscheduledetail> - </div> -</template> - -<style lang="sass" scoped> -th - border-top: 0px - -.card-body - padding-bottom: $small-offset - -.schedulecard - margin-right: $small-offset - min-height: 20rem - -.schedulecard-body - width: 100% - margin-left: auto - margin-right: auto -</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"; -import { HTTP } from "@/lib/http"; -import { displayInfo, displayError } from "@/lib/errors"; -import { sortTable } from "@/lib/mixins"; - -export default { - name: "importschedule", - mixins: [sortTable], - components: { - Importscheduledetail: () => import("./Importscheduledetail"), - Spacer: () => import("@/components/Spacer") - }, - data() { - return { - searchQuery: "" - }; - }, - computed: { - ...mapState("application", ["showSidebar"]), - ...mapState("importschedule", ["schedules", "importScheduleDetailVisible"]), - importScheduleLabel() { - return this.$gettext("Import Schedule"); - }, - idLabel() { - return this.$gettext("ID"); - }, - typeLabel() { - return this.$gettext("Type"); - }, - authorLabel() { - return this.$gettext("Author"); - }, - scheduleLabel() { - return this.$gettext("Schedule"); - }, - emailLabel() { - return this.$gettext("Email"); - }, - spacerStyle() { - return [ - "spacer ml-3", - { - "spacer-expanded": this.showSidebar, - "spacer-collapsed": !this.showSidebar - } - ]; - } - }, - methods: { - filteredSchedules() { - return this.schedules.filter(s => { - return (s.id + s.kind) - .toLowerCase() - .includes(this.searchQuery.toLowerCase()); - }); - }, - editSchedule(id) { - this.$store - .dispatch("importschedule/loadSchedule", id) - .then(() => { - this.$store.commit("importschedule/setImportScheduleDetailVisible"); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - triggerManualImport(id) { - HTTP.get("/imports/config/" + id + "/run", { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - const { id } = response.data; - displayInfo({ - title: this.$gettext("Imports"), - message: this.$gettext("Manually triggered import: #") + id - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - getSchedules() { - this.$store.dispatch("importschedule/loadSchedules").catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - newImport() { - this.$store.commit("importschedule/setImportScheduleDetailVisible"); - }, - deleteSchedule(schedule) { - console.log(schedule); - this.$store.commit("application/popup", { - icon: "trash", - title: this.$gettext("Delete Import"), - content: - this.$gettext("Do you really want to delete the import with ID") + - `<b>${schedule.id}</b>` + - this.$gettext("of type") + - `<b>${schedule.kind.toUpperCase()}</b>?`, - confirm: { - label: this.$gettext("Delete"), - icon: "trash", - callback: () => { - this.$store - .dispatch("importschedule/deleteSchedule", schedule.id) - .then(() => { - this.getSchedules(); - displayInfo({ - title: this.$gettext("Imports"), - message: this.$gettext("Deleted import: #") + schedule.id - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - cancel: { - label: this.$gettext("Cancel"), - icon: "times" - } - }); - } - }, - mounted() { - this.getSchedules(); - } -}; -</script>
--- a/client/src/components/importschedule/Importscheduledetail.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1031 +0,0 @@ -<template> - <div - class="importscheduledetails fadeIn animated" - v-if="importScheduleDetailVisible" - > - <div class="card shadow-xs importscheduledetailscard pb-5"> - <UIBoxHeader :title="dialogLabel" :closeCallback="closeDetailview" /> - <div class="card-body"> - <form @submit.prevent="save" class="ml-2 mr-2"> - <div class="d-flex flex-row"> - <div class="flex-column w-50 mr-3"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Imports</translate> - </small> - </div> - <select v-model="import_" class="custom-select" id="importtype"> - <option :value="$options.IMPORTTYPES.BOTTLENECK" - ><translate>Bottlenecks</translate></option - > - <option :value="$options.IMPORTTYPES.WATERWAYAXIS" - ><translate>Waterway axis</translate></option - > - <option :value="$options.IMPORTTYPES.GAUGEMEASUREMENT" - ><translate>Gauge measurement</translate></option - > - <option :value="$options.IMPORTTYPES.FAIRWAYAVAILABILITY" - ><translate>Available fairway depths</translate></option - > - <option :value="$options.IMPORTTYPES.WATERWAYAREA" - ><translate>Waterway area</translate></option - > - <option :value="$options.IMPORTTYPES.FAIRWAYDIMENSION" - ><translate>Fairway dimension</translate></option - > - <option :value="$options.IMPORTTYPES.WATERWAYGAUGES" - ><translate>Waterway gauges</translate></option - > - <option :value="$options.IMPORTTYPES.DISTANCEMARKSVIRTUAL" - ><translate>Distance marks virtual</translate></option - > - <option :value="$options.IMPORTTYPES.DISTANCEMARKSASHORE" - ><translate>Distance marks ashore</translate></option - > - </select> - </div> - <div class="flex-column ml-4"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Email Notification</translate> - </small> - </div> - <div class="flex-flex-row text-left"> - <toggle-button - v-model="eMailNotification" - class="mt-2" - :speed="100" - :labels="{ - checked: this.$options.on, - unchecked: this.$options.off - }" - :width="60" - :height="30" - /> - </div> - </div> - </div> - <div v-if="directImportAvailable" class="flex-column"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Import via</translate> - </small> - </div> - <div class="flex-flex-row text-left"> - <!-- '#75c791' is the DEFAULT_COLOR_CHECKED - from vue-js-toggle-button as here both states are active --> - <toggle-button - :color="{ unchecked: '#75c791' }" - v-model="directImport" - class="mt-2" - :speed="100" - :labels="{ - checked: this.$options.FILE, - unchecked: this.$options.URL - }" - :width="60" - :height="30" - /> - </div> - </div> - <Availablefairwaydepth - v-if=" - import_ == $options.IMPORTTYPES.FAIRWAYAVAILABILITY && - !directImport - " - @urlChanged="setUrl" - :url="url" - ></Availablefairwaydepth> - <Bottleneck - v-if="import_ == $options.IMPORTTYPES.BOTTLENECK" - @urlChanged="setUrl" - @toleranceChanged="setTolerance" - :url="url" - :tolerance="tolerance" - :directImport="directImport" - ></Bottleneck> - <Distancemarksvirtual - v-if="import_ == $options.IMPORTTYPES.DISTANCEMARKSVIRTUAL" - @urlChanged="setUrl" - @usernameChanged="setUsername" - @passwordChanged="setPassword" - :url="url" - :username="username" - :password="password" - ></Distancemarksvirtual> - <Distancemarksashore - v-if="import_ == $options.IMPORTTYPES.DISTANCEMARKSASHORE" - @urlChanged="setUrl" - @featureTypeChanged="setFeatureType" - @sortByChanged="setSortBy" - :url="url" - :featureType="featureType" - :sortBy="sortBy" - ></Distancemarksashore> - <Faiwaydimensions - v-if="import_ == $options.IMPORTTYPES.FAIRWAYDIMENSION" - @urlChanged="setUrl" - @featureTypeChanged="setFeatureType" - @sortByChanged="setSortBy" - @LOSChanged="setLOS" - @depthChanged="setDepth" - @minWidthChanged="setMinWidth" - @maxWidthChanged="setMaxWidth" - @sourceOrganizationChanged="setSourceOrganization" - :url="url" - :featureType="featureType" - :sortBy="sortBy" - :LOS="LOS" - :minWidth="minWidth" - :maxWidth="maxWidth" - :sourceOrganization="sourceOrganization" - :depth="depth" - ></Faiwaydimensions> - <Gaugemeasurement - v-if=" - import_ == $options.IMPORTTYPES.GAUGEMEASUREMENT && !directImport - " - @urlChanged="setUrl" - :url="url" - ></Gaugemeasurement> - <Waterwayarea - v-if="import_ == $options.IMPORTTYPES.WATERWAYAREA" - @urlChanged="setUrl" - @featureTypeChanged="setFeatureType" - @sortByChanged="setSortBy" - :url="url" - :featureType="featureType" - :sortBy="sortBy" - ></Waterwayarea> - <Waterwaygauges - v-if="import_ == $options.IMPORTTYPES.WATERWAYGAUGES" - @urlChanged="setUrl" - @usernameChanged="setUsername" - @passwordChanged="setPassword" - :url="url" - :username="username" - :password="password" - ></Waterwaygauges> - <Waterwayaxis - v-if="import_ == $options.IMPORTTYPES.WATERWAYAXIS" - @urlChanged="setUrl" - @featureTypeChanged="setFeatureType" - @sortByChanged="setSortBy" - :url="url" - :featureType="featureType" - :sortBy="sortBy" - ></Waterwayaxis> - - <template v-if="!directImport || !directImportAvailable"> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-4"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Scheduled</translate>? - </small> - </div> - <div class="flex-flex-row text-left"> - <toggle-button - v-model="scheduled" - class="mt-2" - :speed="100" - :labels="{ - checked: this.$options.on, - unchecked: this.$options.off - }" - :width="60" - :height="30" - /> - </div> - </div> - <div class="flex-column mt-3 mr-2"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Simple schedule</translate> - </small> - </div> - <div class="flex-flex-row text-left"> - <toggle-button - :disabled="!scheduled" - v-model="easyCron" - class="mt-2" - :speed="100" - :labels="{ - checked: this.$options.on, - unchecked: this.$options.off - }" - :width="60" - :height="30" - /> - </div> - </div> - </div> - <div class="flex-column w-100 mr-2"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Schedule</translate> - </small> - </div> - <div v-if="easyCron" class="text-left w-50"> - <select - :disabled="!scheduled" - v-model="simple" - class="form-control" - ><option value="weekly"><translate>Weekly</translate></option> - <option value="monthly" - ><translate>Monthly</translate> - </option> - </select> - </div> - <div v-if="!easyCron" class="text-left w-100"> - <div class="d-flex flex-row"> - <h4 class="mt-auto mb-auto mr-2">{{ $options.EVERY }}</h4> - <select - :disabled="!scheduled" - style="width: 130px;" - v-model="cronMode" - class="form-control" - @change="clearInputs" - > - <option :value="null"></option> - <option - v-for="(option, key) in $options.CRONMODE" - :value="key" - :key="key" - >{{ option }}</option - > - </select> - <div v-if="cronMode == 'hour'" class="ml-1 d-flex flex-row"> - <h4 class="mt-auto mb-auto">{{ $options.ON }}</h4> - <input - :disabled="!scheduled" - v-model="minutes" - class="cronfield ml-1 mr-1 form-control" - type="number" - /> - <h4 class="mt-auto mb-auto">{{ $options.MINUTESPAST }}</h4> - </div> - <div v-if="cronMode == 'day'" class="ml-1 d-flex flex-row"> - <h4 class="mt-auto mb-auto">{{ $options.AT }}</h4> - <input - :disabled="!scheduled" - v-model="hour" - class="cronfield ml-1 mr-1 form-control" - type="number" - /> - <input - :disabled="!scheduled" - v-model="minutes" - class="cronfield ml-1 mr-1 form-control" - type="number" - /> - <h4 class="mt-auto mb-auto">{{ $options.OCLOCK }}</h4> - </div> - <div v-if="cronMode == 'week'" class="ml-1 d-flex flex-row"> - <h4 class="ml-1 mr-1 mt-auto mb-auto">{{ $options.ON }}</h4> - <select - :disabled="!scheduled" - v-model="day" - class="form-control" - > - <option - v-for="(option, key) in $options.DAYSOFWEEK" - :key="key" - :value="key" - >{{ option }}</option - > - </select> - <h4 class="ml-1 mt-auto mb-auto">{{ $options.AT }}</h4> - <input - :disabled="!scheduled" - v-model="hour" - class="cronfield ml-1 mr-1 form-control" - type="number" - /> - <input - :disabled="!scheduled" - v-model="minutes" - class="cronfield ml-1 mr-1 form-control" - type="number" - /> - </div> - <div v-if="cronMode == 'month'" class="ml-1 d-flex flex-row"> - <h4 class="ml-1 mt-auto mb-auto">{{ $options.ON }}</h4> - <input - :disabled="!scheduled" - v-model="dayOfMonth" - class="cronfield ml-1 mr-1 form-control" - type="number" - /> - <h4 class="mt-auto mb-auto">{{ $options.AT }}</h4> - <input - :disabled="!scheduled" - v-model="hour" - class="cronfield ml-1 mr-2 form-control" - type="number" - /> - <input - :disabled="!scheduled" - v-model="minutes" - class="cronfield ml-1 mr-2 form-control" - type="number" - /> - <h4 class="mt-auto mb-auto">{{ $options.OCLOCK }}</h4> - </div> - <div v-if="cronMode == 'year'" class="ml-1 d-flex flex-row"> - <h4 class="ml-1 mt-auto mb-auto">{{ $options.ON }}</h4> - <input - :disabled="!scheduled" - v-model="dayOfMonth" - class="cronfield ml-1 mr-1 form-control" - type="number" - /> - <h4 class="mt-auto mb-auto">{{ $options.OF }}</h4> - <select - :disabled="!scheduled" - v-model="month" - class="ml-1 mr-1 form-control" - > - <option - v-for="(option, key) in $options.MONTHS" - :value="key" - :key="key" - >{{ option }}</option - > - </select> - <h4 class="mt-auto mb-auto">{{ $options.ON }}</h4> - <input - :disabled="!scheduled" - v-model="hour" - class="cronfield ml-1 mr-1 form-control" - type="number" - /> - <input - :disabled="!scheduled" - v-model="minutes" - class="cronfield ml-1 mr-1 form-control" - type="number" - /> - </div> - </div> - <div class="mt-3 w-50 d-flex flex-row"> - <h5 class="mt-auto mb-auto mr-2"> - <translate>Cronstring</translate> - </h5> - <input - :disabled="!scheduled" - class="form-control" - v-model="cronString" - type="text" - /> - </div> - </div> - </div> - <button - :disabled="!isValid" - type="submit" - class="shadow-sm btn btn-info submit-button" - > - <translate>Save</translate> - </button> - </template> - <div v-else class="d-flex flex-row text-left"> - <div class="mt-3 mb-3 flex-column w-100"> - <div class="custom-file"> - <input - accept=".xml" - type="file" - @change="fileSelected" - class="custom-file-input" - id="uploadFile" - /> - <label class="pointer custom-file-label" for="uploadFile"> - {{ uploadLabel }} - </label> - </div> - </div> - </div> - <button - @click="triggerManualImport" - type="button" - class="shadow-sm btn btn-outline-info trigger" - :disabled="!triggerActive || !isValid" - > - <font-awesome-icon - class="fa-fw mr-2" - fixed-width - icon="play" - ></font-awesome-icon - ><translate>Trigger import</translate> - </button> - </form> - </div> - </div> - </div> -</template> - -<script> -/* 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, 2019 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Tom Gottfried <tom.gottfried@intevation.de> - */ -import { - IMPORTTYPES, - IMPORTTYPEKIND, - initializeCurrentSchedule -} from "@/store/importschedule"; -import { mapState } from "vuex"; -import { displayInfo, displayError } from "@/lib/errors.js"; -import app from "@/main.js"; -import { HTTP } from "@/lib/http"; - -export default { - name: "importscheduledetail", - components: { - Availablefairwaydepth: () => - import("@/components/importschedule/importtypes/Availablefairwaydepth"), - Bottleneck: () => - import("@/components/importschedule/importtypes/Bottleneck"), - Distancemarksvirtual: () => - import("@/components/importschedule/importtypes/Distancemarksvirtual"), - Distancemarksashore: () => - import("@/components/importschedule/importtypes/Distancemarksashore"), - Faiwaydimensions: () => - import("@/components/importschedule/importtypes/Fairwaydimensions"), - Gaugemeasurement: () => - import("@/components/importschedule/importtypes/Gaugemeasurement"), - Waterwayarea: () => - import("@/components/importschedule/importtypes/Waterwayarea"), - Waterwaygauges: () => - import("@/components/importschedule/importtypes/Waterwaygauges"), - Waterwayaxis: () => - import("@/components/importschedule/importtypes/Waterwayaxis") - }, - data() { - return { - directImport: false, - passwordVisible: false, - uploadLabel: this.$gettext("choose file to upload"), - uploadFile: null, - ...initializeCurrentSchedule() - }; - }, - mounted() { - this.initialize(); - }, - watch: { - cronMode() { - this.cronString = this.calcCronString(); - }, - minutes() { - this.cronString = this.calcCronString(); - }, - hour() { - this.cronString = this.calcCronString(); - }, - month() { - this.cronString = this.calcCronString(); - }, - day() { - this.cronString = this.calcCronString(); - }, - dayOfMonth() { - this.cronString = this.calcCronString(); - }, - importScheduleDetailVisible() { - this.initialize(); - }, - cronString() { - if (this.isWeekly(this.cronString)) { - this.simple = "weekly"; - } - if (this.isMonthly(this.cronString)) { - this.simple = "monthly"; - } - } - }, - computed: { - ...mapState("importschedule", [ - "importScheduleDetailVisible", - "currentSchedule" - ]), - dialogLabel() { - if (this.id) return this.$gettext("Import") + " " + this.id; - return this.$gettext("New Import"); - }, - directImportAvailable() { - switch (this.import_) { - case this.$options.IMPORTTYPES.BOTTLENECK: - case this.$options.IMPORTTYPES.FAIRWAYAVAILABILITY: - case this.$options.IMPORTTYPES.GAUGEMEASUREMENT: - return true; - default: - return false; - } - }, - isCredentialsRequired() { - switch (this.import_) { - case this.$options.IMPORTTYPES.WATERWAYGAUGES: - case this.$options.IMPORTTYPES.DISTANCEMARKSVIRTUAL: - return true; - default: - return false; - } - }, - isURLRequired() { - switch (this.import_) { - case this.$options.IMPORTTYPES.BOTTLENECK: - case this.$options.IMPORTTYPES.WATERWAYAXIS: - case this.$options.IMPORTTYPES.GAUGEMEASUREMENT: - case this.$options.IMPORTTYPES.FAIRWAYAVAILABILITY: - case this.$options.IMPORTTYPES.WATERWAYAREA: - case this.$options.IMPORTTYPES.FAIRWAYDIMENSION: - case this.$options.IMPORTTYPES.WATERWAYGAUGES: - case this.$options.IMPORTTYPES.DISTANCEMARKSVIRTUAL: - case this.$options.IMPORTTYPES.DISTANCEMARKSASHORE: - return true; - default: - return false; - } - }, - isFeatureTypeRequired() { - switch (this.import_) { - case this.$options.IMPORTTYPES.WATERWAYAXIS: - case this.$options.IMPORTTYPES.WATERWAYAREA: - case this.$options.IMPORTTYPES.FAIRWAYDIMENSION: - case this.$options.IMPORTTYPES.DISTANCEMARKSASHORE: - return true; - default: - return false; - } - }, - isSortbyRequired() { - switch (this.import_) { - case this.$options.IMPORTTYPES.WATERWAYAXIS: - case this.$options.IMPORTTYPES.WATERWAYAREA: - case this.$options.IMPORTTYPES.FAIRWAYDIMENSION: - case this.$options.IMPORTTYPES.DISTANCEMARKSASHORE: - return true; - default: - return false; - } - }, - isToleranceRequired() { - switch (this.import_) { - case this.$options.IMPORTTYPES.BOTTLENECK: - return true; - default: - return false; - } - }, - isValid() { - if (!this.import_) return false; - if (this.isToleranceRequired && !this.tolerance) return false; - if (this.directImport && !this.uploadFile) return false; - else if (!this.directImport) { - if (this.isURLRequired && !this.url) return false; - if (this.isSortbyRequired && !this.sortBy) return false; - if (this.isFeatureTypeRequired && !this.featureType) return false; - if (this.isCredentialsRequired && (!this.username || !this.password)) - return false; - if (this.import_ == this.$options.IMPORTTYPES.FAIRWAYDIMENSION) { - if ( - !this.LOS || - !this.minWidth || - !this.maxWidth || - !this.depth || - !this.sourceOrganization - ) - return false; - } - } - return true; - } - }, - methods: { - fileSelected(e) { - const files = e.target.files || e.dataTransfer.files; - if (!files) return; - this.uploadLabel = files[0].name; - this.uploadFile = files[0]; - }, - setUrl(value) { - this.url = value; - }, - setFeatureType(value) { - this.featureType = value; - }, - setSortBy(value) { - this.sortBy = value; - }, - setTolerance(value) { - this.tolerance = value; - }, - setUsername(value) { - this.username = value; - }, - setPassword(value) { - this.password = value; - }, - setLOS(value) { - this.LOS = value; - }, - setMinWidth(value) { - this.minWidth = value; - }, - setMaxWidth(value) { - this.maxWidth = value; - }, - setDepth(value) { - this.depth = value; - }, - setSourceOrganization(value) { - this.sourceOrganization = value; - }, - calcCronString() { - let getValue = value => { - return this[value] !== null ? this[value] : "*"; - }; - - const min = getValue("minutes"); - const h = getValue("hour"); - const dm = getValue("dayOfMonth"); - const m = getValue("month"); - const wd = getValue("day"); - - if (this.cronMode === "15minutes") return "0 */15 * * * *"; - if (this.cronMode === "hour") return `0 ${min} * * * *`; - if (this.cronMode === "day") return `0 ${min} ${h} * * *`; - if (this.cronMode === "week") return `0 ${min} ${h} * * ${wd}`; - if (this.cronMode === "month") return `0 ${min} ${h} ${dm} * *`; - if (this.cronMode === "year") return `0 ${min} ${h} ${dm} ${m} *`; - return this.cronString; - }, - validateBottleneckfields() { - return !!this.url; - }, - initialize() { - this.id = this.currentSchedule.id; - this.importType = this.currentSchedule.importType; - this.schedule = this.currentSchedule.schedule; - this.scheduled = this.currentSchedule.scheduled; - this.import_ = this.currentSchedule.import_; - this.importSource = this.currentSchedule.importSource; - this.eMailNotification = this.currentSchedule.eMailNotification; - this.easyCron = this.currentSchedule.easyCron; - this.cronMode = this.currentSchedule.cronMode; - this.minutes = this.currentSchedule.minutes; - this.month = this.currentSchedule.month; - this.hour = this.currentSchedule.hour; - this.day = this.currentSchedule.day; - this.dayOfMonth = this.currentSchedule.dayOfMonth; - this.simple = this.currentSchedule.simple; - this.url = this.currentSchedule.url; - this.insecure = this.currentSchedule.insecure; - this.cronString = this.currentSchedule.cronString; - this.featureType = this.currentSchedule.featureType; - this.sortBy = this.currentSchedule.sortBy; - this.tolerance = this.currentSchedule.tolerance; - this.username = this.currentSchedule.username; - this.password = this.currentSchedule.password; - this.LOS = this.currentSchedule.LOS; - this.minWidth = this.currentSchedule.minWidth; - this.maxWidth = this.currentSchedule.maxWidth; - this.depth = this.currentSchedule.depth; - this.sourceOrganization = this.currentSchedule.sourceOrganization; - this.directImport = false; - }, - isWeekly(cron) { - return /0 \d{1,2} \d{1,2} \* \* \d{1}/.test(cron); - }, - isMonthly(cron) { - return /0 \d{1,2} \d{1,2} \d{1,2} \* \*/.test(cron); - }, - clearInputs() { - this.minutes = this.currentSchedule.minutes; - this.month = this.currentSchedule.month; - this.hour = this.currentSchedule.hour; - this.day = this.currentSchedule.day; - this.dayOfMonth = this.currentSchedule.dayOfMonth; - }, - triggerFileUpload() { - if (!this.uploadFile) return; - let formData = new FormData(); - let routeParam = ""; - switch (this.import_) { - case this.$options.IMPORTTYPES.BOTTLENECK: - formData.append("tolerance", this.tolerance); - routeParam = "ubn"; - break; - case this.$options.IMPORTTYPES.FAIRWAYAVAILABILITY: - routeParam = "ufa"; - break; - case this.$options.IMPORTTYPES.GAUGEMEASUREMENT: - routeParam = "ugm"; - break; - default: - throw new Error("invalid importroute"); - } - - formData.append(routeParam, this.uploadFile); - HTTP.post("/imports/" + routeParam, formData, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-Type": "multipart/form-data" - } - }) - .then(response => { - const { id } = response.data; - displayInfo({ - title: this.$gettext("File Import"), - message: this.$gettext("Import import: #") + id - }); - this.closeDetailview(); - this.$store.dispatch("importschedule/loadSchedules").catch(error => { - const { status, data } = error.response; - displayError({ - title: this.gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }, - triggerManualImport() { - if (!this.triggerActive) return; - if (!this.import_) return; - if (this.directImport) { - if (!this.uploadFile) return; - this.triggerFileUpload(); - return; - } - let data = {}; - if (this.isURLRequired) { - if (!this.url) return; - data["url"] = this.url; - data["insecure"] = this.insecure; - } - if (this.isFeatureTypeRequired) { - if (!this.featureType) return; - data["feature-type"] = this.featureType; - } - if (this.isSortbyRequired) { - if (!this.sortBy) return; - data["sort-by"] = this.sortBy; - } - if (this.isToleranceRequired) { - if (!this.tolerance) return; - data["tolerance"] = parseFloat(this.tolerance); - } - if (this.isCredentialsRequired) { - if (!this.username || !this.password) return; - data["user"] = this.username; - data["password"] = this.password; - } - if (this.import_ == this.$options.IMPORTTYPES.FAIRWAYDIMENSION) { - if ( - !this.LOS || - !this.minWidth || - !this.maxWidth || - !this.depth || - !this.sourceOrganization - ) - return; - data["feature-type"] = this.featureType; - data["sort-by"] = this.sortBy; - data["los"] = this.LOS * 1; - data["min-width"] = this.minWidth * 1; - data["max-width"] = this.maxWidth * 1; - data["depth"] = this.depth * 1; - data["source-organization"] = this.sourceOrganization; - } - data["send-email"] = this.eMailNotification; - this.triggerActive = false; - this.$store - .dispatch("importschedule/triggerImport", { - type: IMPORTTYPEKIND[this.import_], - data - }) - .then(response => { - const { id } = response.data; - displayInfo({ - title: this.$gettext("Import"), - message: this.$gettext("Manually triggered import: #") + id - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }) - .finally(() => { - this.triggerActive = true; - }); - }, - save() { - if (!this.import_) return; - let cron = this.cronString; - if (this.easyCron) { - if (this.simple === "weekly") cron = "0 0 0 * * 0"; - if (this.simple === "monthly") cron = "0 0 0 1 * *"; - } - let data = {}; - let config = {}; - data["kind"] = IMPORTTYPEKIND[this.import_]; - - if (this.isURLRequired) { - if (!this.url) return; - config["url"] = this.url; - config["insecure"] = this.insecure; - } - if (this.isSortbyRequired) { - if (!this.sortBy) return; - config["sort-by"] = this.sortBy; - } - if (this.isFeatureTypeRequired) { - if (!this.featureType) return; - config["feature-type"] = this.featureType; - } - if (this.isToleranceRequired) { - if (!this.tolerance) return; - config["tolerance"] = parseFloat(this.tolerance); - } - if (this.isCredentialsRequired) { - if (!this.username || !this.password) return; - config = { - ...config, - user: this.username, - password: this.password - }; - } - if (this.import_ == this.$options.IMPORTTYPES.FAIRWAYDIMENSION) { - if ( - !this.LOS || - !this.minWidth || - !this.maxWidth || - !this.depth || - !this.sourceOrganization - ) - return; - config = { ...config, los: this.LOS, depth: this.depth }; - config["min-width"] = this.minWidth; - config["max-width"] = this.maxWidth; - config["source-organization"] = this.sourceOrganization; - } - if (this.scheduled) config["cron"] = cron; - config["send-email"] = this.eMailNotification; - if (!this.id) { - data["config"] = config; - this.$store - .dispatch("importschedule/saveCurrentSchedule", data) - .then(response => { - const { id } = response.data; - displayInfo({ - title: this.$gettext("Import"), - message: this.$gettext("Saved import: #") + id - }); - this.closeDetailview(); - this.$store - .dispatch("importschedule/loadSchedules") - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } else { - this.$store - .dispatch("importschedule/updateCurrentSchedule", { - data: config, - id: this.id - }) - .then(response => { - const { id } = response.data; - displayInfo({ - title: this.$gettext("Import"), - message: this.$gettext("update import: #") + id - }); - this.closeDetailview(); - this.$store - .dispatch("importschedule/loadSchedules") - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - closeDetailview() { - this.$store.commit("importschedule/clearCurrentSchedule"); - this.$store.commit("importschedule/setImportScheduleDetailInvisible"); - } - }, - IMPORTTYPES: IMPORTTYPES, - on: "on", - off: "off", - FILE: app.$gettext("File"), - URL: app.$gettext("URL"), - EVERY: app.$gettext("Every"), - MINUTESPAST: app.$gettext("minutes past"), - ON: app.$gettext("on"), - OF: app.$gettext("of"), - AT: app.$gettext("at"), - OCLOCK: app.$gettext("o' clock"), - CRONMODE: { - "15minutes": app.$gettext("15 minutes"), - hour: app.$gettext("hour"), - day: app.$gettext("day"), - week: app.$gettext("week"), - month: app.$gettext("month"), - year: app.$gettext("year") - }, - DAYSOFWEEK: { - 1: app.$gettext("Monday"), - 2: app.$gettext("Tuesday"), - 3: app.$gettext("Wednesday"), - 4: app.$gettext("Thursday"), - 5: app.$gettext("Friday"), - 6: app.$gettext("Saturday"), - 0: app.$gettext("Sunday") - }, - MONTHS: { - 1: app.$gettext("January"), - 2: app.$gettext("February"), - 3: app.$gettext("March"), - 4: app.$gettext("April"), - 5: app.$gettext("May"), - 6: app.$gettext("June"), - 7: app.$gettext("July"), - 8: app.$gettext("August"), - 9: app.$gettext("September"), - 10: app.$gettext("October"), - 11: app.$gettext("November"), - 12: app.$gettext("December") - } -}; -</script> - -<style lang="scss" scoped> -.cronfield { - width: 55px; -} - -.importscheduledetailscard { - min-height: 550px; -} - -.importscheduledetails { - width: 100%; - margin-top: $offset; - margin-right: $offset; -} - -.trigger { - position: absolute; - left: $large-offset; - bottom: $offset; -} - -.submit-button { - position: absolute; - right: $large-offset; - bottom: $offset; -} -</style>
--- a/client/src/components/importschedule/importtypes/Availablefairwaydepth.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +0,0 @@ -<template> - <div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-100"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>URL</translate> </small> - </div> - <div class="w-100"> - <input - @input="urlChanged" - class="url form-control" - type="url" - :value="url" - /> - </div> - </div> - </div> - <div v-if="!url" class="d-flex flex-row"> - <small - ><translate class="text-danger">Please enter a URL</translate></small - > - </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> - */ -export default { - name: "availablefairwaydepth", - props: ["url"], - methods: { - urlChanged(e) { - this.$emit("urlChanged", e.target.value); - } - } -}; -</script> - -<style></style>
--- a/client/src/components/importschedule/importtypes/Bottleneck.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,107 +0,0 @@ -<template> - <div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-100"> - <template v-if="!directImport"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>URL</translate> </small> - </div> - <div class="w-100"> - <input - @input="urlChanged" - class="url form-control" - type="url" - :value="url" - /> - </div> - </template> - </div> - <div v-if="false" class="flex-column mt-3 text-left"> - <div class="d-flex flex-row"> - <small class="text-muted mr-2" - ><translate>Insecure</translate> - </small> - </div> - <div class="d-flex flex-row"> - <toggle-button - v-model="insecure" - class="mt-2" - :speed="100" - :color="{ - checked: '#FF0000', - unchecked: '#E9ECEF', - disabled: '#CCCCCC' - }" - :labels="{ - checked: this.$options.on, - unchecked: this.$options.off - }" - :width="60" - :height="30" - /> - </div> - </div> - </div> - <div v-if="!url" class="d-flex flex-row"> - <small - ><translate class="text-danger">Please enter a URL</translate></small - > - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Tolerance for snapping of waterway axis [m]</translate> - </small> - </div> - <div class="w-100"> - <input - @input="toleranceChanged" - class="tolerance form-control" - type="number" - min="0" - :value="tolerance" - /> - </div> - <div v-if="!tolerance" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a tolerance value</translate - ></small - > - </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, 2019 by via donau - * – Österreichische Wasserstraßen-Gesellschaft mbH - * Software engineering by Intevation GmbH - * - * Author(s): - * Thomas Junk <thomas.junk@intevation.de> - * Tom Gottfried <tom.gottfried@intevation.de> - */ -export default { - name: "bottleneckimport", - props: ["url", "tolerance", "directImport"], - methods: { - urlChanged(e) { - this.$emit("urlChanged", e.target.value); - }, - toleranceChanged(e) { - this.$emit("toleranceChanged", e.target.value); - } - } -}; -</script> - -<style></style>
--- a/client/src/components/importschedule/importtypes/Distancemarksashore.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -<template> - <div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-100"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>URL</translate> </small> - </div> - <div class="w-100"> - <input - @input="urlChanged" - class="url form-control" - type="url" - :value="url" - /> - </div> - </div> - </div> - <div v-if="!url" class="d-flex flex-row"> - <small - ><translate class="text-danger">Please enter a URL</translate></small - > - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>Featuretype</translate> </small> - </div> - <div class="w-100"> - <input - @input="featureTypeChanged" - class="featuretype form-control" - type="text" - :value="featureType" - /> - </div> - <div v-if="!featureType" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a Featuretype</translate - ></small - > - </div> - </div> - <div class="flex-column mt-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>SortBy</translate> </small> - </div> - <div class="w-100"> - <input - @input="sortByChanged" - class="sortby form-control" - type="text" - :value="sortBy" - /> - </div> - <div v-if="!sortBy" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter SortBy</translate - ></small - > - </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> - */ -export default { - name: "distancemarksashore", - props: ["url", "featureType", "sortBy"], - methods: { - urlChanged(e) { - this.$emit("urlChanged", e.target.value); - }, - featureTypeChanged(e) { - this.$emit("featureTypeChanged", e.target.value); - }, - sortByChanged(e) { - this.$emit("sortByChanged", e.target.value); - } - } -}; -</script> - -<style></style>
--- a/client/src/components/importschedule/importtypes/Distancemarksvirtual.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,118 +0,0 @@ -<template> - <div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-100"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>URL</translate> </small> - </div> - <div class="w-100"> - <input - @input="urlChanged" - class="url form-control" - type="url" - :value="url" - /> - </div> - </div> - </div> - <div v-if="!url" class="d-flex flex-row"> - <small - ><translate class="text-danger">Please enter a URL</translate></small - > - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>Username</translate> </small> - </div> - <div class="w-100"> - <input - @input="usernameChanged" - class="username form-control" - type="text" - :value="username" - /> - </div> - <div v-if="!username" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a Username</translate - ></small - > - </div> - </div> - <div class="flex-column mt-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>Password</translate> </small> - </div> - <div class="w-100 d-flex flex-row"> - <input - @input="passwordChanged" - class="pasword form-control" - :type="showPassword" - :value="password" - /> - <span - class="input-group-text ml-2" - @click="passwordVisible = !passwordVisible" - > - <font-awesome-icon - :icon="passwordVisible ? 'eye-slash' : 'eye'" - ></font-awesome-icon> - </span> - </div> - <div v-if="!password" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a Password</translate - ></small - > - </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> - */ -export default { - name: "distancemarksvirtual", - props: ["url", "username", "password"], - data() { - return { - passwordVisible: false - }; - }, - computed: { - showPassword() { - if (this.passwordVisible) return "text"; - return "password"; - } - }, - methods: { - urlChanged(e) { - this.$emit("urlChanged", e.target.value); - }, - usernameChanged(e) { - this.$emit("usernameChanged", e.target.value); - }, - passwordChanged(e) { - this.$emit("passwordChanged", e.target.value); - } - } -}; -</script> - -<style></style>
--- a/client/src/components/importschedule/importtypes/Fairwaydimensions.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,243 +0,0 @@ -<template> - <div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-100"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>URL</translate> </small> - </div> - <div class="w-100"> - <input - @input="urlChanged" - class="url form-control" - type="url" - :value="url" - /> - </div> - </div> - </div> - <div v-if="!url" class="d-flex flex-row"> - <small - ><translate class="text-danger">Please enter a URL</translate></small - > - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>Featuretype</translate> </small> - </div> - <div class="w-100"> - <input - @input="featureTypeChanged" - class="featuretype form-control" - type="text" - :value="featureType" - /> - </div> - <div v-if="!featureType" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a Featuretype</translate - ></small - > - </div> - </div> - <div class="flex-column mt-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>SortBy</translate> </small> - </div> - <div class="w-100"> - <input - @input="sortByChanged" - class="sortby form-control" - type="text" - :value="sortBy" - /> - </div> - <div v-if="!sortBy" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter SortBy</translate - ></small - > - </div> - </div> - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>LOS</translate> </small> - </div> - <div class="w-100"> - <select v-model="los" class="form-control"> - <option>1</option> - <option>2</option> - <option>3</option> - </select> - </div> - <div v-if="!LOS" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a level of service</translate - ></small - > - </div> - </div> - <div class="flex-column mt-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>Depth</translate> </small> - </div> - <div class="d-flex flex-row"> - <input - @input="depthChanged" - class="depth form-control" - type="number" - :value="depth" - /> - <div class="ml-2 my-auto">cm</div> - </div> - <div v-if="!depth" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a depth</translate - ></small - > - </div> - </div> - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>MinWidth</translate> </small> - </div> - <div class="d-flex flex-row"> - <input - @input="minWidthChanged" - class="minwidth form-control" - type="number" - :value="minWidth" - /> - <div class="ml-2 my-auto"> m</div> - </div> - <div v-if="!minWidth" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a minimum width</translate - ></small - > - </div> - </div> - <div class="flex-column mt-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>MaxWidth</translate> </small> - </div> - <div class="d-flex flex-row"> - <input - @input="maxWidthChanged" - class="maxwidth form-control" - type="number" - :value="maxWidth" - /> - <div class="ml-2 my-auto"> m</div> - </div> - <div v-if="!maxWidth" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a maximum width</translate - ></small - > - </div> - </div> - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> - <translate>Source orgranization</translate> - </small> - </div> - <div class="w-100"> - <input - @input="sourceOrganizationChanged" - class="sourceorganization form-control" - type="text" - :value="sourceOrganization" - /> - </div> - <div v-if="!sourceOrganization" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a source orgranization</translate - ></small - > - </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> - */ -export default { - name: "fairwaydimensions", - props: [ - "url", - "featureType", - "sortBy", - "depth", - "LOS", - "minWidth", - "maxWidth", - "sourceOrganization" - ], - methods: { - urlChanged(e) { - this.$emit("urlChanged", e.target.value); - }, - featureTypeChanged(e) { - this.$emit("featureTypeChanged", e.target.value); - }, - sortByChanged(e) { - this.$emit("sortByChanged", e.target.value); - }, - depthChanged(e) { - this.$emit("depthChanged", e.target.value * 1); - }, - LOSChanged(e) { - this.$emit("LOSChanged", e.target.value * 1); - }, - minWidthChanged(e) { - this.$emit("minWidthChanged", e.target.value * 1); - }, - maxWidthChanged(e) { - this.$emit("maxWidthChanged", e.target.value * 1); - }, - sourceOrganizationChanged(e) { - this.$emit("sourceOrganizationChanged", e.target.value); - } - }, - computed: { - los: { - get() { - return this.LOS; - }, - set(value) { - this.$emit("LOSChanged", value * 1); - } - } - } -}; -</script> - -<style></style>
--- a/client/src/components/importschedule/importtypes/Gaugemeasurement.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +0,0 @@ -<template> - <div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-100"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>URL</translate> </small> - </div> - <div class="w-100"> - <input - @input="urlChanged" - class="url form-control" - type="url" - :value="url" - /> - </div> - </div> - </div> - <div v-if="!url" class="d-flex flex-row"> - <small - ><translate class="text-danger">Please enter a URL</translate></small - > - </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> - */ -export default { - name: "gaugemeasurement", - props: ["url"], - methods: { - urlChanged(e) { - this.$emit("urlChanged", e.target.value); - } - } -}; -</script> - -<style></style>
--- a/client/src/components/importschedule/importtypes/Waterwayarea.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -<template> - <div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-100"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>URL</translate> </small> - </div> - <div class="w-100"> - <input - @input="urlChanged" - class="url form-control" - type="url" - :value="url" - /> - </div> - </div> - </div> - <div v-if="!url" class="d-flex flex-row"> - <small - ><translate class="text-danger">Please enter a URL</translate></small - > - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>Featuretype</translate> </small> - </div> - <div class="w-100"> - <input - @input="featureTypeChanged" - class="featuretype form-control" - type="text" - :value="featureType" - /> - </div> - <div v-if="!featureType" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a Featuretype</translate - ></small - > - </div> - </div> - <div class="flex-column mt-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>SortBy</translate> </small> - </div> - <div class="w-100"> - <input - @input="sortByChanged" - class="sortby form-control" - type="text" - :value="sortBy" - /> - </div> - <div v-if="!sortBy" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter SortBy</translate - ></small - > - </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> - */ -export default { - name: "waterwayarea", - props: ["url", "featureType", "sortBy"], - methods: { - urlChanged(e) { - this.$emit("urlChanged", e.target.value); - }, - featureTypeChanged(e) { - this.$emit("featureTypeChanged", e.target.value); - }, - sortByChanged(e) { - this.$emit("sortByChanged", e.target.value); - } - } -}; -</script> - -<style></style>
--- a/client/src/components/importschedule/importtypes/Waterwayaxis.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -<template> - <div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-100"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>URL</translate> </small> - </div> - <div class="w-100"> - <input - @input="urlChanged" - class="url form-control" - type="url" - :value="url" - /> - </div> - </div> - </div> - <div v-if="!url" class="d-flex flex-row"> - <small - ><translate class="text-danger">Please enter a URL</translate></small - > - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>Featuretype</translate> </small> - </div> - <div class="w-100"> - <input - @input="featureTypeChanged" - class="featuretype form-control" - type="text" - :value="featureType" - /> - </div> - <div v-if="!featureType" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a Featuretype</translate - ></small - > - </div> - </div> - <div class="flex-column mt-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>SortBy</translate> </small> - </div> - <div class="w-100"> - <input - @input="sortByChanged" - class="sortby form-control" - type="text" - :value="sortBy" - /> - </div> - <div v-if="!sortBy" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter SortBy</translate - ></small - > - </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> - */ -export default { - name: "waterwayaxis", - props: ["url", "featureType", "sortBy"], - methods: { - urlChanged(e) { - this.$emit("urlChanged", e.target.value); - }, - featureTypeChanged(e) { - this.$emit("featureTypeChanged", e.target.value); - }, - sortByChanged(e) { - this.$emit("sortByChanged", e.target.value); - } - } -}; -</script> - -<style></style>
--- a/client/src/components/importschedule/importtypes/Waterwaygauges.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,118 +0,0 @@ -<template> - <div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-100"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>URL</translate> </small> - </div> - <div class="w-100"> - <input - @input="urlChanged" - class="url form-control" - type="url" - :value="url" - /> - </div> - </div> - </div> - <div v-if="!url" class="d-flex flex-row"> - <small - ><translate class="text-danger">Please enter a URL</translate></small - > - </div> - <div class="d-flex flex-row"> - <div class="flex-column mt-3 mr-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>Username</translate> </small> - </div> - <div class="w-100"> - <input - @input="usernameChanged" - class="username form-control" - type="text" - :value="username" - /> - </div> - <div v-if="!username" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a Username</translate - ></small - > - </div> - </div> - <div class="flex-column mt-3 w-50"> - <div class="flex-row text-left"> - <small class="text-muted"> <translate>Password</translate> </small> - </div> - <div class="w-100 d-flex flex-row"> - <input - @input="passwordChanged" - class="password form-control" - :type="showPassword" - :value="password" - /> - <span - class="input-group-text ml-2" - @click="passwordVisible = !passwordVisible" - > - <font-awesome-icon - :icon="passwordVisible ? 'eye-slash' : 'eye'" - ></font-awesome-icon> - </span> - </div> - <div v-if="!password" class="d-flex flex-row"> - <small - ><translate class="text-danger" - >Please enter a Password</translate - ></small - > - </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> - */ -export default { - name: "waterwaygauges", - props: ["username", "password", "url"], - data() { - return { - passwordVisible: false - }; - }, - computed: { - showPassword() { - if (this.passwordVisible) return "text"; - return "password"; - } - }, - methods: { - urlChanged(e) { - this.$emit("urlChanged", e.target.value); - }, - usernameChanged(e) { - this.$emit("usernameChanged", e.target.value); - }, - passwordChanged(e) { - this.$emit("passwordChanged", e.target.value); - } - } -}; -</script> - -<style></style>
--- a/client/src/components/layers/Layers.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/layers/Layers.vue Mon Jun 03 10:19:18 2019 +0200 @@ -5,22 +5,40 @@ { expanded: showLayers } ]" > - <div style="width: 18rem"> + <div class="position-relative" style="width: 18rem; min-height: 350px;"> <UIBoxHeader icon="layer-group" - :title="layersLabel" + :title="label" :closeCallback="close" + :actions="[ + { + callback: refreshLayers, + icon: sourcesLoading ? 'spinner' : 'sync' + } + ]" /> - <div class="box-body small"> - <Layerselect - v-for="(layer, index) in layersForLegend" - :layerindex="index" - :layername="layer.name" - :key="layer.name" - :isVisible="layer.isVisible" - @visibilityToggled="visibilityToggled" - ></Layerselect> + <div class="small" v-if="openLayersMaps.length"> + <Layerselect layerId="OPENSTREETMAP" /> + <Layerselect layerId="INLANDECDIS" /> + <Layerselect layerId="WATERWAYAREA" /> + <Layerselect layerId="STRETCHES" /> + <Layerselect layerId="SECTIONS" /> + <Layerselect layerId="FAIRWAYDIMENSIONSLOS3" /> + <Layerselect layerId="FAIRWAYDIMENSIONSLOS2" /> + <Layerselect layerId="FAIRWAYDIMENSIONSLOS1" /> + <Layerselect layerId="WATERWAYAXIS" /> + <Layerselect layerId="WATERWAYPROFILES" /> + <Layerselect layerId="BOTTLENECKS" /> + <Layerselect layerId="BOTTLENECKISOLINE" /> + <Layerselect layerId="DIFFERENCES" /> + <Layerselect layerId="BOTTLENECKSTATUS" /> + <Layerselect layerId="BOTTLENECKFAIRWAYAVAILABILITY" /> + <Layerselect layerId="DATAAVAILABILITY" /> + <Layerselect layerId="DISTANCEMARKS" /> + <Layerselect layerId="DISTANCEMARKSAXIS" /> + <Layerselect layerId="GAUGES" /> </div> + <UISpinnerOverlay v-else style="top: 34px;" /> </div> </div> </template> @@ -40,25 +58,35 @@ * Thomas Junk <thomas.junk@intevation.de> * Markus Kottländer <markus.kottlaender@intevation.de> */ -import { mapGetters, mapState } from "vuex"; +import { mapState } from "vuex"; + export default { - name: "layers", components: { Layerselect: () => import("./Layerselect") }, computed: { - ...mapGetters("map", ["layersForLegend"]), ...mapState("application", ["showLayers"]), - layersLabel() { - return this.$gettext("Layers"); + ...mapState("map", ["openLayersMaps"]), + label() { + return this.$gettext("Map Layers"); + }, + sourcesLoading() { + let counter = 0; + this.openLayersMaps.forEach(map => { + let layers = map.getLayers().getArray(); + for (let i = 0; i < layers.length; i++) { + if (layers[i].getSource().loading) counter++; + } + }); + return counter; } }, methods: { close() { this.$store.commit("application/showLayers", false); }, - visibilityToggled(layer) { - this.$store.commit("map/toggleVisibility", layer); + refreshLayers() { + this.$store.dispatch("map/refreshLayers"); } } };
--- a/client/src/components/layers/Layerselect.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/layers/Layerselect.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,38 +1,33 @@ <template> - <div> - <div class="form-check d-flex flex-row flex-start selection"> + <div + class="d-flex flex-column flex-start px-2 border-bottom" + style="border-bottom-style: dashed !important;" + > + <div class="d-flex"> <input - class="form-check-input" - @change="visibilityToggled" - :id="layername" + v-for="map in openLayersMaps" + :key="map.getTarget()" + class="mt-1 mr-1" type="checkbox" - :checked="isVisible" + @change="toggle(map)" + :checked="isVisible(map)" /> - <LegendElement - :layername="layername" - :layerindex="layerindex" - ></LegendElement> - <label - class="pointer layername form-check-label" - @click="visibilityToggled" - >{{ layername }}</label - > + <LegendElement class="pointer" :layer="layer" @click.native="toggle()" /> + <label class="pointer layername form-check-label ml-1" @click="toggle()"> + {{ label }} + </label> </div> - <div v-if="isVisible && isBottleneckIsolineLayer"> - <img class="rounded my-1 d-block" :src="isolinesLegendImgDataURL" /> + <div> + <div v-if="isVisible() && layer.get('id') === 'BOTTLENECKISOLINE'"> + <img class="rounded my-1 d-block" :src="isolinesLegendImgDataURL" /> + </div> + <div v-if="isVisible() && layer.get('id') === 'DIFFERENCES'"> + <img class="rounded my-1 d-block" :src="differencesLegendImgDataURL" /> + </div> </div> </div> </template> -<style lang="scss" scoped> -.selection { - text-align: left; -} -.layername { - margin-left: $small-offset; -} -</style> - <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -49,45 +44,101 @@ * * Bernhard Reiter <bernhard.reiter@intevation.de> */ import { HTTP } from "@/lib/http"; -import { mapState } from "vuex"; -import { LAYERS } from "@/store/map.js"; +import { displayError } from "@/lib/errors"; +import { mapState, mapGetters } from "vuex"; + export default { - props: ["layername", "layerindex", "isVisible"], - name: "layerselect", components: { - LegendElement: () => import("./LegendElement.vue") + LegendElement: () => import("./LegendElement") }, + props: ["layerId"], computed: { - ...mapState("map", ["isolinesLegendImgDataURL"]), - isBottleneckIsolineLayer() { - return this.layername == LAYERS.BOTTLENECKISOLINE; + ...mapState("map", [ + "openLayersMaps", + "isolinesLegendImgDataURL", + "differencesLegendImgDataURL" + ]), + ...mapGetters("map", ["openLayersMap"]), + layer() { + return this.openLayersMap().getLayer(this.layerId); + }, + label() { + return this.$gettext(this.layer.get("label")); } }, methods: { - visibilityToggled() { - this.$emit("visibilityToggled", this.layerindex); + toggle(map) { + if (map) { + map + .getLayer(this.layerId) + .setVisible(!map.getLayer(this.layerId).getVisible()); + if ( + this.layerId === "GAUGES" || + this.layerId === "STRETCHES" || + this.layerId === "BOTTLENECKS" || + this.layerId === "SECTIONS" + ) { + map.getLayer("DATAAVAILABILITY").changed(); + } + } else { + this.openLayersMaps.forEach(m => { + m.getLayer(this.layerId).setVisible( + !m.getLayer(this.layerId).getVisible() + ); + }); + } + }, + isVisible(map) { + if (map) { + return map.getLayer(this.layerId).getVisible(); + } else { + let isVisible = false; + this.openLayersMaps.forEach(m => { + if (m.getLayer(this.layerId).getVisible()) { + isVisible = true; + } + }); + return isVisible; + } + }, + loadLegendImage(layer, storeTarget) { + HTTP.get( + `/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.0.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=${layer}&legend_options=columns:4;fontAntiAliasing:true`, + { + headers: { + Accept: "image/png", + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + } + ) + .then(response => { + const reader = new FileReader(); + reader.onload = event => { + this.$store.commit("map/" + storeTarget, event.target.result); + }; + reader.readAsDataURL(response.data); + }) + .catch(error => { + displayError({ + title: this.$gettext("Backend Error"), + message: `${error.response.status}: ${error.response.statusText}` + }); + }); } }, created() { - // fetch legend image for bottleneck isolines - // directly read it as dataURL so it is reusable later - if (this.isBottleneckIsolineLayer) { - 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 that = this; - const reader = new FileReader(); - reader.onload = function() { - that.$store.commit("map/isolinesLegendImgDataURL", this.result); - }; - reader.readAsDataURL(response.data); - }); + if (this.layer.get("id") === "BOTTLENECKISOLINE") { + this.loadLegendImage( + "sounding_results_contour_lines_geoserver", + "isolinesLegendImgDataURL" + ); + } + if (this.layer.get("id") === "DIFFERENCES") { + this.loadLegendImage( + "sounding_differences", + "differencesLegendImgDataURL" + ); } } };
--- a/client/src/components/layers/LegendElement.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/layers/LegendElement.vue Mon Jun 03 10:19:18 2019 +0200 @@ -16,32 +16,37 @@ * Author(s): * Thomas Junk <thomas.junk@intevation.de> */ -import { mapGetters } from "vuex"; +import { mapState } 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 { Vector as VectorLayer } from "ol/layer"; +import { Vector as VectorSource } from "ol/source"; +import LineString from "ol/geom/LineString"; import Point from "ol/geom/Point"; +import { HTTP } from "@/lib/http"; export default { - name: "legendelement", - props: ["layername", "layerindex"], + props: ["layer"], data: function() { return { - myMap: null, - mapLayer: null + myMap: null }; }, computed: { - ...mapGetters("map", ["getLayerByName"]), + ...mapState("map", ["layers"]), id() { - return "legendelement" + this.layerindex; + return ( + "legendelement-" + + this.layer + .get("label") + .toLowerCase() + .replace(/\s/g, "") + ); }, mstyle() { - if (this.mapLayer && this.mapLayer.data.getStyle) { - return this.mapLayer.data.getStyle(); + if (this.layer && this.layer.getStyle) { + return this.layer.getStyle(); } } }, @@ -57,11 +62,38 @@ } }, mounted() { - this.mapLayer = this.getLayerByName(this.layername); - if (this.mapLayer.data.getType() == "VECTOR") { + if (this.layer.getType() == "VECTOR") { this.initMap(); } else { - // TODO other tiles + if ( + this.layer.get("id") === "OPENSTREETMAP" || + this.layer.get("id") === "INLANDECDIS" || + this.layer.get("id") === "BOTTLENECKISOLINE" || + this.layer.get("id") === "DIFFERENCES" + ) { + // TODO: Do something useful? + return; + } + let img = document.createElement("img"); + img.setAttribute("style", "margin: 0 auto;display: flex;"); + if (this.layer.get("id") === "DISTANCEMARKSAXIS") { + img.setAttribute("src", require("@/assets/distancemarks-axis.png")); + } else { + // for simple WMS legends. + let url = + `/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.0.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=` + + this.layer.getSource().getParams().LAYERS + + `&legend_options=columns:4;fontAntiAliasing:true`; + HTTP.get(url, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + img.setAttribute("src", URL.createObjectURL(response.data)); + }); + } + this.$el.appendChild(img); } }, methods: { @@ -81,28 +113,29 @@ }); }, createVectorLayer() { - let mapStyle = this.mapLayer.data.getStyle(); + let mapStyle = this.layer.getStyle(); let feature = new Feature({ - geometry: new LineString([[-1, 0.5], [0, 0], [0.7, 0], [1.3, -0.7]]) + geometry: new LineString([[-1, -1], [0, 0], [1, 1]]) }); // 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) { + let legendStyle = this.layer.get("forLegendStyle"); + if (legendStyle) { + if (legendStyle) { feature.setGeometry(new Point([0, 0])); } - mapStyle = this.mapLayer.data.getStyleFunction()( + mapStyle = this.layer.getStyleFunction()( feature, - this.mapLayer.forLegendStyle.resolution, + legendStyle.resolution, true ); } // 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. + // this.layer["forLegendStyle"] for it. // FIXME, this is a special case for the Fairway Dimensions style feature.set("level_of_service", ""); return new VectorLayer({
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/Map.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,308 @@ +<template> + <div + :id="'map-' + paneId" + :class="['map', { nocursor: this.hasActiveInteractions }]" + > + <Zoom :map="map" /> + </div> +</template> + +<style lang="sass" scoped> +.map + width: 100% + height: 100% + background-color: #eee + background-image: linear-gradient(45deg, #e8e8e8 25%, transparent 25%, transparent 75%, #e8e8e8 75%, #e8e8e8), linear-gradient(45deg, #e8e8e8 25%, transparent 25%, transparent 75%, #e8e8e8 75%, #e8e8e8) + background-size: 20px 20px + background-position: 0 0, 10px 10px + + &.nocursor + cursor: 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, 2019 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 { mapState } from "vuex"; +import { Map, View } from "ol"; +import { Stroke, Style, Fill } from "ol/style"; +import { displayError } from "@/lib/errors"; +import { pane } from "@/lib/mixins"; +import layers from "@/components/map/layers"; +import "ol/ol.css"; + +/* for the sake of debugging */ +/* eslint-disable no-console */ +export default { + mixins: [pane], + components: { + Zoom: () => import("@/components/map/Zoom") + }, + data() { + return { + map: null + }; + }, + computed: { + ...mapState("map", ["initialLoad", "extent", "syncedMaps", "syncedView"]), + ...mapState("bottlenecks", ["selectedSurvey"]), + ...mapState("fairwayprofile", ["additionalSurvey"]), + ...mapState("application", ["paneSetup", "paneRotate"]), + ...mapState("imports", ["selectedStretchId", "selectedSectionId"]), + layers() { + return layers(this.paneId); + }, + hasActiveInteractions() { + return ( + this.map && + this.map + .getInteractions() + .getArray() + .filter( + i => + ["linetool", "polygontool", "cuttool"].includes(i.get("id")) && + i.getActive() + ).length + ); + } + }, + watch: { + paneSetup() { + this.$nextTick(() => this.map.updateSize()); + }, + paneRotate() { + this.$nextTick(() => this.map.updateSize()); + }, + syncedMaps(syncedMaps) { + if (syncedMaps.includes(this.paneId) || this.paneId === "main") { + this.map.setView(this.syncedView); + } else { + this.map.setView( + new View({ + center: [this.extent.lon, this.extent.lat], + minZoom: 5, // restrict zooming out to ~size of Europe for width 1000px + zoom: this.extent.zoom, + projection: "EPSG:3857" + }) + ); + } + }, + selectedSurvey(survey) { + if (this.paneId === "main") { + if (survey) { + this.updateBottleneckFilter(survey.bottleneck_id, survey.date_info); + } else { + this.updateBottleneckFilter("does_not_exist", "1999-10-01"); + } + } + }, + additionalSurvey(survey) { + if (this.paneId === "compare-survey") { + if (survey) { + this.updateBottleneckFilter(survey.bottleneck_id, survey.date_info); + } else { + this.updateBottleneckFilter("does_not_exist", "1999-10-01"); + } + } + }, + selectedStretchId(id) { + this.layers + .get("STRETCHES") + .getSource() + .getFeatures() + .forEach(f => { + f.set("highlighted", false); + if (id === f.getId()) { + f.set("highlighted", true); + } + }); + }, + selectedSectionId(id) { + this.layers + .get("SECTIONS") + .getSource() + .getFeatures() + .forEach(f => { + f.set("highlighted", false); + if (id === f.getId()) { + f.set("highlighted", true); + } + }); + } + }, + methods: { + updateBottleneckFilter(bottleneck_id, datestr) { + const exists = bottleneck_id != "does_not_exist"; + + if (exists) { + this.layers + .get("BOTTLENECKISOLINE") + .getSource() + .updateParams({ + cql_filter: `date_info='${datestr}' AND bottleneck_id='${bottleneck_id}'` + }); + } + this.layers.get("BOTTLENECKISOLINE").setVisible(exists); + }, + initMap() { + if (!this.syncedView) { + this.$store.commit( + "map/syncedView", + new View({ + center: [this.extent.lon, this.extent.lat], + minZoom: 5, // restrict zooming out to ~size of Europe for width 1000px + zoom: this.extent.zoom, + projection: "EPSG:3857" + }) + ); + } + + // move to user specific default extent if map loads for the first time + // checking initialLoad will be obsolete once we abandoned the separated admin context + if (this.initialLoad) { + this.$store.commit("map/initialLoad", false); + var currentUser = this.$store.state.user.user; + HTTP.get("/users/" + currentUser, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }) + .then(response => { + this.mountMap(); + this.$store.dispatch("map/moveToBoundingBox", { + boundingBox: [ + response.data.extent.x1, + response.data.extent.y1, + response.data.extent.x2, + response.data.extent.y2 + ], + zoom: 17, + preventZoomOut: true, + duration: 0 + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } else { + this.mountMap(); + } + }, + mountMap() { + this.map = new Map({ + layers: this.layers.config, + target: "map-" + this.paneId, + controls: [], + view: + this.syncedMaps.includes(this.paneId) || this.paneId === "main" + ? this.syncedView + : new View({ + center: [this.extent.lon, this.extent.lat], + minZoom: 5, + zoom: this.extent.zoom, + projection: "EPSG:3857" + }) + }); + this.map.getLayer = id => this.layers.get(id); + + // store map position on every move + // will be obsolete once we abandoned the separated admin context + this.map.on("moveend", event => { + const center = event.map.getView().getCenter(); + this.$store.commit("map/extent", { + lat: center[1], + lon: center[0], + zoom: event.map.getView().getZoom() + }); + }); + this.$store.dispatch("map/openLayersMap", this.map); + this.$store.dispatch("map/initIdentifyTool", this.map); + } + }, + mounted() { + // ToDo set path to correct endpoint in order to retrieve an OSM URL + HTTP.get("/system/config", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + if (response.data["osm-url"]) { + this.layers + .get("OPENSTREETMAP") + .getSource() + .setUrl(response.data["osm-url"]); + } + this.initMap(); + + if (this.selectedSurvey && this.paneId === "main") { + this.updateBottleneckFilter( + this.selectedSurvey.bottleneck_id, + this.selectedSurvey.date_info + ); + } + if (this.additionalSurvey && this.paneId === "compare-survey") { + this.updateBottleneckFilter( + this.additionalSurvey.bottleneck_id, + this.additionalSurvey.date_info + ); + } + // load configured bottleneck colors + HTTP.get("/system/style/Bottlenecks/stroke", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + let btlnStrokeC = response.data.code; + HTTP.get("/system/style/Bottlenecks/fill", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + let btlnFillC = response.data.code; + var newStyle = new Style({ + stroke: new Stroke({ + color: btlnStrokeC, + width: 4 + }), + fill: new Fill({ + color: btlnFillC + }) + }); + this.layers.get("BOTTLENECKS").setStyle(newStyle); + }) + .catch(error => { + console.log(error); + }); + }) + .catch(error => { + console.log(error); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }); + }, + destroyed() { + this.$store.commit("map/removeOpenLayersMap", this.map); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/MapPopup.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,255 @@ +<template> + <div class="map-popup rounded" ref="map-popup"> + <UIBoxHeader :title="title" :closeCallback="close" small /> + <div class="p-1 small text-nowrap" style="margin-top: -0.25rem"> + <div + class="d-flex flex-nowrap justify-content-between align-items-center mt-1" + v-if="bottlenecks.length" + v-for="bottleneck in bottlenecks" + :key="bottleneck.get('objnam')" + > + <div class="mr-2"> + <font-awesome-icon icon="ship" class="mr-1" fixed-width /> + {{ bottleneck.get("objnam") }} + </div> + <div> + <button + class="btn btn-xs btn-info" + v-tooltip="surveysLabel" + @click="openSurveys(bottleneck)" + > + <font-awesome-icon icon="chart-area" fixed-width /> + </button> + <button + class="btn btn-xs btn-info ml-1" + v-tooltip="fairwayAvailabilityLabel" + @click="openFairwayAvailabilityForBottleneck(bottleneck)" + > + <font-awesome-icon icon="chart-line" fixed-width /> + </button> + </div> + </div> + + <div + class="d-flex flex-nowrap justify-content-between align-items-center mt-1" + v-if="gauges.length" + v-for="gauge in gauges" + :key="gauge.get('objname')" + > + <div class="mr-2"> + <font-awesome-icon icon="ruler-vertical" class="mr-1" fixed-width /> + {{ gauge.get("objname") }} + </div> + <button + class="btn btn-xs btn-info" + v-tooltip="waterlevelsLabel" + @click="openGauges(gauge)" + > + <font-awesome-icon icon="ruler-vertical" fixed-width /> + </button> + </div> + + <div + class="d-flex flex-nowrap justify-content-between align-items-center mt-1" + v-if="stretches.length" + v-for="stretch in stretches" + :key="stretch.get('objnam')" + > + <div class="mr-2"> + <font-awesome-icon icon="road" class="mr-1" fixed-width /> + {{ stretch.get("objnam") }} + </div> + <button + class="btn btn-xs btn-info" + v-tooltip="fairwayAvailabilityLabel" + @click="openFairwayAvailabilityForStretch(stretch)" + > + <font-awesome-icon icon="chart-line" fixed-width /> + </button> + </div> + + <div + class="d-flex flex-nowrap justify-content-between align-items-center mt-1" + v-if="sections.length" + v-for="section in sections" + :key="section.get('objnam')" + > + <div class="mr-2"> + <font-awesome-icon icon="road" class="mr-1" fixed-width /> + {{ section.get("objnam") }} + </div> + <button + class="btn btn-xs btn-info" + v-tooltip="fairwayAvailabilityLabel" + @click="openFairwayAvailabilityForSection(section)" + > + <font-awesome-icon icon="chart-line" fixed-width /> + </button> + </div> + </div> + <div + v-if="identifiedCoordinates" + class="border-top text-muted p-1 coordinates" + > + Lat: {{ identifiedCoordinates[1].toFixed(8) }}, Lon: + {{ identifiedCoordinates[0].toFixed(8) }} + </div> + </div> +</template> + +<style lang="sass"> +.map-popup + position: absolute + background: #fff + min-width: 200px + min-height: 85px + box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2) + border-top-left-radius: 0 !important + margin-left: 10px + &::before + content: "" + position: absolute + top: 0 + left: -10px + border: 5px solid transparent + border-top: 5px solid white + border-right: 5px solid white + .coordinates + font-size: 70% +</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"; +import Overlay from "ol/Overlay.js"; +import { getCenter } from "ol/extent"; + +export default { + computed: { + ...mapState("map", [ + "mapPopup", + "identifiedFeatures", + "identifiedCoordinates" + ]), + title() { + return this.$gettext("Identified Features"); + }, + bottlenecks() { + return this.identifiedFeatures.filter(f => + /^bottlenecks/.test(f.getId()) + ); + }, + gauges() { + return this.identifiedFeatures.filter(f => /^gauges/.test(f.getId())); + }, + stretches() { + return this.identifiedFeatures.filter(f => /^stretches/.test(f.getId())); + }, + sections() { + return this.identifiedFeatures.filter(f => /^sections/.test(f.getId())); + }, + surveysLabel() { + return this.$gettext("Surveys"); + }, + fairwayAvailabilityLabel() { + return this.$gettext("Fairway Availability"); + }, + waterlevelsLabel() { + return this.$gettext("Waterlevels"); + } + }, + methods: { + close() { + this.mapPopup.setPosition(undefined); + }, + openSurveys(bottleneck) { + this.$store.commit("application/showProfiles", true); + this.$store.dispatch( + "bottlenecks/setSelectedBottleneck", + bottleneck.get("objnam") + ); + this.$store.dispatch("map/moveMap", { + coordinates: getCenter( + bottleneck + .getGeometry() + .clone() + .transform("EPSG:3857", "EPSG:4326") + .getExtent() + ), + zoom: 17, + preventZoomOut: true + }); + this.close(); + }, + openGauges(gauge) { + this.$store.commit("application/showGauges", true); + this.$store.dispatch("gauges/selectedGaugeISRS", gauge.get("isrs_code")); + this.close(); + }, + openFairwayAvailability() { + this.$store.commit("application/showFairwayDepth", true); + this.close(); + }, + openFairwayAvailabilityForBottleneck(bottleneck) { + this.$store.commit("fairwayavailability/type", "bottlenecks"); + this.$store.dispatch( + "bottlenecks/setSelectedBottleneck", + bottleneck.get("objnam") + ); + this.$store.dispatch("map/moveMap", { + coordinates: getCenter( + bottleneck + .getGeometry() + .clone() + .transform("EPSG:3857", "EPSG:4326") + .getExtent() + ), + zoom: 17, + preventZoomOut: true + }); + this.openFairwayAvailability(); + }, + openFairwayAvailabilityForStretch(stretch) { + this.$store.commit("fairwayavailability/type", "stretches"); + this.$store.commit("imports/selectedStretchId", stretch.getId()); + this.$store.dispatch("map/moveToFeauture", { + feature: stretch, + zoom: 17 + }); + this.openFairwayAvailability(); + }, + openFairwayAvailabilityForSection(section) { + this.$store.commit("fairwayavailability/type", "sections"); + this.$store.commit("imports/selectedSectionId", section.getId()); + this.$store.dispatch("map/moveToFeauture", { + feature: section, + zoom: 17 + }); + this.openFairwayAvailability(); + } + }, + mounted() { + const mapPopup = new Overlay({ + element: this.$refs["map-popup"], + autoPan: true, + autoPanAnimation: { + duration: 250 + } + }); + this.$store.commit("map/mapPopup", mapPopup); + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/Zoom.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,72 @@ +<template> + <div class="zoom-buttons shadow-xs"> + <button + class="zoom-button border-0 bg-white rounded-left ui-element" + @click="zoomOut" + > + <font-awesome-icon icon="minus" /> + </button> + <button + class="zoom-button border-0 bg-white rounded-right ui-element border-right" + @click="zoomIn" + > + <font-awesome-icon icon="plus" /> + </button> + </div> +</template> + +<style lang="sass"> +.zoom-buttons + position: absolute + z-index: 1 + bottom: $small-offset + left: 50% + margin-left: -$icon-width + margin-bottom: 0 + transition: margin-bottom 0.3s + + .zoom-button + min-height: $icon-width + min-width: $icon-width + z-index: 1 + outline: none + color: #666 +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus@intevation.de> + * Thomas Junk <thomas.junk@intevation.de> + */ +export default { + props: ["map"], + computed: { + zoomLevel: { + get() { + return this.map.getView().getZoom(); + }, + set(value) { + this.map.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/layers.js Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,644 @@ +import TileWMS from "ol/source/TileWMS"; +import { + Tile as TileLayer, + Vector as VectorLayer, + Image as ImageLayer +} from "ol/layer"; +import { Icon, Stroke, Style } from "ol/style"; +import VectorSource from "ol/source/Vector"; +import { ImageWMS as ImageSource } from "ol/source"; +import Point from "ol/geom/Point"; +import { bbox as bboxStrategy } from "ol/loadingstrategy"; +import { WFS, GeoJSON } from "ol/format"; +import OSM from "ol/source/OSM"; +import { equalTo } from "ol/format/filter"; +import { HTTP } from "@/lib/http"; +import styleFactory from "./styles"; +import store from "@/store/index"; + +const buildVectorLoader = ( + featureRequestOptions, + vectorSource, + bboxStrategyDisabled, + featurePostProcessor +) => { + // 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: the geometryName has to be given in featureRequestOptions if + // bboxStrategy (default) is used + featureRequestOptions.featureNS = "gemma"; + featureRequestOptions.featurePrefix = "gemma"; + featureRequestOptions.outputFormat = "application/json"; + return (extent, resolution, projection) => { + if (!bboxStrategyDisabled) { + featureRequestOptions.bbox = extent; + } + featureRequestOptions.srsName = projection.getCode(); + HTTP.post( + "/internal/wfs", + new XMLSerializer().serializeToString( + new WFS().writeGetFeature(featureRequestOptions) + ), + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + } + ) + .then(response => { + const features = new GeoJSON().readFeatures( + JSON.stringify(response.data) + ); + if (featurePostProcessor) { + features.map(f => featurePostProcessor(f, store, features)); + } + vectorSource.addFeatures(features); + }) + .catch(() => { + vectorSource.removeLoadedExtent(extent); + }); + }; +}; + +// SHARED LAYERS: +// DRAW- and CUTLAYER are shared across maps. E.g. you want to see the cross cut +// arrow on both maps when comparing surveys. So we don't need to initialize a +// new VectorLayer object for each map. Instead we use these two constants so +// that all maps use the same object. +const DRAWLAYER = new VectorLayer({ + id: "DRAWTOOL", + label: "Draw Tool", + visible: true, + source: new VectorSource({ wrapX: false }), + style: function(feature) { + // adapted from OpenLayer's LineString Arrow Example + var geometry = feature.getGeometry(); + var styles = [ + // linestring + new Style({ + stroke: new Stroke({ + color: "#369aca", + width: 2 + }) + }) + ]; + + if (geometry.getType() === "LineString") { + geometry.forEachSegment(function(start, end) { + var dx = end[0] - start[0]; + var dy = end[1] - start[1]; + var rotation = Math.atan2(dy, dx); + // arrows + styles.push( + new Style({ + geometry: new Point(end), + image: new Icon({ + // we need to make sure the image is loaded by Vue Loader + 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 + // anti-aliasing, but the image is not placed with subpixel + // precision + anchor: [0.75, 0.5], + rotateWithView: true, + rotation: -rotation + }) + }) + ); + }); + } + return styles; + } +}); + +const CUTLAYER = new VectorLayer({ + id: "CUTTOOL", + label: "Cut Tool", + visible: true, + source: new VectorSource({ wrapX: false }), + style: function(feature) { + // adapted from OpenLayer's LineString Arrow Example + var geometry = feature.getGeometry(); + var styles = [ + // linestring + new Style({ + stroke: new Stroke({ + color: "#333333", + width: 2, + lineDash: [7, 7] + }) + }) + ]; + + if (geometry.getType() === "LineString") { + geometry.forEachSegment(function(start, end) { + var dx = end[0] - start[0]; + var dy = end[1] - start[1]; + var rotation = Math.atan2(dy, dx); + // arrows + styles.push( + new Style({ + geometry: new Point(end), + image: new Icon({ + // we need to make sure the image is loaded by Vue Loader + 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 + // anti-aliasing, but the image is not placed with subpixel + // precision + anchor: [0.75, 0.5], + rotateWithView: true, + rotation: -rotation + }) + }) + ); + }); + } + return styles; + } +}); + +export default function(mapId) { + const styles = styleFactory(mapId); + // Shared feature source for layers: + // BOTTLENECKS, BOTTLENECKSTATUS and BOTTLENECKFAIRWAYAVAILABILITY + // Reduces bottlenecks_geoserver requests and number of stored feature objects. + const bottlenecksSource = new VectorSource({ strategy: bboxStrategy }); + bottlenecksSource.setLoader( + buildVectorLoader( + { + featureTypes: ["bottlenecks_geoserver"], + geometryName: "area" + }, + bottlenecksSource, + false, + async (f, store) => { + if (f.get("fa_critical")) { + // look for fairway availability data in store. If present and + // not older than 15 min use it or fetch new data and store it. + let data = store.getters["fairwayavailability/fwLNWLOverviewData"](f); + if ( + data && + new Date().getTime() - data.createdAt.getTime() < 900000 + ) { + f.set("fa_data", data.data); + } else { + let date = new Date(); + data = await store.dispatch( + "fairwayavailability/loadAvailableFairwayDepthLNWLForMap", + { + feature: f, + from: date.toISOString().split("T")[0], + to: date.toISOString().split("T")[0], + frequency: "monthly", + LOS: 3 + } + ); + if (data) { + store.commit("fairwayavailability/addFwLNWLOverviewData", { + feature: f, + data, + createdAt: new Date() + }); + f.set("fa_data", data); + } + } + } + return f; + } + ) + ); + + return { + get(id) { + return this.config.find(l => l.get("id") === id); + }, + config: [ + new TileLayer({ + id: "OPENSTREETMAP", + label: "Open Streetmap", + visible: true, + source: new OSM() + }), + new ImageLayer({ + id: "INLANDECDIS", + label: "Inland ECDIS chart Danube", + visible: true, + source: new ImageSource({ + preload: 1, + url: "https://service.d4d-portal.info/wms/", + crossOrigin: "anonymous", + params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true } + }) + }), + new ImageLayer({ + id: "WATERWAYAREA", + label: "Waterway Area", + maxResolution: 100, + minResolution: 0, + source: new ImageSource({ + url: window.location.origin + "/api/internal/wms", + params: { LAYERS: "waterway_area", VERSION: "1.1.1", TILED: true }, + imageLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["stretches_geoserver"], + geometryName: "area" + }, + source, + true, + (f, store) => { + if (f.getId() === store.state.imports.selectedStretchId) { + f.set("highlighted", true); + } + return f; + } + ) + ); + return new VectorLayer({ + id: "STRETCHES", + label: "Stretches", + visible: false, + style: styles.stretches, + source + }); + })(), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["sections_geoserver"], + geometryName: "area" + }, + source, + true, + (f, store) => { + if (f.getId() === store.state.imports.selectedSectionId) { + f.set("highlighted", true); + } + return f; + } + ) + ); + return new VectorLayer({ + id: "SECTIONS", + label: "Sections", + visible: false, + style: styles.sections, + source + }); + })(), + (function() { + const source = new VectorSource(); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["fairway_dimensions"], + filter: equalTo("level_of_service", 1) + }, + source, + true + ) + ); + return new VectorLayer({ + id: "FAIRWAYDIMENSIONSLOS1", + label: "LOS 1 Fairway Dimensions", + visible: false, + style: styles.fwd1, + maxResolution: 80, + minResolution: 0, + source + }); + })(), + (function() { + const source = new VectorSource(); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["fairway_dimensions"], + filter: equalTo("level_of_service", 2) + }, + source, + true + ) + ); + return new VectorLayer({ + id: "FAIRWAYDIMENSIONSLOS2", + label: "LOS 2 Fairway Dimensions", + visible: false, + style: styles.fwd2, + maxResolution: 80, + minResolution: 0, + source + }); + })(), + (function() { + const source = new VectorSource(); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["fairway_dimensions"], + filter: equalTo("level_of_service", 3) + }, + source, + true + ) + ); + return new VectorLayer({ + id: "FAIRWAYDIMENSIONSLOS3", + label: "LOS 3 Fairway Dimensions", + visible: true, + style: styles.fwd3, + maxResolution: 80, + minResolution: 0, + source + }); + })(), + new ImageLayer({ + id: "WATERWAYAXIS", + label: "Waterway Axis", + source: new ImageSource({ + url: window.location.origin + "/api/internal/wms", + params: { LAYERS: "waterway_axis", VERSION: "1.1.1", TILED: true }, + imageLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["waterway_profiles"], + geometryName: "geom" + }, + source + ) + ); + return new VectorLayer({ + id: "WATERWAYPROFILES", + label: "Waterway Profiles", + visible: true, + style: new Style({ + stroke: new Stroke({ + color: "rgba(0, 0, 255, .5)", + lineDash: [5, 5], + width: 2 + }) + }), + maxResolution: 2.5, + minResolution: 0, + source + }); + })(), + (function() { + return new VectorLayer({ + id: "BOTTLENECKS", + label: "Bottlenecks", + visible: true, + style: styles.bottleneck, + source: bottlenecksSource + }); + })(), + new TileLayer({ + id: "BOTTLENECKISOLINE", + label: "Bottleneck isolines", + visible: false, + source: new TileWMS({ + preload: 0, + projection: "EPSG:3857", + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "sounding_results_contour_lines_geoserver", + VERSION: "1.1.1", + TILED: true + }, + tileLoadFunction: function(tile, src) { + // console.log("calling for", tile, src); + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + new TileLayer({ + id: "DIFFERENCES", + label: "Bottleneck Differences", + visible: false, + source: new TileWMS({ + preload: 0, + projection: "EPSG:3857", + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "sounding_differences", + VERSION: "1.1.1", + TILED: true + }, + tileLoadFunction: function(tile, src) { + // console.log("calling for", tile, src); + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + (function() { + return new VectorLayer({ + id: "BOTTLENECKSTATUS", + label: "Critical Bottlenecks", + forLegendStyle: { point: true, resolution: 16 }, + visible: true, + zIndex: 1, + style: styles.bottleneckStatus, + source: bottlenecksSource + }); + })(), + (function() { + return new VectorLayer({ + id: "BOTTLENECKFAIRWAYAVAILABILITY", + label: "Bottlenecks Fairway Availability", + forLegendStyle: { point: true, resolution: 16 }, + visible: false, + zIndex: 1, + style: styles.bottleneckFairwayAvailability, + source: bottlenecksSource + }); + })(), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: [ + "bottlenecks_geoserver", + "gauges_geoserver", + "stretches_geoserver", + "sections_geoserver" + ] + }, + source, + true, + // since we don't use bbox strategy, features will contain all features and we can use it + // to find reference gauges for bottlenecks, yeah! + async (f, store, features) => { + // attach reference gauge to bottleneck + if (f.getId().indexOf("bottlenecks") > -1) { + f.set( + "gauge_obj", + features.find(feat => { + return ( + feat.getId().indexOf("gauges") > -1 && + feat.get("objname") === f.get("gauge_objname") + ); + }) + ); + } + + // attach nsc data to gauge + if (f.getId().indexOf("gauges") > -1) { + // look for nashSutcliffeOverview in store. If present and + // not older than 15 min use it or fetch new data and store it. + let data = store.getters["gauges/nashSutcliffeOverview"](f); + if ( + data && + new Date().getTime() - data.createdAt.getTime() < 900000 + ) { + f.set("nsc_data", data.data); + } else { + data = await store.dispatch( + "gauges/loadNashSutcliffeForOverview", + f.get("isrs_code") + ); + if (data) { + store.commit("gauges/addNashSutcliffeOverviewEntry", { + feature: f, + data, + createdAt: new Date() + }); + f.set("nsc_data", data); + } + } + } + } + ) + ); + return new VectorLayer({ + id: "DATAAVAILABILITY", + label: "Data Availability/Accuracy", + forLegendStyle: { point: true, resolution: 16 }, + visible: false, + zIndex: 1, + style: styles.dataAvailability, + source + }); + })(), + new ImageLayer({ + id: "DISTANCEMARKS", + label: "Distance Marks", + maxResolution: 10, + minResolution: 0, + source: new ImageSource({ + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "distance_marks_ashore_geoserver", + VERSION: "1.1.1", + TILED: true + }, + imageLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + new ImageLayer({ + id: "DISTANCEMARKSAXIS", + label: "Distance Marks, Axis", + source: new ImageSource({ + url: window.location.origin + "/api/internal/wms", + params: { + LAYERS: "distance_marks_geoserver", + VERSION: "1.1.1", + TILED: true + }, + imageLoadFunction: function(tile, src) { + HTTP.get(src, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + }, + responseType: "blob" + }).then(response => { + tile.getImage().src = URL.createObjectURL(response.data); + }); + } // TODO tile.setState(TileState.ERROR); + }) + }), + (function() { + const source = new VectorSource({ strategy: bboxStrategy }); + source.setLoader( + buildVectorLoader( + { + featureTypes: ["gauges_geoserver"], + geometryName: "geom" + }, + source + ) + ); + return new VectorLayer({ + id: "GAUGES", + label: "Gauges", + forLegendStyle: { point: true, resolution: 8 }, + visible: true, + style: styles.gauge, + maxResolution: 100, + minResolution: 0, + source + }); + })(), + DRAWLAYER, + CUTLAYER + ] + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/map/styles.js Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,351 @@ +import { Icon, Stroke, Style, Fill, Text, Circle } from "ol/style"; +import Point from "ol/geom/Point"; +import { getCenter } from "ol/extent"; +import store from "@/store/index"; +import classifications from "../../lib/classifications"; + +const styles = { + blue1: new Style({ + stroke: new Stroke({ + color: "rgba(0, 0, 255, 0.8)", + lineDash: [2, 4], + lineCap: "round", + width: 2 + }), + fill: new Fill({ + color: "rgba(240, 230, 0, 0.2)" + }) + }), + blue2: new Style({ + stroke: new Stroke({ + color: "rgba(0, 0, 255, 0.9)", + lineDash: [3, 6], + lineCap: "round", + width: 2 + }), + fill: new Fill({ + color: "rgba(240, 230, 0, 0.1)" + }) + }), + blue3: new Style({ + stroke: new Stroke({ + color: "rgba(0, 0, 255, 1.0)", + width: 2 + }), + fill: new Fill({ + color: "rgba(255, 255, 255, 0.4)" + }) + }), + yellow1: new Style({ + stroke: new Stroke({ + color: "rgba(230, 230, 10, .8)", + width: 4 + }), + fill: new Fill({ + color: "rgba(230, 230, 10, .3)" + }) + }), + yellow2: new Style({ + stroke: new Stroke({ + color: "rgba(250, 200, 0, .8)", + width: 2 + }), + fill: new Fill({ + color: "rgba(250, 200, 10, .3)" + }) + }), + yellow3: new Style({ + stroke: new Stroke({ + color: "rgba(250, 240, 10, .9)", + width: 5 + }), + fill: new Fill({ + color: "rgba(250, 240, 0, .7)" + }) + }), + orange1: new Style({ + stroke: new Stroke({ + color: "rgba(255, 150, 10, .8)", + width: 2 + }), + fill: new Fill({ + color: "rgba(255, 150, 0, .3)" + }) + }), + orange2: new Style({ + stroke: new Stroke({ + color: "rgba(255, 166, 10, .9)", + width: 5 + }), + fill: new Fill({ + color: "rgba(255, 166, 0, .7)" + }) + }), + red1: new Style({ + stroke: new Stroke({ + color: "rgba(255, 0, 0, 1)", + width: 4 + }) + }), + circleBlue: new Style({ + image: new Circle({ + radius: 5, + fill: new Fill({ color: "rgba(255, 0, 0, 0.1)" }), + stroke: new Stroke({ color: "blue", width: 1 }) + }) + }), + textFW1: new Style({ + text: new Text({ + font: 'bold 12px "Open Sans", "sans-serif"', + placement: "line", + fill: new Fill({ + color: "black" + }), + text: "LOS: 1" + //, zIndex: 10 + }) + }), + textFW2: new Style({ + text: new Text({ + font: 'bold 12px "Open Sans", "sans-serif"', + placement: "line", + fill: new Fill({ + color: "black" + }), + text: "LOS: 2" + //, zIndex: 10 + }) + }), + textFW3: new Style({ + text: new Text({ + font: 'bold 12px "Open Sans", "sans-serif"', + placement: "line", + fill: new Fill({ + color: "black" + }), + text: "LOS: 3" + //, zIndex: 10 + }) + }) +}; + +export default function(mapId) { + return { + stretches(feature) { + let style = styles.yellow2; + if (feature.get("highlighted")) { + style = styles.yellow3; + } + return style; + }, + sections(feature) { + let style = styles.orange1; + if (feature.get("highlighted")) { + style = styles.orange2; + } + return style; + }, + fwd1() { + return [styles.blue1, styles.textFW1]; + }, + fwd2() { + return [styles.blue2, styles.textFW2]; + }, + fwd3() { + return [styles.blue3, styles.textFW3]; + }, + bottleneck() { + return styles.yellow1; + }, + bottleneckStatus(feature, resolution, isLegend) { + let s = []; + if ((feature.get("fa_critical") && resolution > 15) || isLegend) { + let bnCenter = getCenter(feature.getGeometry().getExtent()); + s.push( + new Style({ + geometry: new Point(bnCenter), + image: new Icon({ + src: require("@/assets/marker-bottleneck-critical.png"), + anchor: [0.5, 0.5], + scale: isLegend ? 0.5 : 1 + }) + }) + ); + } + if (feature.get("fa_critical") && !isLegend) { + s.push(styles.red1); + } + return s; + }, + bottleneckFairwayAvailability(feature, resolution, isLegend) { + let s = []; + if (isLegend) { + s.push( + new Style({ + image: new Icon({ + src: require("@/assets/fa-diagram.png"), + anchor: [0.5, 0.5], + scale: 1 + }) + }) + ); + } + if ( + feature.get("fa_critical") && + feature.get("fa_data") && + resolution > 15 + ) { + let data = feature.get("fa_data"); + let lnwlHeight = (80 / 100) * data.ldc; + let belowThresholdHeight = (80 / 100) * data.below; + let betweenThresholdHeight = (80 / 100) * data.between; + let aboveThresholdHeight = (80 / 100) * data.above; + + let frame = `<rect x='0' y='0' width='32' height='84' stroke-width='0' fill='white'/>`; + let lnwl = `<rect x='2' y='${80 - + lnwlHeight + + 2}' width='10' height='${lnwlHeight}' stroke-width='0' fill='aqua'/>`; + let range1 = `<rect x='12' y='2' width='18' height='${belowThresholdHeight}' stroke-width='0' fill='hotpink'/>`; + let range2 = `<rect x='12' y='${belowThresholdHeight + + 2}' width='18' height='${betweenThresholdHeight}' stroke-width='0' fill='darksalmon'/>`; + let range3 = `<rect x='12' y='${80 - + aboveThresholdHeight + + 2}' width='18' height='${aboveThresholdHeight}' stroke-width='0' fill='blue'/>`; + let svg = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='84'><g>${frame}${lnwl}${range1}${range2}${range3}</g></svg>`; + let bnCenter = getCenter(feature.getGeometry().getExtent()); + s.push( + new Style({ + geometry: new Point(bnCenter), + image: new Icon({ + src: svg, + anchor: [1.2, 1.2] + }) + }) + ); + } + return s; + }, + dataAvailability(feature, resolution, isLegend) { + let s = []; + if (isLegend) { + s.push( + new Style({ + image: new Icon({ + src: require("@/assets/da-diagram.png"), + anchor: [0.5, 0.5], + scale: 1 + }) + }) + ); + } else { + // TODO: Get information from feature and check the ranges according to #423, #424, #425 + let colorWaterlevel = classifications.gmAvailability(feature); + let colorComparison = classifications.forecastVsReality(feature); + let colorAccuracy = classifications.forecastAccuracy(feature); + let map = store.getters["map/openLayersMap"](mapId); + let geom = feature.getGeometry(); + if (!(geom instanceof Point)) { + geom = new Point(getCenter(feature.getGeometry().getExtent())); + } + if ( + (map.getLayer("BOTTLENECKS").getVisible() && + feature.getId().indexOf("bottlenecks") > -1) || + (map.getLayer("SECTIONS").getVisible() && + feature.getId().indexOf("sections") > -1) || + (map.getLayer("STRETCHES").getVisible() && + feature.getId().indexOf("stretches") > -1) || + (map.getLayer("GAUGES").getVisible() && + feature.getId().indexOf("gauges") > -1) + ) { + let frame = `<polyline points='16,0 32,32 0,32 16,0' stroke='grey' stroke-width='1' fill='white'/>`; + let waterlevel = `<polyline points="16,0 24,16 16,32 8,16 16,0" stroke='grey' stroke-width='1' fill='${colorWaterlevel}'/>`; + let accuracy = `<polyline points="24,16 32,32 16,32 24,16" stroke='grey' stroke-width='1' fill='${colorAccuracy}'/>`; + let comparison = `<polyline points="8,16 16,32 0,32 8,16" stroke='grey' stroke-width='1' fill='${colorComparison}'/>`; + let svg = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><g>${frame}${waterlevel}${comparison}${accuracy}</g></svg>`; + s.push( + new Style({ + geometry: geom, + image: new Icon({ + src: svg, + anchor: [-0.5, 1] + }) + }) + ); + } + + if ( + map.getLayer("BOTTLENECKS").getVisible() && + feature.getId().indexOf("bottlenecks") > -1 + ) { + let colorUniformTriangle = classifications.surveyCurrency(feature); + let frame = `<polyline points='16,0 32,32 0,32 16,0' stroke='grey' stroke-width='1' fill='${colorUniformTriangle}'/>`; + let svg = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><g>${frame}</g></svg>`; + s.push( + new Style({ + geometry: geom, + image: new Icon({ + src: svg, + anchor: [0.5, 1] + }) + }) + ); + } + } + return s; + }, + dma(feature, resolution) { + if (resolution < 10) { + var s = styles.circleBlue; + if (resolution < 6) { + s.setText( + new Text({ + offsetY: 12, + font: '10px "Open Sans", "sans-serif"', + fill: new Fill({ + color: "black" + }), + text: (feature.get("hectometre") / 10).toString() + }) + ); + } + return s; + } + return []; + }, + gauge(feature, resolution, isLegend) { + let waterlevel = feature.get("gm_waterlevel"); + let text = feature.get("objname"); + let iconColor = "white"; + if (waterlevel) { + text += "\n(" + waterlevel + " cm)"; + let refWaterlevels = JSON.parse(feature.get("reference_water_levels")); + if (waterlevel < refWaterlevels.LDC) iconColor = "brown"; + if (waterlevel > refWaterlevels.LDC && waterlevel < refWaterlevels.HDC) + iconColor = "blue"; + if (waterlevel > refWaterlevels.HDC) iconColor = "red"; + } + + return [ + new Style({ + image: new Icon({ + src: require("@/assets/marker-gauge-" + iconColor + ".png"), + anchor: [0.5, isLegend ? 0.5 : 1], + scale: isLegend ? 0.5 : 1 + }), + text: new Text({ + font: '10px "Open Sans", "sans-serif"', + offsetY: 15, + fill: new Fill({ + color: "black" + }), + backgroundFill: new Fill({ + color: "rgba(255, 255, 255, 0.7)" + }), + padding: [2, 2, 2, 2], + text + }) + }) + ]; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/paneSetups.js Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,52 @@ +const main = { id: "main", component: "Map" }; + +export const DEFAULT = { main }; +export const COMPARESURVEYS = { + main, + compare: { id: "compare-survey", component: "Map" } +}; +export const FAIRWAYPROFILE = { + main, + fairwayprofile: { id: "fairwayprofile", component: "Fairwayprofile" } +}; + +export const AVAILABLEFAIRWAYDEPTH = { + main, + availablefairwaydepth: { + id: "availablefairwaydepth", + component: "AvailableFairwayDepth" + } +}; + +export const AVAILABLEFAIRWAYDEPTHLNWL = { + main, + availablefairwaydepth: { + id: "availablefairwaydepthlnwl", + component: "AvailableFairwayDepthLNWL" + } +}; + +export const COMPARESURVEYS_FAIRWAYPROFILE = { + main, + compare: { id: "compare-survey", component: "Map" }, + fairwayprofile: { id: "fairwayprofile", component: "Fairwayprofile" } +}; +export const GAUGE_WATERLEVEL = { + main, + waterlevel: { id: "gauge-waterlevel", component: "Waterlevel" } +}; +export const GAUGE_HYDROLOGICALCONDITIONS = { + main, + hydrological: { + id: "gauge-hydrologicalconditions", + component: "HydrologicalConditions" + } +}; +export const GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS = { + main, + waterlevel: { id: "gauge-waterlevel", component: "Waterlevel" }, + hydrological: { + id: "gauge-hydrologicalconditions", + component: "HydrologicalConditions" + } +};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/sections/SectionForm.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,360 @@ +<template> + <div class="d-flex flex-column"> + <div class="d-flex justify-content-between mt-2 px-2"> + <div class="text-left flex-fill"> + <small class="text-muted"> + <translate>ID</translate> + </small> + <input + id="id" + type="text" + class="form-control form-control-sm" + placeholder="AT_Section_12" + v-model="id" + :disabled="editSection" + /> + <span class="text-left text-danger"> + <small v-if="errors.id && !id"> + <translate>Please enter an id</translate> + </small> + </span> + </div> + </div> + <div class="d-flex justify-content-between mt-2 px-2"> + <div class="text-left flex-fill"> + <small class="text-muted"> + <translate>Start rhm</translate> + </small> + <div class="d-flex flex-row position-relative"> + <input + id="startrhm" + type="text" + class="form-control form-control-sm" + placeholder="e.g. ATXXX000010000019900" + v-model="startrhm" + ref="startrhm" + @focus="enablePipette('start')" + @blur="disablePipette('start')" + /> + <span + class="input-group-text position-absolute input-button" + @click="$refs.startrhm.focus()" + v-tooltip="pipetteTooltip" + > + <font-awesome-icon + :class="{ 'text-info': pipetteStart }" + icon="crosshairs" + /> + </span> + </div> + <span class="text-left text-danger"> + <small v-if="errors.startrhm && !startrhm"> + <translate>Please enter a start point</translate> + </small> + </span> + </div> + <div class="text-left flex-fill ml-2"> + <small class="text-muted"> + <translate>End rhm</translate> + </small> + <div class="d-flex flex-row position-relative"> + <input + id="endrhm" + type="text" + class="form-control form-control-sm" + placeholder="e.g. ATXXX000010000019900" + v-model="endrhm" + ref="endrhm" + @focus="enablePipette('end')" + @blur="disablePipette('end')" + /> + <span + class="input-group-text position-absolute input-button" + @click="$refs.endrhm.focus()" + v-tooltip="pipetteTooltip" + > + <font-awesome-icon + :class="{ 'text-info': pipetteEnd }" + icon="crosshairs" + /> + </span> + </div> + <span class="text-left text-danger"> + <small v-if="errors.endrhm && !endrhm"> + <translate>Please enter an end point</translate> + </small> + </span> + </div> + <div class="text-left ml-2" v-if="!editSection"> + <small class="text-muted"> + <translate>Tolerance for snapping to axis</translate> + </small> + <div class="d-flex flex-row position-relative"> + <input + class="form-control form-control-sm" + v-model.number="tolerance" + type="number" + min="0" + step="any" + id="tolerance" + /> + <span class="input-group-text position-absolute input-button"> + m + </span> + </div> + <span class="text-left text-danger"> + <small v-if="errors.tolerance && !tolerance"> + <translate>Please enter a tolerance value</translate> + </small> + </span> + </div> + </div> + <div class="d-flex flex-row justify-content-between px-2"> + <div class="mt-2 mr-2 w-50 text-left"> + <small class="text-muted"> + <translate>Object name</translate> + </small> + <input + id="objbn" + type="text" + class="form-control form-control-sm" + placeholder="" + v-model="objbn" + /> + <span class="text-left text-danger"> + <small v-if="errors.objbn && !objbn"> + <translate>Please enter an objectname</translate> + </small> + </span> + </div> + <div class="mt-2 w-50 text-left"> + <small class="text-muted"> + <translate>National Object name</translate> + </small> + <input + id="nobjbn" + type="text" + class="form-control form-control-sm" + v-model="nobjbn" + /> + </div> + </div> + <div class="d-flex flex-row justify-content-between px-2"> + <div class="mt-2 w-50 text-left"> + <small class="text-muted"> + <translate>Date info</translate> + </small> + <input + id="date_info" + type="date" + class="form-control form-control-sm" + placeholder="date_info" + v-model="date_info" + /> + <span class="text-left text-danger"> + <small v-if="errors.date_info && !date_info"> + <translate>Please enter a date</translate> + </small> + </span> + </div> + <div class="mt-2 ml-2 w-50 text-left"> + <small class="text-muted"> + <translate>Source Organization</translate> + </small> + <input + id="source_organization" + type="text" + class="form-control form-control-sm" + v-model="source_organization" + /> + <span class="text-left text-danger"> + <small v-if="errors.source_organization && !source_organization"> + <translate>Please enter a source organization</translate> + </small> + </span> + </div> + </div> + <div class="d-flex justify-content-between mt-2 p-2 border-top"> + <button @click="$parent.showForm = false" class="btn btn-sm btn-warning"> + <translate>Back</translate> + </button> + <button + @click="save" + type="submit" + class="shadow-sm btn btn-sm btn-info submit-button" + > + <translate>Submit</translate> + </button> + </div> + </div> +</template> + +<style lang="sass" scoped> +.input-button + border-top-left-radius: 0 + border-bottom-left-radius: 0 + right: 0 + height: 31px +</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, 2019 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Tom Gottfried <tom.gottfried@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; +import { displayError, displayInfo } from "@/lib/errors"; +import { sortTable } from "@/lib/mixins"; + +export default { + mixins: [sortTable], + props: ["editSection"], + data() { + return { + pipetteStart: false, + pipetteEnd: false, + id: null, + startrhm: null, + endrhm: null, + tolerance: 5, + objbn: null, + nobjbn: null, + date_info: new Date().toISOString().split("T")[0], + source_organization: null, + errors: { + id: false, + startrhm: false, + endrhm: false, + tolerance: false, + objbn: false, + nobjbn: false, + date_info: false, + source_organization: false + } + }; + }, + computed: { + ...mapState("map", ["identifiedFeatures"]), + ...mapGetters("map", ["openLayersMap"]), + pipetteTooltip() { + return this.$gettext("Choose a distance mark by clicking on the map."); + } + }, + watch: { + identifiedFeatures() { + const distanceMark = this.identifiedFeatures.find(x => + /^distance_marks_geoserver/.test(x["id_"]) + ); + if (distanceMark) { + const location = distanceMark.get("location"); + this.startrhm = this.pipetteStart ? location : this.startrhm; + this.endrhm = this.pipetteEnd ? location : this.endrhm; + this.pipetteStart = false; + this.pipetteEnd = false; + this.$store.commit("map/mapPopupEnabled", true); + } + } + }, + methods: { + enablePipette(t) { + this.openLayersMap() + .getLayer("DISTANCEMARKSAXIS") + .setVisible(true); + this.$store.commit("map/mapPopupEnabled", false); + if (t === "start") { + this.pipetteStart = true; + this.pipetteEnd = false; + } else { + this.pipetteStart = false; + this.pipetteEnd = true; + } + }, + disablePipette() { + this.$store.commit("map/mapPopupEnabled", true); + this.pipetteStart = false; + this.pipetteEnd = false; + }, + validate() { + const fields = [ + "id", + "startrhm", + "endrhm", + "objbn", + "date_info", + "source_organization" + ]; + if (!this.editSection) fields.push("tolerance"); + fields.forEach(field => { + if (!this[field]) { + this.errors[field] = true; + } else { + this.errors[field] = false; + } + }); + + // return true if no errors + return !Object.values(this.errors).reduce((a, b) => a + b, 0); + }, + save() { + if (this.validate()) { + const data = { + name: this.id, + from: this.startrhm, + to: this.endrhm, + "source-organization": this.source_organization, + "date-info": this.date_info, + objnam: this.objbn, + nobjnam: this.nobjbn + }; + if (!this.editSection) { + data["tolerance"] = this.tolerance; + } + this.$parent.loading = true; + this.$store + .dispatch("imports/saveSection", data) + .then(() => { + displayInfo({ + title: this.$gettext("Import"), + message: this.$gettext("Starting import of section") + }); + this.$store.dispatch("imports/loadSections").then(() => { + this.$parent.loading = false; + this.$parent.showForm = false; + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + } + }, + mounted() { + if (this.editSection) { + const props = this.editSection.properties; + this.id = props.name; + this.startrhm = props.lower.replace(/[,()]/g, ""); + this.endrhm = props.upper.replace(/[,()]/g, ""); + this.tolerance = props.tolerance; + this.objbn = props.objnam; + this.nobjbn = props.nobjnam; + this.date_info = props.date_info.split("T")[0]; + this.source_organization = props.source_organization; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/sections/Sections.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,249 @@ +<template> + <div class="d-flex flex-column"> + <UIBoxHeader icon="road" :title="title" :closeCallback="$parent.close" /> + <div class="position-relative"> + <UISpinnerOverlay v-if="loading" /> + <SectionForm v-if="showForm" :editSection="editSection" /> + <div v-else> + <UITableHeader + :columns="[ + { id: 'properties.name', title: `${nameLabel}`, class: 'col-4' }, + { + id: 'properties.date_info', + title: `${dateLabel}`, + class: 'col-2' + }, + { + id: 'properties.source_organization', + title: `${sourceorganizationLabel}`, + class: 'col-3' + } + ]" + /> + <UITableBody + :data="filteredSections() | sortTable(sortColumn, sortDirection)" + > + <template v-slot:row="{ item: section }"> + <div class="py-1 px-2 col-4"> + <a @click="moveMapToSection(section)" href="#"> + {{ section.properties.name }} + </a> + </div> + <div class="py-1 px-2 col-2"> + {{ section.properties.date_info | surveyDate }} + </div> + <div class="py-1 px-2 col-3"> + {{ section.properties.source_organization }} + </div> + <div class="py-1 px-2 col text-right"> + <button + v-if="isInStaging(section.properties.name)" + @click="gotoStaging(section.properties.name)" + class="btn btn-xs btn-danger mr-1" + > + <font-awesome-icon + icon="exclamation-triangle" + fixed-width + v-tooltip="reviewTooltip" + /> + </button> + <button + class="btn btn-xs btn-dark mr-1" + @click=" + showForm = true; + editSection = section; + " + > + <font-awesome-icon icon="pencil-alt" fixed-width /> + </button> + <button + class="btn btn-xs btn-dark" + @click="deleteSection(section)" + > + <font-awesome-icon icon="trash" fixed-width /> + </button> + </div> + </template> + </UITableBody> + <div class="text-right p-2 border-top"> + <button + @click=" + showForm = true; + editSection = null; + " + class="btn btn-sm btn-info" + > + <translate>New section</translate> + </button> + </div> + </div> + </div> + </div> +</template> + +<style lang="sass" scoped> +.input-button + border-top-left-radius: 0 + border-bottom-left-radius: 0 + right: 0 + height: 31px +</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, 2019 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Tom Gottfried <tom.gottfried@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; +import { displayError, displayInfo } from "@/lib/errors"; +import { HTTP } from "@/lib/http"; +import { sortTable } from "@/lib/mixins"; + +export default { + mixins: [sortTable], + components: { + SectionForm: () => import("./SectionForm") + }, + data() { + return { + staging: [], + loading: false, + showForm: false, + editSection: null + }; + }, + computed: { + ...mapState("application", ["searchQuery"]), + ...mapGetters("map", ["openLayersMap"]), + ...mapState("imports", ["sections"]), + title() { + return this.$gettext("Define Sections"); + }, + nameLabel() { + return this.$gettext("Name"); + }, + dateLabel() { + return this.$gettext("Date"); + }, + sourceorganizationLabel() { + return this.$gettext("Source organization"); + }, + reviewTooltip() { + return this.$gettext("Review pending import"); + } + }, + methods: { + filteredSections() { + return this.sections.filter(s => { + return (s.properties.name + s.properties.source_organization) + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + }); + }, + gotoStaging(sectionName) { + let pendingImport = this.staging.find(s => s.name === sectionName); + if (pendingImport) + this.$router.push("/imports/overview/" + pendingImport.id); + }, + isInStaging(sectionName) { + return !!this.staging.find(s => s.name === sectionName); + }, + loadStagingData() { + HTTP.get("/imports?states=pending&kinds=sec", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + response.data.imports.forEach(i => { + HTTP.get("/imports/" + i.id, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.staging.push({ + id: i.id, + name: response.data.summary.section + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => (this.loading = false)); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + deleteSection(section) { + this.$store.commit("application/popup", { + icon: "trash", + title: this.$gettext("Delete Section"), + content: + this.$gettext("Do you really want to delete this section:") + + `<br> + <b>${section.properties.name}, ${ + section.properties.source_organization + } (${section.properties.countries})</b>`, + confirm: { + label: this.$gettext("Delete"), + icon: "trash", + callback: () => { + displayInfo({ + title: this.$gettext("Not implemented"), + message: this.$gettext("Deleting ") + section.id + }); + } + }, + cancel: { + label: this.$gettext("Cancel"), + icon: "times" + } + }); + }, + moveMapToSection(section) { + this.$store.commit("imports/selectedSectionId", section.id); + this.$store.commit("fairwayavailability/type", "sections"); + this.$store.commit("application/showFairwayDepth", true); + this.openLayersMap() + .getLayer("SECTIONS") + .setVisible(true); + this.$store.dispatch("map/moveToFeauture", { + feature: section, + zoom: 17, + preventZoomOut: true + }); + } + }, + mounted() { + this.loading = true; + this.$store + .dispatch("imports/loadSections") + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => (this.loading = false)); + this.loadStagingData(); + } +}; +</script>
--- a/client/src/components/splitscreen/MinimizedSplitscreens.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -<template> - <transition-group - name="fade" - tag="div" - class="minimizedSplitscreens ui-element" - > - <UIBoxHeader - v-for="splitscreen in splitscreens" - :key="splitscreen.id" - :icon="splitscreen.icon" - :title="splitscreen.title" - :closeCallback="close(splitscreen)" - :expandCallback="expand(splitscreen)" - :collapsed="true" - class="mt-2" - /> - </transition-group> -</template> - -<style lang="sass" scoped> -.minimizedSplitscreens - position: absolute - bottom: $small-offset - 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): - * Markus Kottländer <markus@intevation.de> - */ - -import { mapState } from "vuex"; - -export default { - computed: { - ...mapState("application", ["splitscreens"]) - }, - methods: { - close(splitscreen) { - return () => { - if (splitscreen.closeCallback) splitscreen.closeCallback(); - this.$store.commit("application/removeSplitscreen", splitscreen.id); - }; - }, - expand(splitscreen) { - return () => { - if (splitscreen.expandCallback) splitscreen.expandCallback(); - this.$store.commit("application/activeSplitscreenId", splitscreen.id); - this.$store.commit("application/showSplitscreen", true); - }; - } - } -}; -</script>
--- a/client/src/components/splitscreen/Splitscreen.vue Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,92 +0,0 @@ -<template> - <div> - <div - :class="[ - 'splitscreen bg-white d-flex flex-column ui-element', - { show: showSplitscreen } - ]" - > - <UIBoxHeader - :icon="activeSplitscreen.icon" - :title="activeSplitscreen.title" - :closeCallback="close" - :collapseCallback="collapse" - v-if="activeSplitscreen" - /> - <div class="d-flex flex-fill"> - <transition name="fade"> - <div class="loading" v-if="splitscreenLoading"> - <font-awesome-icon icon="spinner" spin /> - </div> - </transition> - <component :is="activeSplitscreen.component" v-if="activeSplitscreen" /> - </div> - </div> - </div> -</template> - -<style lang="sass" scoped> -.splitscreen - position: absolute - bottom: -50vh - left: 0 - right: 0 - height: 50vh - overflow: hidden - z-index: 1 - box-shadow: 0 -.125rem .25rem rgba(0, 0, 0, 0.075) - transition: bottom 0.3s - &.show - bottom: 0 - - .loading - top: 34px -</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 { - components: { - Fairwayprofile: () => import("@/components/fairway/Fairwayprofile"), - Waterlevel: () => import("@/components/gauge/Waterlevel"), - HydrologicalConditions: () => - import("@/components/gauge/HydrologicalConditions") - }, - computed: { - ...mapState("application", ["showSplitscreen", "splitscreenLoading"]), - ...mapGetters("application", ["activeSplitscreen"]) - }, - methods: { - collapse() { - if (this.activeSplitscreen.collapseCallback) - this.activeSplitscreen.collapseCallback(); - this.$store.commit("application/showSplitscreen", false); - }, - close() { - this.$store.commit("application/showSplitscreen", false); - setTimeout(() => { - let removeId = this.activeSplitscreen.id; - let callback = this.activeSplitscreen.closeCallback; - this.$store.commit("application/activeSplitscreenId", null); - if (callback) callback(); - this.$store.commit("application/removeSplitscreen", removeId); - }, 350); - } - } -}; -</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/stretches/StretchForm.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,384 @@ +<template> + <div class="d-flex flex-column"> + <div class="d-flex justify-content-between mt-2 px-2"> + <div class="text-left flex-fill mr-1"> + <small class="text-muted"> + <translate>ID</translate> + </small> + <input + id="id" + type="text" + class="form-control form-control-sm" + placeholder="AT_Stretch_12" + v-model="id" + :disabled="editStretch" + /> + <span class="text-left text-danger"> + <small v-if="errors.id && !id"> + <translate>Please enter an id</translate> + </small> + </span> + </div> + <div class="text-left flex-fill ml-1"> + <small class="text-muted"> + <translate>Countrycode</translate> + </small> + <input + id="countryCode" + type="text" + class="form-control form-control-sm" + placeholder="AT" + v-model="countryCode" + /> + <span class="text-left text-danger"> + <small v-if="errors.countryCode && !countryCode"> + <translate>Please enter a countrycode </translate> + </small> + </span> + </div> + </div> + <div class="d-flex justify-content-between mt-2 px-2"> + <div class="text-left flex-fill"> + <small class="text-muted"> + <translate>Start rhm</translate> + </small> + <div class="d-flex flex-row position-relative"> + <input + id="startrhm" + type="text" + class="form-control form-control-sm" + placeholder="e.g. ATXXX000010000019900" + v-model="startrhm" + ref="startrhm" + @focus="enablePipette('start')" + @blur="disablePipette('start')" + /> + <span + class="input-group-text position-absolute input-button" + @click="$refs.startrhm.focus()" + v-tooltip="pipetteTooltip" + > + <font-awesome-icon + :class="{ 'text-info': pipetteStart }" + icon="crosshairs" + /> + </span> + </div> + <span class="text-left text-danger"> + <small v-if="errors.startrhm && !startrhm"> + <translate>Please enter a start point</translate> + </small> + </span> + </div> + <div class="text-left flex-fill ml-2"> + <small class="text-muted"> + <translate>End rhm</translate> + </small> + <div class="d-flex flex-row position-relative"> + <input + id="endrhm" + type="text" + class="form-control form-control-sm" + placeholder="e.g. ATXXX000010000019900" + v-model="endrhm" + ref="endrhm" + @focus="enablePipette('end')" + @blur="disablePipette('end')" + /> + <span + class="input-group-text position-absolute input-button" + @click="$refs.endrhm.focus()" + v-tooltip="pipetteTooltip" + > + <font-awesome-icon + :class="{ 'text-info': pipetteEnd }" + icon="crosshairs" + /> + </span> + </div> + <span class="text-left text-danger"> + <small v-if="errors.endrhm && !endrhm"> + <translate>Please enter an end point</translate> + </small> + </span> + </div> + <div class="text-left ml-2" v-if="!editStretch"> + <small class="text-muted"> + <translate>Tolerance for snapping to axis</translate> + </small> + <div class="d-flex flex-row position-relative"> + <input + class="form-control form-control-sm" + v-model.number="tolerance" + type="number" + min="0" + step="any" + id="tolerance" + /> + <span class="input-group-text position-absolute input-button"> + m + </span> + </div> + <span class="text-left text-danger"> + <small v-if="errors.tolerance && !tolerance"> + <translate>Please enter a tolerance value</translate> + </small> + </span> + </div> + </div> + <div class="d-flex flex-row justify-content-between px-2"> + <div class="mt-2 mr-2 w-50 text-left"> + <small class="text-muted"> + <translate>Object name</translate> + </small> + <input + id="objbn" + type="text" + class="form-control form-control-sm" + placeholder="" + v-model="objbn" + /> + <span class="text-left text-danger"> + <small v-if="errors.objbn && !objbn"> + <translate>Please enter an objectname</translate> + </small> + </span> + </div> + <div class="mt-2 w-50 text-left"> + <small class="text-muted"> + <translate>National Object name</translate> + </small> + <input + id="nobjbn" + type="text" + class="form-control form-control-sm" + v-model="nobjbn" + /> + </div> + </div> + <div class="d-flex flex-row justify-content-between px-2"> + <div class="mt-2 w-50 text-left"> + <small class="text-muted"> + <translate>Date info</translate> + </small> + <input + id="date_info" + type="date" + class="form-control form-control-sm" + placeholder="date_info" + v-model="date_info" + /> + <span class="text-left text-danger"> + <small v-if="errors.date_info && !date_info"> + <translate>Please enter a date</translate> + </small> + </span> + </div> + <div class="mt-2 ml-2 w-50 text-left"> + <small class="text-muted"> + <translate>Source Organization</translate> + </small> + <input + id="source_organization" + type="text" + class="form-control form-control-sm" + v-model="source_organization" + /> + <span class="text-left text-danger"> + <small v-if="errors.source_organization && !source_organization"> + <translate>Please enter a source organization</translate> + </small> + </span> + </div> + </div> + <div class="d-flex justify-content-between mt-2 p-2 border-top"> + <button @click="$parent.showForm = false" class="btn btn-sm btn-warning"> + <translate>Back</translate> + </button> + <button + @click="save" + type="submit" + class="shadow-sm btn btn-sm btn-info submit-button" + > + <translate>Submit</translate> + </button> + </div> + </div> +</template> + +<style lang="sass" scoped> +.input-button + border-top-left-radius: 0 + border-bottom-left-radius: 0 + right: 0 + height: 31px +</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, 2019 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Tom Gottfried <tom.gottfried@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; +import { displayError, displayInfo } from "@/lib/errors"; +import { sortTable } from "@/lib/mixins"; + +export default { + mixins: [sortTable], + props: ["editStretch"], + data() { + return { + pipetteStart: false, + pipetteEnd: false, + id: null, + startrhm: null, + endrhm: null, + tolerance: 5, + objbn: null, + nobjbn: null, + date_info: new Date().toISOString().split("T")[0], + source_organization: null, + countryCode: null, + errors: { + id: false, + startrhm: false, + endrhm: false, + tolerance: false, + objbn: false, + nobjbn: false, + date_info: false, + source_organization: false, + countryCode: false + } + }; + }, + computed: { + ...mapState("map", ["identifiedFeatures"]), + ...mapGetters("map", ["openLayersMap"]), + pipetteTooltip() { + return this.$gettext("Choose a distance mark by clicking on the map."); + } + }, + watch: { + identifiedFeatures() { + const distanceMark = this.identifiedFeatures.find(x => + /^distance_marks_geoserver/.test(x["id_"]) + ); + if (distanceMark) { + const location = distanceMark.get("location"); + this.startrhm = this.pipetteStart ? location : this.startrhm; + this.endrhm = this.pipetteEnd ? location : this.endrhm; + this.pipetteStart = false; + this.pipetteEnd = false; + this.$store.commit("map/mapPopupEnabled", true); + } + } + }, + methods: { + enablePipette(t) { + this.openLayersMap() + .getLayer("DISTANCEMARKSAXIS") + .setVisible(true); + this.$store.commit("map/mapPopupEnabled", false); + if (t === "start") { + this.pipetteStart = true; + this.pipetteEnd = false; + } else { + this.pipetteStart = false; + this.pipetteEnd = true; + } + }, + disablePipette() { + this.$store.commit("map/mapPopupEnabled", true); + this.pipetteStart = false; + this.pipetteEnd = false; + }, + validate() { + const fields = [ + "id", + "startrhm", + "endrhm", + "objbn", + "countryCode", + "date_info", + "source_organization" + ]; + if (!this.editStretch) fields.push("tolerance"); + fields.forEach(field => { + if (!this[field]) { + this.errors[field] = true; + } else { + this.errors[field] = false; + } + }); + + // return true if no errors + return !Object.values(this.errors).reduce((a, b) => a + b, 0); + }, + save() { + if (this.validate()) { + const data = { + name: this.id, + from: this.startrhm, + to: this.endrhm, + "source-organization": this.source_organization, + "date-info": this.date_info, + objnam: this.objbn, + nobjnam: this.nobjbn, + countries: this.countryCode.split(",").map(x => { + return x.trim(); + }) + }; + if (!this.editStretch) { + data["tolerance"] = this.tolerance; + } + this.$parent.loading = true; + this.$store + .dispatch("imports/saveStretch", data) + .then(() => { + displayInfo({ + title: this.$gettext("Import"), + message: this.$gettext("Starting import of stretch") + }); + this.$store.dispatch("imports/loadStretches").then(() => { + this.$parent.loading = false; + this.$parent.showForm = false; + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + } + }, + mounted() { + if (this.editStretch) { + const props = this.editStretch.properties; + this.id = props.name; + this.startrhm = props.lower.replace(/[,()]/g, ""); + this.endrhm = props.upper.replace(/[,()]/g, ""); + this.tolerance = props.tolerance; + this.objbn = props.objnam; + this.nobjbn = props.nobjnam; + this.date_info = props.date_info.split("T")[0]; + this.source_organization = props.source_organization; + this.countryCode = props.countries; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/stretches/Stretches.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,249 @@ +<template> + <div class="d-flex flex-column"> + <UIBoxHeader icon="road" :title="title" :closeCallback="$parent.close" /> + <div class="position-relative"> + <UISpinnerOverlay v-if="loading" /> + <StretchForm v-if="showForm" :editStretch="editStretch" /> + <div v-else> + <UITableHeader + :columns="[ + { id: 'properties.name', title: `${nameLabel}`, class: 'col-4' }, + { + id: 'properties.date_info', + title: `${dateLabel}`, + class: 'col-2' + }, + { + id: 'properties.source_organization', + title: `${sourceorganizationLabel}`, + class: 'col-3' + } + ]" + /> + <UITableBody + :data="filteredStretches() | sortTable(sortColumn, sortDirection)" + > + <template v-slot:row="{ item: stretch }"> + <div class="py-1 px-2 col-4"> + <a @click="moveMapToStretch(stretch)" href="#"> + {{ stretch.properties.name }} + </a> + </div> + <div class="py-1 px-2 col-2"> + {{ stretch.properties.date_info | surveyDate }} + </div> + <div class="py-1 px-2 col-3"> + {{ stretch.properties.source_organization }} + </div> + <div class="py-1 px-2 col text-right"> + <button + v-if="isInStaging(stretch.properties.name)" + @click="gotoStaging(stretch.properties.name)" + class="btn btn-xs btn-danger mr-1" + > + <font-awesome-icon + icon="exclamation-triangle" + fixed-width + v-tooltip="reviewTooltip" + /> + </button> + <button + class="btn btn-xs btn-dark mr-1" + @click=" + showForm = true; + editStretch = stretch; + " + > + <font-awesome-icon icon="pencil-alt" fixed-width /> + </button> + <button + class="btn btn-xs btn-dark" + @click="deleteStretch(stretch)" + > + <font-awesome-icon icon="trash" fixed-width /> + </button> + </div> + </template> + </UITableBody> + <div class="text-right p-2 border-top"> + <button + @click=" + showForm = true; + editStretch = null; + " + class="btn btn-sm btn-info" + > + <translate>New stretch</translate> + </button> + </div> + </div> + </div> + </div> +</template> + +<style lang="sass" scoped> +.input-button + border-top-left-radius: 0 + border-bottom-left-radius: 0 + right: 0 + height: 31px +</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, 2019 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Thomas Junk <thomas.junk@intevation.de> + * Tom Gottfried <tom.gottfried@intevation.de> + */ +import { mapState, mapGetters } from "vuex"; +import { displayError, displayInfo } from "@/lib/errors"; +import { HTTP } from "@/lib/http"; +import { sortTable } from "@/lib/mixins"; + +export default { + mixins: [sortTable], + components: { + StretchForm: () => import("./StretchForm") + }, + data() { + return { + staging: [], + loading: false, + showForm: false, + editStretch: null + }; + }, + computed: { + ...mapState("application", ["searchQuery"]), + ...mapGetters("map", ["openLayersMap"]), + ...mapState("imports", ["stretches"]), + title() { + return this.$gettext("Define Stretches"); + }, + nameLabel() { + return this.$gettext("Name"); + }, + dateLabel() { + return this.$gettext("Date"); + }, + sourceorganizationLabel() { + return this.$gettext("Source organization"); + }, + reviewTooltip() { + return this.$gettext("Review pending import"); + } + }, + methods: { + filteredStretches() { + return this.stretches.filter(s => { + return (s.properties.name + s.properties.source_organization) + .toLowerCase() + .includes(this.searchQuery.toLowerCase()); + }); + }, + gotoStaging(stretchName) { + let pendingImport = this.staging.find(s => s.name === stretchName); + if (pendingImport) + this.$router.push("/imports/overview/" + pendingImport.id); + }, + isInStaging(stretchName) { + return !!this.staging.find(s => s.name === stretchName); + }, + loadStagingData() { + HTTP.get("/imports?states=pending&kinds=st", { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + response.data.imports.forEach(i => { + HTTP.get("/imports/" + i.id, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.staging.push({ + id: i.id, + name: response.data.summary.stretch + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => (this.loading = false)); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, + deleteStretch(stretch) { + this.$store.commit("application/popup", { + icon: "trash", + title: this.$gettext("Delete Stretch"), + content: + this.$gettext("Do you really want to delete this stretch:") + + `<br> + <b>${stretch.properties.name}, ${ + stretch.properties.source_organization + } (${stretch.properties.countries})</b>`, + confirm: { + label: this.$gettext("Delete"), + icon: "trash", + callback: () => { + displayInfo({ + title: this.$gettext("Not implemented"), + message: this.$gettext("Deleting ") + stretch.id + }); + } + }, + cancel: { + label: this.$gettext("Cancel"), + icon: "times" + } + }); + }, + moveMapToStretch(stretch) { + this.$store.commit("imports/selectedStretchId", stretch.id); + this.$store.commit("fairwayavailability/type", "stretches"); + this.$store.commit("application/showFairwayDepth", true); + this.openLayersMap() + .getLayer("STRETCHES") + .setVisible(true); + this.$store.dispatch("map/moveToFeauture", { + feature: stretch, + zoom: 17, + preventZoomOut: true + }); + } + }, + mounted() { + this.loading = true; + this.$store + .dispatch("imports/loadStretches") + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => (this.loading = false)); + this.loadStagingData(); + } +}; +</script>
--- a/client/src/components/systemconfiguration/ColorSettings.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/systemconfiguration/ColorSettings.vue Mon Jun 03 10:19:18 2019 +0200 @@ -43,11 +43,9 @@ * Bernhard Reiter <bernhard@intevation.de> * Markus Kottländer <markus@intevation.de> */ -import { Chrome } from "vue-color"; -import { Compact } from "vue-color"; - +import { Chrome, Compact } from "vue-color"; import { HTTP } from "@/lib/http"; -import { displayError } from "@/lib/errors.js"; +import { displayError } from "@/lib/errors"; export default { name: "colorsettings",
--- a/client/src/components/systemconfiguration/PDFTemplates.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/systemconfiguration/PDFTemplates.vue Mon Jun 03 10:19:18 2019 +0200 @@ -13,43 +13,63 @@ <div class="mt-1 border-bottom pb-4"> <UITableHeader :columns="[ - { id: 'name', title: `${nameLabel}`, class: 'col-4' }, - { id: 'time', title: `${dateLabel}`, class: 'col-4' }, + { id: 'name', title: `${nameLabel}`, class: 'col-3' }, + { id: 'time', title: `${dateLabel}`, class: 'col-3' }, + { id: 'type', title: `${typeLabel}`, class: 'col-2' }, { id: 'country', title: `${countryLabel}`, class: 'col-2' } ]" /> - <UITableBody - :data="templates | sortTable(sortColumn, sortDirection)" - v-slot="{ item: template }" - > - <div class="py-1 col-4">{{ template.name }}</div> - <div class="py-1 col-4">{{ template.time }}</div> - <div class="py-1 col-2" v-if="template.country"> - {{ template.country }} - </div> - <div class="py-1 col-2" v-else><i>global</i></div> - <div class="col py-1 text-right"> - <button - class="btn btn-xs btn-info mr-1" - ref="downloadTemplate" - @click="downloadTemplate(template)" - > - <font-awesome-icon icon="download" fixed-width /> - </button> - <button class="btn btn-xs btn-dark" @click="deleteTemplate(template)"> - <font-awesome-icon icon="trash" fixed-width /> - </button> - </div> + <UITableBody :data="templates | sortTable(sortColumn, sortDirection)"> + <template v-slot:row="{ item: template }"> + <div class="py-1 col-3">{{ template.name }}</div> + <div class="py-1 col-3">{{ template.time }}</div> + <div class="py-1 col-2">{{ template.type }}</div> + <div class="py-1 col-2" v-if="template.country"> + {{ template.country }} + </div> + <div class="py-1 col-2" v-else><i>global</i></div> + <div class="col py-1 text-right"> + <button + class="btn btn-xs btn-info mr-1" + ref="downloadTemplate" + @click="downloadTemplate(template)" + > + <font-awesome-icon icon="download" fixed-width /> + </button> + <button + class="btn btn-xs btn-dark" + @click="deleteTemplate(template)" + > + <font-awesome-icon icon="trash" fixed-width /> + </button> + </div> + </template> </UITableBody> - <button class="btn btn-info mt-2" @click="$refs.uploadTemplate.click()"> - <font-awesome-icon - icon="spinner" - class="fa-spin fa-fw" - v-if="uploading" - /> - <font-awesome-icon icon="upload" class="fa-fw" v-else /> - <translate>Upload new template</translate> - </button> + <div class="d-flex flex-column mt-2 w-25 mr-auto"> + <select + v-model="type" + class="form-control d-block custom-select-sm w-75 h-25" + > + <option :value="null"> + Select template type + </option> + <option value="map"> + Map-template + </option> + <option value="diagram"> + Diagram-template + </option> + </select> + <button class="btn btn-info btn-sm mt-1 w-75" @click="checkUpload"> + <font-awesome-icon + icon="spinner" + class="fa-spin fa-fw" + v-if="uploading" + /> + <font-awesome-icon icon="upload" class="fa-fw" v-else /> + <translate>Upload new template</translate> + </button> + </div> </div> </div> </template> @@ -89,7 +109,8 @@ data() { return { templates: [], - uploading: false + uploading: false, + type: null }; }, computed: { @@ -101,9 +122,23 @@ }, countryLabel() { return this.$gettext("Country"); + }, + typeLabel() { + return this.$gettext("Type"); } }, methods: { + // check if template type is selceted + checkUpload() { + if (this.type) { + this.$refs.uploadTemplate.click(); + } else { + displayError({ + title: this.$gettext("Error"), + message: this.$gettext("Please select template type") + }); + } + }, downloadTemplate(template) { if (template) { var templateData = ""; @@ -111,7 +146,7 @@ element.style.display = "none"; element.setAttribute("download", template.name + ".json"); document.body.appendChild(element); - HTTP.get("/templates/print/" + template.name, { + HTTP.get(`/templates/${template.type}/${template.name}`, { headers: { "X-Gemma-Auth": localStorage.getItem("token"), "Content-type": "text/xml; charset=UTF-8" @@ -161,47 +196,83 @@ "Uploaded file does not contain valid json data." ) }); + // allow the user to upload the same file + // if user wants to upload the same file after edit it. + this.$refs.uploadTemplate.value = null; } if (template.name) { - this.uploading = true; - HTTP.post( - "/templates/print/" + template.name, - { - template_name: template.name, - template_data: template - }, - { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - } - ) - .then(() => { - this.loadTemplates(); - displayInfo({ + // check if an element in the uploaded file does not match the predefind template-elements + let checkElement = false; + template.elements.forEach(e => { + if ( + [ + "text", + "box", + "textbox", + "image", + "bottleneck", + "legend", + "scalebar", + "scale", + "northarrow", + "diagramlegend", + "diagramtitle", + "diagram" + ].indexOf(e.type) === -1 + ) { + checkElement = true; + displayError({ + title: this.$gettext("Invalid element"), message: - template.name + " " + this.$gettext("uploaded successfully") + e.type + + this.$gettext(" does not match any template's element") }); - }) - .catch(e => { - const { status, data } = e.response; - if (status === 400) { - displayError({ - title: this.$gettext("Error"), - message: `${data.message || data}` + // allow the user to upload the same file + this.$refs.uploadTemplate.value = null; + } + }); + + if (!checkElement) { + this.uploading = true; + HTTP.post( + "/templates/" + this.type + "/" + template.name, + { + template_name: template.name, + template_data: template + }, + { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + } + ) + .then(() => { + this.loadTemplates(); + displayInfo({ + message: + template.name + " " + this.$gettext("uploaded successfully") }); - } else { - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - } - }) - .finally(() => { - this.uploading = false; - this.$refs.uploadTemplate.value = null; - }); + }) + .catch(e => { + const { status, data } = e.response; + if (status === 400) { + displayError({ + title: this.$gettext("Error"), + message: `${data.message || data}` + }); + } else { + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + } + }) + .finally(() => { + this.uploading = false; + this.$refs.uploadTemplate.value = null; + }); + } } else { displayError({ title: this.$gettext("Format Error"), @@ -209,13 +280,16 @@ "The provided template has no name property." ) }); + // allow the user to upload the same file + this.$refs.uploadTemplate.value = null; } }; + reader.onerror = error => console.log(error); reader.readAsText(this.$refs.uploadTemplate.files[0]); }, loadTemplates() { - HTTP.get("/templates/print", { + HTTP.get("/templates", { headers: { "X-Gemma-Auth": localStorage.getItem("token"), "Content-type": "text/xml; charset=UTF-8" @@ -246,7 +320,7 @@ label: this.$gettext("Delete"), icon: "trash", callback: () => { - HTTP.delete("/templates/print/" + template.name, { + HTTP.delete(`/templates/${template.type}/${template.name}`, { headers: { "X-Gemma-Auth": localStorage.getItem("token"), "Content-type": "text/xml; charset=UTF-8"
--- a/client/src/components/systemconfiguration/Systemconfiguration.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/systemconfiguration/Systemconfiguration.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,6 +1,6 @@ <template> <div class="d-flex flex-row"> - <Spacer></Spacer> + <Spacer /> <div class="card sysconfig mt-2 shadow-xs"> <UIBoxHeader icon="wrench" :title="systemconfigurationLabel" /> <div class="card-body text-left">
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/toolbar/AvailableFairwayDepth.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,39 @@ +<template> + <div + class="toolbar-button" + v-tooltip.right="label" + @click="$store.commit('application/showFairwayDepth', !showFairwayDepth)" + > + <font-awesome-icon + icon="chart-line" + :class="{ 'text-info': showFairwayDepth }" + /> + </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> + * Thomas Junk <thomas.junk@intevation.de> + */ +import { mapState } from "vuex"; + +export default { + computed: { + ...mapState("application", ["showFairwayDepth"]), + label() { + return this.$gettext("Available fairway depth"); + } + } +}; +</script>
--- a/client/src/components/toolbar/Gauges.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/toolbar/Gauges.vue Mon Jun 03 10:19:18 2019 +0200 @@ -2,11 +2,12 @@ <div @click="$store.commit('application/showGauges', !showGauges)" class="toolbar-button" + v-tooltip.right="label" > <font-awesome-icon icon="ruler-vertical" :class="{ 'text-info': showGauges }" - ></font-awesome-icon> + /> </div> </template> @@ -28,7 +29,10 @@ export default { computed: { - ...mapState("application", ["showGauges"]) + ...mapState("application", ["showGauges"]), + label() { + return this.$gettext("Gauges"); + } } }; </script>
--- a/client/src/components/toolbar/Identify.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/toolbar/Identify.vue Mon Jun 03 10:19:18 2019 +0200 @@ -2,11 +2,9 @@ <div @click="$store.commit('application/showIdentify', !showIdentify)" class="toolbar-button" + v-tooltip.right="label" > - <font-awesome-icon - icon="info" - :class="{ 'text-info': showIdentify }" - ></font-awesome-icon> + <font-awesome-icon icon="info" :class="{ 'text-info': showIdentify }" /> <span :class="[ 'indicator', @@ -46,6 +44,9 @@ ...mapGetters("map", ["filteredIdentifiedFeatures"]), badgeCount() { return this.filteredIdentifiedFeatures.length + !!this.currentMeasurement; + }, + label() { + return this.$gettext("Identified Features"); } } };
--- a/client/src/components/toolbar/Layers.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/toolbar/Layers.vue Mon Jun 03 10:19:18 2019 +0200 @@ -2,11 +2,12 @@ <div @click="$store.commit('application/showLayers', !showLayers)" class="toolbar-button" + v-tooltip.right="label" > <font-awesome-icon icon="layer-group" :class="{ 'text-info': showLayers }" - ></font-awesome-icon> + /> </div> </template> @@ -29,7 +30,10 @@ export default { name: "layers", computed: { - ...mapState("application", ["showLayers"]) + ...mapState("application", ["showLayers"]), + label() { + return this.$gettext("Map Layers"); + } } }; </script>
--- a/client/src/components/toolbar/Linetool.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/toolbar/Linetool.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,9 +1,6 @@ <template> - <div @click="toggleLineTool" class="toolbar-button"> - <font-awesome-icon - icon="ruler" - :class="{ 'text-info': lineTool && lineTool.getActive() }" - ></font-awesome-icon> + <div @click="toggle" class="toolbar-button" v-tooltip.right="label"> + <font-awesome-icon icon="ruler" :class="{ 'text-info': lineToolEnabled }" /> </div> </template> @@ -21,22 +18,27 @@ * Author(s): * Markus Kottländer <markus.kottlaender@intevation.de> */ -import { mapState, mapGetters } from "vuex"; -import { LAYERS } from "@/store/map.js"; +import { mapState } from "vuex"; export default { name: "linetool", computed: { - ...mapGetters("map", ["getVSourceByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]) + ...mapState("map", ["openLayersMaps", "lineToolEnabled"]), + label() { + return this.$gettext("Measure Distance"); + } }, methods: { - toggleLineTool() { - this.lineTool.setActive(!this.lineTool.getActive()); - this.polygonTool.setActive(false); - this.cutTool.setActive(false); + toggle() { + this.$store.commit("map/lineToolEnabled", !this.lineToolEnabled); + this.$store.commit("map/polygonToolEnabled", false); + this.$store.commit("map/cutToolEnabled", false); this.$store.commit("map/setCurrentMeasurement", null); - this.getVSourceByName(LAYERS.DRAWTOOL).clear(); + this.openLayersMaps.forEach(m => { + m.getLayer("DRAWTOOL") + .getSource() + .clear(); + }); } } };
--- a/client/src/components/toolbar/Pdftool.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/toolbar/Pdftool.vue Mon Jun 03 10:19:18 2019 +0200 @@ -2,11 +2,9 @@ <div @click="$store.commit('application/showPdfTool', !showPdfTool)" class="toolbar-button" + v-tooltip.right="label" > - <font-awesome-icon - icon="file-pdf" - :class="{ 'text-info': showPdfTool }" - ></font-awesome-icon> + <font-awesome-icon icon="file-pdf" :class="{ 'text-info': showPdfTool }" /> </div> </template> @@ -29,7 +27,10 @@ export default { name: "pdftool", computed: { - ...mapState("application", ["showPdfTool"]) + ...mapState("application", ["showPdfTool"]), + label() { + return this.$gettext("Generate PDF"); + } } }; </script>
--- a/client/src/components/toolbar/Polygontool.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/toolbar/Polygontool.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,9 +1,9 @@ <template> - <div @click="togglePolygonTool" class="toolbar-button"> + <div @click="toggle" class="toolbar-button" v-tooltip.right="label"> <font-awesome-icon icon="draw-polygon" - :class="{ 'text-info': polygonTool && polygonTool.getActive() }" - ></font-awesome-icon> + :class="{ 'text-info': polygonToolEnabled }" + /> </div> </template> @@ -21,22 +21,27 @@ * Author(s): * Markus Kottländer <markus.kottlaender@intevation.de> */ -import { mapState, mapGetters } from "vuex"; -import { LAYERS } from "@/store/map.js"; +import { mapState } from "vuex"; export default { name: "polygontool", computed: { - ...mapGetters("map", ["getVSourceByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]) + ...mapState("map", ["openLayersMaps", "polygonToolEnabled"]), + label() { + return this.$gettext("Measure Area"); + } }, methods: { - togglePolygonTool() { - this.polygonTool.setActive(!this.polygonTool.getActive()); - this.lineTool.setActive(false); - this.cutTool.setActive(false); + toggle() { + this.$store.commit("map/polygonToolEnabled", !this.polygonToolEnabled); + this.$store.commit("map/lineToolEnabled", false); + this.$store.commit("map/cutToolEnabled", false); this.$store.commit("map/setCurrentMeasurement", null); - this.getVSourceByName(LAYERS.DRAWTOOL).clear(); + this.openLayersMaps.forEach(m => { + m.getLayer("DRAWTOOL") + .getSource() + .clear(); + }); } } };
--- a/client/src/components/toolbar/Profiles.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/toolbar/Profiles.vue Mon Jun 03 10:19:18 2019 +0200 @@ -2,11 +2,12 @@ <div @click="$store.commit('application/showProfiles', !showProfiles)" class="toolbar-button" + v-tooltip.right="label" > <font-awesome-icon icon="chart-area" :class="{ 'text-info': showProfiles }" - ></font-awesome-icon> + /> </div> </template> @@ -29,7 +30,10 @@ export default { name: "profiles", computed: { - ...mapState("application", ["showProfiles"]) + ...mapState("application", ["showProfiles"]), + label() { + return this.$gettext("Bottleneck Surveys"); + } } }; </script>
--- a/client/src/components/toolbar/Toolbar.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/toolbar/Toolbar.vue Mon Jun 03 10:19:18 2019 +0200 @@ -6,13 +6,14 @@ (expandToolbar ? 'expanded' : 'collapsed') " > - <Identify class="pointer" /> - <Layers class="pointer" /> - <Profiles class="pointer" /> - <Gauges class="pointer" /> - <Linetool class="pointer" /> - <Polygontool class="pointer" /> - <Pdftool class="pointer" /> + <Identify /> + <Layers /> + <Profiles /> + <Gauges /> + <AvailableFairwayDepth /> + <Linetool /> + <Polygontool /> + <Pdftool /> </div> <div @click="$store.commit('application/expandToolbar', !expandToolbar)" @@ -21,7 +22,7 @@ <font-awesome-icon class="pointer" :icon="expandToolbar ? 'angle-up' : 'angle-down'" - ></font-awesome-icon> + /> </div> </div> </template> @@ -34,10 +35,11 @@ overflow: hidden; transition: max-height 0.4s; margin-bottom: auto; + cursor: pointer; } .toolbar-collapsed { - max-height: 6rem; + max-height: 4rem; } .toolbar-expanded { @@ -58,6 +60,10 @@ pointer-events: auto; position: relative; overflow: hidden; + &.disabled { + color: #ccc; + cursor: default; + } } .toolbar-button:last-child { @@ -110,37 +116,22 @@ * Author(s): * Markus Kottländer <markus.kottlaender@intevation.de> */ -import { mapState, mapGetters } from "vuex"; -import { LAYERS } from "@/store/map.js"; +import { mapState } from "vuex"; export default { name: "toolbar", components: { - Identify: () => import("./Identify.vue"), - Layers: () => import("./Layers.vue"), - Linetool: () => import("./Linetool.vue"), - Polygontool: () => import("./Polygontool.vue"), - Profiles: () => import("./Profiles.vue"), - Gauges: () => import("./Gauges.vue"), - Pdftool: () => import("./Pdftool.vue") + Identify: () => import("./Identify"), + Layers: () => import("./Layers"), + Linetool: () => import("./Linetool"), + Polygontool: () => import("./Polygontool"), + Profiles: () => import("./Profiles"), + Gauges: () => import("./Gauges"), + Pdftool: () => import("./Pdftool"), + AvailableFairwayDepth: () => import("./AvailableFairwayDepth") }, computed: { - ...mapGetters("map", ["getVSourceByName"]), - ...mapState("map", ["lineTool", "polygonTool", "cutTool"]), ...mapState("application", ["expandToolbar"]) - }, - mounted() { - window.addEventListener("keydown", e => { - // Escape - if (e.keyCode === 27) { - this.lineTool.setActive(false); - this.polygonTool.setActive(false); - this.cutTool.setActive(false); - this.$store.commit("map/setCurrentMeasurement", null); - this.$store.dispatch("map/enableIdentifyTool"); - this.getVSourceByName(LAYERS.DRAWTOOL).clear(); - } - }); } }; </script>
--- a/client/src/components/ui/UIBoxHeader.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/ui/UIBoxHeader.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,5 +1,5 @@ <template> - <h6 :class="['box-header', { 'rounded border-0 shadow-xs': collapsed }]"> + <h6 :class="['box-header', { small }]"> <span class="box-title"> <font-awesome-icon :icon="icon" @@ -9,21 +9,19 @@ /> {{ title }} </span> - <div class="box-controls"> + <div class="d-flex flex-row"> <span + class="box-control" v-for="(action, index) in actions" :key="index" @click="action.callback" > - <font-awesome-icon :icon="action.icon" /> - </span> - <span @click="collapseCallback" v-if="!collapsed && collapseCallback"> - <font-awesome-icon :icon="['far', 'window-minimize']" /> + <font-awesome-icon + :icon="action.icon" + :spin="action.icon === 'spinner'" + /> </span> - <span @click="expandCallback" v-if="collapsed && expandCallback"> - <font-awesome-icon :icon="['far', 'window-maximize']" /> - </span> - <span @click="closeCallback" v-if="closeCallback"> + <span class="box-control" @click="closeCallback" v-if="closeCallback"> <font-awesome-icon icon="times" /> </span> </div> @@ -49,18 +47,11 @@ padding-left: 0.25rem .box-icon margin-right: 0.25rem - .box-controls - span - display: inline-block - margin-left: 3px - color: #888 - padding: 3px 7px - border-radius: 0.25rem - cursor: pointer - transition: background-color 0.3s, color 0.3s - &:hover - color: #666 - background-color: #eee + .box-control + margin-left: 3px + &.small + padding: 0.1rem 0.1rem 0.1rem 0.25rem + min-height: 27px </style> <script> @@ -79,14 +70,12 @@ */ export default { - props: [ - "icon", - "title", - "collapseCallback", - "closeCallback", - "expandCallback", - "actions", - "collapsed" - ] + props: { + icon: String, + title: String, + closeCallback: Function, + actions: Array, + small: Boolean + } }; </script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/ui/UISpinnerButton.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,56 @@ +<template> + <div :class="classesString" @click="$emit('click')"> + <font-awesome-icon :icon="iconString" :spin="loading" fixed-width /> + <slot /> + </div> +</template> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ +export default { + props: { + loading: { + type: Boolean, + default: false + }, + state: [Number, Boolean], + icons: { + type: [String, Array], + default: () => ["angle-down", "angle-up"] + }, + classes: { + type: [String, Array], + default: () => ["text-info", "text-white"] + } + }, + computed: { + classesString() { + return ( + "pointer " + + (Array.isArray(this.classes) + ? this.classes[Number(this.state)] + : this.classes) + ); + }, + iconString() { + return this.loading + ? "spinner" + : Array.isArray(this.icons) + ? this.icons[Number(this.state)] + : this.icons; + } + } +}; +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/ui/UISpinnerOverlay.vue Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,38 @@ +<template> + <transition name="fade"> + <div class="spinner-overlay"> + <font-awesome-icon icon="spinner" spin /> + </div> + </transition> +</template> + +<style lang="sass"> +.spinner-overlay + background: rgba(255, 255, 255, 0.9) + position: absolute + z-index: 99 + top: 0 + right: 0 + bottom: 0 + left: 0 + display: flex + align-items: center + justify-content: center + color: #888 +</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> + */ +</script>
--- a/client/src/components/ui/UITableBody.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/ui/UITableBody.vue Mon Jun 03 10:19:18 2019 +0200 @@ -7,12 +7,14 @@ <div v-for="(item, index) in data" :key="key(index)" - :class="[ - 'border-top row mx-0 align-items-center', - { active: active === item } - ]" + :class="['row-container border-top', { active: isActive(item) }]" > - <slot :item="item" :index="index"></slot> + <div class="row mx-0"> + <slot :item="item" :index="index" name="row"></slot> + </div> + <div class="expand" v-if="isActive(item)"> + <slot :item="item" :index="index" name="expand"></slot> + </div> </div> </div> <div v-else class="small text-center py-3 border-top"> @@ -20,6 +22,35 @@ </div> </template> +<style lang="sass"> +.table-body + .row-container + > .row + &:hover + background-color: #fcfcfc + .table-cell + display: flex + align-items: center + padding: 1.5px 3px + border-right: solid 1px #dee2e6 + &:last-child + border-right: none + &.center + justify-content: center + .expand + border-bottom: solid 2px $color-info + + &.active + > .row + color: #fff + .table-cell + border-right-color: rgba(255, 255, 255, 0.3) + background-color: $color-info + color: #fff + a + color: #fff !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. @@ -44,8 +75,9 @@ type: String, default: "18rem" }, - active: { - type: [Object, Array] + isActive: { + type: Function, + default: () => false } }, methods: {
--- a/client/src/components/ui/UITableHeader.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/ui/UITableHeader.vue Mon Jun 03 10:19:18 2019 +0200 @@ -5,7 +5,7 @@ @click.prevent="!column.disableSorting && sortTable(column.id)" :key="column.id" :class="[ - 'd-flex py-1 align-items-center justify-content-center small ' + + 'd-inline-block py-1 text-center truncate small ' + (column.class || '') + ' ' + (column.disableSorting ? ' sorting-disabled' : ''),
--- a/client/src/components/usermanagement/Passwordfield.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/usermanagement/Passwordfield.vue Mon Jun 03 10:19:18 2019 +0200 @@ -12,14 +12,12 @@ :required="required" /> <span class="input-group-text" @click="showPassword"> - <font-awesome-icon - :icon="readablePassword ? 'eye-slash' : 'eye'" - ></font-awesome-icon> + <font-awesome-icon :icon="readablePassword ? 'eye-slash' : 'eye'" /> </span> </div> <div v-show="passworderrors" class="text-danger"> <small> - <font-awesome-icon icon="exclamation-triangle"></font-awesome-icon> + <font-awesome-icon icon="exclamation-triangle" /> {{ this.passworderrors }} </small> </div>
--- a/client/src/components/usermanagement/Userdetail.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/usermanagement/Userdetail.vue Mon Jun 03 10:19:18 2019 +0200 @@ -20,9 +20,7 @@ /> <div v-show="errors.user" class="text-danger"> <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> + <font-awesome-icon icon="exclamation-triangle" /> {{ errors.user }} </small> </div> @@ -46,9 +44,7 @@ </select> <div v-show="errors.country" class="text-danger"> <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> + <font-awesome-icon icon="exclamation-triangle" /> {{ errors.country }} </small> </div> @@ -65,9 +61,7 @@ /> <div v-show="errors.email" class="text-danger"> <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> + <font-awesome-icon icon="exclamation-triangle" /> {{ errors.email }} </small> </div> @@ -94,9 +88,7 @@ </select> <div v-show="errors.role" class="text-danger"> <small> - <font-awesome-icon - icon="exclamation-triangle" - ></font-awesome-icon> + <font-awesome-icon icon="exclamation-triangle" /> {{ errors.role }} </small> </div> @@ -107,7 +99,7 @@ :placeholder="passwordPlaceholder" :label="passwordLabel" :passworderrors="errors.password" - ></PasswordField> + /> </div> <div class="form-group row"> <PasswordField @@ -115,7 +107,7 @@ :placeholder="passwordRePlaceholder" :label="passwordReLabel" :passworderrors="errors.passwordre" - ></PasswordField> + /> </div> </div> <div> @@ -124,7 +116,7 @@ :disabled="submitted" class="shadow-sm btn btn-info submit-button" > - <translate>Submit</translate> + <translate>Save</translate> </button> </div> </form> @@ -173,7 +165,7 @@ * Author(s): * Thomas Junk <thomas.junk@intevation.de> */ -import { displayError } from "@/lib/errors.js"; +import { displayError } from "@/lib/errors"; import { mapState } from "vuex"; const emptyErrormessages = () => {
--- a/client/src/components/usermanagement/Usermanagement.vue Wed May 29 10:58:45 2019 +0200 +++ b/client/src/components/usermanagement/Usermanagement.vue Mon Jun 03 10:19:18 2019 +0200 @@ -1,6 +1,6 @@ <template> <div class="main d-flex flex-row" style="position: relative;"> - <Spacer></Spacer> + <Spacer /> <div class="d-flex content py-2"> <div :class="userlistStyle"> <div class="card shadow-xs"> @@ -8,86 +8,89 @@ <UITableHeader :columns="[ { id: 'role', title: `${roleForColumLabel}`, class: 'col-1' }, - { id: 'user', title: `${usernameLabel}`, class: 'col-3' }, + { id: 'user', title: `${usernameLabel}`, class: 'col-4' }, { id: 'country', title: `${countryLabel}`, class: 'col-2' }, { id: 'email', title: `${emailLabel}`, class: 'col-3' } ]" /> <UITableBody :data="users | sortTable(sortColumn, sortDirection, page, pageSize)" + :isActive="item => item === currentUser" maxHeight="47rem" - :active="currentUser" - v-slot="{ item: user }" > - <div class="py-1 col-1" @click="selectUser(user.user)"> - <font-awesome-icon - v-tooltip="roleLabel(user.role)" - :icon="roleIcon(user.role)" - class="fa-lg" - ></font-awesome-icon> - </div> - <div class="py-1 col-3" @click="selectUser(user.user)"> - {{ user.user }} - </div> - <div class="py-1 col-2" @click="selectUser(user.user)"> - {{ user.country }} - </div> - <div class="py-1 col-3" @click="selectUser(user.user)"> - {{ user.email }} - </div> - <div class="py-1 col text-right"> + <template v-slot:row="{ item: user }"> + <div + class="table-cell center col-1" + @click="selectUser(user.user)" + > + <font-awesome-icon + v-tooltip="roleLabel(user.role)" + :icon="roleIcon(user.role)" + class="fa-lg" + /> + </div> + <div class="table-cell col-4" @click="selectUser(user.user)"> + {{ user.user }} + </div> + <div + class="table-cell center col-2" + @click="selectUser(user.user)" + > + {{ user.country }} + </div> + <div class="table-cell col-3" @click="selectUser(user.user)"> + {{ user.email }} + </div> + <div class="table-cell col text-right justify-content-end"> + <button + @click="sendTestMail(user.user)" + class="btn btn-xs btn-dark mr-1" + v-tooltip="sendMailLabel" + v-if="user.email" + > + <font-awesome-icon icon="paper-plane" fixed-width /> + </button> + <button + @click="deleteUser(user.user)" + class="btn btn-xs btn-dark" + v-tooltip="deleteUserLabel" + > + <font-awesome-icon icon="trash" fixed-width /> + </button> + </div> + </template> + </UITableBody> + <div class="p-3 border-top d-flex justify-content-between"> + <div></div> + <div> <button - @click="sendTestMail(user.user)" - class="btn btn-xs btn-dark mr-1" - v-tooltip="sendMailLabel" - v-if="user.email" + @click="prevPage" + v-if="this.page !== 1" + class="mr-2 btn btn-sm btn-light align-self-center" > - <font-awesome-icon icon="paper-plane" fixed-width /> + <font-awesome-icon icon="angle-left" /> </button> + {{ this.page }} / {{ this.pages }} <button - @click="deleteUser(user.user)" - class="btn btn-xs btn-dark" - v-tooltip="deleteUserLabel" + @click="nextPage" + v-if="this.page !== this.pages" + class="ml-2 btn btn-sm btn-light align-self-center" > - <font-awesome-icon icon="trash" fixed-width /> + <font-awesome-icon icon="angle-right" /> </button> </div> - </UITableBody> - <div class="d-flex mx-auto align-items-center"> - <button - @click="prevPage" - v-if="this.page !== 1" - class="mr-2 btn btn-sm btn-light align-self-center" - > - <font-awesome-icon icon="angle-left"></font-awesome-icon> - </button> - {{ this.page }} / {{ this.pages }} - <button - @click="nextPage" - v-if="this.page !== this.pages" - class="ml-2 btn btn-sm btn-light align-self-center" - > - <font-awesome-icon icon="angle-right"></font-awesome-icon> - </button> - </div> - <div class="mr-3 py-3 text-right"> <button @click="addUser" class="btn btn-info addbutton shadow-sm"> <translate>Add User</translate> </button> </div> </div> </div> - <Userdetail v-if="isUserDetailsVisible"></Userdetail> + <Userdetail v-if="isUserDetailsVisible" /> </div> </div> </template> <style lang="sass" scoped> -.addbutton - position: absolute - bottom: $offset - right: $offset - .content width: 100% @@ -109,16 +112,6 @@ .userlistextended width: 100% - -.table-body - /deep/.row - > div - transition: background-color 0.3s, color 0.3s - &.active - background-color: $color-info - color: #fff - a - color: #fff !important </style> <script> @@ -137,16 +130,10 @@ */ import store from "@/store"; import { mapGetters, mapState } from "vuex"; -import { displayError, displayInfo } from "@/lib/errors.js"; +import { displayError, displayInfo } from "@/lib/errors"; import { HTTP } from "@/lib/http"; -import Vue from "vue"; -import { VTooltip, VPopover, VClosePopover } from "v-tooltip"; import { sortTable } from "@/lib/mixins"; -Vue.directive("tooltip", VTooltip); -Vue.directive("close-popover", VClosePopover); -Vue.component("v-popover", VPopover); - export default { name: "userview", mixins: [sortTable],
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/lib/classifications.js Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,92 @@ +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Raimund Renkert <raimund.renkert@intevation.de> + */ + +const getGauge = f => { + if (f.getId().indexOf("bottlenecks") > -1) { + return f.get("gauge_obj"); + } + return f; +}; + +export default { + surveyCurrency(bottleneck) { + if ( + bottleneck.get("revisiting_time") === null || + bottleneck.get("revisiting_time") === 0 + ) { + return "white"; + } + if (bottleneck.get("date_max") === null) { + return "red"; + } + let revTime = bottleneck.get("revisiting_time") * 30.5; + let latest = Date.parse(bottleneck.get("date_max").replace("Z", "")); + var diff = Math.floor((Date.now() - latest) / 86400000); + if (diff <= revTime) { + return "lime"; + } else if (revTime < diff && diff <= revTime * 1.5) { + return "yellow"; + } else if (revTime * 1.5 < diff) { + return "red"; + } + }, + gmAvailability(feature) { + let gauge = getGauge(feature); + let gmDate = gauge.get("gm_measuredate"); + let gmN = gauge.get("gm_n_14d"); + if ( + gmDate !== undefined && + gmDate !== null && + Date.parse(gmDate) > Date.now() - 86400000 // latest value within 24 h + ) { + // 1344: one value every 15 min in 14 days, but the Hydra says: + // let 85% be enough for now. + const valuesAtLeast = 1124; + if (gmN !== undefined && gmN !== null && gmN >= valuesAtLeast) { + return "lime"; + } + return "yellow"; + } + return "red"; + }, + forecastAccuracy(feature) { + let gauge = getGauge(feature); + let fa3d = gauge.get("forecast_accuracy_3d"); + let fa1d = gauge.get("forecast_accuracy_1d"); + if (typeof fa3d == "number" && typeof fa1d == "number") { + if (fa1d > 15) { + return "red"; + } else if (fa3d > 15) { + return "yellow"; + } else { + return "lime"; + } + } + return "white"; + }, + forecastVsReality(feature) { + let gauge = getGauge(feature); + let nsc = gauge.get("nsc_data"); + if (nsc && nsc.coeffs.reduce((sum, coeff) => sum + coeff.samples, 0)) { + // 24h < 12.5 + if (nsc.coeffs[0].samples && nsc.coeffs[0].value < -12.5) return "red"; + // 72h < 12.5 + if (nsc.coeffs[2].samples && nsc.coeffs[2].value < -12.5) return "yellow"; + // both > 12.5 + return "lime"; + } + // no data available + return "white"; + } +};
--- a/client/src/lib/errors.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/lib/errors.js Mon Jun 03 10:19:18 2019 +0200 @@ -12,7 +12,7 @@ * Thomas Junk <thomas.junk@intevation.de> */ -import app from "../main"; +import app from "@/main"; let displayOptions = { timeout: 2500,
--- a/client/src/lib/filters.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/lib/filters.js Mon Jun 03 10:19:18 2019 +0200 @@ -25,20 +25,22 @@ }) : ""; }, - dateTime(date) { + dateTime(date, hideTime) { if (!date) return ""; const d = new Date(date); - return ( - d.toLocaleDateString(locale2, { - day: "2-digit", - month: "2-digit", - year: "numeric" - }) + - " - " + - d.toLocaleTimeString(locale2, { - hour12: false - }) - ); + let dateString = d.toLocaleDateString(locale2, { + day: "2-digit", + month: "2-digit", + year: "numeric" + }); + if (!hideTime) { + dateString += + " - " + + d.toLocaleTimeString(locale2, { + hour12: false + }); + } + return dateString; }, sortTable(data, sortColumn, sortDirection, page, pageSize) { // clone the array and leave the original intact
--- a/client/src/lib/geo.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/lib/geo.js Mon Jun 03 10:19:18 2019 +0200 @@ -20,7 +20,7 @@ * */ -import { GeoJSON } from "ol/format.js"; +import { GeoJSON } from "ol/format"; import Feature from "ol/Feature"; import distance from "@turf/distance"; import {
--- a/client/src/lib/http.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/lib/http.js Mon Jun 03 10:19:18 2019 +0200 @@ -12,7 +12,7 @@ * Thomas Junk <thomas.junk@intevation.de> */ -import { logOff } from "@/lib/session.js"; +import { logOff } from "@/lib/session"; import axios from "axios";
--- a/client/src/lib/mixins.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/lib/mixins.js Mon Jun 03 10:19:18 2019 +0200 @@ -10,9 +10,11 @@ * * Author(s): * Markus Kottländer <markus.kottlaender@intevation.de> + * Fadi Abbud <fadi.abbud@intevation.de> */ - -const sortTable = { +import locale2 from "locale2"; +import { mapState } from "vuex"; +export const sortTable = { data() { return { sortColumn: "", @@ -29,4 +31,199 @@ } }; -export { sortTable }; +export const diagram = { + methods: { + getDimensions({ main, nav }) { + //dimensions and margins + const elem = document.querySelector("#" + this.containerId); + const svgWidth = elem != null ? elem.clientWidth : 0; + const svgHeight = elem != null ? elem.clientHeight : 0; + const mainMargin = main || { top: 20, right: 20, bottom: 110, left: 80 }; + const navMargin = nav || { + top: svgHeight - mainMargin.top - 65, + right: 20, + bottom: 30, + left: 80 + }; + const width = Number(svgWidth) - mainMargin.left - mainMargin.right; + const mainHeight = Number(svgHeight) - mainMargin.top - mainMargin.bottom; + const navHeight = Number(svgHeight) - navMargin.top - navMargin.bottom; + return { width, mainHeight, navHeight, mainMargin, navMargin }; + } + } +}; + +export const pane = { + computed: { + paneId() { + return this.$parent.pane.id; + } + } +}; + +export const pdfgen = { + computed: { + ...mapState("application", ["logoForPDF"]), + ...mapState("user", ["user"]) + }, + methods: { + // add text at specific coordinates and determine how many wrolds in a single line + addText(position, offset, width, fontSize, color, text) { + text = this.replacePlaceholders(text); + // split the incoming string to an array, each element is a string of + // words in a single line + this.pdf.doc.setFontStyle("normal"); + this.pdf.doc.setTextColor(color); + this.pdf.doc.setFontSize(fontSize); + var textLines = this.pdf.doc.splitTextToSize(text, width); + // x/y defaults to offset for topleft corner (normal x/y coordinates) + let x = offset.x; + let y = offset.y; + // if position is on the right, x needs to be calculate with pdf width and + // the size of the element + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - this.getTextHeight(textLines.length); + } + this.pdf.doc.text(textLines, x, y, { baseline: "hanging" }); + }, + replacePlaceholders(text) { + if (text.includes("{date}")) { + text = text.replace("{date}", new Date().toLocaleString(locale2)); + } + // get only day,month and year from the Date object + if (text.includes("{date-minor}")) { + var date = new Date(); + var dt = + (date.getDate() < 10 ? "0" : "") + + date.getDate() + + "." + + (date.getMonth() + 1 < 10 ? "0" : "") + + (date.getMonth() + 1) + + "." + + date.getFullYear(); + text = text.replace("{date-minor}", dt.toLocaleString(locale2)); + } + if (text.includes("{user}")) { + text = text.replace("{user}", this.user); + } + return text; + }, + addImage(url, format, position, offset, width, height) { + let x = offset.x; + let y = offset.y; + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - height; + } + let image = new Image(); + if (url) { + image.src = url; + } else { + if (this.logoForPDF) { + image.src = this.logoForPDF; + } else { + image.src = "/img/gemma-logo-for-pdf.png"; + } + } + if (format === "") { + let tmp = image.src.split("."); + format = tmp[tmp.length - 1].toUpperCase(); + } + this.pdf.doc.addImage(image, format, x, y, width, height); + }, + // add text at specific coordinates with a background box + addBox(position, offset, width, height, rounding, color, brcolor) { + // x/y defaults to offset for topleft corner (normal x/y coordinates) + let x = offset.x; + let y = offset.y; + + // if position is on the right, x needs to be calculate with pdf width and + // the size of the element + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - height; + } + this.addRoundedBox(x, y, width, height, color, rounding, brcolor); + }, + getTextHeight(numberOfLines) { + return ( + numberOfLines * + ((this.pdf.doc.getFontSize() * 25.4) / 80) * + this.pdf.doc.getLineHeightFactor() + ); + }, + // title for diagram + addDiagramTitle(position, offset, size, color, text) { + let x = offset.x, + y = offset.y; + this.pdf.doc.setFontSize(size); + this.pdf.doc.setFontStyle("bold"); + this.pdf.doc.setTextColor(color); + let width = + (this.pdf.doc.getStringUnitWidth(text) * size) / (72 / 25.6) + size / 2; + // if position is on the right, x needs to be calculate with pdf width and + // the size of the element + if (["topright", "bottomright"].indexOf(position) !== -1) { + x = this.pdf.width - offset.x - width; + } + if (["bottomright", "bottomleft"].indexOf(position) !== -1) { + y = this.pdf.height - offset.y - this.getTextHeight(1); + } + this.pdf.doc.text(text, x, y, { baseline: "hanging" }); + }, + addRoundedBox(x, y, w, h, color, rounding, brcolor) { + this.pdf.doc.setDrawColor(brcolor); + this.pdf.doc.setFillColor(color); + this.pdf.doc.roundedRect(x, y, w, h, rounding, rounding, "FD"); + }, + addTextBox( + position, + offset, + width, + height, + rounding, + padding, + fontSize, + color, + background, + text, + brcolor + ) { + this.pdf.doc.setFontSize(fontSize); + text = this.replacePlaceholders(text); + + if (!width) { + width = this.pdf.doc.getTextWidth(text) + 2 * padding; + } + let textWidth = width - 2 * padding; + if (!height) { + let textLines = this.pdf.doc.splitTextToSize(text, textWidth); + height = this.getTextHeight(textLines.length) + 2 * padding; + } + this.addBox( + position, + offset, + width, + height, + rounding, + background, + brcolor + ); + this.addText( + position, + { x: offset.x + padding, y: offset.y + padding }, + textWidth, + fontSize, + color, + text + ); + } + } +};
--- a/client/src/main.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/main.js Mon Jun 03 10:19:18 2019 +0200 @@ -28,11 +28,13 @@ import store from "@/store"; import translations from "@/locale/translations.json"; import filters from "@/lib/filters"; -import { supportedLanguages, defaultLanguage } from "./locale/languages.js"; -import App from "@/components/App.vue"; +import { supportedLanguages, defaultLanguage } from "./locale/languages"; +import App from "@/components/App"; import UIBoxHeader from "@/components/ui/UIBoxHeader"; import UITableHeader from "@/components/ui/UITableHeader"; import UITableBody from "@/components/ui/UITableBody"; +import UISpinnerOverlay from "@/components/ui/UISpinnerOverlay"; +import UISpinnerButton from "@/components/ui/UISpinnerButton"; // styles import "../node_modules/bootstrap/dist/css/bootstrap.min.css"; @@ -47,10 +49,12 @@ faAngleLeft, faAngleRight, faAngleUp, + faAngleDoubleRight, faBars, faBook, - faBullseye, faChartArea, + faChartBar, + faChartLine, faCheck, faCity, faClipboardCheck, @@ -64,10 +68,13 @@ faEyeSlash, faFilePdf, faFolderPlus, + faFrownOpen, faInfo, faLayerGroup, + faLink, faMapMarkedAlt, faMinus, + faObjectGroup, faPaperPlane, faPencilAlt, faPlay, @@ -88,12 +95,14 @@ faTasks, faTimes, faTrash, + faUnlink, faUpload, faUser, faUsersCog, faWater, faWrench, faRedo, + faSync, faCrosshairs } from "@fortawesome/free-solid-svg-icons"; import { @@ -108,10 +117,12 @@ faAngleLeft, faAngleRight, faAngleUp, + faAngleDoubleRight, faBars, faBook, - faBullseye, faChartArea, + faChartBar, + faChartLine, faCheck, faCity, faClipboardCheck, @@ -125,10 +136,13 @@ faEyeSlash, faFilePdf, faFolderPlus, + faFrownOpen, faInfo, faLayerGroup, + faLink, faMapMarkedAlt, faMinus, + faObjectGroup, faPaperPlane, faPencilAlt, faPlay, @@ -149,22 +163,24 @@ faTasks, faTimes, faTrash, + faUnlink, faUpload, faUser, faUsersCog, faWater, faWrench, faRedo, + faSync, faWindowMinimize, faWindowMaximize, faCrosshairs ); - // register plugins Vue.use(GetTextPlugin, { translations: translations, availableLanguages: supportedLanguages, - defaultLanguage: defaultLanguage + defaultLanguage: defaultLanguage, + silent: process.env.VUE_APP_SILENCE_TRANSLATIONWARNINGS }); Vue.use(Snotify, { toast: { position: SnotifyPosition.centerBottom } }); Vue.use(ToggleButton); @@ -176,6 +192,8 @@ Vue.component("UIBoxHeader", UIBoxHeader); Vue.component("UITableHeader", UITableHeader); Vue.component("UITableBody", UITableBody); +Vue.component("UISpinnerOverlay", UISpinnerOverlay); +Vue.component("UISpinnerButton", UISpinnerButton); // register global filters for (let name in filters) Vue.filter(name, filters[name]);
--- a/client/src/router.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/router.js Mon Jun 03 10:19:18 2019 +0200 @@ -19,8 +19,8 @@ import { sessionStillActive, toMillisFromString } from "./lib/session"; /* facilitate codesplitting */ -const Login = () => import("./components/Login.vue"); -const Maplayer = () => import("./components/Maplayer.vue"); +const Login = () => import("./components/Login"); +const Main = () => import("./components/Main"); Vue.use(Router); @@ -34,14 +34,14 @@ { path: "/usermanagement", name: "usermanagement", - component: () => import("./components/usermanagement/Usermanagement.vue"), + component: () => import("./components/usermanagement/Usermanagement"), meta: { requiresAuth: true }, beforeEnter: (to, from, next) => { const isSysadmin = store.getters["user/isSysAdmin"]; if (!isSysadmin) { - next("/"); + next("/login"); } else { next(); } @@ -50,14 +50,14 @@ { path: "/logs", name: "logs", - component: () => import("./components/Logs.vue"), + component: () => import("./components/Logs"), meta: { requiresAuth: true }, beforeEnter: (to, from, next) => { const isSysadmin = store.getters["user/isSysAdmin"]; if (!isSysadmin) { - next("/"); + next("/login"); } else { next(); } @@ -67,78 +67,14 @@ path: "/systemconfiguration", name: "systemconfiguration", component: () => - import("./components/systemconfiguration/Systemconfiguration.vue"), - meta: { - requiresAuth: true - }, - beforeEnter: (to, from, next) => { - const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; - if (!isWaterwayAdmin) { - next("/"); - } else { - next(); - } - } - }, - { - path: "/importsoundingresults", - name: "importsoundingresults", - component: () => import("./components/ImportSoundingresults.vue"), + import("./components/systemconfiguration/Systemconfiguration"), meta: { requiresAuth: true }, beforeEnter: (to, from, next) => { const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; if (!isWaterwayAdmin) { - next("/"); - } else { - next(); - } - } - }, - { - path: "/importwaterwayprofiles", - name: "waterwayprofiles", - component: () => import("./components/ImportWaterwayProfiles"), - meta: { - requiresAuth: true - }, - beforeEnter: (to, from, next) => { - const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; - if (!isWaterwayAdmin) { - next("/"); - } else { - next(); - } - } - }, - { - path: "/importapprovedgaugemeasurement", - name: "approvedgaugemeasurement", - component: () => import("./components/ImportApprovedGaugeMeasurement"), - meta: { - requiresAuth: true - }, - beforeEnter: (to, from, next) => { - const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; - if (!isWaterwayAdmin) { - next("/"); - } else { - next(); - } - } - }, - { - path: "/importschedule", - name: "importschedule", - component: () => import("./components/importschedule/Importschedule.vue"), - meta: { - requiresAuth: true - }, - beforeEnter: (to, from, next) => { - const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; - if (!isWaterwayAdmin) { - next("/"); + next("/login"); } else { next(); } @@ -147,7 +83,7 @@ { path: "/", name: "mainview", - component: Maplayer, + component: Main, meta: { requiresAuth: true }, @@ -162,7 +98,7 @@ { path: "/bottlenecks", name: "bottlenecks", - component: Maplayer, + component: Main, meta: { requiresAuth: true }, @@ -175,16 +111,36 @@ } }, { - path: "/imports/overview/:id?", - name: "importoverview", - component: Maplayer, + path: "/imports/configuration", + name: "importconfiguration", + component: Main, meta: { requiresAuth: true }, beforeEnter: (to, from, next) => { const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; if (!isWaterwayAdmin) { - next("/"); + next("/login"); + } else { + store.commit("application/searchQuery", ""); + store.commit("application/showContextBox", true); + store.commit("application/contextBoxContent", "importconfiguration"); + store.commit("application/showSearchbar", true); + next(); + } + } + }, + { + path: "/imports/overview/:id?", + name: "importoverview", + component: Main, + meta: { + requiresAuth: true + }, + beforeEnter: (to, from, next) => { + const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; + if (!isWaterwayAdmin) { + next("/login"); } else { store.commit("application/showContextBox", true); store.commit("application/contextBoxContent", "importoverview"); @@ -196,14 +152,14 @@ { path: "/stretches", name: "stretches", - component: Maplayer, + component: Main, meta: { requiresAuth: true }, beforeEnter: (to, from, next) => { const isSysadmin = store.getters["user/isSysAdmin"]; if (!isSysadmin) { - next("/"); + next("/login"); } else { store.commit("application/searchQuery", ""); store.commit("application/showContextBox", true); @@ -214,8 +170,36 @@ } }, { + path: "/sections", + name: "sections", + component: Main, + meta: { + requiresAuth: true + }, + beforeEnter: (to, from, next) => { + const isWaterwayAdmin = store.getters["user/isWaterwayAdmin"]; + if (!isWaterwayAdmin) { + next("/login"); + } else { + store.commit("application/searchQuery", ""); + store.commit("application/showContextBox", true); + store.commit("application/contextBoxContent", "sections"); + store.commit("application/showSearchbar", true); + next(); + } + } + }, + { + path: "/fairwaydepth", + name: "fairwaydepth", + component: () => import("./components/fairway/AvailableFairwayDepth"), + meta: { + requiresAuth: true + } + }, + { path: "*", - component: Login + component: () => import("./components/PageNotFound") } ] }); @@ -234,16 +218,12 @@ store.commit("user/clearAuth"); } const requiresAuth = to.matched.some(record => record.meta.requiresAuth); - const loggedIn = store.state.user.isAuthenticated; - const authRequired = - requiresAuth && !(loggedIn || sessionStillActive(expiresFromPastSession)); - if (authRequired) { + const redirectToLogin = requiresAuth && !store.state.user.isAuthenticated; + if (redirectToLogin) { + localStorage.setItem("tempRoute", to.path); next("/login"); - } else if (!authRequired) { - next(); - } else { - next(); } + next(); }); export default router;
--- a/client/src/store/application.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/store/application.js Mon Jun 03 10:19:18 2019 +0200 @@ -14,20 +14,18 @@ * Bernhard E. Reiter <bernhard.reiter@intevation.de> */ -import Vue from "vue"; import { version } from "../../package.json"; // initial state const init = () => { return { + userManualUrl: process.env.VUE_APP_USER_MANUAL_URL, appTitle: process.env.VUE_APP_TITLE, secondaryLogo: process.env.VUE_APP_SECONDARY_LOGO_URL, logoForPDF: process.env.VUE_APP_LOGO_FOR_PDF_URL, popup: null, - splitscreens: [], - splitscreenLoading: false, - activeSplitscreenId: null, - showSplitscreen: false, + paneSetup: "DEFAULT", + paneRotate: 1, showSidebar: false, showUsermenu: false, showSearchbar: false, @@ -38,11 +36,14 @@ showContextBox: false, showProfiles: false, showGauges: false, + showFairwayDepth: false, + showFairwayDepthLNWL: false, contextBoxContent: null, // bottlenecks, imports, staging expandToolbar: false, countries: ["AT", "SK", "HU", "HR", "RS", "BiH", "BG", "RO", "UA"], searchQuery: "", - version + version, + tempRoute: "" }; }; @@ -68,36 +69,29 @@ versionStr += "+" + process.env.VUE_APP_HGREV; return versionStr; - }, - activeSplitscreen: state => { - return state.splitscreens.find(s => s.id === state.activeSplitscreenId); } }, mutations: { + setTempRoute: (state, tempRoute) => { + state.tempRoute = tempRoute; + }, popup: (state, popup) => { state.popup = popup; }, + paneSetup: (state, setup) => { + state.paneSetup = setup; + }, + paneRotate: (state, rotate) => { + if (rotate) { + state.paneRotate = rotate; + } else { + state.paneRotate++; + if (state.paneRotate === 5) state.paneRotate = 1; + } + }, showSidebar: (state, show) => { state.showSidebar = show; }, - showSplitscreen: (state, show) => { - state.showSplitscreen = show; - }, - splitscreenLoading: (state, loading) => { - state.splitscreenLoading = loading; - }, - activeSplitscreenId: (state, id) => { - state.activeSplitscreenId = id; - }, - addSplitscreen: (state, config) => { - let index = state.splitscreens.findIndex(s => s.id === config.id); - if (index !== -1) Vue.set(state.splitscreens, index, config); - else state.splitscreens.push(config); - }, - removeSplitscreen: (state, id) => { - let index = state.splitscreens.findIndex(s => s.id === id); - if (index !== -1) state.splitscreens.splice(index, 1); - }, showUsermenu: (state, show) => { state.showUsermenu = show; }, @@ -122,6 +116,12 @@ showGauges: (state, show) => { state.showGauges = show; }, + showFairwayDepth: (state, show) => { + state.showFairwayDepth = show; + }, + showFairwayDepthLNWL: (state, show) => { + state.showFairwayDepthLNWL = show; + }, contextBoxContent: (state, context) => { state.contextBoxContent = context; if (context) {
--- a/client/src/store/bottlenecks.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/store/bottlenecks.js Mon Jun 03 10:19:18 2019 +0200 @@ -13,9 +13,8 @@ * Thomas Junk <thomas.junk@intevation.de> */ import { HTTP } from "@/lib/http"; -import { WFS } from "ol/format.js"; -import { displayError } from "@/lib/errors.js"; -import { LAYERS } from "@/store/map.js"; +import { WFS } from "ol/format"; +import { displayError } from "@/lib/errors"; // initial state const init = () => { @@ -33,6 +32,36 @@ init, namespaced: true, state: init(), + getters: { + limitingFactorsPerBottleneck: state => { + if (state.bottlenecks.length === 0) return {}; + return state.bottlenecks.reduce((o, n) => { + o[n.properties.objnam] = n.properties.limiting; + return o; + }, {}); + }, + orderedBottlenecks: state => { + let groupedBottlenecks = {}, + orderedGroups = {}; + + // group bottlenecks by cc + state.bottlenecksList.forEach(bn => { + let cc = bn.properties.responsible_country; + if (groupedBottlenecks.hasOwnProperty(cc)) { + groupedBottlenecks[cc].push(bn); + } else { + groupedBottlenecks[cc] = [bn]; + } + }); + + // order groups by cc + Object.keys(groupedBottlenecks) + .sort() + .forEach(cc => (orderedGroups[cc] = groupedBottlenecks[cc])); + + return orderedGroups; + } + }, mutations: { setBottlenecks: (state, bottlenecks) => { state.bottlenecks = bottlenecks; @@ -61,24 +90,18 @@ } }, actions: { - setSelectedBottleneck({ state, commit, rootState, rootGetters }, name) { + setSelectedBottleneck({ state, commit, rootState }, name) { return new Promise((resolve, reject) => { if (name !== state.selectedBottleneck) { commit("selectedSurvey", null); - commit("application/splitscreenLoading", true, { root: true }); - commit("application/showSplitscreen", false, { root: true }); - commit("application/removeSplitscreen", "fairwayprofile", { - root: true + commit("fairwayprofile/additionalSurvey", null, { root: true }); + commit("fairwayprofile/clearCurrentProfile", null, { root: true }); + commit("map/cutToolEnabled", false, { root: true }); + rootState.map.openLayersMaps.forEach(m => { + m.getLayer("CUTTOOL") + .getSource() + .clear(); }); - setTimeout(() => { - commit("fairwayprofile/clearCurrentProfile", null, { root: true }); - commit("application/splitscreenLoading", false, { root: true }); - }, 350); - rootState.map.cutTool.setActive(false); - rootGetters["map/getVSourceByName"](LAYERS.CUTTOOL).clear(); - } - if (name) { - commit("application/showProfiles", true, { root: true }); } commit("setSelectedBottleneck", name); if (name) { @@ -94,6 +117,7 @@ a.date_info < b.date_info ? 1 : -1 ); commit("setSurveys", surveys); + commit("setFirstSurveySelected"); resolve(response); }) .catch(error => { @@ -150,7 +174,8 @@ featureNS: "gemma", featurePrefix: "gemma", featureTypes: ["bottlenecks_geoserver"], - outputFormat: "application/json" + outputFormat: "application/json", + propertyNames: ["objnam", "limiting", "reference_water_levels"] }); HTTP.post( "/internal/wfs",
--- a/client/src/store/fairway.js Wed May 29 10:58:45 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,346 +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 <markuks.kottlaender@intevation.de> - */ -import Vue from "vue"; -import { HTTP } from "../lib/http"; -import { prepareProfile } from "../lib/geo"; -import LineString from "ol/geom/LineString.js"; -import { generateFeatureRequest } from "../lib/geo.js"; -import { getLength } from "ol/sphere.js"; -import { displayError } from "../lib/errors.js"; -import { featureToFairwayCoordinates } from "../lib/geo.js"; -import { LAYERS } from "@/store/map.js"; - -// initial state -const init = () => { - return { - additionalSurvey: null, - minAlt: 0, - maxAlt: 0, - currentProfile: {}, - referenceWaterLevel: null, - waterLevels: {}, - selectedWaterLevel: "", - fairwayData: [], - startPoint: null, - endPoint: null, - previousCuts: [], - profileLoading: false, - selectedCut: null - }; -}; - -export default { - init, - namespaced: true, - state: init(), - getters: { - totalLength: state => { - const keys = Object.keys(state.currentProfile); - return keys.length - ? Math.max(...keys.map(x => state.currentProfile[x].length)) - : 0; - }, - additionalSurvey: state => { - return state.additionalSurvey; - } - }, - mutations: { - additionalSurvey: (state, additionalSurvey) => { - state.additionalSurvey = additionalSurvey; - }, - setSelectedWaterLevel: (state, level) => { - state.selectedWaterLevel = state.waterLevels[level]; - }, - profileLoaded: (state, answer) => { - const { response, surveyDate } = answer; - const { data } = response; - const { waterlevel } = response.data.properties; - const { value, when } = waterlevel; - const coordinates = data.geometry.coordinates; - if (!coordinates) return; - const startPoint = state.startPoint; - const endPoint = state.endPoint; - const geoJSON = data; - const result = prepareProfile({ geoJSON, startPoint, endPoint }); - // Use Vue.set() to make new object properties rective - // https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats - const entry = { - date: when, - value: value - }; - state.waterLevels = { [when]: entry }; - state.selectedWaterLevel = entry; - Vue.set(state.currentProfile, surveyDate, { - points: result.points, - length: result.lengthPolyLine - }); - if (!state.minAlt || state.minAlt > result.minAlt) { - state.minAlt = result.minAlt; - } - if (!state.maxAlt || state.maxAlt < result.maxAlt) { - state.maxAlt = result.maxAlt; - } - }, - setStartPoint: (state, start) => { - state.startPoint = start; - }, - setEndPoint: (state, end) => { - state.endPoint = end; - }, - addFairwayData: (state, coordinates) => { - state.fairwayData.push(coordinates); - }, - clearFairwayData: state => { - state.fairwayData = []; - }, - clearCurrentProfile: state => { - state.additionalSurvey = null; - state.currentProfile = {}; - state.minAlt = null; - state.maxAlt = null; - state.totalLength = null; - state.fairwayData = []; - state.startPoint = null; - state.endPoint = null; - state.referenceWaterLevel = null; - state.waterLevels = {}; - state.selectedWaterLevel = ""; - }, - previousCuts: (state, previousCuts) => { - state.previousCuts = previousCuts; - }, - profileLoading: (state, loading) => { - state.profileLoading = loading; - }, - selectedCut: (state, cut) => { - state.selectedCut = cut; - } - }, - actions: { - clearSelection({ commit, dispatch, rootGetters, rootState }) { - dispatch("bottlenecks/setSelectedBottleneck", null, { root: true }); - dispatch("map/enableIdentifyTool", null, { root: true }); - commit("clearCurrentProfile"); - rootState.map.cutTool.setActive(false); - rootGetters["map/getVSourceByName"](LAYERS.CUTTOOL).clear(); - }, - loadProfile({ commit, state }, survey) { - if (state.startPoint && state.endPoint) { - return new Promise((resolve, reject) => { - const profileLine = new LineString([ - state.startPoint, - state.endPoint - ]); - const geoJSON = generateFeatureRequest( - profileLine, - survey.bottleneck_id, - survey.date_info - ); - HTTP.post("/cross", geoJSON, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - if (response.data.geometry.coordinates.length) { - commit("profileLoaded", { - response: response, - surveyDate: survey.date_info - }); - resolve(response); - } else { - commit("clearCurrentProfile"); - reject({ - response: { - status: null, - data: "No intersection with sounding data." - } - }); - } - }) - .catch(error => reject(error)); - }); - } - }, - cut({ commit, dispatch, rootState, rootGetters }, cut) { - return new Promise(resolve => { - const length = getLength(cut.getGeometry()); - commit( - "map/setCurrentMeasurement", - { - quantity: "Length", - unitSymbol: "m", - value: Math.round(length * 10) / 10 - }, - { root: true } - ); - commit("clearFairwayData"); - // if a survey has been selected, request a profile - // TODO an improvement could be to check if the line intersects - // with the bottleneck area's polygon before trying the server request - if (rootState.bottlenecks.selectedSurvey) { - const inputLineString = cut.getGeometry().clone(); - inputLineString.transform("EPSG:3857", "EPSG:4326"); - const [start, end] = inputLineString - .getCoordinates() - .map(coords => coords.map(coord => parseFloat(coord.toFixed(8)))); - commit("setStartPoint", start); - commit("setEndPoint", end); - const profileLine = new LineString([start, end]); - - const profileLoaders = [ - dispatch("loadProfile", rootState.bottlenecks.selectedSurvey) - ]; - if (rootState.fairwayprofile.additionalSurvey) { - profileLoaders.push( - dispatch("loadProfile", rootState.fairwayprofile.additionalSurvey) - ); - } - - commit("application/showSplitscreen", true, { root: true }); - commit("application/splitscreenLoading", true, { root: true }); - commit("profileLoading", true); - Promise.all(profileLoaders) - .then(() => { - rootState.map.cutTool.setActive(false); - const los3 = rootGetters["map/getLayerByName"]( - LAYERS.FAIRWAYDIMENSIONSLOS3 - ); - los3.data.getSource().forEachFeatureIntersectingExtent( - profileLine - .clone() - .transform("EPSG:4326", "EPSG:3857") - .getExtent(), - feature => { - const fairwayCoordinates = featureToFairwayCoordinates( - feature, - profileLine - ); - let fairwayData = { - coordinates: fairwayCoordinates, - style: los3.data.getStyle() - }; - if (fairwayCoordinates.length > 0) { - commit("addFairwayData", fairwayData); - } - } - ); - const los2 = rootGetters["map/getLayerByName"]( - LAYERS.FAIRWAYDIMENSIONSLOS2 - ); - los2.data.getSource().forEachFeatureIntersectingExtent( - profileLine - .clone() - .transform("EPSG:4326", "EPSG:3857") - .getExtent(), - feature => { - let fairwayCoordinates = featureToFairwayCoordinates( - feature, - profileLine - ); - let fairwayData = { - coordinates: fairwayCoordinates, - style: los2.data.getStyle() - }; - if (fairwayCoordinates.length > 0) { - commit("addFairwayData", fairwayData); - } - } - ); - const los1 = rootGetters["map/getLayerByName"]( - LAYERS.FAIRWAYDIMENSIONSLOS1 - ); - los1.data.getSource().forEachFeatureIntersectingExtent( - profileLine - .clone() - .transform("EPSG:4326", "EPSG:3857") - .getExtent(), - feature => { - const fairwayCoordinates = featureToFairwayCoordinates( - feature, - profileLine - ); - let fairwayData = { - coordinates: fairwayCoordinates, - style: los1.data.getStyle() - }; - if (fairwayCoordinates.length > 0) { - commit("addFairwayData", fairwayData); - } - } - ); - resolve(); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: "Backend Error", - message: `${status ? status + ":" : ""} ${data.message || data}` - }); - }) - .finally(() => { - let splitscreenConf = { - id: "fairwayprofile", - component: "fairwayprofile", - title: `${rootState.bottlenecks.selectedBottleneck} (${ - rootState.bottlenecks.selectedSurvey.date_info - })`, - icon: "chart-area", - closeCallback: () => { - dispatch("clearSelection"); - }, - expandCallback: () => { - let bottleneck = rootState.bottlenecks.bottlenecksList.find( - bn => - bn.properties.name === - rootState.bottlenecks.selectedBottleneck - ); - commit( - "map/moveToExtent", - { - feature: bottleneck, - zoom: 17, - preventZoomOut: true - }, - { root: true } - ); - } - }; - commit("application/addSplitscreen", splitscreenConf, { - root: true - }); - commit("application/activeSplitscreenId", "fairwayprofile", { - root: true - }); - commit("application/splitscreenLoading", false, { root: true }); - commit("profileLoading", false); - }); - } - }); - }, - previousCuts({ commit, rootState }) { - const previousCuts = - JSON.parse(localStorage.getItem("previousCuts")) || []; - commit( - "previousCuts", - previousCuts - .filter(cut => { - return ( - cut.bottleneckName === rootState.bottlenecks.selectedBottleneck - ); - }) - .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)) - ); - } - } -};
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/store/fairwayavailability.js Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,334 @@ +/* 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> + */ + +/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "_" }]*/ + +import { HTTP } from "@/lib/http"; +import { + format, + subYears, + startOfMonth, + endOfMonth, + startOfYear, + endOfYear, + startOfQuarter, + endOfQuarter +} from "date-fns"; + +const LIMITINGFACTORS = { + WIDTH: "width", + DEPTH: "depth" +}; + +const TYPES = { + BOTTLENECK: "bottleneck", + SECTION: "section", + STRETCH: "stretch" +}; + +const FREQUENCIES = { + MONTHLY: "monthly", + QUARTERLY: "quarterly", + YEARLY: "yearly" +}; + +const isoFormat = date => { + return format(date, "YYYY-MM-DD"); +}; + +const getIntervallBorders = (start, end, frequency) => { + switch (frequency) { + case FREQUENCIES.MONTHLY: + return [isoFormat(startOfMonth(start)), isoFormat(endOfMonth(end))]; + case FREQUENCIES.YEARLY: + return [isoFormat(startOfYear(start)), isoFormat(endOfYear(end))]; + case FREQUENCIES.QUARTERLY: + return [isoFormat(startOfQuarter(start)), isoFormat(endOfQuarter(end))]; + default: + throw new Error("Boom!"); + } +}; + +const init = () => { + return { + type: TYPES.BOTTLENECK, + selectedFairwayAvailabilityFeature: null, + to: isoFormat(new Date()), + from: isoFormat(subYears(new Date(), 1)), + frequency: FREQUENCIES.MONTHLY, + limitingFactor: null, + depthlimit1: 230, + depthlimit2: 250, + widthlimit1: null, + widthlimit2: null, + csv: null, + fwData: null, + fwLNWLData: null, + fwLNWLOverviewData: [], + legendLNWL: null, + legend: null, + LOS: 3 + }; +}; + +/** + * transformAFD + * @param {string} csv + * + * takes the afd csv and transforms it to an intermediary format + * for display of diagrams + * + * Incoming csv Format + * #label,# <LDC ,# >= LDC [h],# < 230.00 [h],# >= 230.00 [h],# >= 250.00 [h] + * 05-2019,215.500,0.000,0.000,215.500 + * + * Format: + * $LABEL, $LDC, $BELOWLIMIT1, $BETWEENLIMIT12, $ABOVELIMIT2^ + * + * This format is assumed to be fix + * + */ +const transformAFD = csv => { + return csv.map(e => { + const result = e.split(","); + let [label, _, ldc, lower, middle, highestLevel] = result; + let levelsWithSum = [ + { + height: Number(lower), + translateY: Number(middle) + }, + { + height: Number(middle), + translateY: 0 + } + ]; + return { + label: label, + ldc: ldc, + highestLevel: highestLevel, + lowerLevels: levelsWithSum + }; + }); +}; + +const fairwayavailability = { + init, + namespaced: true, + state: init(), + getters: { + fwLNWLOverviewData: state => feature => { + return state.fwLNWLOverviewData.find( + d => d.feature.get("id") === feature.get("id") + ); + } + }, + mutations: { + type: (state, type) => { + state.type = type; + }, + setLOS: (state, LOS) => { + state.LOS = LOS; + }, + setFrequency: (state, frequency) => { + state.frequency = frequency; + }, + setFrom: (state, from) => { + state.from = from; + }, + setTo: (state, to) => { + state.to = to; + }, + setDepthlimit1: (state, depthlimit1) => { + state.depthlimit1 = depthlimit1; + }, + setDepthlimit2: (state, depthlimit2) => { + state.depthlimit2 = depthlimit2; + }, + setWidthlimit1: (state, widthlimit1) => { + state.widthlimit1 = widthlimit1; + }, + setWidthlimit2: (state, widthlimit2) => { + state.widthlimit2 = widthlimit2; + }, + setSelectedFairwayAvailability: (state, feature) => { + state.selectedFairwayAvailabilityFeature = feature; + }, + setFwData: (state, fwData) => { + state.fwData = fwData; + }, + setFwLNWLData: (state, fwLNWLData) => { + state.fwLNWLData = fwLNWLData; + }, + setCSV: (state, csv) => { + state.csv = csv; + }, + addFwLNWLOverviewData: (state, data) => { + let existingIndex = state.fwLNWLOverviewData.findIndex( + d => d.feature.get("id") === data.feature.get("id") + ); + if (existingIndex !== -1) + state.fwLNWLOverviewData.splice(existingIndex, 1); + state.fwLNWLOverviewData.push(data); + }, + setLegend: (state, header) => { + const headerEntries = header.split(","); + headerEntries.shift(); + headerEntries.shift(); + state.legend = headerEntries.map(x => { + let entry = x.split("#")[1]; // split leading # + entry = entry.replace("[h]", "").trim(); // omit unit + return entry; + }); + }, + setLegendLNWL: (state, headerLNWL) => { + this.headerLNWL = headerLNWL; + } + }, + actions: { + loadAvailableFairwayDepth: ({ commit }, options) => { + return new Promise((resolve, reject) => { + const { + feature, + frequency, + LOS, + depthLimit1, + depthLimit2, + widthLimit1, + widthLimit2, + limitingFactor, + type + } = options; + let { from, to } = options; + let name = feature.hasOwnProperty("properties") + ? feature.properties.name + : feature.get("objnam"); + [from, to] = getIntervallBorders(from, to, frequency); + let additionalParams = ""; + let endpoint = type; + if (type === TYPES.BOTTLENECK) { + if (limitingFactor === LIMITINGFACTORS.DEPTH) + additionalParams = `&breaks=${depthLimit1},${depthLimit2}`; + if (limitingFactor === LIMITINGFACTORS.WIDTH) + additionalParams = `&breaks=${widthLimit1},${widthLimit2}`; + } else if (type == TYPES.SECTION || type == TYPES.STRETCH) { + additionalParams = `&depthbreaks=${depthLimit1},${depthLimit2}&widthbreaks=${widthLimit1},${widthLimit2}`; + } + const start = encodeURIComponent("00:00:00+00:00"); + const end = encodeURIComponent("23:59:59+00:00"); + const URL = `data/${endpoint}/fairway-depth/${encodeURIComponent( + name + )}?from=${from}T${start}&to=${to}T${end}&mode=${frequency}&los=${LOS}${additionalParams}`; + HTTP.get(URL, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + const { data } = response; + commit("setCSV", data); + const csv = data.split("\n").filter(x => x !== ""); //omit empty lines + commit("setLegend", csv.shift()); + let transformed = transformAFD(csv); + commit("setFwData", transformed); + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + }, + loadAvailableFairwayDepthLNWL: (context, options) => { + return new Promise((resolve, reject) => { + const { + feature, + frequency, + LOS, + depthLimit1, + depthLimit2, + widthLimit1, + widthLimit2, + limitingFactor, + type + } = options; + let { from, to } = options; + let name = feature.hasOwnProperty("properties") + ? feature.properties.name + : feature.get("objnam"); + [from, to] = getIntervallBorders(from, to, frequency); + const start = encodeURIComponent("00:00:00+00:00"); + const end = encodeURIComponent("23:59:59+00:00"); + let additionalParams = ""; + let endpoint = type || TYPES.BOTTLENECK; + if (type === TYPES.BOTTLENECK) { + if (limitingFactor === LIMITINGFACTORS.DEPTH) + additionalParams = `&breaks=${depthLimit1},${depthLimit2}`; + if (limitingFactor === LIMITINGFACTORS.WIDTH) + additionalParams = `&breaks=${widthLimit1},${widthLimit2}`; + } else if (type == TYPES.SECTION || type == TYPES.STRETCH) { + additionalParams = `&depthbreaks=${depthLimit1},${depthLimit2}&widthbreaks=${widthLimit1},${widthLimit2}`; + } + const URL = `data/${endpoint}/availability/${encodeURIComponent( + name + )}?from=${from}T${start}&to=${to}T${end}&mode=${frequency}&los=${LOS}${additionalParams}`; + HTTP.get(URL, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + const { data } = response; + resolve(data); + }) + .catch(error => { + reject(error); + }); + }); + }, + loadAvailableFairwayDepthLNWLDiagram: ({ commit, dispatch }, options) => { + dispatch("loadAvailableFairwayDepthLNWL", options).then(response => { + commit("setCSV", response); + let data = response.split("\n").filter(d => d); + data.shift(); // remove header line + data = data.map(d => { + let columns = d.split(","); + return { + date: columns[0], + ldc: Number(columns[2]), + below: Number(columns[3]), + between: Number(columns[4]), + above: Number(columns[5]) + }; + }); + commit("setFwLNWLData", data); + return data; + }); + }, + loadAvailableFairwayDepthLNWLForMap: ({ dispatch }, options) => { + return dispatch("loadAvailableFairwayDepthLNWL", options).then( + response => { + let data = response.split("\n").filter(d => d); + data.shift(); // remove header line + data = data.map(d => { + let columns = d.split(","); + return { + ldc: Number(columns[2]), + below: Number(columns[3]), + between: Number(columns[4]), + above: Number(columns[5]) + }; + }); + return data[0]; + } + ); + } + } +}; + +export { LIMITINGFACTORS, FREQUENCIES, fairwayavailability };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/store/fairwayprofile.js Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,314 @@ +/* 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 <markuks.kottlaender@intevation.de> + */ +import Vue from "vue"; +import { HTTP } from "@/lib/http"; +import { prepareProfile } from "@/lib/geo"; +import LineString from "ol/geom/LineString"; +import { generateFeatureRequest } from "@/lib/geo"; +import { getLength } from "ol/sphere"; +import { displayError } from "@/lib/errors"; +import { featureToFairwayCoordinates } from "@/lib/geo"; + +// initial state +const init = () => { + return { + additionalSurvey: null, + minAlt: 0, + maxAlt: 0, + currentProfile: {}, + selectedWaterLevel: "ref", + fairwayData: [], + startPoint: null, + endPoint: null, + previousCuts: [], + profileLoading: false, + selectedCut: null, + differencesLoading: false + }; +}; + +export default { + init, + namespaced: true, + state: init(), + getters: { + totalLength: state => { + const keys = Object.keys(state.currentProfile); + return keys.length + ? Math.max(...keys.map(x => state.currentProfile[x].length)) + : 0; + }, + additionalSurvey: state => { + return state.additionalSurvey; + } + }, + mutations: { + additionalSurvey: (state, additionalSurvey) => { + state.additionalSurvey = additionalSurvey; + }, + setSelectedWaterLevel: (state, level) => { + state.selectedWaterLevel = level; + }, + setDifferencesLoading: (state, value) => { + state.differencesLoading = value; + }, + profileLoaded: (state, answer) => { + const { response, surveyDate } = answer; + const { data } = response; + const coordinates = data.geometry.coordinates; + if (!coordinates) return; + const startPoint = state.startPoint; + const endPoint = state.endPoint; + const geoJSON = data; + const result = prepareProfile({ geoJSON, startPoint, endPoint }); + // Use Vue.set() to make new object properties rective + // https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats + Vue.set(state.currentProfile, surveyDate, { + points: result.points, + length: result.lengthPolyLine + }); + if (!state.minAlt || state.minAlt > result.minAlt) { + state.minAlt = result.minAlt; + } + if (!state.maxAlt || state.maxAlt < result.maxAlt) { + state.maxAlt = result.maxAlt; + } + }, + setStartPoint: (state, start) => { + state.startPoint = start; + }, + setEndPoint: (state, end) => { + state.endPoint = end; + }, + addFairwayData: (state, coordinates) => { + state.fairwayData.push(coordinates); + }, + clearFairwayData: state => { + state.fairwayData = []; + }, + clearCurrentProfile: state => { + state.currentProfile = {}; + state.minAlt = null; + state.maxAlt = null; + state.totalLength = null; + state.fairwayData = []; + state.startPoint = null; + state.endPoint = null; + state.selectedWaterLevel = "ref"; + }, + previousCuts: (state, previousCuts) => { + state.previousCuts = previousCuts; + }, + profileLoading: (state, loading) => { + state.profileLoading = loading; + }, + selectedCut: (state, cut) => { + state.selectedCut = cut; + } + }, + actions: { + clearCurrentProfile({ commit, rootState }) { + commit("clearCurrentProfile"); + commit("map/cutToolEnabled", false, { root: true }); + rootState.map.openLayersMaps.forEach(m => { + m.getLayer("CUTTOOL") + .getSource() + .clear(); + }); + }, + loadProfile({ commit, state }, survey) { + if (state.startPoint && state.endPoint) { + return new Promise((resolve, reject) => { + const profileLine = new LineString([ + state.startPoint, + state.endPoint + ]); + const geoJSON = generateFeatureRequest( + profileLine, + survey.bottleneck_id, + survey.date_info + ); + HTTP.post("/cross", geoJSON, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + if (response.data.geometry.coordinates.length) { + commit("profileLoaded", { + response: response, + surveyDate: survey.date_info + }); + resolve(response); + } else { + commit("clearCurrentProfile"); + reject({ + response: { + status: null, + data: "No intersection with sounding data." + } + }); + } + }) + .catch(error => reject(error)); + }); + } + }, + cut({ commit, dispatch, state, rootState, rootGetters }, cut) { + return new Promise(resolve => { + const length = getLength(cut.getGeometry()); + commit( + "map/setCurrentMeasurement", + { + quantity: "Length", + unitSymbol: "m", + value: Math.round(length * 10) / 10 + }, + { root: true } + ); + commit("clearFairwayData"); + // if a survey has been selected, request a profile + // TODO an improvement could be to check if the line intersects + // with the bottleneck area's polygon before trying the server request + if (rootState.bottlenecks.selectedSurvey) { + const inputLineString = cut.getGeometry().clone(); + inputLineString.transform("EPSG:3857", "EPSG:4326"); + const [start, end] = inputLineString + .getCoordinates() + .map(coords => coords.map(coord => parseFloat(coord.toFixed(8)))); + commit("setStartPoint", start); + commit("setEndPoint", end); + const profileLine = new LineString([start, end]); + + const profileLoaders = [ + dispatch("loadProfile", rootState.bottlenecks.selectedSurvey) + ]; + if (state.additionalSurvey) { + profileLoaders.push( + dispatch("loadProfile", state.additionalSurvey) + ); + } + + commit("profileLoading", true); + Promise.all(profileLoaders) + .then(() => { + commit("map/cutToolEnabled", false, { root: true }); + const los3 = rootGetters["map/openLayersMap"]().getLayer( + "FAIRWAYDIMENSIONSLOS3" + ); + los3.getSource().forEachFeatureIntersectingExtent( + profileLine + .clone() + .transform("EPSG:4326", "EPSG:3857") + .getExtent(), + feature => { + const fairwayCoordinates = featureToFairwayCoordinates( + feature, + profileLine + ); + let fairwayData = { + coordinates: fairwayCoordinates, + style: los3.getStyle() + }; + if (fairwayCoordinates.length > 0) { + commit("addFairwayData", fairwayData); + } + } + ); + const los2 = rootGetters["map/openLayersMap"]().getLayer( + "FAIRWAYDIMENSIONSLOS2" + ); + los2.getSource().forEachFeatureIntersectingExtent( + profileLine + .clone() + .transform("EPSG:4326", "EPSG:3857") + .getExtent(), + feature => { + let fairwayCoordinates = featureToFairwayCoordinates( + feature, + profileLine + ); + let fairwayData = { + coordinates: fairwayCoordinates, + style: los2.getStyle() + }; + if (fairwayCoordinates.length > 0) { + commit("addFairwayData", fairwayData); + } + } + ); + const los1 = rootGetters["map/openLayersMap"]().getLayer( + "FAIRWAYDIMENSIONSLOS1" + ); + los1.getSource().forEachFeatureIntersectingExtent( + profileLine + .clone() + .transform("EPSG:4326", "EPSG:3857") + .getExtent(), + feature => { + const fairwayCoordinates = featureToFairwayCoordinates( + feature, + profileLine + ); + let fairwayData = { + coordinates: fairwayCoordinates, + style: los1.getStyle() + }; + if (fairwayCoordinates.length > 0) { + commit("addFairwayData", fairwayData); + } + } + ); + resolve(); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status ? status + ":" : ""} ${data.message || data}` + }); + }) + .finally(() => { + commit("application/paneRotate", 1, { root: true }); + if (state.additionalSurvey) { + commit( + "application/paneSetup", + "COMPARESURVEYS_FAIRWAYPROFILE", + { root: true } + ); + } else { + commit("application/paneSetup", "FAIRWAYPROFILE", { + root: true + }); + } + commit("profileLoading", false); + }); + } + }); + }, + previousCuts({ commit, rootState }) { + const previousCuts = + JSON.parse(localStorage.getItem("previousCuts")) || []; + commit( + "previousCuts", + previousCuts + .filter(cut => { + return ( + cut.bottleneckName === rootState.bottlenecks.selectedBottleneck + ); + }) + .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)) + ); + } + } +};
--- a/client/src/store/gauges.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/store/gauges.js Mon Jun 03 10:19:18 2019 +0200 @@ -12,7 +12,7 @@ * Markus Kottländer <markus@intevation.de> */ import { HTTP } from "@/lib/http"; -import { WFS } from "ol/format.js"; +import { WFS } from "ol/format"; import { isPast } from "date-fns"; let dateFrom = new Date(); @@ -25,10 +25,15 @@ gauges: [], selectedGaugeISRS: null, waterlevels: [], - meanWaterlevels: [], + waterlevelsCSV: "", + longtermWaterlevels: [], + longtermInterval: [], + yearWaterlevels: [], nashSutcliffe: null, + nashSutcliffeOverview: [], dateFrom: dateFrom, - dateTo: dateTo + dateTo: dateTo, + yearCompare: new Date().getFullYear() }; }; @@ -41,6 +46,11 @@ return state.gauges.find( g => g.properties.isrs_code === state.selectedGaugeISRS ); + }, + nashSutcliffeOverview: state => feature => { + return state.nashSutcliffeOverview.find( + d => d.feature.get("id") === feature.get("id") + ); } }, mutations: { @@ -53,23 +63,49 @@ waterlevels: (state, data) => { state.waterlevels = data; }, - meanWaterlevels: (state, data) => { - state.meanWaterlevels = data; + waterlevelsCSV: (state, data) => { + state.waterlevelsCSV = data; + }, + longtermWaterlevels: (state, data) => { + state.longtermWaterlevels = data; + }, + longtermInterval: (state, interval) => { + state.longtermInterval = interval; + }, + yearWaterlevels: (state, data) => { + state.yearWaterlevels = data; }, nashSutcliffe: (state, data) => { state.nashSutcliffe = data; }, + addNashSutcliffeOverviewEntry: (state, data) => { + let existingIndex = state.nashSutcliffeOverview.findIndex( + d => d.feature.get("id") === data.feature.get("id") + ); + if (existingIndex !== -1) + state.nashSutcliffeOverview.splice(existingIndex, 1); + state.nashSutcliffeOverview.push(data); + }, dateFrom: (state, dateFrom) => { state.dateFrom = dateFrom; }, dateTo: (state, dateTo) => { state.dateTo = dateTo; + }, + yearCompare: (state, year) => { + state.yearCompare = year; } }, actions: { - selectedGaugeISRS: ({ commit }, isrs) => { - commit("selectedGaugeISRS", isrs); - commit("application/showGauges", true, { root: true }); + selectedGaugeISRS: ({ commit, dispatch, state }, isrs) => { + if (state.selectedGaugeISRS !== isrs) { + commit("selectedGaugeISRS", isrs); + commit("application/showGauges", true, { root: true }); + dispatch("loadWaterlevels"); + dispatch("loadLongtermWaterlevels"); + dispatch("loadYearWaterlevels"); + dispatch("loadNashSutcliffe"); + } }, loadGauges: ({ commit }) => { return new Promise((resolve, reject) => { @@ -115,15 +151,19 @@ } ) .then(response => { + commit("waterlevelsCSV", response.data); let data = response.data .split("\n") - .filter(wl => wl) + // remove empty rows and rows starting with # + .filter(wl => wl && wl[0] !== "#") .map(wl => { wl = wl.split(","); return { date: new Date(wl[0]), waterlevel: Number(wl[1]), - predicted: wl[2] === "f" ? false : true + min: Number(wl[2]), + max: Number(wl[3]), + predicted: wl[4] === "f" ? false : true }; }) .filter(wl => !(wl.predicted && isPast(wl.date))); @@ -137,26 +177,91 @@ }); }); }, - loadMeanWaterlevels({ /*state,*/ commit }) { - return new Promise(resolve => { - setTimeout(() => { - commit("meanWaterlevels", [1]); - resolve(); - }, 2000); + loadLongtermWaterlevels({ state, commit }) { + return new Promise((resolve, reject) => { + HTTP.get(`/data/longterm-waterlevels/${state.selectedGaugeISRS}`, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + const now = new Date(); + let data = response.data.split("\n"); + // get result interval from first line + let interval = data[0] + .split(",")[0] + .split(" ")[1] + .split("-") + .map(y => Number(y)); + if (interval[0] === interval[1]) interval = [interval[0]]; + commit("longtermInterval", interval); + data = data + // remove empty rows and rows starting with # + .filter(wl => wl && wl[0] !== "#") + .map(wl => { + wl = wl.split(","); + let dayAndMonth = wl[0].split("-").map(n => Number(n)); + let date = new Date( + now.getFullYear(), + dayAndMonth[1] - 1, + dayAndMonth[0] + ); + return { + date: date, + min: Number(wl[1]), + max: Number(wl[2]), + mean: Number(wl[3]), + median: Number(wl[4]), + q25: Number(wl[5]), + q75: Number(wl[6]) + }; + }); + data = data.sort((a, b) => a.date - b.date); + commit("longtermWaterlevels", data); + resolve(data); + }) + .catch(error => { + commit("longtermWaterlevels", []); + reject(error); + }); }); - // return new Promise((resolve, reject) => { - // HTTP.get(`/data/mean-waterlevels/${state.selectedGaugeISRS}`, { - // headers: { "X-Gemma-Auth": localStorage.getItem("token") } - // }) - // .then(response => { - // commit("meanWaterlevels", response.data); - // resolve(response.data); - // }) - // .catch(error => { - // commit("meanWaterlevels", []); - // reject(error); - // }) - // }); + }, + loadYearWaterlevels({ state, commit }) { + return new Promise((resolve, reject) => { + HTTP.get( + `/data/year-waterlevels/${state.selectedGaugeISRS}/${ + state.yearCompare + }`, + { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + } + ) + .then(response => { + const now = new Date(); + let data = response.data + .split("\n") + // remove empty rows and rows starting with # + .filter(wl => wl && wl[0] !== "#") + .map(wl => { + wl = wl.split(","); + let dayAndMonth = wl[0].split("-").map(n => Number(n)); + let date = new Date( + now.getFullYear(), + dayAndMonth[1] - 1, + dayAndMonth[0] + ); + return { + date: date, + mean: Number(wl[1]) + }; + }); + data = data.sort((a, b) => a.date - b.date); + commit("yearWaterlevels", data); + resolve(data); + }) + .catch(error => { + commit("yearWaterlevels", []); + reject(error); + }); + }); }, loadNashSutcliffe({ state, commit }) { return new Promise((resolve, reject) => { @@ -172,6 +277,19 @@ reject(error); }); }); + }, + loadNashSutcliffeForOverview(context, isrsCode) { + return new Promise((resolve, reject) => { + HTTP.get(`/data/nash-sutcliffe/${isrsCode}`, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + resolve(response.data); + }) + .catch(error => { + reject(error); + }); + }); } } };
--- a/client/src/store/imports.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/store/imports.js Mon Jun 03 10:19:18 2019 +0200 @@ -13,20 +13,16 @@ */ import { HTTP } from "@/lib/http"; -import { WFS } from "ol/format.js"; -import { equalTo as equalToFilter } from "ol/format/filter.js"; -import { startOfHour } from "date-fns"; +import { WFS } from "ol/format"; +import { equalTo as equalToFilter } from "ol/format/filter"; +import { startOfHour, endOfHour } from "date-fns"; -/* eslint-disable no-unused-vars */ -/* eslint-disable no-unreachable */ const STATES = { NEEDSAPPROVAL: "pending", APPROVED: "accepted", REJECTED: "declined" }; -const NODETAILS = -1; - // initial state const init = () => { return { @@ -36,32 +32,35 @@ declined: false, warning: false, stretches: [], + selectedStretchId: null, + sections: [], + selectedSectionId: null, imports: [], reviewed: [], - show: NODETAILS, - showAdditional: NODETAILS, - showLogs: NODETAILS, + show: null, + showAdditional: null, + showLogs: null, details: [], startDate: startOfHour(new Date()), - endDate: new Date(), + endDate: endOfHour(new Date()), prev: null, next: null }; }; -const getStretchFromWFS = filter => { +const getFromWFS = (type, filter) => { return new Promise((resolve, reject) => { - var stretchesFeatureCollectionRequest = new WFS().writeGetFeature({ + var featureCollectionRequest = new WFS().writeGetFeature({ srsName: "EPSG:4326", featureNS: "gemma", featurePrefix: "gemma", - featureTypes: ["stretches_geoserver"], + featureTypes: [type], outputFormat: "application/json", filter: filter }); HTTP.post( "/internal/wfs", - new XMLSerializer().serializeToString(stretchesFeatureCollectionRequest), + new XMLSerializer().serializeToString(featureCollectionRequest), { headers: { "X-Gemma-Auth": localStorage.getItem("token"), @@ -132,6 +131,15 @@ setStretches: (state, stretches) => { state.stretches = stretches; }, + selectedStretchId: (state, id) => { + state.selectedStretchId = id; + }, + setSections: (state, sections) => { + state.sections = sections; + }, + selectedSectionId: (state, id) => { + state.selectedSectionId = id; + }, setReviewed: (state, reviewed) => { state.reviewed = reviewed; }, @@ -158,19 +166,19 @@ state.show = id; }, hideDetails: state => { - state.show = NODETAILS; + state.show = null; }, showAdditionalInfoFor: (state, id) => { state.showAdditional = id; }, hideAdditionalInfo: state => { - state.showAdditional = NODETAILS; + state.showAdditional = false; }, showAdditionalLogsFor: (state, id) => { state.showLogs = id; }, hideAdditionalLogs: state => { - state.showLogs = NODETAILS; + state.showLogs = false; }, toggleApprove: (state, change) => { const { id, newStatus } = change; @@ -192,9 +200,9 @@ } }, actions: { - loadStretch({ commit }, name) { + loadStretch(context, name) { return new Promise((resolve, reject) => { - getStretchFromWFS(equalToFilter("name", name)) + getFromWFS("stretches_geoserver", equalToFilter("name", name)) .then(response => { resolve(response); }) @@ -205,7 +213,7 @@ }, loadStretches({ commit }) { return new Promise((resolve, reject) => { - getStretchFromWFS(equalToFilter("staging_done", true)) + getFromWFS("stretches_geoserver", equalToFilter("staging_done", true)) .then(response => { if (response.data.features) { commit("setStretches", response.data.features); @@ -219,7 +227,7 @@ }); }); }, - saveStretch({ commit }, stretch) { + saveStretch(context, stretch) { return new Promise((resolve, reject) => { HTTP.post("/imports/st", stretch, { headers: { "X-Gemma-Auth": localStorage.getItem("token") } @@ -232,6 +240,46 @@ }); }); }, + loadSection(context, name) { + return new Promise((resolve, reject) => { + getFromWFS("sections_geoserver", equalToFilter("name", name)) + .then(response => { + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + }, + loadSections({ commit }) { + return new Promise((resolve, reject) => { + getFromWFS("sections_geoserver", equalToFilter("staging_done", true)) + .then(response => { + if (response.data.features) { + commit("setSections", response.data.features); + } else { + commit("setSections", []); + } + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + }, + saveSection(context, section) { + return new Promise((resolve, reject) => { + HTTP.post("/imports/sec", section, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + resolve(response); + }) + .catch(error => { + reject(error); + }); + }); + }, getImports({ commit }, options) { let { filter, from, to, query } = options; let queryParams = ""; @@ -271,7 +319,7 @@ }); }); }, - confirmReview({ state }, reviewResults) { + confirmReview(context, reviewResults) { return new Promise((resolve, reject) => { HTTP.patch("/imports", reviewResults, { headers: {
--- a/client/src/store/importschedule.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/store/importschedule.js Mon Jun 03 10:19:18 2019 +0200 @@ -27,7 +27,10 @@ FAIRWAYDIMENSION: "fairwaydimension", WATERWAYGAUGES: "waterwaygauges", DISTANCEMARKSVIRTUAL: "distancemarksvirtual", - DISTANCEMARKSASHORE: "distancemarksashore" + DISTANCEMARKSASHORE: "distancemarksashore", + SOUNDINGRESULTS: "soundingresults", + APPROVEDGAUGEMEASUREMENTS: "approvedgaugemeasurements", + WATERWAYPROFILES: "waterwayprofiles" }; const KINDIMPORTTYPE = { @@ -59,7 +62,6 @@ id: null, importType: null, schedule: null, - import_: null, importSource: null, eMailNotification: false, scheduled: false, @@ -84,16 +86,24 @@ minWidth: null, maxWidth: null, depth: null, - sourceOrganization: null + sourceOrganization: null, + trys: null, + waitRetry: null }; }; +const MODES = { + LIST: "list", + EDIT: "edit" +}; + // initial state const init = () => { return { schedules: [], importScheduleDetailVisible: false, - currentSchedule: initializeCurrentSchedule() + currentSchedule: initializeCurrentSchedule(), + mode: MODES.LIST }; }; @@ -102,6 +112,16 @@ namespaced: true, state: init(), mutations: { + setEditMode: state => { + state.mode = MODES.EDIT; + }, + setListMode: state => { + state.currentSchedule = initializeCurrentSchedule(); + state.mode = MODES.LIST; + }, + setImportType: (state, type) => { + Vue.set(state.currentSchedule, "importType", type); + }, clearCurrentSchedule: state => { state.currentSchedule = initializeCurrentSchedule(); }, @@ -118,12 +138,13 @@ const { kind, config, id } = payload; const eMailNotification = config["send-email"]; const { cron, url } = config; - Vue.set(state.currentSchedule, "import_", KINDIMPORTTYPE[kind]); + Vue.set(state.currentSchedule, "importType", KINDIMPORTTYPE[kind]); Vue.set(state.currentSchedule, "id", id); if (cron) { Vue.set(state.currentSchedule, "scheduled", true); Vue.set(state.currentSchedule, "cronString", cron); - + Vue.set(state.currentSchedule, "trys", config["trys"]); + Vue.set(state.currentSchedule, "waitRetry", config["wait-retry"]); // simple weekly or monthly? if (cron === "0 0 0 * * 0") { Vue.set(state.currentSchedule, "simple", "weekly"); @@ -320,5 +341,6 @@ importschedule, initializeCurrentSchedule, IMPORTTYPES, - IMPORTTYPEKIND + IMPORTTYPEKIND, + MODES };
--- a/client/src/store/index.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/store/index.js Mon Jun 03 10:19:18 2019 +0200 @@ -19,7 +19,8 @@ import user from "./user"; import usermanagement from "./usermanagement"; import map from "./map"; -import fairwayprofile from "./fairway"; +import { fairwayavailability } from "./fairwayavailability"; +import fairwayprofile from "./fairwayprofile"; import bottlenecks from "./bottlenecks"; import { imports } from "./imports"; import { importschedule } from "./importschedule"; @@ -32,6 +33,7 @@ reset() { this.replaceState({ application: application.init(), + fairwayavailability: fairwayavailability.init(), fairwayprofile: fairwayprofile.init(), imports: imports.init(), importschedule: importschedule.init(), @@ -45,6 +47,7 @@ }, modules: { application, + fairwayavailability, fairwayprofile, imports, importschedule,
--- a/client/src/store/map.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/store/map.js Mon Jun 03 10:19:18 2019 +0200 @@ -14,544 +14,41 @@ * * Thomas Junk <thomas.junk@intevation.de> */ -//import { HTTP } from "../lib/http"; - -import TileWMS from "ol/source/TileWMS.js"; -import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer.js"; -import OSM from "ol/source/OSM"; -import Draw from "ol/interaction/Draw.js"; -import { Icon, Stroke, Style, Fill, Text, Circle } from "ol/style.js"; -import VectorSource from "ol/source/Vector.js"; -import Point from "ol/geom/Point.js"; -import { bbox as bboxStrategy } from "ol/loadingstrategy"; -import { HTTP } from "../lib/http"; +import Draw from "ol/interaction/Draw"; +import { Stroke, Style, Fill, Circle } from "ol/style"; import { fromLonLat } from "ol/proj"; -import { getLength, getArea } from "ol/sphere.js"; -import { unByKey } from "ol/Observable"; -import { getCenter } from "ol/extent"; -import { transformExtent } from "ol/proj.js"; +import { getLength, getArea } from "ol/sphere"; +import { transformExtent } from "ol/proj"; import bbox from "@turf/bbox"; -import app from "../main"; - -const LAYERS = { - OPENSTREETMAP: "Open Streetmap", - INLANDECDIS: "Inland ECDIS chart Danube", - WATERWAYAREA: "Waterway Area", - STRETCHES: "Stretches", - FAIRWAYDIMENSIONSLOS1: "LOS 1 Fairway Dimensions", - FAIRWAYDIMENSIONSLOS2: "LOS 2 Fairway Dimensions", - FAIRWAYDIMENSIONSLOS3: "LOS 3 Fairway Dimensions", - WATERWAYAXIS: "Waterway Axis", - WATERWAYPROFILES: "Waterway Profiles", - BOTTLENECKS: "Bottlenecks", - BOTTLENECKSTATUS: "Critical Bottlenecks", - BOTTLENECKISOLINE: "Bottleneck isolines", - DISTANCEMARKS: "Distance marks", - DISTANCEMARKSAXIS: "Distance marks, Axis", - GAUGES: "Gauges", - DRAWTOOL: "Draw Tool", - CUTTOOL: "Cut Tool" -}; - -const moveMap = ({ view, extent, zoom, preventZoomOut }) => { - const currentZoom = view.getZoom(); - zoom = zoom || currentZoom; - view.fit(extent, { - maxZoom: preventZoomOut ? Math.max(zoom, currentZoom) : zoom, - duration: 700 - }); -}; +import app from "@/main"; +import { HTTP } from "@/lib/http"; +import Feature from "ol/Feature"; +import Point from "ol/geom/Point"; +import { Vector as VectorLayer } from "ol/layer"; +import { toLonLat } from "ol/proj"; // initial state const init = () => { return { - openLayersMap: null, + openLayersMaps: [], + syncedMaps: [], + syncedView: null, + mapPopup: null, + mapPopupEnabled: true, initialLoad: true, extent: { lat: 6155376, lon: 1819178, zoom: 11 }, - identifyTool: null, // event binding (singleclick, dblclick) identifiedFeatures: [], // map features identified by clicking on the map + identifiedCoordinates: null, currentMeasurement: null, // distance or area from line-/polygon-/cutTool - lineTool: null, // open layers interaction object (Draw) - polygonTool: null, // open layers interaction object (Draw) - cutTool: null, // open layers interaction object (Draw) + lineToolEnabled: false, + polygonToolEnabled: false, + cutToolEnabled: false, isolinesLegendImgDataURL: "", - layers: [ - { - name: LAYERS.OPENSTREETMAP, - data: new TileLayer({ - source: new OSM() - }), - isVisible: true, - showInLegend: true - }, - { - name: LAYERS.INLANDECDIS, - data: new TileLayer({ - source: new TileWMS({ - preload: 1, - url: "https://service.d4d-portal.info/wms/", - crossOrigin: "anonymous", - params: { LAYERS: "d4d", VERSION: "1.1.1", TILED: true } - }) - }), - isVisible: true, - showInLegend: true - }, - { - name: LAYERS.WATERWAYAREA, - data: new VectorLayer({ - source: new VectorSource({ - strategy: bboxStrategy - }), - style: new Style({ - stroke: new Stroke({ - color: "rgba(0, 102, 0, 1)", - width: 2 - }) - }) - }), - isVisible: true, - showInLegend: true - }, - { - name: LAYERS.STRETCHES, - data: new VectorLayer({ - source: new VectorSource({ - strategy: bboxStrategy - }), - style: new Style({ - stroke: new Stroke({ - color: "rgba(250, 200, 0, .8)", - width: 2 - }), - fill: new Fill({ - color: "rgba(250, 200, 10, .3)" - }) - }) - }), - isVisible: false, - showInLegend: true - }, - { - name: LAYERS.FAIRWAYDIMENSIONSLOS3, - data: new VectorLayer({ - source: new VectorSource(), - style: function() { - return [ - new Style({ - stroke: new Stroke({ - color: "rgba(0, 0, 255, 1.0)", - width: 2 - }), - fill: new Fill({ - color: "rgba(255, 255, 255, 0.4)" - }) - }), - new Style({ - text: new Text({ - font: 'bold 12px "Open Sans", "sans-serif"', - placement: "line", - fill: new Fill({ - color: "black" - }), - text: "LOS: 3" - //, zIndex: 10 - }) - }) - ]; - } - }), - isVisible: true, - showInLegend: true - }, - { - name: LAYERS.FAIRWAYDIMENSIONSLOS2, - data: new VectorLayer({ - source: new VectorSource(), - style: function() { - return [ - new Style({ - stroke: new Stroke({ - color: "rgba(0, 0, 255, 0.9)", - lineDash: [3, 6], - lineCap: "round", - width: 2 - }), - fill: new Fill({ - color: "rgba(240, 230, 0, 0.1)" - }) - }), - new Style({ - text: new Text({ - font: 'bold 12px "Open Sans", "sans-serif"', - placement: "line", - fill: new Fill({ - color: "black" - }), - text: "LOS: 2" - //, zIndex: 10 - }) - }) - ]; - } - }), - isVisible: false, - showInLegend: true - }, - { - name: LAYERS.FAIRWAYDIMENSIONSLOS1, - data: new VectorLayer({ - source: new VectorSource(), - style: function() { - return [ - new Style({ - stroke: new Stroke({ - color: "rgba(0, 0, 255, 0.8)", - lineDash: [2, 4], - lineCap: "round", - width: 2 - }), - fill: new Fill({ - color: "rgba(240, 230, 0, 0.2)" - }) - }), - new Style({ - text: new Text({ - font: 'bold 12px "Open Sans", "sans-serif"', - placement: "line", - fill: new Fill({ - color: "black" - }), - text: "LOS: 1" - //, zIndex: 10 - }) - }) - ]; - } - }), - isVisible: false, - showInLegend: true - }, - { - name: LAYERS.WATERWAYAXIS, - data: new VectorLayer({ - source: new VectorSource({ - strategy: bboxStrategy - }), - style: new Style({ - stroke: new Stroke({ - color: "rgba(0, 0, 255, .5)", - lineDash: [5, 5], - width: 2 - }) - }), - // TODO: Set layer in layertree active/inactive depending on - // resolution. - maxResolution: 5, - minResolution: 0 - }), - isVisible: true, - showInLegend: true - }, - { - name: LAYERS.WATERWAYPROFILES, - data: new VectorLayer({ - source: new VectorSource({ - strategy: bboxStrategy - }), - style: new Style({ - stroke: new Stroke({ - color: "rgba(0, 0, 255, .5)", - lineDash: [5, 5], - width: 2 - }) - }), - maxResolution: 2.5, - minResolution: 0 - }), - isVisible: true, - showInLegend: true - }, - { - name: LAYERS.BOTTLENECKS, - data: new VectorLayer({ - source: new VectorSource({ - strategy: bboxStrategy - }), - style: function() { - return new Style({ - stroke: new Stroke({ - color: "rgba(230, 230, 10, .8)", - width: 4 - }), - fill: new Fill({ - color: "rgba(230, 230, 10, .3)" - }) - }); - } - }), - isVisible: true, - showInLegend: true - }, - { - name: LAYERS.BOTTLENECKISOLINE, - data: new TileLayer({ - source: new TileWMS({ - preload: 0, - projection: "EPSG:3857", - url: window.location.origin + "/api/internal/wms", - params: { - LAYERS: "sounding_results_contour_lines_geoserver", - VERSION: "1.1.1", - TILED: true - }, - tileLoadFunction: function(tile, src) { - // console.log("calling for", tile, src); - HTTP.get(src, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token") - }, - responseType: "blob" - }).then(response => { - tile.getImage().src = URL.createObjectURL(response.data); - }); - } // TODO tile.setState(TileState.ERROR); - }) - }), - isVisible: false, - showInLegend: true - }, - { - name: LAYERS.BOTTLENECKSTATUS, - forLegendStyle: { point: true, resolution: 16 }, - data: new VectorLayer({ - source: new VectorSource({ - strategy: bboxStrategy - }), - style: function(feature, resolution, isLegend) { - let styles = []; - if ((feature.get("fa_critical") && resolution > 15) || isLegend) { - let bnCenter = getCenter(feature.getGeometry().getExtent()); - styles.push( - new Style({ - geometry: new Point(bnCenter), - image: new Icon({ - src: require("../assets/marker-bottleneck-critical.png"), - anchor: [0.5, 0.5], - scale: isLegend ? 0.5 : 1 - }) - }) - ); - } - if (feature.get("fa_critical") && !isLegend) { - styles.push( - new Style({ - stroke: new Stroke({ - color: "rgba(255, 0, 0, 1)", - width: 4 - }) - }) - ); - } - return styles; - } - }), - isVisible: true, - showInLegend: true - }, - { - name: LAYERS.DISTANCEMARKS, - forLegendStyle: { point: true, resolution: 8 }, - data: new VectorLayer({ - source: new VectorSource({ - strategy: bboxStrategy - }) - }), - isVisible: false, - showInLegend: true - }, - { - name: LAYERS.DISTANCEMARKSAXIS, - forLegendStyle: { point: true, resolution: 8 }, - data: new VectorLayer({ - source: new VectorSource({ - strategy: bboxStrategy - }), - style: function(feature, resolution) { - if (resolution < 10) { - var s = new Style({ - image: new Circle({ - radius: 5, - fill: new Fill({ color: "rgba(255, 0, 0, 0.1)" }), - stroke: new Stroke({ color: "blue", width: 1 }) - }) - }); - if (resolution < 6) { - s.setText( - new Text({ - offsetY: 12, - font: '10px "Open Sans", "sans-serif"', - fill: new Fill({ - color: "black" - }), - text: (feature.get("hectometre") / 10).toString() - }) - ); - } - return s; - } else { - return []; - } - } - }), - isVisible: true, - showInLegend: true - }, - { - name: LAYERS.GAUGES, - forLegendStyle: { point: true, resolution: 8 }, - data: new VectorLayer({ - source: new VectorSource({ - strategy: bboxStrategy - }), - style: function(feature, resolution, isLegend) { - return [ - new Style({ - image: new Icon({ - src: require("../assets/marker-gauge.png"), - anchor: [0.5, isLegend ? 0.5 : 1], - scale: isLegend ? 0.5 : 1 - }), - text: new Text({ - font: '10px "Open Sans", "sans-serif"', - offsetY: 8, - fill: new Fill({ - color: "white" - }), - text: feature.get("objname") - }) - }), - new Style({ - text: new Text({ - font: '10px "Open Sans", "sans-serif"', - offsetY: 7, - offsetX: -1, - fill: new Fill({ - color: "black" - }), - text: feature.get("objname") - }) - }) - ]; - }, - maxResolution: 100, - minResolution: 0 - }), - isVisible: true, - showInLegend: true - }, - { - name: LAYERS.DRAWTOOL, - data: new VectorLayer({ - source: new VectorSource({ wrapX: false }), - style: function(feature) { - // adapted from OpenLayer's LineString Arrow Example - var geometry = feature.getGeometry(); - var styles = [ - // linestring - new Style({ - stroke: new Stroke({ - color: "#369aca", - width: 2 - }) - }) - ]; - - if (geometry.getType() === "LineString") { - geometry.forEachSegment(function(start, end) { - var dx = end[0] - start[0]; - var dy = end[1] - start[1]; - var rotation = Math.atan2(dy, dx); - // arrows - styles.push( - new Style({ - geometry: new Point(end), - image: new Icon({ - // we need to make sure the image is loaded by Vue Loader - 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 - // anti-aliasing, but the image is not placed with subpixel - // precision - anchor: [0.75, 0.5], - rotateWithView: true, - rotation: -rotation - }) - }) - ); - }); - } - return styles; - } - }), - isVisible: true, - showInLegend: false - }, - { - name: LAYERS.CUTTOOL, - data: new VectorLayer({ - source: new VectorSource({ wrapX: false }), - style: function(feature) { - // adapted from OpenLayer's LineString Arrow Example - var geometry = feature.getGeometry(); - var styles = [ - // linestring - new Style({ - stroke: new Stroke({ - color: "#333333", - width: 2, - lineDash: [7, 7] - }) - }) - ]; - - if (geometry.getType() === "LineString") { - geometry.forEachSegment(function(start, end) { - var dx = end[0] - start[0]; - var dy = end[1] - start[1]; - var rotation = Math.atan2(dy, dx); - // arrows - styles.push( - new Style({ - geometry: new Point(end), - image: new Icon({ - // we need to make sure the image is loaded by Vue Loader - 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 - // anti-aliasing, but the image is not placed with subpixel - // precision - anchor: [0.75, 0.5], - rotateWithView: true, - rotation: -rotation - }) - }) - ); - }); - } - return styles; - } - }), - isVisible: true, - showInLegend: false - } - ] + differencesLegendImgDataURL: "" }; }; @@ -560,14 +57,10 @@ namespaced: true, state: init(), getters: { - layersForLegend: state => { - return state.layers.filter(layer => layer.showInLegend); - }, - getLayerByName: state => name => { - return state.layers.find(layer => layer.name === name); - }, - getVSourceByName: (state, getters) => name => { - return getters.getLayerByName(name).data.getSource(); + openLayersMap: state => id => { + return state.openLayersMaps.find( + map => map.getTarget() === "map-" + (id || "main") + ); }, filteredIdentifiedFeatures: state => { return state.identifiedFeatures.filter(f => f.getId()); @@ -580,85 +73,106 @@ extent: (state, extent) => { state.extent = extent; }, - setLayerVisible: (state, name) => { - const layer = state.layers.findIndex(l => l.name === name); - state.layers[layer].isVisible = true; - state.layers[layer].data.setVisible(true); + addOpenLayersMap: (state, map) => { + state.openLayersMaps.push(map); }, - setLayerInvisible: (state, name) => { - const layer = state.layers.findIndex(l => l.name === name); - state.layers[layer].isVisible = false; - state.layers[layer].data.setVisible(false); + removeOpenLayersMap: (state, map) => { + let index = state.openLayersMaps.findIndex( + m => m.getTarget() === map.getTarget() + ); + if (index !== -1) { + state.openLayersMaps.splice(index, 1); + } }, - toggleVisibilityByName: (state, name) => { - const layer = state.layers.findIndex(l => l.name === name); - state.layers[layer].isVisible = !state.layers[layer].isVisible; - state.layers[layer].data.setVisible(state.layers[layer].isVisible); + syncedMaps: (state, ids) => { + state.syncedMaps = ids; + }, + syncedView: (state, view) => { + state.syncedView = view; }, - toggleVisibility: (state, layer) => { - state.layers[layer].isVisible = !state.layers[layer].isVisible; - state.layers[layer].data.setVisible(state.layers[layer].isVisible); + mapPopup: (state, popup) => { + state.mapPopup = popup; }, - openLayersMap: (state, map) => { - state.openLayersMap = map; - }, - identifyTool: (state, events) => { - state.identifyTool = events; + mapPopupEnabled: (state, enabled) => { + state.mapPopupEnabled = enabled; }, setIdentifiedFeatures: (state, identifiedFeatures) => { state.identifiedFeatures = identifiedFeatures; }, + addIdentifiedFeatures: (state, identifiedFeatures) => { + state.identifiedFeatures = state.identifiedFeatures.concat( + identifiedFeatures + ); + }, + identifiedCoordinates: (state, coordinates) => { + state.identifiedCoordinates = coordinates; + }, setCurrentMeasurement: (state, measurement) => { state.currentMeasurement = measurement; }, - lineTool: (state, lineTool) => { - state.lineTool = lineTool; - }, - polygonTool: (state, polygonTool) => { - state.polygonTool = polygonTool; - }, - cutTool: (state, cutTool) => { - state.cutTool = cutTool; - }, - moveToBoundingBox: (state, { boundingBox, zoom, preventZoomOut }) => { - const extent = transformExtent(boundingBox, "EPSG:4326", "EPSG:3857"); - let view = state.openLayersMap.getView(); - moveMap({ view, extent, zoom, preventZoomOut }); + lineToolEnabled: (state, enabled) => { + state.lineToolEnabled = enabled; + state.openLayersMaps.forEach(m => { + let tool = m + .getInteractions() + .getArray() + .find(i => i.get("id") === "linetool"); + if (tool) { + tool.setActive(enabled); + } + }); }, - moveToExtent: (state, { feature, zoom, preventZoomOut }) => { - const boundingBox = bbox(feature.geometry); - const extent = transformExtent(boundingBox, "EPSG:4326", "EPSG:3857"); - let view = state.openLayersMap.getView(); - moveMap({ view, extent, zoom, preventZoomOut }); + polygonToolEnabled: (state, enabled) => { + state.polygonToolEnabled = enabled; + state.openLayersMaps.forEach(m => { + let tool = m + .getInteractions() + .getArray() + .find(i => i.get("id") === "polygontool"); + if (tool) { + tool.setActive(enabled); + } + }); }, - moveMap: (state, { coordinates, zoom, preventZoomOut }) => { - let view = state.openLayersMap.getView(); - const currentZoom = view.getZoom(); - zoom = zoom || currentZoom; - view.animate({ - zoom: preventZoomOut ? Math.max(zoom, currentZoom) : zoom, - center: fromLonLat(coordinates, view.getProjection()), - duration: 700 + cutToolEnabled: (state, enabled) => { + state.cutToolEnabled = enabled; + state.openLayersMaps.forEach(m => { + let tool = m + .getInteractions() + .getArray() + .find(i => i.get("id") === "cuttool"); + if (tool) { + tool.setActive(enabled); + } }); }, isolinesLegendImgDataURL: (state, isolinesLegendImgDataURL) => { state.isolinesLegendImgDataURL = isolinesLegendImgDataURL; + }, + differencesLegendImgDataURL: (state, differencesLegendImgDataURL) => { + state.differencesLegendImgDataURL = differencesLegendImgDataURL; } }, actions: { - openLayersMap({ commit, dispatch, getters }, map) { - const drawVectorSrc = getters.getVSourceByName("Draw Tool"); - const cutVectorSrc = getters.getVSourceByName("Cut Tool"); + openLayersMap({ state, commit, dispatch }, map) { + const drawVectorSrc = map.getLayer("DRAWTOOL").getSource(); + const cutVectorSrc = map.getLayer("CUTTOOL").getSource(); // init line tool const lineTool = new Draw({ source: drawVectorSrc, type: "LineString", - maxPoints: 2 + maxPoints: 2, + stopClick: true }); + lineTool.set("id", "linetool"); lineTool.setActive(false); lineTool.on("drawstart", () => { - drawVectorSrc.clear(); + state.openLayersMaps.forEach(m => { + m.getLayer("DRAWTOOL") + .getSource() + .clear(); + }); commit("setCurrentMeasurement", null); }); lineTool.on("drawend", event => { @@ -674,11 +188,17 @@ const polygonTool = new Draw({ source: drawVectorSrc, type: "Polygon", - maxPoints: 50 + maxPoints: 50, + stopClick: true }); + polygonTool.set("id", "polygontool"); polygonTool.setActive(false); polygonTool.on("drawstart", () => { - drawVectorSrc.clear(); + state.openLayersMaps.forEach(m => { + m.getLayer("DRAWTOOL") + .getSource() + .clear(); + }); commit("setCurrentMeasurement", null); }); polygonTool.on("drawend", event => { @@ -699,6 +219,7 @@ source: cutVectorSrc, type: "LineString", maxPoints: 2, + stopClick: true, style: new Style({ stroke: new Stroke({ color: "#444", @@ -712,151 +233,358 @@ }) }) }); + cutTool.set("id", "cuttool"); cutTool.setActive(false); cutTool.on("drawstart", () => { - dispatch("disableIdentifyTool"); - cutVectorSrc.clear(); + state.openLayersMaps.forEach(m => { + m.getLayer("CUTTOOL") + .getSource() + .clear(); + }); }); cutTool.on("drawend", event => { commit("fairwayprofile/selectedCut", null, { root: true }); - dispatch("fairwayprofile/cut", event.feature, { root: true }).then(() => - // This setTimeout is an ugly workaround. If we would enable the - // identifyTool here immediately then the click event from ending the - // cut will trigger it. We don't want that. - setTimeout(() => dispatch("enableIdentifyTool"), 1000) - ); + dispatch("fairwayprofile/cut", event.feature, { root: true }); }); map.addInteraction(lineTool); map.addInteraction(cutTool); map.addInteraction(polygonTool); - commit("lineTool", lineTool); - commit("polygonTool", polygonTool); - commit("cutTool", cutTool); - commit("openLayersMap", map); - }, - disableIdentifyTool({ state }) { - unByKey(state.identifyTool); - state.identifyTool = null; + // If there are multiple maps and you enable one of the draw tools, when + // moving the mouse to another map, the cursor for the draw tool stays + // visible in the first map, right next to the edge where the cursor left + // the map. So here we disable all draw layers except the ones in the map + // that the user currently hovering with the mouse. + map.getTargetElement().addEventListener("mouseenter", () => { + if ( + state.lineToolEnabled || + state.polygonToolEnabled || + state.cutToolEnabled + ) { + state.openLayersMaps.forEach(m => { + let lineTool = m + .getInteractions() + .getArray() + .find(i => i.get("id") === "linetool"); + let polygonTool = m + .getInteractions() + .getArray() + .find(i => i.get("id") === "polygontool"); + let cutTool = m + .getInteractions() + .getArray() + .find(i => i.get("id") === "cuttool"); + if (lineTool) lineTool.setActive(false); + if (polygonTool) polygonTool.setActive(false); + if (cutTool) cutTool.setActive(false); + }); + let lineTool = map + .getInteractions() + .getArray() + .find(i => i.get("id") === "linetool"); + let polygonTool = map + .getInteractions() + .getArray() + .find(i => i.get("id") === "polygontool"); + let cutTool = map + .getInteractions() + .getArray() + .find(i => i.get("id") === "cuttool"); + if (lineTool && state.lineToolEnabled) lineTool.setActive(true); + if (polygonTool && state.polygonToolEnabled) + polygonTool.setActive(true); + if (cutTool && state.cutToolEnabled) cutTool.setActive(true); + } + }); + + commit("addOpenLayersMap", map); }, - enableIdentifyTool({ state, rootState, commit, dispatch, getters }) { - if (!state.identifyTool) { - state.identifyTool = state.openLayersMap.on( - ["singleclick", "dblclick"], - event => { - commit("setIdentifiedFeatures", []); - // checking our WFS layers - var features = state.openLayersMap.getFeaturesAtPixel(event.pixel, { - hitTolerance: 7 - }); - if (features) { - let identifiedFeatures = []; + initIdentifyTool({ state, rootState, commit, dispatch }, map) { + map.on(["singleclick", "dblclick"], event => { + commit( + "identifiedCoordinates", + toLonLat(event.coordinate, map.getView().getProjection()) + ); + state.mapPopup.setPosition(undefined); + commit("setIdentifiedFeatures", []); + // checking our WFS layers + var features = map.getFeaturesAtPixel(event.pixel, { hitTolerance: 7 }); + if (features) { + let all = []; + let bottlenecks = []; + let gauges = []; + let stretches = []; + let sections = []; - for (let feature of features) { - let id = feature.getId(); - - // avoid identifying the same feature twice - if ( - identifiedFeatures.findIndex( - f => f.getId() === feature.getId() - ) === -1 - ) { - identifiedFeatures.push(feature); - } + for (let feature of features) { + // avoid identifying the same feature twice + if (all.findIndex(f => f.getId() === feature.getId()) === -1) + all.push(feature); - // get selected bottleneck - // RegExp.prototype.test() works with number, str and undefined - if (/^bottlenecks/.test(id)) { - if ( - rootState.bottlenecks.selectedBottleneck != - feature.get("objnam") - ) { - dispatch( - "bottlenecks/setSelectedBottleneck", - feature.get("objnam"), - { root: true } - ).then(() => { - this.commit("bottlenecks/setFirstSurveySelected"); - }); - commit("moveMap", { - coordinates: getCenter( - feature - .getGeometry() - .clone() - .transform("EPSG:3857", "EPSG:4326") - .getExtent() - ), - zoom: 17, - preventZoomOut: true - }); - } - } + let id = feature.getId(); + // RegExp.prototype.test() works with number, str and undefined + // get selected bottleneck + if (/^bottlenecks/.test(id)) bottlenecks.push(feature); + // get selected gauge + if (/^gauges/.test(id)) gauges.push(feature); + // get selected stretch + if (/^stretches/.test(id)) stretches.push(feature); + // get selected section + if (/^sections/.test(id)) sections.push(feature); + } + + commit("setIdentifiedFeatures", all); - // get selected gauge - if (/^gauges/.test(id)) { - if ( - rootState.gauges.selectedGaugeISRS !== - feature.get("isrs_code") - ) { - dispatch( - "gauges/selectedGaugeISRS", - feature.get("isrs_code"), - { - root: true - } - ); - commit("moveMap", { - coordinates: getCenter( - feature - .getGeometry() - .clone() - .transform("EPSG:3857", "EPSG:4326") - .getExtent() - ), - zoom: null, - preventZoomOut: true - }); - } - } - } + // Decide whether we open a related dialog immediately or show the + // popup with possible options first. + // The following cases require a manual decision via the popup because + // the targeted feature is not clear. + if ( + (bottlenecks.length || + gauges.length > 1 || + stretches.length > 1 || + sections.length > 1 || + (sections.length && stretches.length) || + (gauges.length && sections.length) || + (gauges.length && stretches.length)) && + state.mapPopupEnabled + ) { + state.mapPopup.setMap(map); + state.mapPopup.setPosition(event.coordinate); + } + // The following scenarios lead to a distinct action without popup. + if ( + gauges.length === 1 && + !bottlenecks.length && + !sections.length && + !stretches.length + ) { + commit("application/showGauges", true, { root: true }); + dispatch("gauges/selectedGaugeISRS", gauges[0].get("isrs_code"), { + root: true + }); + } + if ( + stretches.length === 1 && + !sections.length && + !bottlenecks.length && + !gauges.length + ) { + if (rootState.imports.selectedStretchId === stretches[0].getId()) { + commit("imports/selectedStretchId", null, { + root: true + }); + } else { + commit("imports/selectedStretchId", stretches[0].getId(), { + root: true + }); + commit("fairwayavailability/type", "stretches", { root: true }); + commit("application/showFairwayDepth", true, { root: true }); + dispatch("moveToFeauture", { feature: stretches[0], zoom: 17 }); + } + } + if ( + sections.length === 1 && + !stretches.length && + !bottlenecks.length && + !gauges.length + ) { + if (rootState.imports.selectedSectionId === sections[0].getId()) { + commit("imports/selectedSectionId", null, { + root: true + }); + } else { + commit("imports/selectedSectionId", sections[0].getId(), { + root: true + }); + commit("fairwayavailability/type", "sections", { root: true }); + commit("application/showFairwayDepth", true, { root: true }); + dispatch("moveToFeauture", { feature: sections[0], zoom: 17 }); + } + } + } - commit("setIdentifiedFeatures", identifiedFeatures); - } - - // 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)); - } + // 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)); } } - */ + } + */ + + let currentResolution = map.getView().getResolution(); + + var waterwayAxisSource = map.getLayer("WATERWAYAXIS").getSource(); + var waxisUrl = waterwayAxisSource.getGetFeatureInfoUrl( + event.coordinate, + currentResolution /* resolution */, + "EPSG:3857", + // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d + { INFO_FORMAT: "application/json" } + ); + + if (waxisUrl) { + // cannot directly query here because of SOP + HTTP.get(waxisUrl, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + } + }).then(response => { + let features = response.data.features.map(f => { + let feat = new Feature({ + geometry: new Point(f.geometry.coordinates) + }); + feat.setId(f.id); + feat.setProperties(f.properties); + return feat; + }); + commit("addIdentifiedFeatures", features); + }); + } + var waterwayAreaSource = map.getLayer("WATERWAYAREA").getSource(); + var wareaUrl = waterwayAreaSource.getGetFeatureInfoUrl( + event.coordinate, + currentResolution /* resolution */, + "EPSG:3857", + // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d + { INFO_FORMAT: "application/json" } + ); + + if (wareaUrl) { + HTTP.get(wareaUrl, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + } + }).then(response => { + let features = response.data.features.map(f => { + let feat = new Feature({ + geometry: new Point(f.geometry.coordinates) + }); + feat.setId(f.id); + feat.setProperties(f.properties); + return feat; + }); + commit("addIdentifiedFeatures", features); + }); + } + var dmSource = map.getLayer("DISTANCEMARKS").getSource(); + var dmUrl = dmSource.getGetFeatureInfoUrl( + event.coordinate, + currentResolution /* resolution */, + "EPSG:3857", + // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d + { INFO_FORMAT: "application/json" } + ); - // trying the GetFeatureInfo way for WMS - var wmsSource = getters.getVSourceByName( - "Inland ECDIS chart Danube" - ); - var url = wmsSource.getGetFeatureInfoUrl( - event.coordinate, - 100 /* resolution */, - "EPSG:3857", - // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d - { INFO_FORMAT: "text/plain" } - ); + if (dmUrl) { + HTTP.get(dmUrl + "&BUFFER=5", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") + } + }).then(response => { + let features = response.data.features.map(f => { + let feat = new Feature({ + geometry: new Point(f.geometry.coordinates) + }); + feat.setId(f.id); + feat.setProperties(f.properties); + return feat; + }); + commit("addIdentifiedFeatures", features); + }); + } + var dmaSource = map.getLayer("DISTANCEMARKSAXIS").getSource(); + var dmaUrl = dmaSource.getGetFeatureInfoUrl( + event.coordinate, + currentResolution /* resolution */, + "EPSG:3857", + // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d + { INFO_FORMAT: "application/json" } + ); - if (url) { - // cannot directly query here because of SOP - console.log("GetFeatureInfo url:", url); + if (dmaUrl) { + HTTP.get(dmaUrl + "&BUFFER=5", { + headers: { + "X-Gemma-Auth": localStorage.getItem("token") } - } + }).then(response => { + let features = response.data.features.map(f => { + let feat = new Feature({ + geometry: new Point(f.geometry.coordinates) + }); + feat.setId(f.id); + feat.setProperties(f.properties); + return feat; + }); + commit("addIdentifiedFeatures", features); + }); + } + // trying the GetFeatureInfo way for WMS + var iecdisSource = map.getLayer("INLANDECDIS").getSource(); + var iecdisUrl = iecdisSource.getGetFeatureInfoUrl( + event.coordinate, + currentResolution /* resolution */, + "EPSG:3857", + // { INFO_FORMAT: "application/vnd.ogc.gml" } // not allowed by d4d + { INFO_FORMAT: "text/plain" } ); - } + + if (iecdisUrl) { + // cannot directly query here because of SOP + console.log("GetFeatureInfo url:", iecdisUrl); + } + }); + }, + refreshLayers({ state }) { + state.openLayersMaps.forEach(map => { + let layers = map.getLayers().getArray(); + for (let i = 0; i < layers.length; i++) { + let layer = layers[i]; + if ( + layer instanceof VectorLayer && + layer.get("source").loader_.name != "VOID" + ) { + layer.getSource().clear(true); + layer.getSource().refresh({ force: true }); + } + } + }); + }, + moveToBoundingBox( + { state }, + { boundingBox, zoom, preventZoomOut, duration } + ) { + const extent = transformExtent(boundingBox, "EPSG:4326", "EPSG:3857"); + const currentZoom = state.syncedView.getZoom(); + zoom = zoom || currentZoom; + state.syncedView.fit(extent, { + maxZoom: preventZoomOut ? Math.max(zoom, currentZoom) : zoom, + duration: duration || 700 + }); + }, + moveToFeauture({ dispatch }, { feature, zoom, preventZoomOut }) { + const boundingBox = feature.hasOwnProperty("geometry") + ? bbox(feature.geometry) + : feature + .getGeometry() + .clone() + .transform("EPSG:3857", "EPSG:4326") + .getExtent(); + dispatch("moveToBoundingBox", { boundingBox, zoom, preventZoomOut }); + }, + moveMap({ state }, { coordinates, zoom, preventZoomOut }) { + const currentZoom = state.syncedView.getZoom(); + zoom = zoom || currentZoom; + state.syncedView.animate({ + zoom: preventZoomOut ? Math.max(zoom, currentZoom) : zoom, + center: fromLonLat(coordinates, state.syncedView.getProjection()), + duration: 700 + }); } } }; - -export { LAYERS };
--- a/client/src/store/user.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/store/user.js Mon Jun 03 10:19:18 2019 +0200 @@ -13,8 +13,8 @@ * Markus Kottländer <markus@intevation.de> */ -import { HTTP } from "../lib/http"; -import { toMillisFromString } from "../lib/session"; +import { HTTP } from "@/lib/http"; +import { toMillisFromString } from "@/lib/session"; const init = () => { return {
--- a/client/src/store/usermanagement.js Wed May 29 10:58:45 2019 +0200 +++ b/client/src/store/usermanagement.js Mon Jun 03 10:19:18 2019 +0200 @@ -13,7 +13,7 @@ * Markus Kottländer <markus@intevation.de> */ -import { HTTP } from "../lib/http"; +import { HTTP } from "@/lib/http"; // initial state const init = () => { @@ -41,6 +41,12 @@ namespaced: true, state: init(), getters: { + userCountries: state => { + return state.users.reduce((o, n) => { + o[n.user] = n.role !== "sys_admin" ? n.country : "global"; + return o; + }, {}); + }, isUserDetailsVisible: state => { return state.userDetailsVisible; },
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/tests/e2e/reports/import.xml Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<testsuites errors="0" + failures="0" + tests="6"> + + <testsuite name="import" + errors="0" failures="0" hostname="" id="" package="import" skipped="6" + tests="6" time="0.000" timestamp="Thu, 04 Apr 2019 10:16:03 GMT"> + + + + <system-err> + Error while running [Protocols / Loading protocols]: + + TypeError: browser.url(...).waitForElementVisible(...).setValue(...).setValue(...).click(...).pause(...).click(...).pause(...).click(...).pause(...).url.contains is not a function + at Object.Loading protocols (/home/thomas/go/src/gemma.intevation.de/gemma/client/tests/e2e/specs/protocols.js:31:12) + at Module.call (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/nightwatch/lib/runner/module.js:62:34) + at /home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/nightwatch/lib/runner/testcase.js:70:29 + at _fulfilled (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:834:54) + at self.promiseDispatch.done (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:863:30) + at Promise.promise.promiseDispatch (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:796:13) + at /home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:556:49 + at runSingle (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:137:13) + at flush (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:125:13) + at _combinedTickCallback (internal/process/next_tick.js:132:7) + </system-err> + + + + + <testcase + name="Bottleneck import" classname="import"> + <skipped /> + </testcase> + + <testcase + name="Available fairwaydepth" classname="import"> + <skipped /> + </testcase> + + <testcase + name="Gauge measurement" classname="import"> + <skipped /> + </testcase> + + <testcase + name="Import Fairway Dimensions" classname="import"> + <skipped /> + </testcase> + + <testcase + name="Import Waterway Axis" classname="import"> + <skipped /> + </testcase> + + <testcase + name="Import Waterway Area " classname="import"> + <skipped /> + </testcase> + + + </testsuite> +</testsuites>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/tests/e2e/reports/login.xml Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<testsuites errors="0" + failures="0" + tests="5"> + + <testsuite name="login" + errors="0" failures="0" hostname="" id="" package="login" skipped="5" + tests="5" time="0.000" timestamp="Thu, 04 Apr 2019 10:16:03 GMT"> + + + + <system-err> + Error while running [Protocols / Loading protocols]: + + TypeError: browser.url(...).waitForElementVisible(...).setValue(...).setValue(...).click(...).pause(...).click(...).pause(...).click(...).pause(...).url.contains is not a function + at Object.Loading protocols (/home/thomas/go/src/gemma.intevation.de/gemma/client/tests/e2e/specs/protocols.js:31:12) + at Module.call (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/nightwatch/lib/runner/module.js:62:34) + at /home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/nightwatch/lib/runner/testcase.js:70:29 + at _fulfilled (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:834:54) + at self.promiseDispatch.done (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:863:30) + at Promise.promise.promiseDispatch (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:796:13) + at /home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:556:49 + at runSingle (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:137:13) + at flush (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:125:13) + at _combinedTickCallback (internal/process/next_tick.js:132:7) + </system-err> + + + + + <testcase + name="Page Load" classname="login"> + <skipped /> + </testcase> + + <testcase + name="Login failed" classname="login"> + <skipped /> + </testcase> + + <testcase + name="Login oana success" classname="login"> + <skipped /> + </testcase> + + <testcase + name="Login oana switch url" classname="login"> + <skipped /> + </testcase> + + <testcase + name="Login switch user from oana to sophie" classname="login"> + <skipped /> + </testcase> + + + </testsuite> +</testsuites>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/tests/e2e/reports/protocols.xml Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<testsuites errors="1" + failures="0" + tests="1"> + + <testsuite name="protocols" + errors="1" failures="0" hostname="" id="" package="protocols" skipped="0" + tests="1" time="0.005000" timestamp="Thu, 04 Apr 2019 10:16:03 GMT"> + + <testcase name="Loading protocols" classname="protocols" time="0.005000" assertions="0"> + </testcase> + + + + <system-err> + Error while running [Protocols / Loading protocols]: + + TypeError: browser.url(...).waitForElementVisible(...).setValue(...).setValue(...).click(...).pause(...).click(...).pause(...).click(...).pause(...).url.contains is not a function + at Object.Loading protocols (/home/thomas/go/src/gemma.intevation.de/gemma/client/tests/e2e/specs/protocols.js:31:12) + at Module.call (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/nightwatch/lib/runner/module.js:62:34) + at /home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/nightwatch/lib/runner/testcase.js:70:29 + at _fulfilled (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:834:54) + at self.promiseDispatch.done (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:863:30) + at Promise.promise.promiseDispatch (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:796:13) + at /home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:556:49 + at runSingle (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:137:13) + at flush (/home/thomas/go/src/gemma.intevation.de/gemma/client/node_modules/q/q.js:125:13) + at _combinedTickCallback (internal/process/next_tick.js:132:7) + </system-err> + + + + </testsuite> +</testsuites>
--- a/client/tests/e2e/specs/import.js Wed May 29 10:58:45 2019 +0200 +++ b/client/tests/e2e/specs/import.js Mon Jun 03 10:19:18 2019 +0200 @@ -16,6 +16,7 @@ // http://nightwatchjs.org/guide#usage module.exports = { + "@disabled": true, "Bottleneck import": browser => { browser .url(process.env.VUE_DEV_SERVER_URL)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/tests/e2e/specs/protocols.js Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,37 @@ +/* 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> + */ + +// For authoring Nightwatch tests, see +// http://nightwatchjs.org/guide#usage + +module.exports = { + "Loading protocols": browser => { + browser + .url(process.env.VUE_DEV_SERVER_URL) + .waitForElementVisible("#app", 5000) + .setValue("input[id='inputUsername']", "sophie") + .setValue("input[id='inputPassword']", "so2Phie4") + .click("button[type='submit']") + .pause(1000) + .click(".menubutton") + .pause(1000) + .click("a[href='#/logs']") + .pause(1000) + .assert.urlContains("#/logs") + .click("#accesslog") + .click("#errorlog") + .assert.urlContains("#/logs") + .end(); + } +};
--- a/client/yarn.lock Wed May 29 10:58:45 2019 +0200 +++ b/client/yarn.lock Mon Jun 03 10:19:18 2019 +0200 @@ -10,33 +10,33 @@ "@babel/highlight" "^7.0.0" "@babel/core@^7.0.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.2.2.tgz#07adba6dde27bb5ad8d8672f15fde3e08184a687" - integrity sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.4.tgz#84055750b05fcd50f9915a826b44fa347a825250" + integrity sha512-lQgGX3FPRgbz2SKmhMtYgJvVzGZrmjaF4apZ2bLwofAKiSjxU0drPh4S/VasyYXwaTs+A1gvQ45BN8SQJzHsQQ== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" - "@babel/helpers" "^7.2.0" - "@babel/parser" "^7.2.2" - "@babel/template" "^7.2.2" - "@babel/traverse" "^7.2.2" - "@babel/types" "^7.2.2" + "@babel/generator" "^7.4.4" + "@babel/helpers" "^7.4.4" + "@babel/parser" "^7.4.4" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" - lodash "^4.17.10" + lodash "^4.17.11" resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.2.2": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.2.tgz#fff31a7b2f2f3dad23ef8e01be45b0d5c2fc0132" - integrity sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ== - dependencies: - "@babel/types" "^7.3.2" +"@babel/generator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041" + integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ== + dependencies: + "@babel/types" "^7.4.4" jsesc "^2.5.1" - lodash "^4.17.10" + lodash "^4.17.11" source-map "^0.5.0" trim-right "^1.0.1" @@ -55,34 +55,35 @@ "@babel/helper-explode-assignable-expression" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-call-delegate@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz#6a957f105f37755e8645343d3038a22e1449cc4a" - integrity sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ== - dependencies: - "@babel/helper-hoist-variables" "^7.0.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" - -"@babel/helper-create-class-features-plugin@^7.3.0": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.2.tgz#ba1685603eb1c9f2f51c9106d5180135c163fe73" - integrity sha512-tdW8+V8ceh2US4GsYdNVNoohq5uVwOf9k6krjwW4E1lINcHgttnWcNqgdoessn12dAy8QkbezlbQh2nXISNY+A== +"@babel/helper-call-delegate@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz#87c1f8ca19ad552a736a7a27b1c1fcf8b1ff1f43" + integrity sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ== + dependencies: + "@babel/helper-hoist-variables" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + +"@babel/helper-create-class-features-plugin@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.4.4.tgz#fc3d690af6554cc9efc607364a82d48f58736dba" + integrity sha512-UbBHIa2qeAGgyiNR9RszVF7bUHEdgS4JAUNT8SiqrAN6YJVxlOxeLr5pBzb5kan302dejJ9nla4RyKcR1XT6XA== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.2.3" - -"@babel/helper-define-map@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz#3b74caec329b3c80c116290887c0dd9ae468c20c" - integrity sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg== + "@babel/helper-replace-supers" "^7.4.4" + "@babel/helper-split-export-declaration" "^7.4.4" + +"@babel/helper-define-map@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a" + integrity sha512-IX3Ln8gLhZpSuqHJSnTNBWGDE9kdkTEWl21A/K7PQ00tseBwbqCHTvNLHSBd9M0R5rER4h5Rsvj9vw0R5SieBg== dependencies: "@babel/helper-function-name" "^7.1.0" - "@babel/types" "^7.0.0" - lodash "^4.17.10" + "@babel/types" "^7.4.4" + lodash "^4.17.11" "@babel/helper-explode-assignable-expression@^7.1.0": version "7.1.0" @@ -108,12 +109,12 @@ dependencies: "@babel/types" "^7.0.0" -"@babel/helper-hoist-variables@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz#46adc4c5e758645ae7a45deb92bab0918c23bb88" - integrity sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w== - dependencies: - "@babel/types" "^7.0.0" +"@babel/helper-hoist-variables@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz#0298b5f25c8c09c53102d52ac4a98f773eb2850a" + integrity sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w== + dependencies: + "@babel/types" "^7.4.4" "@babel/helper-member-expression-to-functions@^7.0.0": version "7.0.0" @@ -129,17 +130,17 @@ dependencies: "@babel/types" "^7.0.0" -"@babel/helper-module-transforms@^7.1.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.2.2.tgz#ab2f8e8d231409f8370c883d20c335190284b963" - integrity sha512-YRD7I6Wsv+IHuTPkAmAS4HhY0dkPobgLftHp0cRGZSdrRvmZY8rFvae/GVu3bD00qscuvK3WPHB3YdNpBXUqrA== +"@babel/helper-module-transforms@^7.1.0", "@babel/helper-module-transforms@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz#96115ea42a2f139e619e98ed46df6019b94414b8" + integrity sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/template" "^7.2.2" - "@babel/types" "^7.2.2" - lodash "^4.17.10" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/template" "^7.4.4" + "@babel/types" "^7.4.4" + lodash "^4.17.11" "@babel/helper-optimise-call-expression@^7.0.0": version "7.0.0" @@ -153,12 +154,12 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== -"@babel/helper-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.0.0.tgz#2c1718923b57f9bbe64705ffe5640ac64d9bdb27" - integrity sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg== - dependencies: - lodash "^4.17.10" +"@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.4.tgz#a47e02bc91fb259d2e6727c2a30013e3ac13c4a2" + integrity sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q== + dependencies: + lodash "^4.17.11" "@babel/helper-remap-async-to-generator@^7.1.0": version "7.1.0" @@ -171,15 +172,15 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz#19970020cf22677d62b3a689561dbd9644d8c5e5" - integrity sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA== +"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27" + integrity sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg== dependencies: "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.2.3" - "@babel/types" "^7.0.0" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" "@babel/helper-simple-access@^7.1.0": version "7.1.0" @@ -189,12 +190,12 @@ "@babel/template" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-split-export-declaration@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813" - integrity sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag== - dependencies: - "@babel/types" "^7.0.0" +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== + dependencies: + "@babel/types" "^7.4.4" "@babel/helper-wrap-function@^7.1.0": version "7.2.0" @@ -206,14 +207,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.2.0" -"@babel/helpers@^7.2.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.3.1.tgz#949eec9ea4b45d3210feb7dc1c22db664c9e44b9" - integrity sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA== - dependencies: - "@babel/template" "^7.1.2" - "@babel/traverse" "^7.1.5" - "@babel/types" "^7.3.0" +"@babel/helpers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5" + integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A== + dependencies: + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" "@babel/highlight@^7.0.0": version "7.0.0" @@ -224,10 +225,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.2.tgz#95cdeddfc3992a6ca2a1315191c1679ca32c55cd" - integrity sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ== +"@babel/parser@^7.0.0", "@babel/parser@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.4.tgz#5977129431b8fe33471730d255ce8654ae1250b6" + integrity sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w== "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -239,19 +240,19 @@ "@babel/plugin-syntax-async-generators" "^7.2.0" "@babel/plugin-proposal-class-properties@^7.0.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.0.tgz#272636bc0fa19a0bc46e601ec78136a173ea36cd" - integrity sha512-wNHxLkEKTQ2ay0tnsam2z7fGZUi+05ziDJflEt3AZTP3oXLKHJp9HqhfroB/vdMvt3sda9fAbq7FsG8QPDrZBg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.3.0" + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz#93a6486eed86d53452ab9bab35e368e9461198ce" + integrity sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-decorators@^7.1.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.3.0.tgz#637ba075fa780b1f75d08186e8fb4357d03a72a7" - integrity sha512-3W/oCUmsO43FmZIqermmq6TKaRSYhmh/vybPfVFwQWdSb8xwki38uAIvknCRzuyHRuYfCYmJzL9or1v0AffPjg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.3.0" + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.4.4.tgz#de9b2a1a8ab0196f378e2a82f10b6e2a36f21cc0" + integrity sha512-z7MpQz3XC/iQJWXH9y+MaWcLPNSMY9RQSthrLzak8R8hCj0fuyNk+Dzi9kfNe/JxxlWQ2g7wkABbgWjW36MTcw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-decorators" "^7.2.0" @@ -263,10 +264,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.3.1": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.2.tgz#6d1859882d4d778578e41f82cc5d7bf3d5daf6c1" - integrity sha512-DjeMS+J2+lpANkYLLO+m6GjoTMygYglKmRe6cDTbFv3L9i6mmiE8fe6B8MtCSLZpVXscD5kn7s6SgtHrDoBWoA== +"@babel/plugin-proposal-object-rest-spread@^7.3.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.4.tgz#1ef173fcf24b3e2df92a678f027673b55e7e3005" + integrity sha512-dMBG6cSPBbHeEBdFXeQ2QLc5gUpg4Vkaz8octD4aoW/ISO+jBOcsuxYL7bsb5WSu8RLP6boxrBIALEHgoHtO9g== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" @@ -280,13 +281,13 @@ "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" "@babel/plugin-proposal-unicode-property-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520" - integrity sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78" + integrity sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.2.0" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" "@babel/plugin-syntax-async-generators@^7.2.0": version "7.2.0" @@ -344,10 +345,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-async-to-generator@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz#68b8a438663e88519e65b776f8938f3445b1a2ff" - integrity sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ== +"@babel/plugin-transform-async-to-generator@^7.3.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz#a3f1d01f2f21cadab20b33a82133116f14fb5894" + integrity sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -360,26 +361,26 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-block-scoping@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz#f17c49d91eedbcdf5dd50597d16f5f2f770132d4" - integrity sha512-vDTgf19ZEV6mx35yiPJe4fS02mPQUUcBNwWQSZFXSzTSbsJFQvHt7DqyS3LK8oOWALFOsJ+8bbqBgkirZteD5Q== +"@babel/plugin-transform-block-scoping@^7.3.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz#c13279fabf6b916661531841a23c4b7dae29646d" + integrity sha512-jkTUyWZcTrwxu5DD4rWz6rDB5Cjdmgz6z7M7RLXOJyCUkFBawssDGcGh8M/0FTSB87avyJI1HsTwUXp9nKA1PA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - lodash "^4.17.10" - -"@babel/plugin-transform-classes@^7.2.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.2.tgz#6c90542f210ee975aa2aa8c8b5af7fa73a126953" - integrity sha512-gEZvgTy1VtcDOaQty1l10T3jQmJKlNVxLDCs+3rCVPr6nMkODLELxViq5X9l+rfxbie3XrfrMCYYY6eX3aOcOQ== + lodash "^4.17.11" + +"@babel/plugin-transform-classes@^7.3.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz#0ce4094cdafd709721076d3b9c38ad31ca715eb6" + integrity sha512-/e44eFLImEGIpL9qPxSRat13I5QNRgBLu2hOQJCF7VLy/otSM/sypV1+XaIw5+502RX/+6YaSAPmldk+nhHDPw== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" - "@babel/helper-define-map" "^7.1.0" + "@babel/helper-define-map" "^7.4.4" "@babel/helper-function-name" "^7.1.0" "@babel/helper-optimise-call-expression" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" + "@babel/helper-replace-supers" "^7.4.4" + "@babel/helper-split-export-declaration" "^7.4.4" globals "^11.1.0" "@babel/plugin-transform-computed-properties@^7.2.0": @@ -390,20 +391,20 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-destructuring@^7.2.0": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz#f2f5520be055ba1c38c41c0e094d8a461dd78f2d" - integrity sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.4.tgz#9d964717829cc9e4b601fc82a26a71a4d8faf20f" + integrity sha512-/aOx+nW0w8eHiEHm+BTERB2oJn5D127iye/SUQl7NjHy0lf+j7h4MKMMSOwdazGq9OxgiNADncE+SRJkCxjZpQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-dotall-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz#f0aabb93d120a8ac61e925ea0ba440812dbe0e49" - integrity sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3" + integrity sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.1.3" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" "@babel/plugin-transform-duplicate-keys@^7.2.0": version "7.2.0" @@ -421,16 +422,16 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-for-of@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz#ab7468befa80f764bb03d3cb5eef8cc998e1cad9" - integrity sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556" + integrity sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-function-name@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.2.0.tgz#f7930362829ff99a3174c39f0afcc024ef59731a" - integrity sha512-kWgksow9lHdvBC2Z4mxTsvc7YdY7w/V6B2vy9cTIPtLEE9NhwoWivaxdNM/S37elu5bqlLP/qOY906LukO9lkQ== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz#e1436116abb0610c2259094848754ac5230922ad" + integrity sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -451,20 +452,20 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-modules-commonjs@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz#c4f1933f5991d5145e9cfad1dfd848ea1727f404" - integrity sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ== - dependencies: - "@babel/helper-module-transforms" "^7.1.0" + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz#0bef4713d30f1d78c2e59b3d6db40e60192cac1e" + integrity sha512-4sfBOJt58sEo9a2BQXnZq+Q3ZTSAUXyK3E30o36BOGnJ+tvJ6YSxF0PG6kERvbeISgProodWuI9UVG3/FMY6iw== + dependencies: + "@babel/helper-module-transforms" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" -"@babel/plugin-transform-modules-systemjs@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz#912bfe9e5ff982924c81d0937c92d24994bb9068" - integrity sha512-aYJwpAhoK9a+1+O625WIjvMY11wkB/ok0WClVwmeo3mCjcNRjt+/8gHWrB5i+00mUju0gWsBkQnPpdvQ7PImmQ== - dependencies: - "@babel/helper-hoist-variables" "^7.0.0" +"@babel/plugin-transform-modules-systemjs@^7.3.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz#dc83c5665b07d6c2a7b224c00ac63659ea36a405" + integrity sha512-MSiModfILQc3/oqnG7NrP1jHaSPryO6tA2kOMmAQApz5dayPxWiHqmq4sWH2xF5LcQK56LlbKByCd8Aah/OIkQ== + dependencies: + "@babel/helper-hoist-variables" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-modules-umd@^7.2.0": @@ -476,16 +477,16 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-named-capturing-groups-regex@^7.3.0": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz#140b52985b2d6ef0cb092ef3b29502b990f9cd50" - integrity sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.4.tgz#5611d96d987dfc4a3a81c4383bb173361037d68d" + integrity sha512-Ki+Y9nXBlKfhD+LXaRS7v95TtTGYRAf9Y1rTDiE75zf8YQz4GDaWRXosMfJBXxnk88mGFjWdCRIeqDbon7spYA== dependencies: regexp-tree "^0.1.0" "@babel/plugin-transform-new-target@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz#ae8fbd89517fa7892d20e6564e641e8770c3aa4a" - integrity sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz#18d120438b0cc9ee95a47f2c72bc9768fbed60a5" + integrity sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -498,25 +499,25 @@ "@babel/helper-replace-supers" "^7.1.0" "@babel/plugin-transform-parameters@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.2.0.tgz#0d5ad15dc805e2ea866df4dd6682bfe76d1408c2" - integrity sha512-kB9+hhUidIgUoBQ0MsxMewhzr8i60nMa2KgeJKQWYrqQpqcBYtnpR+JgkadZVZoaEZ/eKu9mclFaVwhRpLNSzA== - dependencies: - "@babel/helper-call-delegate" "^7.1.0" + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz#7556cf03f318bd2719fe4c922d2d808be5571e16" + integrity sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw== + dependencies: + "@babel/helper-call-delegate" "^7.4.4" "@babel/helper-get-function-arity" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-regenerator@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz#5b41686b4ed40bef874d7ed6a84bdd849c13e0c1" - integrity sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw== - dependencies: - regenerator-transform "^0.13.3" - -"@babel/plugin-transform-runtime@^7.0.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.2.0.tgz#566bc43f7d0aedc880eaddbd29168d0f248966ea" - integrity sha512-jIgkljDdq4RYDnJyQsiWbdvGeei/0MOTtSHKO/rfbd/mXBxNpdlulMx49L0HQ4pug1fXannxoqCI+fYSle9eSw== +"@babel/plugin-transform-regenerator@^7.3.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.4.tgz#5b4da4df79391895fca9e28f99e87e22cfc02072" + integrity sha512-Zz3w+pX1SI0KMIiqshFZkwnVGUhDZzpX2vtPzfJBKQQq8WsP/Xy9DNdELWivxcKOCX/Pywge4SiEaPaLtoDT4g== + dependencies: + regenerator-transform "^0.13.4" + +"@babel/plugin-transform-runtime@^7.4.0": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.4.tgz#a50f5d16e9c3a4ac18a1a9f9803c107c380bce08" + integrity sha512-aMVojEjPszvau3NRg+TIH14ynZLvPewH4xhlCW1w6A3rkxTS1m4uwzRclYR9oS+rl/dr+kT+pzbfHuAWP/lc7Q== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -546,9 +547,9 @@ "@babel/helper-regex" "^7.0.0" "@babel/plugin-transform-template-literals@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz#d87ed01b8eaac7a92473f608c97c089de2ba1e5b" - integrity sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz#9d28fea7bbce637fb7612a0750989d8321d4bcb0" + integrity sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -561,24 +562,24 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-unicode-regex@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b" - integrity sha512-m48Y0lMhrbXEJnVUaYly29jRXbQ3ksxPrS1Tg8t+MHqzXhtBYAvI51euOBaoAlZLPHsieY9XPVMf80a5x0cPcA== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f" + integrity sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.1.3" - -"@babel/preset-env@^7.0.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.1.tgz#389e8ca6b17ae67aaf9a2111665030be923515db" - integrity sha512-FHKrD6Dxf30e8xgHQO0zJZpUPfVZg+Xwgz5/RdSWCbza9QLNk4Qbp40ctRoqDxml3O8RMzB1DU55SXeDG6PqHQ== + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" + +"@babel/preset-env@^7.0.0 < 7.4.0": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1" + integrity sha512-2mwqfYMK8weA0g0uBKOt4FE3iEodiHy9/CW0b+nWXcbL+pGzLx8ESYc+j9IIxr6LTDHWKgPm71i9smo02bw+gA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-async-generator-functions" "^7.2.0" "@babel/plugin-proposal-json-strings" "^7.2.0" - "@babel/plugin-proposal-object-rest-spread" "^7.3.1" + "@babel/plugin-proposal-object-rest-spread" "^7.3.4" "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" "@babel/plugin-syntax-async-generators" "^7.2.0" @@ -586,10 +587,10 @@ "@babel/plugin-syntax-object-rest-spread" "^7.2.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" "@babel/plugin-transform-arrow-functions" "^7.2.0" - "@babel/plugin-transform-async-to-generator" "^7.2.0" + "@babel/plugin-transform-async-to-generator" "^7.3.4" "@babel/plugin-transform-block-scoped-functions" "^7.2.0" - "@babel/plugin-transform-block-scoping" "^7.2.0" - "@babel/plugin-transform-classes" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.3.4" + "@babel/plugin-transform-classes" "^7.3.4" "@babel/plugin-transform-computed-properties" "^7.2.0" "@babel/plugin-transform-destructuring" "^7.2.0" "@babel/plugin-transform-dotall-regex" "^7.2.0" @@ -600,13 +601,13 @@ "@babel/plugin-transform-literals" "^7.2.0" "@babel/plugin-transform-modules-amd" "^7.2.0" "@babel/plugin-transform-modules-commonjs" "^7.2.0" - "@babel/plugin-transform-modules-systemjs" "^7.2.0" + "@babel/plugin-transform-modules-systemjs" "^7.3.4" "@babel/plugin-transform-modules-umd" "^7.2.0" "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0" "@babel/plugin-transform-new-target" "^7.0.0" "@babel/plugin-transform-object-super" "^7.2.0" "@babel/plugin-transform-parameters" "^7.2.0" - "@babel/plugin-transform-regenerator" "^7.0.0" + "@babel/plugin-transform-regenerator" "^7.3.4" "@babel/plugin-transform-shorthand-properties" "^7.2.0" "@babel/plugin-transform-spread" "^7.2.0" "@babel/plugin-transform-sticky-regex" "^7.2.0" @@ -619,90 +620,90 @@ semver "^5.3.0" "@babel/runtime-corejs2@^7.2.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.3.1.tgz#0c113242e2328f9674d42703a89bee6ebebe9a82" - integrity sha512-YpO13776h3e6Wy8dl2J8T9Qwlvopr+b4trCEhHE+yek6yIqV8sx6g3KozdHMbXeBpjosbPi+Ii5Z7X9oXFHUKA== - dependencies: - core-js "^2.5.7" - regenerator-runtime "^0.12.0" + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.4.4.tgz#4d4519a4c85e9d98fdff59f5371758a34ae07923" + integrity sha512-hE7oVwVsRws84u5/nkaWWdN2J4SXEGuXKjrAsP0E4nkYImjSbpdHfGTS2nvFc82aDGIuG6OzhAQMpIzTHuZeKA== + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.2" "@babel/runtime@^7.0.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a" - integrity sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA== - dependencies: - regenerator-runtime "^0.12.0" - -"@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" - integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.4.tgz#dc2e34982eb236803aa27a07fea6857af1b9171d" + integrity sha512-w0+uT71b6Yi7i5SE0co4NioIpSYS6lLiXvCzWzGSKvpK5vdQtCbICHMj+gbAKAOtxiV6HsVh/MBdaF9EQ6faSg== + dependencies: + regenerator-runtime "^0.13.2" + +"@babel/template@^7.1.0", "@babel/template@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.2.2" - "@babel/types" "^7.2.2" - -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" - integrity sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw== + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.4.tgz#0776f038f6d78361860b6823887d4f3937133fe8" + integrity sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.2.2" + "@babel/generator" "^7.4.4" "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.2.3" - "@babel/types" "^7.2.2" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.10" - -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.2": - version "7.3.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.2.tgz#424f5be4be633fff33fb83ab8d67e4a8290f5a2f" - integrity sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ== + lodash "^4.17.11" + +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" + integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== dependencies: esutils "^2.0.2" - lodash "^4.17.10" + lodash "^4.17.11" to-fast-properties "^2.0.0" -"@fortawesome/fontawesome-common-types@^0.2.14": - version "0.2.14" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.14.tgz#0eb86a77ac88e8c12c48591735283f0bf0ea5606" - integrity sha512-LOcvERCI96KioXSIfOYN4IATv2ROJOyf72dYnpoWfOIkuyLp45oMej1kL03kYdzvktLCzYhykgHwiu6nkg9Xbw== +"@fortawesome/fontawesome-common-types@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.17.tgz#d8c36e6f6f3b3415fa1f83eaffe4f41bd313715c" + integrity sha512-DEYsEb/iiGVoMPQGjhG2uOylLVuMzTxOxysClkabZ5n80Q3oFDWGnshCLKvOvKoeClsgmKhWVrnnqvsMI1cAbw== "@fortawesome/fontawesome-svg-core@^1.2.8": - version "1.2.14" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.14.tgz#48fde5fbfa223cffd5fac9c0402cfbe87605a7d0" - integrity sha512-T1qCqkwm9PuvK53J64D1ovfrOTa1kG+SrHNj5cFst/rrskhCnbxpRdbqFIdc/thmXC0ebBX8nOUyja2/mrxe4g== - dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.14" + version "1.2.17" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.17.tgz#8fce4402e824ebe99a04b1949d56d696eeae2e6d" + integrity sha512-TORMW/wIX2QyyGBd4XwHGPir4/0U18Wxf+iDBAUW3EIJ0/VC/ZMpJOiyiCe1f8g9h0PPzA7sqVtl8JtTUtm4uA== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.17" "@fortawesome/free-brands-svg-icons@^5.5.0": - version "5.7.1" - resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.7.1.tgz#20711fe4d6a459161d052171169baa175b90fca1" - integrity sha512-YU+np8UJGjHUmzfGS5yyK0wWR0QHbx5lTFRSylBfEkm8QXvOkRxB03sUhOSIWVXU7iPiePuqrsglQRgxoG4nrw== - dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.14" + version "5.8.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.8.1.tgz#8f188a4699adb31e505abca64740d7046222b9a1" + integrity sha512-NN5Nap2D5e7Lusa5uarAUkcaO7PMbme5wmUF8kofZzPUZR753zDg/UFffi+LLE2Mi9zRXCJEYmIRfMON9SxLPg== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.17" "@fortawesome/free-regular-svg-icons@^5.5.0": - version "5.7.1" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.7.1.tgz#eab182769eef910f961ed4c3a581ebd9934b4b38" - integrity sha512-JFLJ4M11lZEfi+bmfJdWGVUe5fvmr5k/bqshN7VbJZvEJ6i12Yr6uaByQUM0U1tgw+hJkd8xAwVvKxpJ2HDVTA== - dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.14" + version "5.8.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.8.1.tgz#9477a973fe3f681f871375fa95ff32a87dcb5111" + integrity sha512-U+tFjDyQpVdD0UPWoKRBVLhh0J1/q3iaWDrnxNMJKuKRmerc4d0jfiZdM2X7agOTcG7amvcllRBiWCu2FwYlMA== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.17" "@fortawesome/free-solid-svg-icons@^5.5.0": - version "5.7.1" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.7.1.tgz#df41b8643383862a2af93456e7129e5ffc0fb7ae" - integrity sha512-5V/Q+JoPzuiIHW2JwmZGvE9bHguvNJKa7611DPo51uIvYv9LweX/SnDF+HC23X2W5T3myHhnGi+EZJTmidAmyg== - dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.14" + version "5.8.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.8.1.tgz#086c70f95b34a4bcf6f50ff1078d46e53486eb52" + integrity sha512-FUcxR75PtMOo3ihRHJOZz64IsWIVdWgB2vCMLJjquTv487wVVCMH5H5gWa72et2oI9lKKD2jvjQ+y+7mxhscVQ== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.17" "@fortawesome/vue-fontawesome@^0.1.2": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-0.1.5.tgz#ea70a4d4ad0d6d617048a5b2a2ff33e21fe47d61" - integrity sha512-tiNZCgh+ZkUsyFfm2MQMMdHKRrKj82M7g0XFPSNNY+s5nRB82soy0US+xj0jGRy433b0c4WpylCOhgle3294Uw== + version "0.1.6" + resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-0.1.6.tgz#18a0f4263b90f65180fc927325ba01896276ea04" + integrity sha512-HAGRbrOuGDwwUmCYdpzR0hhNQ3EE30dOS4JiJKcoZ+S4M210CxyU0OXCgzIg3HzK/23rlpHbV8zi9PDDZDnuIw== "@intervolga/optimize-cssnano-plugin@^1.0.5": version "1.0.6" @@ -962,41 +963,55 @@ "@turf/meta" "^5.1.5" "@types/babel-types@*", "@types/babel-types@^7.0.0": - version "7.0.4" - resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.4.tgz#bfd5b0d0d1ba13e351dff65b6e52783b816826c8" - integrity sha512-WiZhq3SVJHFRgRYLXvpf65XnV6ipVHhnNaNvE8yCimejrGglkg38kEj0JcizqwSHxmPSjcTlig/6JouxLGEhGw== + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.7.tgz#667eb1640e8039436028055737d2b9986ee336e3" + integrity sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ== "@types/babylon@^6.16.2": - version "6.16.4" - resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.4.tgz#d3df72518b34a6a015d0dc58745cd238b5bb8ad2" - integrity sha512-8dZMcGPno3g7pJ/d0AyJERo+lXh9i1JhDuCUs+4lNIN9eUe5Yh6UCLrpgSEi05Ve2JMLauL2aozdvKwNL0px1Q== + version "6.16.5" + resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.5.tgz#1c5641db69eb8cdf378edd25b4be7754beeb48b4" + integrity sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w== dependencies: "@types/babel-types" "*" +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + +"@types/glob@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + "@types/node@*": - version "11.9.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.0.tgz#35fea17653490dab82e1d5e69731abfdbf13160d" - integrity sha512-ry4DOrC+xenhQbzk1iIPzCZGhhPGEFv7ia7Iu6XXSLVluiJIe9FfG7Iu3mObH9mpxEXCWLCMU4JWbCCR9Oy1Zg== - -"@types/node@^10.11.7": - version "10.12.25" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.25.tgz#0d01a7dd6127de60d861ece4a650963042abb538" - integrity sha512-IcvnGLGSQFDvC07Bz2I8SX+QKErDZbUdiQq7S2u3XyzTyJfUmT0sWJMbeQkMzpTAkO7/N7sZpW/arUM2jfKsbQ== + version "12.0.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.0.tgz#d11813b9c0ff8aaca29f04cbc12817f4c7d656e5" + integrity sha512-Jrb/x3HT4PTJp6a4avhmJCDEVrPdqLfl3e8GGMbpkGGdwAV5UGlIs4vVEfsHHfylZVOKZWpOqmqFH8CbfOZ6kg== "@types/node@^8.0.7": - version "8.10.40" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.40.tgz#4314888d5cd537945d73e9ce165c04cc550144a4" - integrity sha512-RRSjdwz63kS4u7edIwJUn8NqKLLQ6LyqF/X4+4jp38MBT3Vwetewi2N4dgJEshLbDwNgOJXNYoOwzVZUSSLhkQ== + version "8.10.48" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.48.tgz#e385073561643a9ba6199a1985ffc03530f90781" + integrity sha512-c35YEBTkL4rzXY2ucpSKy+UYHjUBIIkuJbWYbsGIrKLEWU5dgJMmLkkIb3qeC3O3Tpb1ZQCwecscvJTDjDjkRw== + +"@types/normalize-package-data@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== "@types/q@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18" - integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA== - -"@types/semver@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" - integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" + integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== "@types/strip-bom@^3.0.0": version "3.0.0" @@ -1008,202 +1023,211 @@ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== -"@vue/babel-helper-vue-jsx-merge-props@^1.0.0-beta.2": - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0-beta.2.tgz#f3e20d77b89ddb7a4b9b7a75372f05cd3ac22d92" - integrity sha512-Yj92Q1GcGjjctecBfnBmVqKSlMdyZaVq10hlZB4HSd1DJgu4cWgpEImJSzcJRUCZmas6UigwE7f4IjJuQs+JvQ== - -"@vue/babel-plugin-transform-vue-jsx@^1.0.0-beta.2": - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.0.0-beta.2.tgz#6f7903fe66a34a02163f418c426cf419e862d97e" - integrity sha512-fvAymRZAPHitomRE+jIipWRj0STXNSMqeOSdOFu9Ffjqg9WGOxSdCjORxexManfZ2y5QDv7gzI1xfgprsK3nlw== +"@vue/babel-helper-vue-jsx-merge-props@^1.0.0-beta.3": + version "1.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0-beta.3.tgz#e4c2e7125b3e0d2a9d493e457850b2abb0fd3cad" + integrity sha512-cbFQnd3dDPsfWuxbWW2phynX2zsckwC4GfAkcE1QH1lZL2ZAD2V97xY3BmvTowMkjeFObRKQt1P3KKA6AoB0hQ== + +"@vue/babel-plugin-transform-vue-jsx@^1.0.0-beta.3": + version "1.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.0.0-beta.3.tgz#a1a44e801d8ed615e49f145ef1b3eaca2c16e2e6" + integrity sha512-yn+j2B/2aEagaxXrMSK3qcAJnlidfXg9v+qmytqrjUXc4zfi8QVC/b4zCev1FDmTip06/cs/csENA4law6Xhpg== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/plugin-syntax-jsx" "^7.2.0" - "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0-beta.2" + "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0-beta.3" html-tags "^2.0.0" lodash.kebabcase "^4.1.1" svg-tags "^1.0.0" -"@vue/babel-preset-app@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-3.4.0.tgz#926c718066babc0117ee70cebf890d3aaa812bfa" - integrity sha512-P7IaOFtMUd5iic2PH/iY6YPgtPnyd7SzA+ACv1283F5RcLutTURhl2smC1cWUJFGVrUhTmsYEcbS4+06wKymWw== - dependencies: +"@vue/babel-preset-app@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-3.7.0.tgz#f37535ea60b71732ddd4395ec143aaa0b10d4c67" + integrity sha512-6PHZ1TYO8OGy22TLyKm/+VmCzLB9L1UxaA3CFxXJH0h/YUOmgdmuAk3AWhomYSwk2GF51On3aQzYouoaWhvBDQ== + dependencies: + "@babel/helper-module-imports" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-decorators" "^7.1.0" "@babel/plugin-syntax-dynamic-import" "^7.0.0" "@babel/plugin-syntax-jsx" "^7.0.0" - "@babel/plugin-transform-runtime" "^7.0.0" - "@babel/preset-env" "^7.0.0" + "@babel/plugin-transform-runtime" "^7.4.0" + "@babel/preset-env" "^7.0.0 < 7.4.0" "@babel/runtime" "^7.0.0" "@babel/runtime-corejs2" "^7.2.0" - "@vue/babel-preset-jsx" "^1.0.0-beta.2" + "@vue/babel-preset-jsx" "^1.0.0-beta.3" babel-plugin-dynamic-import-node "^2.2.0" - core-js "^2.6.3" - -"@vue/babel-preset-jsx@^1.0.0-beta.2": - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.0.0-beta.2.tgz#3e5dc2b73da58391c1c7327c2bd2ef154fe4e46e" - integrity sha512-nZoAKBR/h6iPMQ66ieQcIdlpPBmqhtUUcgjBS541jIVxSog1rwzrc00jlsuecLonzUMWPU0PabyitsG74vhN1w== - dependencies: - "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0-beta.2" - "@vue/babel-plugin-transform-vue-jsx" "^1.0.0-beta.2" - "@vue/babel-sugar-functional-vue" "^1.0.0-beta.2" - "@vue/babel-sugar-inject-h" "^1.0.0-beta.2" - "@vue/babel-sugar-v-model" "^1.0.0-beta.2" - "@vue/babel-sugar-v-on" "^1.0.0-beta.2" - -"@vue/babel-sugar-functional-vue@^1.0.0-beta.2": - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.0.0-beta.2.tgz#8831f686e7614f282d5170b902483ef538deef38" - integrity sha512-5qvi4hmExgjtrESDk0vflL69dIxkDAukJcYH9o4663E8Nh12Jpbmr+Ja8WmgkAPtTVhk90UVcVUFCCZLHBmhkQ== + babel-plugin-module-resolver "3.2.0" + core-js "^2.6.5" + +"@vue/babel-preset-jsx@^1.0.0-beta.3": + version "1.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.0.0-beta.3.tgz#15c584bd62c0286a80f0196749ae38cde5cd703b" + integrity sha512-qMKGRorTI/0nE83nLEM7MyQiBZUqc62sZyjkBdVaaU7S61MHI8RKHPtbLMMZlWXb2NCJ0fQci8xJWUK5JE+TFA== + dependencies: + "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0-beta.3" + "@vue/babel-plugin-transform-vue-jsx" "^1.0.0-beta.3" + "@vue/babel-sugar-functional-vue" "^1.0.0-beta.3" + "@vue/babel-sugar-inject-h" "^1.0.0-beta.3" + "@vue/babel-sugar-v-model" "^1.0.0-beta.3" + "@vue/babel-sugar-v-on" "^1.0.0-beta.3" + +"@vue/babel-sugar-functional-vue@^1.0.0-beta.3": + version "1.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.0.0-beta.3.tgz#41a855786971dacbbe8044858eefe98de089bf12" + integrity sha512-CBIa0sQWn3vfBS2asfTgv0WwdyKvNTKtE/cCfulZ7MiewLBh0RlvvSmdK9BIMTiHErdeZNSGUGlU6JuSHLyYkQ== dependencies: "@babel/plugin-syntax-jsx" "^7.2.0" -"@vue/babel-sugar-inject-h@^1.0.0-beta.2": - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.0.0-beta.2.tgz#5f92f994bf4b4126fad8633f554e8a426b51b413" - integrity sha512-qGXZ6yE+1trk82xCVJ9j3shsgI+R2ePj3+o8b2Ee7JNaRqQvMfTwpgx5BRlk4q1+CTjvYexdqBS+q4Kg7sSxcg== +"@vue/babel-sugar-inject-h@^1.0.0-beta.3": + version "1.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.0.0-beta.3.tgz#be1d00b74a1a89fed35a9b1415a738c36f125966" + integrity sha512-HKMBMmFfdK9GBp3rX2bHIwILBdgc5F3ahmCB72keJxzaAQrgDAnD+ho70exUge+inAGlNF34WsQcGPElTf9QZg== dependencies: "@babel/plugin-syntax-jsx" "^7.2.0" -"@vue/babel-sugar-v-model@^1.0.0-beta.2": - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.0.0-beta.2.tgz#051d3ae3ef5e70d514e09058ec5790f6a42e8c28" - integrity sha512-63US3IMEtATJzzK2le/Na53Sk2bp3LHfwZ8eMFwbTaz6e2qeV9frBl3ZYaha64ghT4IDSbrDXUmm0J09EAzFfA== +"@vue/babel-sugar-v-model@^1.0.0-beta.3": + version "1.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.0.0-beta.3.tgz#ea935b0e08bf58c125a1349b819156059590993c" + integrity sha512-et39eTEh7zW4wfZoSl9Jf0/n2r9OTT8U02LtSbXsjgYcqaDQFusN0+n7tw4bnOqvnnSVjEp7bVsQCWwykC3Wgg== dependencies: "@babel/plugin-syntax-jsx" "^7.2.0" - "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0-beta.2" - "@vue/babel-plugin-transform-vue-jsx" "^1.0.0-beta.2" + "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0-beta.3" + "@vue/babel-plugin-transform-vue-jsx" "^1.0.0-beta.3" camelcase "^5.0.0" html-tags "^2.0.0" svg-tags "^1.0.0" -"@vue/babel-sugar-v-on@^1.0.0-beta.2": - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.0.0-beta.2.tgz#3e2d122e229b10017f091d178346b601d9245260" - integrity sha512-XH/m3k11EKdMY0MrTg4+hQv8BFM8juzHT95chYkgxDmvDdVJnSCuf9+mcysEJttWD4PVuUGN7EHoIWsIhC0dRw== +"@vue/babel-sugar-v-on@^1.0.0-beta.3": + version "1.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.0.0-beta.3.tgz#2f5fedb43883f603fe76010f253b85c7465855fe" + integrity sha512-F+GapxCiy50jf2Q2B4exw+KYBzlGdeKMAMW1Dbvb0Oa59SA0CH6tsUOIAsXb0A05jwwg/of0LaVeo+4aLefVxQ== dependencies: "@babel/plugin-syntax-jsx" "^7.2.0" - "@vue/babel-plugin-transform-vue-jsx" "^1.0.0-beta.2" + "@vue/babel-plugin-transform-vue-jsx" "^1.0.0-beta.3" camelcase "^5.0.0" -"@vue/cli-overlay@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@vue/cli-overlay/-/cli-overlay-3.4.0.tgz#7fe7cf41eacbaf1f1579efcad93e23b65d4581db" - integrity sha512-uLfQZvMChAf3UQNR+WN8a7vAPqvaw2tJs1TrNxPg+Dr7bm7HWoitvFremF0vLWkxIRM5e+VJgYV3wHk9EwWhzg== +"@vue/cli-overlay@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@vue/cli-overlay/-/cli-overlay-3.7.0.tgz#0f520c98e1be7618b7a68b768666fffa1f589f94" + integrity sha512-QO1rsBVKPZrt+5rHSZXc5UEPVwVgiayOk/cDl+GwSJoR36gnWs1wy1oUX1Awd7QpGiMBK/1+A7aAGhfzKR23Cg== "@vue/cli-plugin-babel@^3.2.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-3.4.0.tgz#d6e7967995f860b94204bdfb17900fadd4aea2d9" - integrity sha512-8ViOzJa8UqUnmMl1422t8EIlCUc5PegSMsdU6xoqfavL83uEGjR+fE4gAI+g7xKo7Qk9+8Z8VvaredXMbmxCzA== + version "3.7.0" + resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-3.7.0.tgz#2be01288980b058f097d26812f65d4d4e8136cca" + integrity sha512-QysJYerzaGzvJ5iT61KpE4hFHiDU8NQ7QjSwIkOAJAx0KY8o0WCjLpAVvjmKtZqNXPBc5Jc3P+eeaz9qQPWNeQ== dependencies: "@babel/core" "^7.0.0" - "@vue/babel-preset-app" "^3.4.0" - "@vue/cli-shared-utils" "^3.4.0" + "@vue/babel-preset-app" "^3.7.0" + "@vue/cli-shared-utils" "^3.7.0" babel-loader "^8.0.5" webpack ">=4 < 4.29" "@vue/cli-plugin-e2e-nightwatch@^3.2.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@vue/cli-plugin-e2e-nightwatch/-/cli-plugin-e2e-nightwatch-3.4.0.tgz#0726fa46ccb2735adc3fd1cdf1ef43c79aaf56b1" - integrity sha512-XIL6NKD/N4ZhbniAZLYB46OmzAg+ibAlUg3W1JURpoBBs9+EP6XV+vzI9lUaS8JNU3Cuj45PqB455fDwUrHt1g== - dependencies: - "@vue/cli-shared-utils" "^3.4.0" - chromedriver "^2.45.0" - deepmerge "^3.1.0" + version "3.7.0" + resolved "https://registry.yarnpkg.com/@vue/cli-plugin-e2e-nightwatch/-/cli-plugin-e2e-nightwatch-3.7.0.tgz#3a6ed55eb057a9a328d52faf7a4920055cd1333c" + integrity sha512-mjxjfYko3/tamdCcPZTabaYnhiC2HuEXc+AXt+ek/m054ZOEysRhqWgbAOHqh5PPqcaytSIuVvGtJelp7IVwDQ== + dependencies: + "@vue/cli-shared-utils" "^3.7.0" + chromedriver "^2.46.0" + deepmerge "^3.2.0" execa "^1.0.0" nightwatch "^0.9.21" selenium-server "^3.141.59" "@vue/cli-plugin-eslint@^3.2.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-3.4.0.tgz#e9fceff873661848be7e1341b31dad23302cd0ac" - integrity sha512-KbUpN3Zd/V5zCah9nT9cukTHmd9g4IRskyuIeBw5KZqRDoUgCS7I2+OWlcAMneRuqZwgFbTFYmr9N3s6gz4SVg== - dependencies: - "@vue/cli-shared-utils" "^3.4.0" + version "3.7.0" + resolved "https://registry.yarnpkg.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-3.7.0.tgz#6b495fe3c82ec94347c424a9de3cca467a53f90e" + integrity sha512-oFdOLQu6PQKbxinF55XH1lH8hgiDRyb3gIvSKu5YV5r6dnsRdKDxOKLE1PTbaZzQot3Ny/Y7gk025x1qpni3IA== + dependencies: + "@vue/cli-shared-utils" "^3.7.0" babel-eslint "^10.0.1" + eslint-loader "^2.1.2" + globby "^9.2.0" + webpack ">=4 < 4.29" + optionalDependencies: eslint "^4.19.1" - eslint-loader "^2.1.1" eslint-plugin-vue "^4.7.1" - globby "^9.0.0" - webpack ">=4 < 4.29" "@vue/cli-plugin-unit-jest@^3.2.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@vue/cli-plugin-unit-jest/-/cli-plugin-unit-jest-3.4.0.tgz#f501d16344aef5d7c83d9a8821dc372d2a362afe" - integrity sha512-F1FzKG2JQmVPXH5OKFN4htBkGERDj5Kxd47Wmts2H0rhmtHR4a+k0X7+WyCzbb1aSRKNYdG4f2eSwyu6tSq28A== - dependencies: - "@vue/cli-shared-utils" "^3.4.0" + version "3.7.0" + resolved "https://registry.yarnpkg.com/@vue/cli-plugin-unit-jest/-/cli-plugin-unit-jest-3.7.0.tgz#94cd7928f7f9e134ee32e6621742be8cdf898c9f" + integrity sha512-3z8yCAhgwsUc6hpghN8Ej5xBGIaxQTC/g3Ry5QPjjZ4up4G3lKukzvwMk7JFzO+Qj+mt4xAbhR9+stOI4Qyk/Q== + dependencies: + "@vue/cli-shared-utils" "^3.7.0" babel-jest "^23.6.0" babel-plugin-transform-es2015-modules-commonjs "^6.26.2" jest "^23.6.0" jest-serializer-vue "^2.0.2" - jest-transform-stub "^1.0.0" - vue-jest "^3.0.2" + jest-transform-stub "^2.0.0" + jest-watch-typeahead "0.2.1" + vue-jest "^3.0.4" "@vue/cli-service@^3.2.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@vue/cli-service/-/cli-service-3.4.0.tgz#d2160ee4cf9de8dc4e9d780ccd61cc15862f2950" - integrity sha512-AtLiin5Jlw0ULKXJtBhUaykz0VzDgYq2RCf7nxfB7Vsi5fTbJyOVeWYe9KsnsM6VTRBWRUI8NzPPMYxV2uxtQA== + version "3.7.0" + resolved "https://registry.yarnpkg.com/@vue/cli-service/-/cli-service-3.7.0.tgz#af56526cea64042b48c50a15a9d33c84a71abd31" + integrity sha512-RMVwpCE3EB9cL9VAgu1Dy/tGxz5zrVG4UMPk5t4KDu8jJhHxvcAzgIEIfS6KRp0AKfA6iDW4J0NU0fopnpyL+g== dependencies: "@intervolga/optimize-cssnano-plugin" "^1.0.5" "@soda/friendly-errors-webpack-plugin" "^1.7.1" - "@vue/cli-overlay" "^3.4.0" - "@vue/cli-shared-utils" "^3.4.0" - "@vue/component-compiler-utils" "^2.5.2" + "@vue/cli-overlay" "^3.7.0" + "@vue/cli-shared-utils" "^3.7.0" + "@vue/component-compiler-utils" "^2.6.0" "@vue/preload-webpack-plugin" "^1.1.0" "@vue/web-component-wrapper" "^1.2.0" - acorn "^6.0.6" + acorn "^6.1.1" acorn-walk "^6.1.1" address "^1.0.3" - autoprefixer "^9.4.7" + autoprefixer "^9.5.1" + browserslist "^4.5.4" cache-loader "^2.0.1" case-sensitive-paths-webpack-plugin "^2.2.0" chalk "^2.4.2" - clipboardy "^1.2.3" - cliui "^4.1.0" + cli-highlight "^2.1.0" + clipboardy "^2.0.0" + cliui "^5.0.0" copy-webpack-plugin "^4.6.0" css-loader "^1.0.1" - cssnano "^4.1.8" + cssnano "^4.1.10" + current-script-polyfill "^1.0.0" debug "^4.1.1" + dotenv "^7.0.0" + dotenv-expand "^5.1.0" escape-string-regexp "^1.0.5" file-loader "^3.0.1" fs-extra "^7.0.1" - globby "^9.0.0" + globby "^9.2.0" hash-sum "^1.0.2" html-webpack-plugin "^3.2.0" launch-editor-middleware "^2.2.1" lodash.defaultsdeep "^4.6.0" lodash.mapvalues "^4.6.0" lodash.transform "^4.6.0" - mini-css-extract-plugin "^0.5.0" + mini-css-extract-plugin "^0.6.0" minimist "^1.2.0" - ora "^3.0.0" + ora "^3.4.0" portfinder "^1.0.20" postcss-loader "^3.0.0" - read-pkg "^4.0.1" - semver "^5.6.0" + read-pkg "^5.0.0" + semver "^6.0.0" slash "^2.0.0" source-map-url "^0.4.0" ssri "^6.0.1" string.prototype.padend "^3.0.0" - terser-webpack-plugin "^1.2.1" + terser-webpack-plugin "^1.2.3" thread-loader "^2.1.2" url-loader "^1.1.2" - vue-loader "^15.6.2" + vue-loader "^15.7.0" webpack ">=4 < 4.29" - webpack-bundle-analyzer "^3.0.3" + webpack-bundle-analyzer "^3.3.0" webpack-chain "^4.11.0" - webpack-dev-server "^3.1.14" + webpack-dev-server "^3.3.1" webpack-merge "^4.2.1" yorkie "^2.0.0" -"@vue/cli-shared-utils@^3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-3.4.0.tgz#4331cb926351e399bf7305a9306bf09d0f2a1b9d" - integrity sha512-w9j2qIroUUC2ym4Lb0lLMdlGmYThhwV0OizOEVigB5eZOEUEBV2Mv43K+nWJ6OyRBACnvhJTDi1gIwJo8zUvOw== +"@vue/cli-shared-utils@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-3.7.0.tgz#957dd3c31a31208caf9f119cac6008fd4960d46e" + integrity sha512-+LPDAQ1CE3ci1ADOvNqJMPdqyxgJxOq5HUgGDSKCHwviXF6GtynfljZXiSzgWh5ueMFxJphCfeMsTZqFWwsHVg== dependencies: chalk "^2.4.1" execa "^1.0.0" @@ -1212,10 +1236,10 @@ lru-cache "^5.1.1" node-ipc "^9.1.1" opn "^5.3.0" - ora "^3.0.0" + ora "^3.4.0" request "^2.87.0" - request-promise-native "^1.0.5" - semver "^5.5.0" + request-promise-native "^1.0.7" + semver "^6.0.0" string.prototype.padstart "^3.0.0" "@vue/component-compiler-utils@^1.2.1": @@ -1233,10 +1257,10 @@ source-map "^0.5.6" vue-template-es2015-compiler "^1.6.0" -"@vue/component-compiler-utils@^2.5.1", "@vue/component-compiler-utils@^2.5.2": - version "2.5.2" - resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-2.5.2.tgz#a8d57e773354ab10e4742c7d6a8dd86184d4d7be" - integrity sha512-3exq9O89GXo9E+CGKzgURCbasG15FtFMs8QRrCUVWGaKue4Egpw41MHb3Avtikv1VykKfBq3FvAnf9Nx3sdVJg== +"@vue/component-compiler-utils@^2.5.1", "@vue/component-compiler-utils@^2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-2.6.0.tgz#aa46d2a6f7647440b0b8932434d22f12371e543b" + integrity sha512-IHjxt7LsOFYc0DkTncB7OXJL7UzwOLPPQCfEUNyxL2qt+tF12THV+EO33O1G2Uk4feMSWua3iD39Itszx0f0bw== dependencies: consolidate "^0.15.1" hash-sum "^1.0.2" @@ -1246,7 +1270,7 @@ postcss-selector-parser "^5.0.0" prettier "1.16.3" source-map "~0.6.1" - vue-template-es2015-compiler "^1.8.2" + vue-template-es2015-compiler "^1.9.0" "@vue/eslint-config-prettier@^4.0.1": version "4.0.1" @@ -1444,12 +1468,12 @@ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== accepts@~1.3.4, accepts@~1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" - integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= - dependencies: - mime-types "~2.1.18" - negotiator "0.6.1" + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" acorn-bigint@^0.2.0: version "0.2.0" @@ -1487,9 +1511,9 @@ acorn "^4.0.4" acorn-globals@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103" - integrity sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw== + version "4.3.2" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.2.tgz#4e2c2313a597fd589720395f6354b41cd5ec8006" + integrity sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ== dependencies: acorn "^6.0.1" acorn-walk "^6.0.1" @@ -1571,20 +1595,20 @@ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= -acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.4.0, acorn@^5.4.1, acorn@^5.5.0, acorn@^5.5.3, acorn@^5.6.2, acorn@^5.7.3: +acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.4.0, acorn@^5.4.1, acorn@^5.5.0, acorn@^5.5.3, acorn@^5.6.2: version "5.7.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== -acorn@^6.0.1, acorn@^6.0.6: - version "6.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818" - integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw== +acorn@^6.0.1, acorn@^6.0.7, acorn@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" + integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== address@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" - integrity sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/address/-/address-1.1.0.tgz#ef8e047847fcd2c5b6f50c16965f924fd99fe709" + integrity sha512-4diPfzWbLEIElVG4AnqP+00SULlPzNuyJFNnmMrLgyaxG6tZXJ1sn7mjBu4fHrJE+Yp/jgylOweJn2xsLMFggQ== agent-base@2: version "2.1.1" @@ -1620,9 +1644,9 @@ json-schema-traverse "^0.3.0" ajv@^6.1.0, ajv@^6.5.5: - version "6.9.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.1.tgz#a4d3683d74abc5670e75f0b16520f70a20ea8dc1" - integrity sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA== + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" @@ -1654,9 +1678,9 @@ integrity sha512-u3iMXDJr0cxMdQocIciDiou9Au4L5f9uT+/jCtprw3s1j3HcfCuI+khF+90Ps2KdsEhM2soF7SXB4WUvI3HlXg== ansi-colors@^3.0.0: - version "3.2.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" - integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== ansi-escapes@^3.0.0: version "3.2.0" @@ -1678,10 +1702,10 @@ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" - integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w== +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== ansi-styles@^2.2.1: version "2.2.1" @@ -1695,6 +1719,11 @@ dependencies: color-convert "^1.9.0" +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -1715,7 +1744,7 @@ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -arch@^2.1.0: +arch@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== @@ -1758,9 +1787,9 @@ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= array-differ@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-2.0.3.tgz#0195bb00ccccf271106efee4a4786488b7180712" - integrity sha1-AZW7AMzM8nEQbv7kpHhkiLcYBxI= + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-2.1.0.tgz#4b9c1c3f14b906757082925769e8ab904f4801b1" + integrity sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w== array-equal@^1.0.0: version "1.0.0" @@ -1868,9 +1897,9 @@ integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= ast-types@0.x.x: - version "0.12.2" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.12.2.tgz#341656049ee328ac03fc805c156b49ebab1e4462" - integrity sha512-8c83xDLJM/dLDyXNLiR6afRRm4dPKN6KAnKqytRK3DBJul9lA+atxdQkNDkSVPdTqea5HiRq3lnnOIZ0MBpvdg== + version "0.12.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.12.4.tgz#71ce6383800f24efc9a1a3308f3a6e420a0974d1" + integrity sha512-ky/YVYCbtVAS8TdMIaTiPFHwEpRB5z1hctepJplTr3UW5q8TDrpIMCILyk8pmLxGtn2KCtC/lSn7zOsaI7nzDw== astral-regex@^1.0.0: version "1.0.0" @@ -1878,9 +1907,9 @@ integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== async-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" - integrity sha1-GdOGodntxufByF04iu28xW0zYC0= + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== async-foreach@^0.1.3: version "0.1.3" @@ -1897,12 +1926,12 @@ resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@^2.1.4, async@^2.5.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== - dependencies: - lodash "^4.17.10" +async@^2.1.4: + version "2.6.2" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" + integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== + dependencies: + lodash "^4.17.11" asynckit@^0.4.0: version "0.4.0" @@ -1914,13 +1943,13 @@ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@^9.4.7: - version "9.4.7" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.7.tgz#f997994f9a810eae47b38fa6d8a119772051c4ff" - integrity sha512-qS5wW6aXHkm53Y4z73tFGsUhmZu4aMPV9iHXYlF0c/wxjknXNHuj/1cIQb+6YH692DbJGGWcckAXX+VxKvahMA== - dependencies: - browserslist "^4.4.1" - caniuse-lite "^1.0.30000932" +autoprefixer@^9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.5.1.tgz#243b1267b67e7e947f28919d786b50d3bb0fb357" + integrity sha512-KJSzkStUl3wP0D5sdMlP82Q52JLy5+atf2MHAre48+ckWkXgixmfHyWmA77wFDy6jTHU6mIgXv6hAQ2mf1PjJQ== + dependencies: + browserslist "^4.5.4" + caniuse-lite "^1.0.30000957" normalize-range "^0.1.2" num2fraction "^1.2.2" postcss "^7.0.14" @@ -2064,6 +2093,17 @@ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167" integrity sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc= +babel-plugin-module-resolver@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-module-resolver/-/babel-plugin-module-resolver-3.2.0.tgz#ddfa5e301e3b9aa12d852a9979f18b37881ff5a7" + integrity sha512-tjR0GvSndzPew/Iayf4uICWZqjBwnlMWjSx6brryfQ81F9rxBVqwDJtFCV8oOs0+vJeefK9TmdZtkIFdFe1UnA== + dependencies: + find-babel-config "^1.1.0" + glob "^7.1.2" + pkg-up "^2.0.0" + reselect "^3.0.1" + resolve "^1.4.0" + babel-plugin-syntax-object-rest-spread@^6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" @@ -2218,9 +2258,9 @@ integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^1.0.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.0.tgz#9523e001306a32444b907423f1de2164222f6ab1" - integrity sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw== + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== block-stream@*: version "0.0.9" @@ -2230,9 +2270,9 @@ inherits "~2.0.0" bluebird@^3.1.1, bluebird@^3.5.1, bluebird@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" - integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== + version "3.5.4" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.4.tgz#d6cc661595de30d5b3af5fcedd3c0b3ef6ec5714" + integrity sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -2273,9 +2313,9 @@ integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= bootstrap@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.0.tgz#2559ccb8d45426ac6c54db23eb3d1c9f4257fa22" - integrity sha512-M0vqY0Z6UDweV2nLFl5dXcb+GIo53EBCGMMVxCGH5vJxl/jsr+HkULBMd4kn9rdpdBZwd3BduCgMOYOwJybo4Q== + version "4.3.1" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac" + integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag== brace-expansion@^1.0.0, brace-expansion@^1.1.7: version "1.1.11" @@ -2391,14 +2431,14 @@ dependencies: pako "~1.0.5" -browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.1.tgz#42e828954b6b29a7a53e352277be429478a69062" - integrity sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A== - dependencies: - caniuse-lite "^1.0.30000929" - electron-to-chromium "^1.3.103" - node-releases "^1.1.3" +browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.5.4: + version "4.5.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.6.tgz#ea42e8581ca2513fa7f371d4dd66da763938163d" + integrity sha512-o/hPOtbU9oX507lIqon+UvPYqpx3mHc8cV3QemSBTXwkG8gSQSK6UKvXcE/DcleU3+A59XTUHyCvZ5qGy8xVAg== + dependencies: + caniuse-lite "^1.0.30000963" + electron-to-chromium "^1.3.127" + node-releases "^1.1.17" bser@^2.0.0: version "2.0.0" @@ -2441,6 +2481,11 @@ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + cacache@^10.0.4: version "10.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" @@ -2579,9 +2624,9 @@ integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= camelcase@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" - integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== caniuse-api@^3.0.0: version "3.0.0" @@ -2593,10 +2638,10 @@ lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000932: - version "1.0.30000936" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000936.tgz#5d33b118763988bf721b9b8ad436d0400e4a116b" - integrity sha512-orX4IdpbFhdNO7bTBhSbahp1EBpqzBc+qrvTRVUFfZgA4zta7TdM6PN5ZxkEUgDnz36m+PfWGcdX7AVfFWItJw== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000957, caniuse-lite@^1.0.30000963: + version "1.0.30000966" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000966.tgz#f3c6fefacfbfbfb981df6dfa68f2aae7bff41b64" + integrity sha512-qqLQ/uYrpZmFhPY96VuBkMEo8NhVFBZ9y/Bh+KnvGzGJ5I8hvpIaWlF2pw5gqe4PLAL+ZjsPgMOvoXSpX21Keg== canvg@1.5.3: version "1.5.3" @@ -2679,21 +2724,21 @@ integrity sha512-YbulWHdfP99UfZ73NcUDlNJhEIDgm9Doq9GhpyXbF+7Aegi3CVV7qqMCKTTqJxlvEvnQBp9IA+dxsGN6xK/nSg== cheerio@^1.0.0-rc.2: - version "1.0.0-rc.2" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" - integrity sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs= + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" + integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== dependencies: css-select "~1.2.0" - dom-serializer "~0.1.0" + dom-serializer "~0.1.1" entities "~1.1.1" htmlparser2 "^3.9.1" lodash "^4.15.0" parse5 "^3.0.1" -chokidar@^2.0.0, chokidar@^2.0.2: - version "2.1.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.1.tgz#adc39ad55a2adf26548bd2afa048f611091f9184" - integrity sha512-gfw3p2oQV2wEt+8VuMlNsPjCxDxvvgnm/kz+uATu805mWVF8IJN7uz9DN7iBz+RMJISmiVbCOBFs9qBGMjtPfQ== +chokidar@^2.0.2, chokidar@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.5.tgz#0ae8434d962281a5f56c72869e79cb6d9d86ad4d" + integrity sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A== dependencies: anymatch "^2.0.0" async-each "^1.0.1" @@ -2705,7 +2750,7 @@ normalize-path "^3.0.0" path-is-absolute "^1.0.0" readdirp "^2.2.1" - upath "^1.1.0" + upath "^1.1.1" optionalDependencies: fsevents "^1.2.7" @@ -2721,7 +2766,7 @@ dependencies: tslib "^1.9.0" -chromedriver@^2.45.0: +chromedriver@^2.46.0: version "2.46.0" resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-2.46.0.tgz#3d78e7eb9bb65dd804fe327a6bf76fced12be053" integrity sha512-dLtKIJW3y/PuFrPmcw6Mb8Nh+HwSqgVrK1rWgTARXhHfWvV822X2VRkx2meU/tg2+YQL6/nNgT6n5qWwIDHbwg== @@ -2779,20 +2824,21 @@ dependencies: restore-cursor "^2.0.0" -cli-spinners@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a" - integrity sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg== - -cli-table3@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" - integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== - dependencies: - object-assign "^4.1.0" - string-width "^2.1.1" - optionalDependencies: - colors "^1.1.2" +cli-highlight@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.1.tgz#2180223d51618b112f4509cf96e4a6c750b07e97" + integrity sha512-0y0VlNmdD99GXZHYnvrQcmHxP8Bi6T00qucGgBgGv4kJ0RyDthNnnFPupHV7PYv/OXSVk+azFbOeaW6+vGmx9A== + dependencies: + chalk "^2.3.0" + highlight.js "^9.6.0" + mz "^2.4.0" + parse5 "^4.0.0" + yargs "^13.0.0" + +cli-spinners@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.1.0.tgz#22c34b4d51f573240885b201efda4e4ec9fff3c7" + integrity sha512-8B00fJOEh1HPrx4fo5eW16XmE1PcL1tGpGrxy63CXGP9nHdPBN63X75hA1zhvQuhVztJWLqV58Roj2qlNM7cAA== cli-width@^2.0.0: version "2.2.0" @@ -2808,13 +2854,13 @@ select "^1.1.2" tiny-emitter "^2.0.0" -clipboardy@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-1.2.3.tgz#0526361bf78724c1f20be248d428e365433c07ef" - integrity sha512-2WNImOvCRe6r63Gk9pShfkwXsVtKCroMAevIbiae021mS850UkWPbevxsBz3tnvjZIEGvlwaqCPsw+4ulzNgJA== - dependencies: - arch "^2.1.0" - execa "^0.8.0" +clipboardy@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-2.0.0.tgz#3fcee421fdeca4e6a62ce72b66f3eb0c42165acd" + integrity sha512-XbVjHMsss0giNUkp/tV/3eEAZe8i1fZTLzmPKqjE1RGIAWOTiF5D014f6R+g53ZAq0IK3cPrJXFvqE8eQjhFYQ== + dependencies: + arch "^2.1.1" + execa "^1.0.0" cliui@^2.1.0: version "2.1.0" @@ -2834,7 +2880,7 @@ strip-ansi "^3.0.1" wrap-ansi "^2.0.0" -cliui@^4.0.0, cliui@^4.1.0: +cliui@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== @@ -2843,6 +2889,15 @@ strip-ansi "^4.0.0" wrap-ansi "^2.0.0" +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + clone-deep@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" @@ -2873,7 +2928,7 @@ resolved "https://registry.yarnpkg.com/co/-/co-3.0.6.tgz#1445f226c5eb956138e68c9ac30167ea7d2e6bda" integrity sha1-FEXyJsXrlWE45oyawwFn6n0ua9o= -coa@~2.0.1: +coa@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== @@ -2921,23 +2976,13 @@ simple-swizzle "^0.2.2" color@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.0.tgz#d8e9fb096732875774c84bf922815df0308d0ffc" - integrity sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg== + version "3.1.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.1.tgz#7abf5c0d38e89378284e873c207ae2172dcc8a61" + integrity sha512-PvUltIXRjehRKPSy89VnDWFKY58xyhTLyxIg21vwQBI6qLwZNPmC8k3C1uytIgFKEpOIzN4y32iPm8231zFHIg== dependencies: color-convert "^1.9.1" color-string "^1.5.2" -colors@^1.1.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" - integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== - -colors@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" - integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= - combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" @@ -2945,12 +2990,12 @@ dependencies: delayed-stream "~1.0.0" -commander@2, commander@^2.18.0, commander@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== - -commander@2.17.x, commander@~2.17.1: +commander@2, commander@^2.18.0, commander@^2.19.0, commander@~2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + +commander@2.17.x: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== @@ -2962,33 +3007,38 @@ dependencies: graceful-readlink ">= 1.0.0" +commander@~2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= component-emitter@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= - -compressible@~2.0.14: - version "2.0.15" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.15.tgz#857a9ab0a7e5a07d8d837ed43fe2defff64fe212" - integrity sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw== - dependencies: - mime-db ">= 1.36.0 < 2" - -compression@^1.5.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db" - integrity sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg== + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +compressible@~2.0.16: + version "2.0.17" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" + integrity sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw== + dependencies: + mime-db ">= 1.40.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== dependencies: accepts "~1.3.5" bytes "3.0.0" - compressible "~2.0.14" + compressible "~2.0.16" debug "2.6.9" - on-headers "~1.0.1" + on-headers "~1.0.2" safe-buffer "5.1.2" vary "~1.1.2" @@ -3039,7 +3089,7 @@ ini "^1.3.4" proto-list "~1.2.1" -connect-history-api-fallback@^1.3.0: +connect-history-api-fallback@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== @@ -3136,10 +3186,10 @@ p-limit "^1.0.0" serialize-javascript "^1.4.0" -core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.7, core-js@^2.6.3: - version "2.6.4" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.4.tgz#b8897c062c4d769dd30a0ac5c73976c47f92ea0d" - integrity sha512-05qQ5hXShcqGkPZpXEFLIpxayZscVD2kuMBZewxiIPPEagukO4mqgPA9CWhUvFBJfy3ODdK2p9xyHh7FTU9/7A== +core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" + integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -3157,13 +3207,13 @@ require-from-string "^2.0.1" cosmiconfig@^5.0.0: - version "5.0.7" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.7.tgz#39826b292ee0d78eda137dfa3173bd1c21a43b04" - integrity sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA== + version "5.2.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.0.tgz#45038e4d28a7fe787203aede9c25bca4a08b12c8" + integrity sha512-nxt+Nfc3JAqf4WIWd0jXLjTJZmsPLrA9DDc4nRw2KFJQJK7DNooqSXrNI7tzLG50CF8axczly5UV929tBmh/7g== dependencies: import-fresh "^2.0.0" is-directory "^0.3.1" - js-yaml "^3.9.0" + js-yaml "^3.13.0" parse-json "^4.0.0" create-ecdh@^4.0.0: @@ -3280,7 +3330,7 @@ postcss-value-parser "^3.3.0" source-list-map "^2.0.0" -css-select-base-adapter@~0.1.0: +css-select-base-adapter@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== @@ -3341,9 +3391,9 @@ integrity sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w= css-what@2.1, css-what@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.2.tgz#c0876d9d0480927d7d4920dcd72af3595649554d" - integrity sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== css@^2.1.0: version "2.2.4" @@ -3365,40 +3415,45 @@ resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== -cssnano-preset-default@^4.0.0, cssnano-preset-default@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.6.tgz#92379e2a6db4a91c0ea727f5f556eeac693eab6a" - integrity sha512-UPboYbFaJFtDUhJ4fqctThWbbyF4q01/7UhsZbLzp35l+nUxtzh1SifoVlEfyLM3n3Z0htd8B1YlCxy9i+bQvg== +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^4.0.0, cssnano-preset-default@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" + integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA== dependencies: css-declaration-sorter "^4.0.1" cssnano-util-raw-cache "^4.0.1" postcss "^7.0.0" - postcss-calc "^7.0.0" - postcss-colormin "^4.0.2" + postcss-calc "^7.0.1" + postcss-colormin "^4.0.3" postcss-convert-values "^4.0.1" - postcss-discard-comments "^4.0.1" + postcss-discard-comments "^4.0.2" postcss-discard-duplicates "^4.0.2" postcss-discard-empty "^4.0.1" postcss-discard-overridden "^4.0.1" - postcss-merge-longhand "^4.0.10" - postcss-merge-rules "^4.0.2" + postcss-merge-longhand "^4.0.11" + postcss-merge-rules "^4.0.3" postcss-minify-font-values "^4.0.2" - postcss-minify-gradients "^4.0.1" - postcss-minify-params "^4.0.1" - postcss-minify-selectors "^4.0.1" + postcss-minify-gradients "^4.0.2" + postcss-minify-params "^4.0.2" + postcss-minify-selectors "^4.0.2" postcss-normalize-charset "^4.0.1" - postcss-normalize-display-values "^4.0.1" - postcss-normalize-positions "^4.0.1" - postcss-normalize-repeat-style "^4.0.1" - postcss-normalize-string "^4.0.1" - postcss-normalize-timing-functions "^4.0.1" + postcss-normalize-display-values "^4.0.2" + postcss-normalize-positions "^4.0.2" + postcss-normalize-repeat-style "^4.0.2" + postcss-normalize-string "^4.0.2" + postcss-normalize-timing-functions "^4.0.2" postcss-normalize-unicode "^4.0.1" postcss-normalize-url "^4.0.1" - postcss-normalize-whitespace "^4.0.1" - postcss-ordered-values "^4.1.1" - postcss-reduce-initial "^4.0.2" - postcss-reduce-transforms "^4.0.1" - postcss-svgo "^4.0.1" + postcss-normalize-whitespace "^4.0.2" + postcss-ordered-values "^4.1.2" + postcss-reduce-initial "^4.0.3" + postcss-reduce-transforms "^4.0.2" + postcss-svgo "^4.0.2" postcss-unique-selectors "^4.0.1" cssnano-util-get-arguments@^4.0.0: @@ -3423,17 +3478,17 @@ resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== -cssnano@^4.0.0, cssnano@^4.1.8: - version "4.1.8" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.8.tgz#8014989679d5fd42491e4499a521dbfb85c95fd1" - integrity sha512-5GIY0VzAHORpbKiL3rMXp4w4M1Ki+XlXgEXyuWXVd3h6hlASb+9Vo76dNP56/elLMVBBsUfusCo1q56uW0UWig== +cssnano@^4.0.0, cssnano@^4.1.10: + version "4.1.10" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" + integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== dependencies: cosmiconfig "^5.0.0" - cssnano-preset-default "^4.0.6" + cssnano-preset-default "^4.0.7" is-resolvable "^1.0.0" postcss "^7.0.0" -csso@^3.5.0: +csso@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg== @@ -3453,12 +3508,17 @@ cssom "0.3.x" cssstyle@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb" - integrity sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog== + version "1.2.2" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.2.2.tgz#427ea4d585b18624f6fdbf9de7a2a1a3ba713077" + integrity sha512-43wY3kl1CVQSvL7wUY1qXkxVGkStjpkDmVjiIKX8R97uhajy8Bybay78uOtqvh7Q5GK75dNPfW0geWjE6qQQow== dependencies: cssom "0.3.x" +current-script-polyfill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/current-script-polyfill/-/current-script-polyfill-1.0.0.tgz#f31cf7e4f3e218b0726e738ca92a02d3488ef615" + integrity sha1-8xz35PPiGLBybnOMqSoC00iO9hU= + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -3552,9 +3612,9 @@ d3-dsv "1" d3-force@1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.0.tgz#60713f7efe8764f53e685d69433c06914dc4ea4c" - integrity sha512-PFLcDnRVANHMudbQlIB87gcfQorEsDIAvRpZ2bNddfM/WxdsEkyrEaOIPoydhH1I1V4HPjNLGOMLXCA0AuGQ9w== + version "1.2.1" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b" + integrity sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg== dependencies: d3-collection "1" d3-dispatch "1" @@ -3650,9 +3710,9 @@ integrity sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg== d3-shape@1, d3-shape@^1.0.3: - version "1.3.4" - resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.4.tgz#358e76014645321eecc7c364e188f8ae3d2a07d4" - integrity sha512-izaz4fOpOnY3CD17hkZWNxbaN70sIGagLR/5jb6RS96Y+6VqX+q1BQf1av6QSBRdfULi3Gb8Js4CzG4+KAPjMg== + version "1.3.5" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.5.tgz#e81aea5940f59f0a79cfccac012232a8987c6033" + integrity sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg== dependencies: d3-path "1" @@ -3702,9 +3762,9 @@ d3-transition "1" d3@^5.7.0: - version "5.9.1" - resolved "https://registry.yarnpkg.com/d3/-/d3-5.9.1.tgz#fde73fa9af7281d2ff0d2a32aa8f306e93a6d1cd" - integrity sha512-JceuBn5VVWySPQc9EA0gfq0xQVgEQXGokHhe+359bmgGeUITLK2r2b9idMzquQne9DKxb7JDCE1gDRXe9OIF2Q== + version "5.9.2" + resolved "https://registry.yarnpkg.com/d3/-/d3-5.9.2.tgz#64e8a7e9c3d96d9e6e4999d2c8a2c829767e67f5" + integrity sha512-ydrPot6Lm3nTWH+gJ/Cxf3FcwuvesYQ5uk+j/kXEH/xbuYWYWTMAHTJQkyeuG8Y5WM5RSEYB41EctUrXQQytRQ== dependencies: d3-array "1" d3-axis "1" @@ -3746,9 +3806,9 @@ assert-plus "^1.0.0" data-uri-to-buffer@2: - version "2.0.0" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.0.tgz#0ba23671727349828c32cfafddea411908d13d23" - integrity sha512-YbKCNLPPP4inc0E5If4OaalBc7gpaM2MRv77Pv2VThVComLKfbGYtJcdDCViDyp1Wd4SebhHLz94vp91zbK6bw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.1.tgz#ca8f56fe38b1fd329473e9d1b4a9afcd8ce1c045" + integrity sha512-OkVVLrerfAKZlW2ZZ3Ve2y65jgiWqBKsTfUIAFbn8nVbPcCZg6l6gikKlEYv0kXcmzqGm6mFq/Jf2vriuEkv8A== dependencies: "@types/node" "^8.0.7" @@ -3781,7 +3841,7 @@ resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== -debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: +debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -3809,14 +3869,7 @@ dependencies: ms "^2.1.1" -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@^3.1.0, debug@^3.2.5: +debug@^3.1.0, debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -3828,13 +3881,6 @@ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decamelize@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7" - integrity sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg== - dependencies: - xregexp "4.0.0" - decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -3867,17 +3913,17 @@ resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ== -deepmerge@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.1.0.tgz#a612626ce4803da410d77554bfd80361599c034d" - integrity sha512-/TnecbwXEdycfbsM2++O3eGiatEFHjjNciHEwJclM+T5Kd94qD1AP+2elP/Mq0L5b9VZJao5znR01Mz6eX8Seg== - -default-gateway@^2.6.0: - version "2.7.2" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-2.7.2.tgz#b7ef339e5e024b045467af403d50348db4642d0f" - integrity sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ== - dependencies: - execa "^0.10.0" +deepmerge@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.2.0.tgz#58ef463a57c08d376547f8869fdc5bcee957f44e" + integrity sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow== + +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== + dependencies: + execa "^1.0.0" ip-regex "^2.1.0" default-require-extensions@^1.0.0: @@ -3944,6 +3990,19 @@ pify "^3.0.0" rimraf "^2.2.8" +del@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -4023,7 +4082,7 @@ miller-rabin "^4.0.0" randombytes "^2.0.0" -dir-glob@^2.0.0, dir-glob@^2.2.1: +dir-glob@^2.0.0, dir-glob@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== @@ -4062,7 +4121,7 @@ resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk= -dom-converter@~0.2: +dom-converter@^0.2: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== @@ -4074,29 +4133,24 @@ resolved "https://registry.yarnpkg.com/dom-event-types/-/dom-event-types-1.0.0.tgz#5830a0a29e1bf837fe50a70cd80a597232813cae" integrity sha512-2G2Vwi2zXTHBGqXHsJ4+ak/iP0N8Ar+G8a7LiD2oup5o4sQWytwqqrZu/O6hIMV0KMID2PL69OhpshLO0n7UJQ== -dom-serializer@0, dom-serializer@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" - integrity sha1-BzxpdUbOB4DOI75KKOKT5AvDDII= - dependencies: - domelementtype "~1.1.1" - entities "~1.1.1" +dom-serializer@0, dom-serializer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@1, domelementtype@^1.3.0: +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@~1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" - integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs= - domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -4104,13 +4158,6 @@ dependencies: webidl-conversions "^4.0.2" -domhandler@2.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594" - integrity sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ= - dependencies: - domelementtype "1" - domhandler@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" @@ -4118,13 +4165,6 @@ dependencies: domelementtype "1" -domutils@1.1: - version "1.1.6" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485" - integrity sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU= - dependencies: - domelementtype "1" - domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" @@ -4148,6 +4188,16 @@ dependencies: is-obj "^1.0.0" +dotenv-expand@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== + +dotenv@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c" + integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g== + duplexer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" @@ -4190,15 +4240,13 @@ jsbn "~0.1.0" safer-buffer "^2.1.0" -editorconfig@^0.15.2: - version "0.15.2" - resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.2.tgz#047be983abb9ab3c2eefe5199cb2b7c5689f0702" - integrity sha512-GWjSI19PVJAM9IZRGOS+YKI8LN+/sjkSjNyvxL5ucqP9/IqtYNXBaQ/6c/hkPNYQHyOHra2KoXZI/JVpuqwmcQ== - dependencies: - "@types/node" "^10.11.7" - "@types/semver" "^5.5.0" +editorconfig@^0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" + integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g== + dependencies: commander "^2.19.0" - lru-cache "^4.1.3" + lru-cache "^4.1.5" semver "^5.6.0" sigmund "^1.0.1" @@ -4217,10 +4265,10 @@ resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== -electron-to-chromium@^1.3.103: - version "1.3.113" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9" - integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g== +electron-to-chromium@^1.3.127: + version "1.3.131" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.131.tgz#205a0b7a276b3f56bc056f19178909243054252a" + integrity sha512-NSO4jLeyGLWrT4mzzfYX8vt1MYCoMI5LxSYAjt0H9+LF/14JyiKJSyyjA6AJTxflZlEM5v3QU33F0ohbPMCAPg== elliptic@^6.0.0: version "6.4.1" @@ -4235,6 +4283,11 @@ minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -4319,9 +4372,9 @@ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= escodegen@1.x.x, escodegen@^1.6.1, escodegen@^1.9.1: - version "1.11.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" - integrity sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw== + version "1.11.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" + integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== dependencies: esprima "^3.1.3" estraverse "^4.2.0" @@ -4337,7 +4390,7 @@ dependencies: get-stdin "^6.0.0" -eslint-loader@^2.1.1: +eslint-loader@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.1.2.tgz#453542a1230d6ffac90e4e7cb9cadba9d851be68" integrity sha512-rA9XiXEOilLYPOIInvVH5S/hYfyTPyxag6DZhoQOduM+3TkghAEQ3VcFO8VnX4J4qg/UIBzp72aOf/xvYmpmsg== @@ -4379,9 +4432,9 @@ estraverse "^4.1.1" eslint-scope@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" - integrity sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA== + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" @@ -4488,9 +4541,9 @@ integrity sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ== eventemitter3@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" - integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== events@^3.0.0: version "3.0.0" @@ -4519,19 +4572,6 @@ dependencies: merge "^1.2.0" -execa@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" - integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== - dependencies: - cross-spawn "^6.0.0" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -4622,7 +4662,7 @@ jest-message-util "^23.4.0" jest-regex-util "^23.3.0" -express@^4.16.2, express@^4.16.3: +express@^4.16.3, express@^4.16.4: version "4.16.4" resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== @@ -4833,6 +4873,11 @@ loader-utils "^1.0.2" schema-utils "^1.0.0" +file-saver@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.2.tgz#06d6e728a9ea2df2cce2f8d9e84dfcdc338ec17a" + integrity sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw== + file-saver@eligrey/FileSaver.js#1.3.8: version "1.3.8" resolved "https://codeload.github.com/eligrey/FileSaver.js/tar.gz/e865e37af9f9947ddcced76b549e27dc45c1cb2e" @@ -4895,9 +4940,9 @@ unpipe "~1.0.0" find-babel-config@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.1.0.tgz#acc01043a6749fec34429be6b64f542ebb5d6355" - integrity sha1-rMAQQ6Z0n+w0Qpvmtk9ULrtdY1U= + version "1.2.0" + resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.2.0.tgz#a9b7b317eb5b9860cda9d54740a8c8337a2283a2" + integrity sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA== dependencies: json5 "^0.5.1" path-exists "^3.0.0" @@ -4921,12 +4966,12 @@ pkg-dir "^2.0.0" find-cache-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d" - integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA== + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== dependencies: commondir "^1.0.1" - make-dir "^1.0.0" + make-dir "^2.0.0" pkg-dir "^3.0.0" find-up@^1.0.0: @@ -4980,11 +5025,11 @@ readable-stream "^2.3.6" follow-redirects@^1.0.0, follow-redirects@^1.3.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.6.1.tgz#514973c44b5757368bad8bddfe52f81f015c94cb" - integrity sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ== - dependencies: - debug "=3.1.0" + version "1.7.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" + integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== + dependencies: + debug "^3.2.6" for-in@^0.1.3: version "0.1.8" @@ -5081,12 +5126,12 @@ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^1.2.3, fsevents@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4" - integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw== - dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== + dependencies: + nan "^2.12.1" + node-pre-gyp "^0.12.0" fstream@^1.0.0, fstream@^1.0.2: version "1.0.11" @@ -5161,6 +5206,11 @@ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -5288,9 +5338,9 @@ which "^1.2.14" globals@^11.0.1, globals@^11.1.0: - version "11.11.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e" - integrity sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw== + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^9.18.0: version "9.18.0" @@ -5320,13 +5370,14 @@ pify "^3.0.0" slash "^1.0.0" -globby@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-9.0.0.tgz#3800df736dc711266df39b4ce33fe0d481f94c23" - integrity sha512-q0qiO/p1w/yJ0hk8V9x1UXlgsXUxlGd0AHUOXZVXBO6aznDtpx7M8D1kBrCAItoPm+4l8r6ATXV1JpjY2SBQOw== - dependencies: +globby@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" + integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg== + dependencies: + "@types/glob" "^7.1.1" array-union "^1.0.2" - dir-glob "^2.2.1" + dir-glob "^2.2.2" fast-glob "^2.2.6" glob "^7.1.3" ignore "^4.0.3" @@ -5370,12 +5421,12 @@ integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= gzip-size@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.0.0.tgz#a55ecd99222f4c48fd8c01c625ce3b349d0a0e80" - integrity sha512-5iI7omclyqrnWw4XbXAmGhPsABkSIDQonv2K0h61lybgofWa6iZyvrI3r2zsJH4P8Nb64fFVzlvfhs0g7BBxAA== + version "5.1.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.0.tgz#2db0396c71f5c902d5cf6b52add5030b93c99bd2" + integrity sha512-wfSnvypBDRW94v5W3ckvvz/zFUNdJ81VgOP6tE4bPpRUcc0wGqU+y0eZjJEvKxwubJFix6P84sE8M51YWLT7rQ== dependencies: duplexer "^0.1.1" - pify "^3.0.0" + pify "^4.0.1" handle-thing@^2.0.0: version "2.0.0" @@ -5383,11 +5434,11 @@ integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== handlebars@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.0.tgz#0d6a6f34ff1f63cecec8423aa4169827bf787c3a" - integrity sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w== - dependencies: - async "^2.5.0" + version "4.1.2" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" + integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== + dependencies: + neo-async "^2.6.0" optimist "^0.6.1" source-map "^0.6.1" optionalDependencies: @@ -5507,10 +5558,10 @@ resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -highlight.js@*: - version "9.14.2" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.14.2.tgz#efbfb22dc701406e4da406056ef8c2b70ebe5b26" - integrity sha512-Nc6YNECYpxyJABGYJAyw7dBAYbXEuIzwzkqoJnwbc1nIpCiN+3ioYf0XrBnLiyyG0JLuJhpPtt2iTSbXiKLoyA== +highlight.js@*, highlight.js@^9.6.0: + version "9.15.6" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.6.tgz#72d4d8d779ec066af9a17cb14360c3def0aa57c4" + integrity sha512-zozTAWM1D6sozHo8kqhfYgsac+B+q0PmsjXeyDrYIHHcBN0zTVT66+s2GW1GZv7DbyaROdLXKdabwS/WqPyIdQ== hmac-drbg@^1.0.0: version "1.0.1" @@ -5522,9 +5573,9 @@ minimalistic-crypto-utils "^1.0.1" hoek@6.x.x: - version "6.1.2" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.2.tgz#99e6d070561839de74ee427b61aa476bd6bddfd6" - integrity sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q== + version "6.1.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" + integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== home-or-tmp@^2.0.0: version "2.0.0" @@ -5535,9 +5586,9 @@ os-tmpdir "^1.0.1" homedir-polyfill@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" - integrity sha1-TCu8inWJmP7r9e1oWA921GdotLw= + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== dependencies: parse-passwd "^1.0.0" @@ -5583,7 +5634,7 @@ dependencies: whatwg-encoding "^1.0.1" -html-entities@^1.2.0: +html-entities@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= @@ -5626,27 +5677,17 @@ dependencies: css-line-break "1.0.1" -htmlparser2@^3.9.1: - version "3.10.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464" - integrity sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ== - dependencies: - domelementtype "^1.3.0" +htmlparser2@^3.3.0, htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" domhandler "^2.3.0" domutils "^1.5.1" entities "^1.1.1" inherits "^2.0.1" - readable-stream "^3.0.6" - -htmlparser2@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe" - integrity sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4= - dependencies: - domelementtype "1" - domhandler "2.1" - domutils "1.1" - readable-stream "1.0" + readable-stream "^3.1.1" http-deceiver@^1.2.7: version "1.2.7" @@ -5663,6 +5704,17 @@ setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-parser-js@>=0.4.0: version "0.5.0" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.0.tgz#d65edbede84349d0dc30320815a15d39cc3cbbd8" @@ -5677,17 +5729,17 @@ debug "2" extend "3" -http-proxy-middleware@~0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz#0987e6bb5a5606e5a69168d8f967a87f15dd8aab" - integrity sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q== - dependencies: - http-proxy "^1.16.2" +http-proxy-middleware@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== + dependencies: + http-proxy "^1.17.0" is-glob "^4.0.0" - lodash "^4.17.5" - micromatch "^3.1.9" - -http-proxy@^1.16.2: + lodash "^4.17.11" + micromatch "^3.1.10" + +http-proxy@^1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== @@ -5746,9 +5798,9 @@ postcss "^6.0.1" ieee754@^1.1.4, ieee754@^1.1.6: - version "1.1.12" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" - integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA== + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== iferr@^0.1.5: version "0.1.5" @@ -5880,13 +5932,13 @@ strip-ansi "^4.0.0" through "^2.3.6" -internal-ip@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27" - integrity sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q== - dependencies: - default-gateway "^2.6.0" - ipaddr.js "^1.5.2" +internal-ip@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== + dependencies: + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" interpret@^1.1.0: version "1.2.0" @@ -5925,12 +5977,7 @@ resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= -ipaddr.js@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" - integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4= - -ipaddr.js@^1.5.2: +ipaddr.js@1.9.0, ipaddr.js@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== @@ -6123,9 +6170,9 @@ is-extglob "^2.1.0" is-glob@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" - integrity sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A= + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== dependencies: is-extglob "^2.1.1" @@ -6158,6 +6205,11 @@ resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= +is-path-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.1.0.tgz#2e0c7e463ff5b7a0eb60852d851a6809347a124c" + integrity sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw== + is-path-in-cwd@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" @@ -6165,6 +6217,13 @@ dependencies: is-path-inside "^1.0.0" +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + is-path-inside@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" @@ -6172,6 +6231,18 @@ dependencies: path-is-inside "^1.0.1" +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -6654,10 +6725,10 @@ pretty-format "^23.6.0" semver "^5.5.0" -jest-transform-stub@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/jest-transform-stub/-/jest-transform-stub-1.0.0.tgz#e4e941454f31a8bbc4db96b31f46a08b294372b1" - integrity sha512-7eilMk4sxi2Fiy223I+BYTS5wJQEGEBqR3D8dy5A6RWmMTnmjipw2ImGDfXzEUBieebyrnitzkJfpNOJSFklLQ== +jest-transform-stub@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz#19018b0851f7568972147a5d60074b55f0225a7d" + integrity sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg== jest-util@^23.4.0: version "23.4.0" @@ -6683,7 +6754,19 @@ leven "^2.1.0" pretty-format "^23.6.0" -jest-watcher@^23.4.0: +jest-watch-typeahead@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-0.2.1.tgz#6c40f232996ca6c39977e929e9f79b189e7d87e4" + integrity sha512-xdhEtKSj0gmnkDQbPTIHvcMmXNUDzYpHLEJ5TFqlaI+schi2NI96xhWiZk9QoesAS7oBmKwWWsHazTrYl2ORgg== + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.4.1" + jest-watcher "^23.1.0" + slash "^2.0.0" + string-length "^2.0.0" + strip-ansi "^5.0.0" + +jest-watcher@^23.1.0, jest-watcher@^23.4.0: version "23.4.0" resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-23.4.0.tgz#d2e28ce74f8dad6c6afc922b92cabef6ed05c91c" integrity sha1-0uKM50+NrWxq/JIrksq+9u0FyRw= @@ -6722,14 +6805,14 @@ integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw== js-beautify@^1.6.12, js-beautify@^1.6.14: - version "1.8.9" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.8.9.tgz#08e3c05ead3ecfbd4f512c3895b1cda76c87d523" - integrity sha512-MwPmLywK9RSX0SPsUJjN7i+RQY9w/yC17Lbrq9ViEefpLRgqAR2BgrMN2AbifkUuhDV8tRauLhLda/9+bE0YQA== + version "1.10.0" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.10.0.tgz#9753a13c858d96828658cd18ae3ca0e5783ea672" + integrity sha512-OMwf/tPDpE/BLlYKqZOhqWsd3/z2N3KOlyn1wsCRGFwViE8LOQTcDtathQvHvZc+q+zWmcNAbwKSC+iJoMaH2Q== dependencies: config-chain "^1.1.12" - editorconfig "^0.15.2" + editorconfig "^0.15.3" glob "^7.1.3" - mkdirp "~0.5.0" + mkdirp "~0.5.1" nopt "~4.0.1" js-levenshtein@^1.1.3: @@ -6764,10 +6847,10 @@ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0, js-yaml@^3.9.1: - version "3.12.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" - integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA== +js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.7.0, js-yaml@^3.9.0, js-yaml@^3.9.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -6943,7 +7026,7 @@ is-promise "^2.0.0" promise "^7.0.1" -killable@^1.0.0: +killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== @@ -7041,9 +7124,9 @@ strip-bom "^2.0.0" loader-fs-cache@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz#56e0bf08bd9708b26a765b68509840c8dec9fdbc" - integrity sha1-VuC/CL2XCLJqdltoUJhAyN7J/bw= + version "1.0.2" + resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.2.tgz#54cedf6b727e1779fd8f01205f05f6e88706f086" + integrity sha512-70IzT/0/L+M20jUlEqZhZyArTU6VKLRTYRDAYN26g4jfzpJqjipLL3/hgYpySqI9PwsVRHHFja0LfEmsx9X2Cw== dependencies: find-cache-dir "^0.1.1" mkdirp "0.5.1" @@ -7163,11 +7246,6 @@ resolved "https://registry.yarnpkg.com/lodash._stack/-/lodash._stack-4.1.3.tgz#751aa76c1b964b047e76d14fc72a093fcb5e2dd0" integrity sha1-dRqnbBuWSwR+dtFPxyoJP8teLdA= -lodash.assign@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= - lodash.clone@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-3.0.3.tgz#84688c73d32b5a90ca25616963f189252a997043" @@ -7177,11 +7255,6 @@ lodash._bindcallback "^3.0.0" lodash._isiterateecall "^3.0.0" -lodash.clonedeep@^4.3.2: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - lodash.create@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" @@ -7257,12 +7330,7 @@ resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.merge@^4.6.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" - integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== - -lodash.mergewith@^4.0.0, lodash.mergewith@^4.6.0: +lodash.mergewith@^4.0.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ== @@ -7297,7 +7365,7 @@ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.x, lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10: +lodash@4.x, lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -7309,7 +7377,7 @@ dependencies: chalk "^2.0.1" -loglevel@^1.4.1: +loglevel@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= @@ -7339,7 +7407,7 @@ resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= -lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3: +lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -7366,6 +7434,14 @@ dependencies: pify "^3.0.0" +make-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -7434,15 +7510,15 @@ mimic-fn "^1.0.0" mem@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a" - integrity sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg== + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== dependencies: map-age-cleaner "^0.1.1" - mimic-fn "^1.0.0" + mimic-fn "^2.0.0" p-is-promise "^2.0.0" -memory-fs@^0.4.0, memory-fs@~0.4.1: +memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -7519,7 +7595,7 @@ parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9: +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -7546,22 +7622,17 @@ bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.36.0 < 2": - version "1.38.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" - integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== - -mime-db@~1.37.0: - version "1.37.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" - integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== - -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.19: - version "2.1.21" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" - integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== - dependencies: - mime-db "~1.37.0" +mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" mime@1.4.1: version "1.4.1" @@ -7569,21 +7640,27 @@ integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== mime@^2.0.3, mime@^2.3.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6" - integrity sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w== + version "2.4.2" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.2.tgz#ce5229a5e99ffc313abac806b482c10e7ba6ac78" + integrity sha512-zJBfZDkwRu+j3Pdd2aHsR5GfH2jIWhmL1ZzBoc+X+3JEti2hbArWcyJ+1laC1D2/U/W1a/+Cegj0/OnEU2ybjg== mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mini-css-extract-plugin@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0" - integrity sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw== +mimic-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mini-css-extract-plugin@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.6.0.tgz#a3f13372d6fcde912f3ee4cd039665704801e3b9" + integrity sha512-79q5P7YGI6rdnVyIAV4NXpBQJFWdkzJxCim3Kog4078fM0piAaFlwocqbejdWtLW1cEzCexPrh6EdyFsPgVdAw== dependencies: loader-utils "^1.1.0" + normalize-url "^2.0.1" schema-utils "^1.0.0" webpack-sources "^1.1.0" @@ -7783,10 +7860,19 @@ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@^2.10.0, nan@^2.9.2: - version "2.12.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" - integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== +mz@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nan@^2.12.1, nan@^2.13.2: + version "2.13.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7" + integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw== nanomatch@^1.2.9: version "1.2.13" @@ -7811,18 +7897,18 @@ integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= needle@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" - integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA== - dependencies: - debug "^2.1.2" + version "2.3.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.3.1.tgz#d272f2f4034afb9c4c9ab1379aabc17fc85c9388" + integrity sha512-CaLXV3W8Vnbps8ZANqDGz7j4x7Yj1LW4TWF/TQuDfj7Cfx4nAPTvw98qgTevtto1oHDrh3pQkaODbqupXlsWTg== + dependencies: + debug "^4.1.0" iconv-lite "^0.4.4" sax "^1.2.4" -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" - integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== neo-async@^2.5.0, neo-async@^2.6.0: version "2.6.0" @@ -7947,10 +8033,10 @@ shellwords "^0.1.1" which "^1.3.0" -node-pre-gyp@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" - integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" @@ -7963,17 +8049,17 @@ semver "^5.3.0" tar "^4" -node-releases@^1.1.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.7.tgz#b09a10394d0ed8f7778f72bb861dde68b146303b" - integrity sha512-bKdrwaqJUPHqlCzDD7so/R+Nk0jGv9a11ZhLrD9f6i947qGLrGAhU3OxRENa19QQmwzGy/g6zCDEuLGDO8HPvA== +node-releases@^1.1.17: + version "1.1.17" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.17.tgz#71ea4631f0a97d5cd4f65f7d04ecf9072eac711a" + integrity sha512-/SCjetyta1m7YXLgtACZGDYJdCSIBAWorDWkGCGZlydP2Ll7J48l7j/JxNYZ+xsgSPbWfdulVS/aY+GdjUsQ7Q== dependencies: semver "^5.3.0" node-sass@^4.10.0: - version "4.11.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a" - integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA== + version "4.12.0" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017" + integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -7982,12 +8068,10 @@ get-stdin "^4.0.1" glob "^7.0.3" in-publish "^2.0.0" - lodash.assign "^4.2.0" - lodash.clonedeep "^4.3.2" - lodash.mergewith "^4.6.0" + lodash "^4.17.11" meow "^3.7.0" mkdirp "^0.5.1" - nan "^2.10.0" + nan "^2.13.2" node-gyp "^3.8.0" npmlog "^4.0.0" request "^2.88.0" @@ -8010,7 +8094,7 @@ abbrev "1" osenv "^0.1.4" -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -8042,6 +8126,15 @@ resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= +normalize-url@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" + integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== + dependencies: + prepend-http "^2.0.0" + query-string "^5.0.1" + sort-keys "^2.0.0" + normalize-url@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" @@ -8053,9 +8146,9 @@ integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== npm-packlist@^1.1.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.3.0.tgz#7f01e8e44408341379ca98cfd756e7b29bd2626c" - integrity sha512-qPBc6CnxEzpOcc4bjoIBJbYdy0D/LFFPUdxvfwor4/w3vxeE0h6TiOVurCEPpQ6trjN77u/ShyfeJGsbAfB3dA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc" + integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw== dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" @@ -8100,9 +8193,9 @@ integrity sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ== nwsapi@^2.0.7: - version "2.1.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.0.tgz#781065940aed90d9bb01ca5d0ce0fcf81c32712f" - integrity sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg== + version "2.1.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f" + integrity sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw== oauth-sign@~0.9.0: version "0.9.0" @@ -8129,9 +8222,9 @@ integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== object-keys@^1.0.11, object-keys@^1.0.12: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032" - integrity sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg== + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object-visit@^1.0.0: version "1.0.1" @@ -8173,7 +8266,7 @@ dependencies: isobject "^3.0.1" -object.values@^1.0.4: +object.values@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== @@ -8189,9 +8282,9 @@ integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== ol@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ol/-/ol-5.3.0.tgz#106b870561fabb9b790b5869b3d93025cb03d389" - integrity sha512-UrOJGNI5XdYfE9n43RJdsMq25SjI4nIi5Kf0kxi+q6vEknzeRxM/wgYf8FMs7Ss3URuIbsKmetW9dVMOYB/DkQ== + version "5.3.2" + resolved "https://registry.yarnpkg.com/ol/-/ol-5.3.2.tgz#dfc70b315b2dcce3cb4b9b79a2e9eb4ef856dd72" + integrity sha512-PfS8Fe1iy4YNJ7P+TvebKME+8gp5NBfQuIldAHfBCkc7agmTezscQrsJWggz5B6Sprm/M/4YBtbyQtw4pIC65w== dependencies: pbf "3.1.0" pixelworks "1.1.0" @@ -8209,10 +8302,10 @@ dependencies: ee-first "1.1.1" -on-headers@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" - integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -8233,10 +8326,10 @@ resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed" integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA== -opn@^5.1.0, opn@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" - integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw== +opn@^5.3.0, opn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== dependencies: is-wsl "^1.1.0" @@ -8260,16 +8353,16 @@ type-check "~0.3.2" wordwrap "~1.0.0" -ora@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-3.1.0.tgz#dbedd8c03b5d017fb67083e87ee52f5ec89823ed" - integrity sha512-vRBPaNCclUi8pUxRF/G8+5qEQkc6EgzKK1G2ZNJUIGu088Un5qIxFXeDgymvPRM9nmrcUOGzQgS1Vmtz+NtlMw== +ora@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" + integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg== dependencies: chalk "^2.4.2" cli-cursor "^2.1.0" - cli-spinners "^1.3.1" + cli-spinners "^2.0.0" log-symbols "^2.2.0" - strip-ansi "^5.0.0" + strip-ansi "^5.2.0" wcwidth "^1.0.1" original@^1.0.0: @@ -8305,7 +8398,7 @@ lcid "^1.0.0" mem "^1.1.0" -os-locale@^3.0.0: +os-locale@^3.0.0, os-locale@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== @@ -8338,9 +8431,9 @@ integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= p-is-promise@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.0.0.tgz#7554e3d572109a87e1f3f53f6a7d85d1b194f4c5" - integrity sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg== + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== p-limit@^1.0.0, p-limit@^1.1.0: version "1.3.0" @@ -8350,9 +8443,9 @@ p-try "^1.0.0" p-limit@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.1.0.tgz#1d5a0d20fb12707c758a655f6bbc4386b5930d68" - integrity sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g== + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== dependencies: p-try "^2.0.0" @@ -8375,15 +8468,20 @@ resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= p-try@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" - integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== pac-proxy-agent@1: version "1.1.0" @@ -8412,9 +8510,9 @@ thunkify "~2.1.1" pako@~1.0.5: - version "1.0.8" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.8.tgz#6844890aab9c635af868ad5fecc62e8acbba3ea4" - integrity sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA== + version "1.0.10" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" + integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== parallel-transform@^1.1.0: version "1.1.0" @@ -8433,9 +8531,9 @@ no-case "^2.2.0" parse-asn1@^5.0.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.3.tgz#1600c6cc0727365d68b97f3aa78939e735a75204" - integrity sha512-VrPoetlz7B/FqjBLD2f5wBVZvsZVLnRUrxVLfRYhGXCODa/NWE4p3Wp+6+aV3ZPL3KM7/OZmxDIwwijD7yuucg== + version "5.1.4" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" + integrity sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw== dependencies: asn1.js "^4.0.0" browserify-aes "^1.0.0" @@ -8474,7 +8572,7 @@ resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= -parse5@4.0.0: +parse5@4.0.0, parse5@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== @@ -8492,9 +8590,9 @@ "@types/node" "*" parseurl@~1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" - integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== pascalcase@^0.1.1: version "0.1.1" @@ -8654,6 +8752,13 @@ dependencies: find-up "^3.0.0" +pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" + integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= + dependencies: + find-up "^2.1.0" + pluralize@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" @@ -8669,12 +8774,12 @@ resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954" integrity sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg== -popper.js@^1.12.9: - version "1.14.7" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e" - integrity sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ== - -portfinder@^1.0.20, portfinder@^1.0.9: +popper.js@^1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" + integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== + +portfinder@^1.0.20: version "1.0.20" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.20.tgz#bea68632e54b2e13ab7b0c4775e9b41bf270e44a" integrity sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw== @@ -8688,7 +8793,7 @@ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -postcss-calc@^7.0.0: +postcss-calc@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.1.tgz#36d77bab023b0ecbb9789d84dcb23c4941145436" integrity sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ== @@ -8698,10 +8803,10 @@ postcss-selector-parser "^5.0.0-rc.4" postcss-value-parser "^3.3.1" -postcss-colormin@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.2.tgz#93cd1fa11280008696887db1a528048b18e7ed99" - integrity sha512-1QJc2coIehnVFsz0otges8kQLsryi4lo19WD+U5xCWvXd0uw/Z+KKYnbiNDCnO9GP+PvErPHCG0jNvWTngk9Rw== +postcss-colormin@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" + integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== dependencies: browserslist "^4.0.0" color "^3.0.0" @@ -8717,10 +8822,10 @@ postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-discard-comments@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.1.tgz#30697735b0c476852a7a11050eb84387a67ef55d" - integrity sha512-Ay+rZu1Sz6g8IdzRjUgG2NafSNpp2MSMOQUb+9kkzzzP+kh07fP0yNbhtFejURnyVXSX3FYy2nVNW1QTnNjgBQ== +postcss-discard-comments@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" + integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== dependencies: postcss "^7.0.0" @@ -8763,20 +8868,20 @@ postcss-load-config "^2.0.0" schema-utils "^1.0.0" -postcss-merge-longhand@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.10.tgz#c4d63ab57bdc054ab4067ab075d488c8c2978380" - integrity sha512-hME10s6CSjm9nlVIcO1ukR7Jr5RisTaaC1y83jWCivpuBtPohA3pZE7cGTIVSYjXvLnXozHTiVOkG4dnnl756g== +postcss-merge-longhand@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" + integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== dependencies: css-color-names "0.0.4" postcss "^7.0.0" postcss-value-parser "^3.0.0" stylehacks "^4.0.0" -postcss-merge-rules@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.2.tgz#2be44401bf19856f27f32b8b12c0df5af1b88e74" - integrity sha512-UiuXwCCJtQy9tAIxsnurfF0mrNHKc4NnNx6NxqmzNNjXpQwLSukUxELHTRF0Rg1pAmcoKLih8PwvZbiordchag== +postcss-merge-rules@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" + integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== dependencies: browserslist "^4.0.0" caniuse-api "^3.0.0" @@ -8793,20 +8898,20 @@ postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-minify-gradients@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.1.tgz#6da95c6e92a809f956bb76bf0c04494953e1a7dd" - integrity sha512-pySEW3E6Ly5mHm18rekbWiAjVi/Wj8KKt2vwSfVFAWdW6wOIekgqxKxLU7vJfb107o3FDNPkaYFCxGAJBFyogA== +postcss-minify-gradients@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" + integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== dependencies: cssnano-util-get-arguments "^4.0.0" is-color-stop "^1.0.0" postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-minify-params@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.1.tgz#5b2e2d0264dd645ef5d68f8fec0d4c38c1cf93d2" - integrity sha512-h4W0FEMEzBLxpxIVelRtMheskOKKp52ND6rJv+nBS33G1twu2tCyurYj/YtgU76+UDCvWeNs0hs8HFAWE2OUFg== +postcss-minify-params@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" + integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== dependencies: alphanum-sort "^1.0.0" browserslist "^4.0.0" @@ -8815,10 +8920,10 @@ postcss-value-parser "^3.0.0" uniqs "^2.0.0" -postcss-minify-selectors@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.1.tgz#a891c197977cc37abf60b3ea06b84248b1c1e9cd" - integrity sha512-8+plQkomve3G+CodLCgbhAKrb5lekAnLYuL1d7Nz+/7RANpBEVdgBkPNwljfSKvZ9xkkZTZITd04KP+zeJTJqg== +postcss-minify-selectors@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" + integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== dependencies: alphanum-sort "^1.0.0" has "^1.0.0" @@ -8863,48 +8968,48 @@ dependencies: postcss "^7.0.0" -postcss-normalize-display-values@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.1.tgz#d9a83d47c716e8a980f22f632c8b0458cfb48a4c" - integrity sha512-R5mC4vaDdvsrku96yXP7zak+O3Mm9Y8IslUobk7IMP+u/g+lXvcN4jngmHY5zeJnrQvE13dfAg5ViU05ZFDwdg== +postcss-normalize-display-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" + integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== dependencies: cssnano-util-get-match "^4.0.0" postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-positions@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.1.tgz#ee2d4b67818c961964c6be09d179894b94fd6ba1" - integrity sha512-GNoOaLRBM0gvH+ZRb2vKCIujzz4aclli64MBwDuYGU2EY53LwiP7MxOZGE46UGtotrSnmarPPZ69l2S/uxdaWA== +postcss-normalize-positions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" + integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== dependencies: cssnano-util-get-arguments "^4.0.0" has "^1.0.0" postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-repeat-style@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.1.tgz#5293f234b94d7669a9f805495d35b82a581c50e5" - integrity sha512-fFHPGIjBUyUiswY2rd9rsFcC0t3oRta4wxE1h3lpwfQZwFeFjXFSiDtdJ7APCmHQOnUZnqYBADNRPKPwFAONgA== +postcss-normalize-repeat-style@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" + integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== dependencies: cssnano-util-get-arguments "^4.0.0" cssnano-util-get-match "^4.0.0" postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-string@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.1.tgz#23c5030c2cc24175f66c914fa5199e2e3c10fef3" - integrity sha512-IJoexFTkAvAq5UZVxWXAGE0yLoNN/012v7TQh5nDo6imZJl2Fwgbhy3J2qnIoaDBrtUP0H7JrXlX1jjn2YcvCQ== +postcss-normalize-string@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" + integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== dependencies: has "^1.0.0" postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-timing-functions@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.1.tgz#8be83e0b9cb3ff2d1abddee032a49108f05f95d7" - integrity sha512-1nOtk7ze36+63ONWD8RCaRDYsnzorrj+Q6fxkQV+mlY5+471Qx9kspqv0O/qQNMeApg8KNrRf496zHwJ3tBZ7w== +postcss-normalize-timing-functions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" + integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== dependencies: cssnano-util-get-match "^4.0.0" postcss "^7.0.0" @@ -8929,37 +9034,37 @@ postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-normalize-whitespace@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.1.tgz#d14cb639b61238418ac8bc8d3b7bdd65fc86575e" - integrity sha512-U8MBODMB2L+nStzOk6VvWWjZgi5kQNShCyjRhMT3s+W9Jw93yIjOnrEkKYD3Ul7ChWbEcjDWmXq0qOL9MIAnAw== +postcss-normalize-whitespace@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" + integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== dependencies: postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-ordered-values@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.1.tgz#2e3b432ef3e489b18333aeca1f1295eb89be9fc2" - integrity sha512-PeJiLgJWPzkVF8JuKSBcylaU+hDJ/TX3zqAMIjlghgn1JBi6QwQaDZoDIlqWRcCAI8SxKrt3FCPSRmOgKRB97Q== +postcss-ordered-values@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" + integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== dependencies: cssnano-util-get-arguments "^4.0.0" postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-reduce-initial@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.2.tgz#bac8e325d67510ee01fa460676dc8ea9e3b40f15" - integrity sha512-epUiC39NonKUKG+P3eAOKKZtm5OtAtQJL7Ye0CBN1f+UQTHzqotudp+hki7zxXm7tT0ZAKDMBj1uihpPjP25ug== +postcss-reduce-initial@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" + integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== dependencies: browserslist "^4.0.0" caniuse-api "^3.0.0" has "^1.0.0" postcss "^7.0.0" -postcss-reduce-transforms@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.1.tgz#8600d5553bdd3ad640f43bff81eb52f8760d4561" - integrity sha512-sZVr3QlGs0pjh6JAIe6DzWvBaqYw05V1t3d9Tp+VnFRT5j+rsqoWsysh/iSD7YNsULjq9IAylCznIwVd5oU/zA== +postcss-reduce-transforms@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" + integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== dependencies: cssnano-util-get-match "^4.0.0" has "^1.0.0" @@ -8975,7 +9080,7 @@ indexes-of "^1.0.1" uniq "^1.0.1" -postcss-selector-parser@^5.0.0, postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: +postcss-selector-parser@^5.0.0, postcss-selector-parser@^5.0.0-rc.4: version "5.0.0" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== @@ -8984,10 +9089,19 @@ indexes-of "^1.0.1" uniq "^1.0.1" -postcss-svgo@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.1.tgz#5628cdb38f015de6b588ce6d0bf0724b492b581d" - integrity sha512-YD5uIk5NDRySy0hcI+ZJHwqemv2WiqqzDgtvgMzO8EGSkK5aONyX8HMVFRFJSdO8wUWTuisUFn/d7yRRbBr5Qw== +postcss-selector-parser@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" + integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-svgo@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" + integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw== dependencies: is-svg "^3.0.0" postcss "^7.0.0" @@ -9018,9 +9132,9 @@ supports-color "^5.4.0" postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.5: - version "7.0.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5" - integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg== + version "7.0.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.16.tgz#48f64f1b4b558cb8b52c88987724359acb010da2" + integrity sha512-MOo8zNSlIqh22Uaa3drkdIAgUGEL+AD1ESiSdmElLUmE2uVDo1QloiT/IfW9qRw8Gw+Y/w69UVMGwbufMSftxA== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -9031,6 +9145,11 @@ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" @@ -9049,9 +9168,9 @@ integrity sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw== prettier@^1.13.0, prettier@^1.15.2, prettier@^1.15.3: - version "1.16.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717" - integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g== + version "1.17.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.0.tgz#53b303676eed22cc14a9f0cec09b477b3026c008" + integrity sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw== pretty-error@^2.0.2: version "2.1.1" @@ -9146,12 +9265,12 @@ integrity sha512-Xdayp8sB/mU+sUV4G7ws8xtYMGdQnxbeIfLjyO9TZZRJdztBGhlmbI5x1qcY4TG5hBkIKGnc28i7nXxaugu88w== proxy-addr@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" - integrity sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA== + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== dependencies: forwarded "~0.1.2" - ipaddr.js "1.8.0" + ipaddr.js "1.9.0" proxy-agent@2.0.0: version "2.0.0" @@ -9340,22 +9459,22 @@ integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= purgecss-webpack-plugin@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/purgecss-webpack-plugin/-/purgecss-webpack-plugin-1.4.0.tgz#accf6f8f41a7d48fe830da16a4c94d1ab884d6c0" - integrity sha512-kCVR8RvmtJ6IwzxMBNFmAucItyvY6db0Ui5DBmQHCe8GvY2ST03a26wFCU8XwfzN8gpKUGZPyuD3OtL+9WOT0w== - dependencies: - purgecss "^1.1.0" + version "1.5.0" + resolved "https://registry.yarnpkg.com/purgecss-webpack-plugin/-/purgecss-webpack-plugin-1.5.0.tgz#18c0fb8815d79364a80d2701b8d62ba6bc2f8cc0" + integrity sha512-ZSU6lok2DuDBuR7VCte5V12eke0Tx8xsCKxMbOnMfuJNPccPGv4jflRUm2Wvr2yGB8lFzKNZaTWaSk9g3kCv5A== + dependencies: + purgecss "^1.3.0" webpack-sources "^1.3.0" -purgecss@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-1.1.0.tgz#0d27c18bb4070d246a845ea22697271eabb59ffe" - integrity sha512-/XYpiMvbehpeJqxu8k0hzCai9F2RQGjprjpJzRMq9e2qkT8Fk7AW9zLr7bAuqQfxgMIV/+DTNlks3Ckn6J9WEw== - dependencies: - glob "^7.1.2" - postcss "^7.0.0" - postcss-selector-parser "^5.0.0-rc.3" - yargs "^12.0.1" +purgecss@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-1.3.0.tgz#fc3c303df9a74a75547545b2c0da28a9ec63da00" + integrity sha512-0UMnr8aUsPO7RbzAT72UELRvwMHhadtuunDm7rcgRS6b8pCVO8yglIqikiYFwQk2XP606mk+GpjI1G74Auxgtg== + dependencies: + glob "^7.1.3" + postcss "^7.0.14" + postcss-selector-parser "^6.0.0" + yargs "^13.2.2" q@1.4.1: version "1.4.1" @@ -9372,6 +9491,15 @@ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +query-string@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -9382,10 +9510,10 @@ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= -querystringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" - integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== quickselect@^1.0.1: version "1.1.1" @@ -9402,9 +9530,9 @@ math-random "^1.0.1" randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" - integrity sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A== + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" @@ -9421,7 +9549,17 @@ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= -raw-body@2, raw-body@2.3.3: +raw-body@2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== @@ -9474,6 +9612,16 @@ parse-json "^4.0.0" pify "^3.0.0" +read-pkg@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.1.1.tgz#5cf234dde7a405c90c88a519ab73c467e9cb83f5" + integrity sha512-dFcTLQi6BZ+aFUaICg7er+/usEoqFdQxiEBsEMNGoipenihtxxtdrQuBXvyANCEI8VuUIVYFgeHGx9sLLvim4w== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^4.0.0" + type-fest "^0.4.1" + "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -9487,16 +9635,6 @@ string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@1.0: - version "1.0.34" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" - integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readable-stream@1.1.x: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -9507,10 +9645,10 @@ isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@3, readable-stream@^3.0.6: - version "3.1.1" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06" - integrity sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA== +readable-stream@3, readable-stream@^3.0.6, readable-stream@^3.1.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.3.0.tgz#cb8011aad002eb717bf040291feba8569c986fb9" + integrity sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -9526,9 +9664,9 @@ readable-stream "^2.0.2" realpath-native@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.2.tgz#cd51ce089b513b45cf9b1516c82989b51ccc6560" - integrity sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g== + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== dependencies: util.promisify "^1.0.0" @@ -9540,10 +9678,10 @@ indent-string "^2.1.0" strip-indent "^1.0.1" -regenerate-unicode-properties@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" - integrity sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw== +regenerate-unicode-properties@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662" + integrity sha512-SbA/iNrBUf6Pv2zU8Ekv1Qbhv92yxL4hiDa2siuxs4KKn4oOoMDHXjAf7+Nz9qinUQ46B1LcWEi/PhJfPWpZWQ== dependencies: regenerate "^1.4.0" @@ -9557,15 +9695,15 @@ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.12.0: - version "0.12.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" - integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== - -regenerator-transform@^0.13.3: - version "0.13.3" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" - integrity sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA== +regenerator-runtime@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447" + integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA== + +regenerator-transform@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb" + integrity sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A== dependencies: private "^0.1.6" @@ -9585,13 +9723,9 @@ safe-regex "^1.1.0" regexp-tree@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.1.tgz#27b455f9b138ca2e84c090e9aff1ffe2a04d97fa" - integrity sha512-HwRjOquc9QOwKTgbxvZTcddS5mlNlwePMQ3NFL8broajMLD5CXDAqas8Y5yxJH5QtZp5iRor3YCILd5pz71Cgw== - dependencies: - cli-table3 "^0.5.0" - colors "^1.1.2" - yargs "^12.0.5" + version "0.1.6" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.6.tgz#84900fa12fdf428a2ac25f04300382a7c0148479" + integrity sha512-LFrA98Dw/heXqDojz7qKFdygZmFoiVlvE1Zp7Cq2cvF+ZA+03Gmhy0k0PQlsC1jvHPiTUSs+pDHEuSWv6+6D7w== regexpp@^1.0.1: version "1.1.0" @@ -9607,17 +9741,17 @@ regjsgen "^0.2.0" regjsparser "^0.1.4" -regexpu-core@^4.1.3, regexpu-core@^4.2.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.4.0.tgz#8d43e0d1266883969720345e70c275ee0aec0d32" - integrity sha512-eDDWElbwwI3K0Lo6CqbQbA6FwgtCz4kYTarrri1okfkRLZAqstU+B3voZBCjg8Fl6iq0gXrJG6MvRgLthfvgOA== +regexpu-core@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae" + integrity sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ== dependencies: regenerate "^1.4.0" - regenerate-unicode-properties "^7.0.0" + regenerate-unicode-properties "^8.0.2" regjsgen "^0.5.0" regjsparser "^0.6.0" unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.0.2" + unicode-match-property-value-ecmascript "^1.1.0" regjsgen@^0.2.0: version "0.2.0" @@ -9654,13 +9788,13 @@ integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= renderkid@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.2.tgz#12d310f255360c07ad8fde253f6c9e9de372d2aa" - integrity sha512-FsygIxevi1jSiPY9h7vZmBFUbAOcbYm9UwyiLNdVsLRs/5We9Ob5NMPbGYUTWiLq5L+ezlVdE0A8bbME5CWTpg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" + integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== dependencies: css-select "^1.1.0" - dom-converter "~0.2" - htmlparser2 "~3.3.0" + dom-converter "^0.2" + htmlparser2 "^3.3.0" strip-ansi "^3.0.0" utila "^0.4.0" @@ -9681,21 +9815,21 @@ dependencies: is-finite "^1.0.0" -request-promise-core@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6" - integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY= - dependencies: - lodash "^4.13.1" - -request-promise-native@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5" - integrity sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU= - dependencies: - request-promise-core "1.1.1" - stealthy-require "^1.1.0" - tough-cookie ">=2.3.3" +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== + dependencies: + lodash "^4.17.11" + +request-promise-native@^1.0.5, request-promise-native@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + dependencies: + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" request@^2.55.0, request@^2.87.0, request@^2.88.0: version "2.88.0" @@ -9738,6 +9872,11 @@ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + require-uncached@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" @@ -9751,6 +9890,11 @@ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" + integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc= + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -9793,10 +9937,10 @@ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.8.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" - integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.1.tgz#664842ac960795bbe758221cdccda61fb64b5f18" + integrity sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA== dependencies: path-parse "^1.0.6" @@ -9835,7 +9979,7 @@ dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -9887,9 +10031,9 @@ integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ= rxjs@^6.3.3: - version "6.4.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504" - integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw== + version "6.5.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.1.tgz#f7a005a9386361921b8524f38f54cbf80e5d08f4" + integrity sha512-y0j31WJc83wPu31vS1VlAFW5JGrnGC+j+TtGAa1fRQphy48+fDYiDmX8tjGloToEsMkxnouOg/1IzXGKkJnZMg== dependencies: tslib "^1.9.0" @@ -9993,7 +10137,7 @@ resolved "https://registry.yarnpkg.com/selenium-server/-/selenium-server-3.141.59.tgz#cbefdf50aae636ee4c67b819532a8233ce3fd6b0" integrity sha512-pL7T1YtAqOEXiBbTx0KdZMkE2U7PYucemd7i0nDLcxcR1APXYZlJfNr5hrvL3mZgwXb7AJEZPINzC6mDU3eP5g== -selfsigned@^1.9.1: +selfsigned@^1.10.4: version "1.10.4" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.4.tgz#cdd7eccfca4ed7635d47a08bf2d5d3074092e2cd" integrity sha512-9AukTiDmHXGXWtWjembZ5NDmVvP2695EtpgbCsxCa68w3c88B+alqbmZ4O3hZ4VWGXeGWzEVdvqgAJD8DQPCDw== @@ -10001,9 +10145,14 @@ node-forge "0.7.5" "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" - integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +semver@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" + integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== semver@~5.0.1: version "5.0.3" @@ -10035,11 +10184,11 @@ statuses "~1.4.0" serialize-javascript@^1.4.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879" - integrity sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw== - -serve-index@^1.7.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" + integrity sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA== + +serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= @@ -10097,6 +10246,11 @@ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" @@ -10252,6 +10406,13 @@ ip "^1.1.4" smart-buffer "^1.0.13" +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= + dependencies: + is-plain-obj "^1.0.0" + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -10275,10 +10436,10 @@ dependencies: source-map "^0.5.6" -source-map-support@^0.5.6, source-map-support@~0.5.9: - version "0.5.10" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" - integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== +source-map-support@^0.5.6, source-map-support@~0.5.10: + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -10332,9 +10493,9 @@ spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e" - integrity sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g== + version "3.0.4" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1" + integrity sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA== spdy-transport@^3.0.0: version "3.0.0" @@ -10400,7 +10561,7 @@ dependencies: figgy-pudding "^3.5.1" -stable@~0.1.6: +stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== @@ -10433,7 +10594,7 @@ define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.4.0 < 2": +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -10450,7 +10611,7 @@ dependencies: readable-stream "^2.0.1" -stealthy-require@^1.1.0: +stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= @@ -10487,6 +10648,11 @@ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" @@ -10512,6 +10678,15 @@ is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + string.prototype.padend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0" @@ -10563,12 +10738,12 @@ dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" - integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow== - dependencies: - ansi-regex "^4.0.0" +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" strip-bom@3.0.0, strip-bom@^3.0.0: version "3.0.0" @@ -10605,9 +10780,9 @@ integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= stylehacks@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.1.tgz#3186595d047ab0df813d213e51c8b94e0b9010f2" - integrity sha512-TK5zEPeD9NyC1uPIdjikzsgWxdQQN/ry1X3d1iOz1UkYDCmcr928gWD1KHgyC27F50UnE0xCTrBOO1l6KR8M4w== + version "4.0.3" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" + integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== dependencies: browserslist "^4.0.0" postcss "^7.0.0" @@ -10639,7 +10814,7 @@ dependencies: has-flag "^2.0.0" -supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: +supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -10659,22 +10834,22 @@ integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= svgo@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985" - integrity sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g== - dependencies: - coa "~2.0.1" - colors "~1.1.2" + version "1.2.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.2.2.tgz#0253d34eccf2aed4ad4f283e11ee75198f9d7316" + integrity sha512-rAfulcwp2D9jjdGu+0CuqlrAUin6bBWrpoqXWwKDZZZJfXcUXQSxLJOFJCQCSA0x0pP2U0TxSlJu2ROq5Bq6qA== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" css-select "^2.0.0" - css-select-base-adapter "~0.1.0" + css-select-base-adapter "^0.1.1" css-tree "1.0.0-alpha.28" css-url-regex "^1.1.0" - csso "^3.5.0" - js-yaml "^3.12.0" + csso "^3.5.1" + js-yaml "^3.13.1" mkdirp "~0.5.1" - object.values "^1.0.4" + object.values "^1.1.0" sax "~1.2.4" - stable "~0.1.6" + stable "^0.1.8" unquote "~1.1.1" util.promisify "~1.0.0" @@ -10696,9 +10871,9 @@ string-width "^2.1.1" tapable@^1.0.0, tapable@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e" - integrity sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA== + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tar@^2.0.0: version "2.2.1" @@ -10730,10 +10905,10 @@ debug "4.1.0" is2 "2.0.1" -terser-webpack-plugin@^1.1.0, terser-webpack-plugin@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz#9bff3a891ad614855a7dde0d707f7db5a927e3d9" - integrity sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg== +terser-webpack-plugin@^1.1.0, terser-webpack-plugin@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz#3f98bc902fac3e5d0de730869f50668561262ec8" + integrity sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA== dependencies: cacache "^11.0.2" find-cache-dir "^2.0.0" @@ -10745,13 +10920,13 @@ worker-farm "^1.5.2" terser@^3.16.1: - version "3.16.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-3.16.1.tgz#5b0dd4fa1ffd0b0b43c2493b2c364fd179160493" - integrity sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow== - dependencies: - commander "~2.17.1" + version "3.17.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" + integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ== + dependencies: + commander "^2.19.0" source-map "~0.6.1" - source-map-support "~0.5.9" + source-map-support "~0.5.10" test-exclude@^4.2.1: version "4.2.3" @@ -10769,6 +10944,20 @@ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.0" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" + integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk= + dependencies: + any-promise "^1.0.0" + thread-loader@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/thread-loader/-/thread-loader-2.1.2.tgz#f585dd38e852c7f9cded5d092992108148f5eb30" @@ -10880,6 +11069,11 @@ regex-not "^1.0.2" safe-regex "^1.1.0" +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + token-stream@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-0.0.1.tgz#ceeefc717a76c4316f126d0b9dbaa55d7e7df01a" @@ -10897,16 +11091,7 @@ resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= -tough-cookie@>=2.3.3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" - integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== - dependencies: - ip-regex "^2.1.0" - psl "^1.1.28" - punycode "^2.1.1" - -tough-cookie@^2.2.0, tough-cookie@^2.3.4: +tough-cookie@^2.2.0, tough-cookie@^2.3.3, tough-cookie@^2.3.4: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -11005,25 +11190,30 @@ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" integrity sha1-C6XsKohWQORw6k6FBZcZANrFiCI= +type-fest@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8" + integrity sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw== + type-is@~1.6.16: - version "1.6.16" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" - integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q== + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" - mime-types "~2.1.18" + mime-types "~2.1.24" typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -uglify-js@3.4.x, uglify-js@^3.1.4: - version "3.4.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" - integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q== - dependencies: - commander "~2.17.1" +uglify-js@3.4.x: + version "3.4.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" + integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== + dependencies: + commander "~2.19.0" source-map "~0.6.1" uglify-js@^2.6.1: @@ -11036,6 +11226,14 @@ optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.1.4: + version "3.5.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.5.10.tgz#652bef39f86d9dbfd6674407ee05a5e2d372cf2d" + integrity sha512-/GTF0nosyPLbdJBd+AwYiZ+Hu5z8KXWnO0WCGt1BQ/u9Iamhejykqmz5o1OHJ53+VAk6xVxychonnApDjuqGsw== + dependencies: + commander "~2.20.0" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -11054,15 +11252,15 @@ unicode-canonical-property-names-ecmascript "^1.0.4" unicode-property-aliases-ecmascript "^1.0.4" -unicode-match-property-value-ecmascript@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz#9f1dc76926d6ccf452310564fd834ace059663d4" - integrity sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ== +unicode-match-property-value-ecmascript@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277" + integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g== unicode-property-aliases-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" - integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg== + version "1.0.5" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57" + integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw== union-value@^1.0.0: version "1.0.0" @@ -11121,10 +11319,10 @@ has-value "^0.3.1" isobject "^3.0.0" -upath@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" - integrity sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw== +upath@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== upper-case@^1.1.1: version "1.1.3" @@ -11153,11 +11351,11 @@ schema-utils "^1.0.0" url-parse@^1.4.3: - version "1.4.4" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.4.tgz#cac1556e95faa0303691fec5cf9d5a1bc34648f8" - integrity sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg== - dependencies: - querystringify "^2.0.0" + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" requires-port "^1.0.0" url@^0.11.0: @@ -11223,13 +11421,13 @@ integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== v-tooltip@^2.0.0-rc.33: - version "2.0.0-rc.33" - resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.0-rc.33.tgz#78f7d8e9c34265622be65ba9dc78c67f1dc02b73" - integrity sha1-ePfY6cNCZWIr5lup3HjGfx3AK3M= - dependencies: - lodash.merge "^4.6.0" - popper.js "^1.12.9" - vue-resize "^0.4.3" + version "2.0.2" + resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.2.tgz#8610d9eece2cc44fd66c12ef2f12eec6435cab9b" + integrity sha512-xQ+qzOFfywkLdjHknRPgMMupQNS8yJtf9Utd5Dxiu/0n4HtrxqsgDtN2MLZ0LKbburtSAQgyypuE/snM8bBZhw== + dependencies: + lodash "^4.17.11" + popper.js "^1.15.0" + vue-resize "^0.4.5" v8-compile-cache@^2.0.2: version "2.0.2" @@ -11305,9 +11503,9 @@ lodash "^4.17.4" vue-gettext@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/vue-gettext/-/vue-gettext-2.1.2.tgz#b6b5160bed8ce61b6ead6adf5f4029ff04f57720" - integrity sha512-issGZfHvGntflaoLd+46Ru305lWOVEG/0vTI7uB5zvq/4JVGoF7J/+DUrcYswowiCKpJluYqf6inILat2HNeRQ== + version "2.1.4" + resolved "https://registry.yarnpkg.com/vue-gettext/-/vue-gettext-2.1.4.tgz#f42aa8480ad45c3f63ee67ea2f56891740010e37" + integrity sha512-UkJ+tKMp4/cn5RKK7Nm0l5apvpTqeuzlP22/SkNYdEfAzFBasX6qxK1EMTZGTXGWlc6h/KMu9X6W8KjWCnFbtQ== vue-highlightjs@^1.3.3: version "1.3.3" @@ -11317,14 +11515,14 @@ highlight.js "*" vue-hot-reload-api@^2.3.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.2.tgz#1fcc1495effe08a790909b46bf7b5c4cfeb6f21b" - integrity sha512-NpznMQoe/DzMG7nJjPkJKT7FdEn9xXfnntG7POfTmqnSaza97ylaBf1luZDh4IgV+vgUoR//id5pf8Ru+Ym+0g== - -vue-jest@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/vue-jest/-/vue-jest-3.0.3.tgz#80f664712f2678b1d8bb3af0f2c0bef5efa8de31" - integrity sha512-QwFQjkv2vXYPKUkNZkMbV/ZTHyQhRM1JY8nP68dRLQmdvCN+VUEKhlByH/PgPqDr2p/NuhaM3PUjJ9nreR++3w== + version "2.3.3" + resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz#2756f46cb3258054c5f4723de8ae7e87302a1ccf" + integrity sha512-KmvZVtmM26BQOMK1rwUZsrqxEGeKiYSZGA7SNWE6uExx8UX/cj9hq2MRV/wWC3Cq6AoeDGk57rL9YMFRel/q+g== + +vue-jest@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/vue-jest/-/vue-jest-3.0.4.tgz#b6a2b0d874968f26fa775ac901903fece531e08b" + integrity sha512-PY9Rwt4OyaVlA+KDJJ0614CbEvNOkffDI9g9moLQC/2DDoo0YrqZm7dHi13Q10uoK5Nt5WCYFdeAheOExPah0w== dependencies: babel-plugin-transform-es2015-modules-commonjs "^6.26.0" chalk "^2.1.0" @@ -11338,14 +11536,14 @@ vue-template-es2015-compiler "^1.6.0" vue-js-toggle-button@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/vue-js-toggle-button/-/vue-js-toggle-button-1.3.1.tgz#a129cf7493ad5bcd2a54c905cd5935d1c271e30c" - integrity sha512-2KpkUVSVvxFtT6LG+JXNpARo/E9jBNWUPvWKvoYA/kJJJtdJfOPdCLA3GKaIzeR/Cy0Tb72CNEy9qkoN0GhMHg== - -vue-loader@^15.6.2: - version "15.6.2" - resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.6.2.tgz#892741d96260936ff69e892f72ec361ba4d100d2" - integrity sha512-T6fONodj861M3PqZ1jlbUFjeezbUnPRY2bd+3eZuDvYADgkN3VFU2H5feqySNg9XBt8rcbyBGmFWTZtrOX+v5w== + version "1.3.2" + resolved "https://registry.yarnpkg.com/vue-js-toggle-button/-/vue-js-toggle-button-1.3.2.tgz#d2e538465c321967144d035824cb71eedd82984e" + integrity sha512-LS+pvX5lXEhX+Gei5MOAEw7bx99/A+9idFhMtBgz72ApsEHlW69Y7bk+ZKC1rLRUxchL5WlQ+MhJXqXewhkfjg== + +vue-loader@^15.7.0: + version "15.7.0" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.7.0.tgz#27275aa5a3ef4958c5379c006dd1436ad04b25b3" + integrity sha512-x+NZ4RIthQOxcFclEcs8sXGEWqnZHodL2J9Vq+hUz+TDZzBaDIh1j3d9M2IUlTjtrHTZy4uMuRdTi8BGws7jLA== dependencies: "@vue/component-compiler-utils" "^2.5.1" hash-sum "^1.0.2" @@ -11353,15 +11551,15 @@ vue-hot-reload-api "^2.3.0" vue-style-loader "^4.1.0" -vue-resize@^0.4.3: +vue-resize@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea" integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg== vue-router@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be" - integrity sha512-opKtsxjp9eOcFWdp6xLQPLmRGgfM932Tl56U9chYTnoWqKxQ8M20N7AkdEbM5beUh6wICoFGYugAX9vQjyJLFg== + version "3.0.6" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.6.tgz#2e4f0f9cbb0b96d0205ab2690cfe588935136ac3" + integrity sha512-Ox0ciFLswtSGRTHYhGvx2L44sVbTPNS+uD2kRISuo8B39Y79rOo0Kw0hzupTmiVtftQYCZl87mwldhh2L9Aquw== vue-snotify@^3.2.1: version "3.2.1" @@ -11377,22 +11575,22 @@ loader-utils "^1.0.2" vue-template-compiler@^2.5.16, vue-template-compiler@^2.5.17: - version "2.6.6" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.6.tgz#a807acbf3d51971d3721d75ecb1b927b517c1a02" - integrity sha512-OakxDGyrmMQViCjkakQFbDZlG0NibiOzpLauOfyCUVRQc9yPmTqpiz9nF0VeA+dFkXegetw0E5x65BFhhLXO0A== + version "2.6.10" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc" + integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg== dependencies: de-indent "^1.0.2" he "^1.1.0" -vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.8.2.tgz#dd73e80ba58bb65dd7a8aa2aeef6089cf6116f2a" - integrity sha512-cliV19VHLJqFUYbz/XeWXe5CO6guzwd0yrrqqp0bmjlMP3ZZULY7fu8RTC4+3lmHwo6ESVDHFDsvjB15hcR5IA== +vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" + integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== vue@^2.5.16: - version "2.6.6" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.6.tgz#dde41e483c11c46a7bf523909f4f2f816ab60d25" - integrity sha512-Y2DdOZD8sxApS+iUlwv1v8U1qN41kq6Kw45lM6nVZKhygeWA49q7VCCXkjXqeDBXgurrKWkYQ9cJeEJwAq0b9Q== + version "2.6.10" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637" + integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ== vuex@^3.0.1: version "3.1.0" @@ -11454,12 +11652,13 @@ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== -webpack-bundle-analyzer@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.3.tgz#dbc7fff8f52058b6714a20fddf309d0790e3e0a0" - integrity sha512-naLWiRfmtH4UJgtUktRTLw6FdoZJ2RvCR9ePbwM9aRMsS/KjFerkPZG9epEvXRAw5d5oPdrs9+3p+afNjxW8Xw== - dependencies: - acorn "^5.7.3" +webpack-bundle-analyzer@^3.0.3, webpack-bundle-analyzer@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.3.2.tgz#3da733a900f515914e729fcebcd4c40dde71fc6f" + integrity sha512-7qvJLPKB4rRWZGjVp5U1KEjwutbDHSKboAl0IfafnrdXMrgC0tOtZbQD6Rw0u4cmpgRN4O02Fc0t8eAT+FgGzA== + dependencies: + acorn "^6.0.7" + acorn-walk "^6.1.1" bfj "^6.1.1" chalk "^2.4.1" commander "^2.18.0" @@ -11481,9 +11680,9 @@ javascript-stringify "^1.6.0" webpack-cli@^3.1.2: - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.2.3.tgz#13653549adfd8ccd920ad7be1ef868bacc22e346" - integrity sha512-Ik3SjV6uJtWIAN5jp5ZuBMWEAaP5E4V78XJ2nI+paFPh8v4HPSwo/myN0r29Xc/6ZKnd2IdrAlpSgNOu2CDQ6Q== + version "3.3.2" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.2.tgz#aed2437b0db0a7faa2ad28484e166a5360014a91" + integrity sha512-FLkobnaJJ+03j5eplxlI0TUxhGCOdfewspIGuvDVtpOlrAuKMFC57K42Ukxqs1tn8947/PM6tP95gQc0DCzRYA== dependencies: chalk "^2.4.1" cross-spawn "^6.0.5" @@ -11495,53 +11694,53 @@ loader-utils "^1.1.0" supports-color "^5.5.0" v8-compile-cache "^2.0.2" - yargs "^12.0.4" - -webpack-dev-middleware@3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz#1132fecc9026fd90f0ecedac5cbff75d1fb45890" - integrity sha512-Q9Iyc0X9dP9bAsYskAVJ/hmIZZQwf/3Sy4xCAZgL5cUkjZmUZLt4l5HpbST/Pdgjn3u6pE7u5OdGd1apgzRujA== - dependencies: - memory-fs "~0.4.1" + yargs "^12.0.5" + +webpack-dev-middleware@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.2.tgz#f37a27ad7c09cd7dc67cd97655413abaa1f55942" + integrity sha512-A47I5SX60IkHrMmZUlB0ZKSWi29TZTcPz7cha1Z75yYOsgWh/1AcPmQEbC8ZIbU3A1ytSv1PMU0PyPz2Lmz2jg== + dependencies: + memory-fs "^0.4.1" mime "^2.3.1" range-parser "^1.0.3" webpack-log "^2.0.0" -webpack-dev-server@^3.1.14: - version "3.1.14" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz#60fb229b997fc5a0a1fc6237421030180959d469" - integrity sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ== +webpack-dev-server@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.3.1.tgz#7046e49ded5c1255a82c5d942bcdda552b72a62d" + integrity sha512-jY09LikOyGZrxVTXK0mgIq9y2IhCoJ05848dKZqX1gAGLU1YDqgpOT71+W53JH/wI4v6ky4hm+KvSyW14JEs5A== dependencies: ansi-html "0.0.7" bonjour "^3.5.0" - chokidar "^2.0.0" - compression "^1.5.2" - connect-history-api-fallback "^1.3.0" - debug "^3.1.0" - del "^3.0.0" - express "^4.16.2" - html-entities "^1.2.0" - http-proxy-middleware "~0.18.0" + chokidar "^2.1.5" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.0" + express "^4.16.4" + html-entities "^1.2.1" + http-proxy-middleware "^0.19.1" import-local "^2.0.0" - internal-ip "^3.0.1" + internal-ip "^4.2.0" ip "^1.1.5" - killable "^1.0.0" - loglevel "^1.4.1" - opn "^5.1.0" - portfinder "^1.0.9" + killable "^1.0.1" + loglevel "^1.6.1" + opn "^5.5.0" + portfinder "^1.0.20" schema-utils "^1.0.0" - selfsigned "^1.9.1" - semver "^5.6.0" - serve-index "^1.7.2" + selfsigned "^1.10.4" + semver "^6.0.0" + serve-index "^1.9.1" sockjs "0.3.19" sockjs-client "1.3.0" spdy "^4.0.0" - strip-ansi "^3.0.0" - supports-color "^5.1.0" + strip-ansi "^3.0.1" + supports-color "^6.1.0" url "^0.11.0" - webpack-dev-middleware "3.4.0" + webpack-dev-middleware "^3.6.2" webpack-log "^2.0.0" - yargs "12.0.2" + yargs "12.0.5" webpack-log@^2.0.0: version "2.0.0" @@ -11700,9 +11899,9 @@ integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= worker-farm@^1.5.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0" - integrity sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ== + version "1.7.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" + integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== dependencies: errno "~0.1.7" @@ -11714,6 +11913,15 @@ string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -11743,9 +11951,9 @@ async-limiter "~1.0.0" ws@^6.0.0: - version "6.1.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.3.tgz#d2d2e5f0e3c700ef2de89080ebc0ac6e1bf3a72d" - integrity sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg== + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== dependencies: async-limiter "~1.0.0" @@ -11769,11 +11977,6 @@ resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM= -xregexp@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" - integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg== - xtend@^4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -11799,13 +12002,6 @@ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== -yargs-parser@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" @@ -11814,6 +12010,14 @@ camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^13.0.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.0.tgz#7016b6dd03e28e1418a510e258be4bff5a31138f" + integrity sha512-Yq+32PrijHRri0vVKQEm+ys8mbqWjLiwQkMFNXEENutzLPP0bE4Lcd4iA3OQY5HF+GD3xXxf0MEHb8E4/SA3AA== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" @@ -11828,13 +12032,13 @@ dependencies: camelcase "^4.1.0" -yargs@12.0.2: - version "12.0.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" - integrity sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ== +yargs@12.0.5, yargs@^12.0.1, yargs@^12.0.5: + version "12.0.5" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== dependencies: cliui "^4.0.0" - decamelize "^2.0.0" + decamelize "^1.2.0" find-up "^3.0.0" get-caller-file "^1.0.1" os-locale "^3.0.0" @@ -11844,7 +12048,7 @@ string-width "^2.0.0" which-module "^2.0.0" y18n "^3.2.1 || ^4.0.0" - yargs-parser "^10.1.0" + yargs-parser "^11.1.1" yargs@^11.0.0: version "11.1.0" @@ -11864,23 +12068,22 @@ y18n "^3.2.1" yargs-parser "^9.0.2" -yargs@^12.0.1, yargs@^12.0.4, yargs@^12.0.5: - version "12.0.5" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" - integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== +yargs@^13.0.0, yargs@^13.2.2: + version "13.2.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.2.tgz#0c101f580ae95cea7f39d927e7770e3fdc97f993" + integrity sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA== dependencies: cliui "^4.0.0" - decamelize "^1.2.0" find-up "^3.0.0" - get-caller-file "^1.0.1" - os-locale "^3.0.0" + get-caller-file "^2.0.1" + os-locale "^3.1.0" require-directory "^2.1.1" - require-main-filename "^1.0.1" + require-main-filename "^2.0.0" set-blocking "^2.0.0" - string-width "^2.0.0" + string-width "^3.0.0" which-module "^2.0.0" - y18n "^3.2.1 || ^4.0.0" - yargs-parser "^11.1.1" + y18n "^4.0.0" + yargs-parser "^13.0.0" yargs@^7.0.0: version "7.1.0"
--- a/cmd/gemma/main.go Wed May 29 10:58:45 2019 +0200 +++ b/cmd/gemma/main.go Mon Jun 03 10:19:18 2019 +0200 @@ -31,6 +31,8 @@ "gemma.intevation.de/gemma/pkg/config" "gemma.intevation.de/gemma/pkg/controllers" "gemma.intevation.de/gemma/pkg/geoserver" + "gemma.intevation.de/gemma/pkg/imports" + "gemma.intevation.de/gemma/pkg/scheduler" ) func prepareSessionStore() { @@ -56,6 +58,10 @@ // Do GeoServer setup in background. geoserver.Reconfigure(geoserver.PrepareGeoServer) + // Log what it is rgistered to the import queue and scheduler. + imports.LogImportKindNames() + scheduler.LogActionNames() + m := mux.NewRouter() controllers.BindRoutes(m) @@ -107,6 +113,7 @@ func main() { config.RootCmd.Run = start + log.SetFlags(log.LstdFlags | log.Lshortfile) if err := config.RootCmd.Execute(); err != nil { log.Fatalln(err) }
--- a/docker/Dockerfile.geoserv Wed May 29 10:58:45 2019 +0200 +++ b/docker/Dockerfile.geoserv Mon Jun 03 10:19:18 2019 +0200 @@ -15,7 +15,7 @@ ENV GS_URL https://downloads.sourceforge.net/project/geoserver/GeoServer -ENV GS_VERSION 2.15.0 +ENV GS_VERSION 2.15.1 ENV GS_DATADIR /opt/geoserver/data ENV CATALINA_OPTS="-DGEOSERVER_DATA_DIR=$GS_DATADIR"
--- a/example_conf.toml Wed May 29 10:58:45 2019 +0200 +++ b/example_conf.toml Mon Jun 03 10:19:18 2019 +0200 @@ -72,3 +72,4 @@ # ----------------------- # Schema for "Testclient imports" # schema-dirs = "$PATH_TO_SCHEMATA" +# published-config ="$PATH/pub-config.json"
--- a/pkg/common/nashsutcliffe.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/common/nashsutcliffe.go Mon Jun 03 10:19:18 2019 +0200 @@ -14,68 +14,58 @@ package common import ( - "sort" + "fmt" "time" ) -type NSMeasurement struct { - When time.Time - Predicted float64 - Observed float64 +type TimedValue struct { + When time.Time + Value float64 +} + +type TimedValues []TimedValue + +func epsEquals(a, b time.Time) bool { + d := a.Sub(b) + return -10*time.Millisecond < d && d < 10*time.Millisecond } -func NashSutcliffeSort(measurements []NSMeasurement) { - sort.Slice(measurements, func(i, j int) bool { - return measurements[i].When.Before(measurements[j].When) - }) +func (mvs TimedValues) Find(when time.Time) (float64, bool) { + for i := range mvs { + if epsEquals(when, mvs[i].When) { + return mvs[i].Value, true + } + } + return 0, false } -func NashSutcliffe(measurements []NSMeasurement, from, to time.Time) (float64, int) { +func NashSutcliffe(predicted, observed []float64) float64 { - if len(measurements) == 0 { - return 0, 0 + if len(predicted) != len(observed) { + panic(fmt.Sprintf( + "NashSutcliffe: predicted and observed len differ: %d != %d", + len(predicted), + len(observed))) } - if to.Before(from) { - from, to = to, from - } - - begin := sort.Search(len(measurements), func(i int) bool { - return !measurements[i].When.Before(from) - }) - if begin >= len(measurements) { - return 0, 0 - } - - end := sort.Search(len(measurements), func(i int) bool { - return measurements[i].When.After(to) - }) - if end >= len(measurements) { - end = len(measurements) - 1 - } - if end <= begin { - return 0, 0 - } - sample := measurements[begin:end] - - if len(sample) == 0 { - return 0, 0 + if len(observed) == 0 { + return 0 } var mo float64 - for i := range sample { - mo += sample[i].Observed + for _, v := range observed { + mo += v } - mo /= float64(len(sample)) + mo /= float64(len(observed)) var num, denom float64 - for i := range sample { - d1 := sample[i].Predicted - sample[i].Observed + for i, o := range observed { + d1 := predicted[i] - o num += d1 * d1 - d2 := sample[i].Observed - mo + d2 := o - mo denom += d2 * d2 } - return 1 - num/denom, len(sample) + return 1 - num/denom }
--- a/pkg/common/time.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/common/time.go Mon Jun 03 10:19:18 2019 +0200 @@ -4,16 +4,53 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // License-Filename: LICENSES/AGPL-3.0.txt // -// Copyright (C) 2018 by via donau +// Copyright (C) 2018, 2019 by via donau // – Österreichische Wasserstraßen-Gesellschaft mbH // Software engineering by Intevation GmbH // // Author(s): // * Sascha L. Teichmann <sascha.teichmann@intevation.de> +// * Bernhard E. Reiter <bernhard.reiter@intevation.de> package common +import ( + "math" + "time" +) + const ( - TimeFormat = "2006-01-02T15:04:05" + // time.RFC3339 equals "simplified ISO format as defined by ECMA-262" + // https://tc39.github.io/ecma262/#sec-date-time-string-format + // and "SHOULD be used in new protocols on the Internet." (RFC section 5.6) + TimeFormat = time.RFC3339 DateFormat = "2006-01-02" ) + +var utc0 = time.Unix(0, 0) + +func InterpolateTime(t1 time.Time, m1 float64, t2 time.Time, m2 float64) func(float64) time.Time { + + // f(m1) = t1 + // f(m2) = t2 + // t1 = m1*a + b <=> b = t1 - m1*a + // t2 = m2*a + b + + // t1 - t2 = a*(m1 - m2) + // a = (t1-t2)/(m1 - m2) for m1 != m2 + + if m1 == m2 { + t := t1.Add(t2.Sub(t1) / 2) + return func(float64) time.Time { return t } + } + + a := t1.Sub(t2).Seconds() / (m1 - m2) + b := t1.Sub(utc0).Seconds() - m1*a + + return func(m float64) time.Time { + x := m*a + b + secs := math.Ceil(x) + nsecs := math.Ceil((x - secs) * (999999999 + 1)) + return time.Unix(int64(secs), int64(nsecs)) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/common/time_test.go Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,61 @@ +// 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, 2019 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package common + +import ( + "testing" + "time" +) + +func TestInterpolateTimeByValue(t *testing.T) { + + t1 := time.Now().UTC() + t2 := t1.Add(time.Hour).UTC() + + f := InterpolateTime(t1, 10, t2, 20) + + v1 := f(10) + v2 := f(20) + v3 := f(15) + + t3 := t1.Add(time.Hour / 2) + + d1 := v1.Sub(t1) + d2 := v2.Sub(t2) + d3 := v3.Sub(t3) + + if d1 < 0 { + d1 = -d1 + } + + if d1 > 100*time.Microsecond { + t.Errorf("difference too big t1: %v\n", d1) + } + + if d2 < 0 { + d2 = -d2 + } + + if d2 > 100*time.Microsecond { + t.Errorf("difference too big t2: %v\n", d2) + } + + if d3 < 0 { + d3 = -d3 + } + + if d3 > 100*time.Microsecond { + t.Errorf("difference too big t3: %v\n", d3) + } +}
--- a/pkg/config/config.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/config/config.go Mon Jun 03 10:19:18 2019 +0200 @@ -4,7 +4,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // License-Filename: LICENSES/AGPL-3.0.txt // -// Copyright (C) 2018 by via donau +// Copyright (C) 2018, 2019 by via donau // – Österreichische Wasserstraßen-Gesellschaft mbH // Software engineering by Intevation GmbH // @@ -22,6 +22,7 @@ "time" homedir "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" "github.com/spf13/viper" @@ -106,6 +107,13 @@ // If left empty the system default for temporary files is used. func TmpDir() string { return viper.GetString("tmp-dir") } +// PublishedConfig is a name of a JSON file where extra configuration is stored +// to be served to to the web client. +func PublishedConfig() string { return viper.GetString("published-config") } + +// SOAPTimeout is the timeout till a SOAP request is canceled. +func SOAPTimeout() time.Duration { return viper.GetDuration("soap-timeout") } + var ( proxyKeyOnce sync.Once proxyKey []byte @@ -140,7 +148,6 @@ } // ProxyPrefix is the prefix used in generated URLs by the proxy. -// It defauls to http://${WebHost}:${WebPort}". // You may need to set this if you run gemma behind a proxy // on a specific domain. func ProxyPrefix() string { @@ -156,7 +163,6 @@ } // ExternalURL is the URL to find this server from the outside. -// It defauls to http://${WebHost}:${WebPort}". func ExternalURL() string { fetchExternal := func() { if externalURL == "" { @@ -183,7 +189,7 @@ return sessionTimeout } -// The root directories where to find schema files. +// SchemaDirs are the root directories where to find schema files. func SchemaDirs() string { return viper.GetString("schema-dirs") } // RootCmd is cobra command to be bound th the cobra/viper infrastructure. @@ -227,6 +233,10 @@ fl.Bool(name, value, usage) vbind(name) } + d := func(name string, value time.Duration, usage string) { + fl.Duration(name, value, usage) + vbind(name) + } strP("db-host", "H", "localhost", "host of the database") uiP("db-port", "P", 5432, "port of the database") @@ -236,7 +246,7 @@ strP("db-ssl", "S", "prefer", "SSL mode of the database") strP("sessions", "s", "", "path to the sessions file") - str("session-timeout", "3h", "duration until sessions expire") + d("session-timeout", 3*time.Hour, "duration until sessions expire") strP("web", "w", "./web", "path to the web files") strP("host", "o", "localhost", "host of the web app") @@ -260,15 +270,19 @@ str("proxy-key", "", "signing key for proxy URLs.\n"+ "Defaults to random key.") str("proxy-prefix", "", "URL prefix of proxy.\n"+ - "Defaults to 'http://${web-host}:${web-port}'") + "Defaults to 'http://${host}:${port}'") str("external-url", "", "URL to find the server from the outside.\n"+ - "Defaults to 'http://${web-host}:${web-port}'") + "Defaults to 'http://${host}:${port}'") str("tmp-dir", "", "Temp directory of gemma server.\n"+ "Defaults to system temp directory.") str("schema-dirs", ".", "Directories to find XSD schema files in (recursive).") + + str("published-config", "", "path to a config file served to client.") + + d("soap-timeout", time.Minute, "Timeout till a SOAP request is canceled.") } var (
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/controllers/bottlenecks.go Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,731 @@ +// 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) 2019 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package controllers + +import ( + "context" + "database/sql" + "encoding/csv" + "fmt" + "log" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/middleware" +) + +const ( + selectLimitingSQL = ` +SELECT limiting from waterway.bottlenecks WHERE objnam = $1` + + selectAvailableDepthSQL = ` +WITH data AS ( + SELECT + efa.measure_date, + efa.available_depth_value, + efa.available_width_value, + efa.water_level_value + FROM waterway.effective_fairway_availability efa + JOIN waterway.fairway_availability fa + ON efa.fairway_availability_id = fa.id + JOIN waterway.bottlenecks bn + ON fa.bottleneck_id = bn.id + WHERE + bn.objnam = $1 AND + efa.level_of_service = $2 AND + efa.measure_type = 'Measured' AND + (efa.available_depth_value IS NOT NULL OR + efa.available_width_value IS NOT NULL) AND + efa.water_level_value IS NOT NULL +), +before AS ( + SELECT * FROM data WHERE measure_date < $3 + ORDER BY measure_date DESC LIMIT 1 +), +inside AS ( + SELECT * FROM data WHERE measure_date BETWEEN $3 AND $4 +), +after AS ( + SELECT * FROM data WHERE measure_date > $4 + ORDER BY measure_date LIMIT 1 +) +SELECT * FROM before +UNION ALL +SELECT * FROM inside +UNION ALL +SELECT * FROM after +ORDER BY measure_date +` + + selectGaugeLevelsSQL = ` +SELECT + grwl.depth_reference, + grwl.value +FROM waterway.gauges_reference_water_levels grwl + JOIN waterway.bottlenecks bns + ON grwl.location = bns.gauge_location + AND grwl.validity = bns.gauge_validity +WHERE bns.objnam = $1 AND ( + grwl.depth_reference like 'HDC%' OR + grwl.depth_reference like 'LDC%' OR + grwl.depth_reference like 'MW%' +) +` + selectGaugeLDCSQL = ` +SELECT + grwl.value +FROM waterway.gauges_reference_water_levels grwl + JOIN waterway.bottlenecks bns + ON grwl.location = bns.gauge_location + AND grwl.validity = bns.gauge_validity +WHERE bns.objnam = $1 AND grwl.depth_reference like 'LDC%' +` +) + +type ( + availMeasurement struct { + when time.Time + depth int16 + width int16 + value int16 + } + + availMeasurements []availMeasurement +) + +// afdRefs are the typical available fairway depth reference values. +var afdRefs = []float64{ + 230, + 250, +} + +func (measurement *availMeasurement) getDepth() float64 { + return float64(measurement.depth) +} + +func (measurement *availMeasurement) getValue() float64 { + return float64(measurement.value) +} + +func (measurement *availMeasurement) getWidth() float64 { + return float64(measurement.width) +} + +func limitingFactor(limiting string) func(*availMeasurement) float64 { + switch limiting { + case "depth": + return (*availMeasurement).getDepth + case "width": + return (*availMeasurement).getWidth + default: + log.Printf("warn: unknown limitation '%s'. default to 'depth'\n", limiting) + return (*availMeasurement).getDepth + } +} + +func (measurements availMeasurements) classify( + from, to time.Time, + breaks []float64, + access func(*availMeasurement) float64, +) []time.Duration { + + if len(breaks) == 0 { + return []time.Duration{} + } + + result := make([]time.Duration, len(breaks)+1) + classes := make([]float64, len(breaks)+2) + values := make([]time.Time, len(classes)) + + // Add sentinels + classes[0] = breaks[0] - 9999 + classes[len(classes)-1] = breaks[len(breaks)-1] + 9999 + for i := range breaks { + classes[i+1] = breaks[i] + } + + idx := sort.Search(len(measurements), func(i int) bool { + // All values before from can be ignored. + return !measurements[i].when.Before(from) + }) + + if idx >= len(measurements) { + return result + } + + // Be safe for interpolation. + if idx > 0 { + idx-- + } + + measurements = measurements[idx:] + + for i := 0; i < len(measurements)-1; i++ { + p1 := &measurements[i] + p2 := &measurements[i+1] + + if p1.when.After(to) { + return result + } + + if p2.when.Before(from) { + continue + } + + lo, hi := maxTime(p1.when, from), minTime(p2.when, to) + + m1, m2 := access(p1), access(p2) + if m1 == m2 { // The whole interval is in only one class. + for j := 0; j < len(classes)-1; j++ { + if classes[j] <= m1 && m1 <= classes[j+1] { + result[j] += hi.Sub(lo) + break + } + } + continue + } + + f := common.InterpolateTime( + p1.when, m1, + p2.when, m2, + ) + + for j, c := range classes { + values[j] = f(c) + } + + for j := 0; j < len(values)-1; j++ { + start, end := orderTime(values[j], values[j+1]) + + if start.After(hi) || end.Before(lo) { + continue + } + + start, end = maxTime(start, lo), minTime(end, hi) + result[j] += end.Sub(start) + } + } + + return result +} + +func orderTime(a, b time.Time) (time.Time, time.Time) { + if a.Before(b) { + return a, b + } + return b, a +} + +func minTime(a, b time.Time) time.Time { + if a.Before(b) { + return a + } + return b +} + +func maxTime(a, b time.Time) time.Time { + if a.After(b) { + return a + } + return b +} + +func durationsToPercentage(duration time.Duration, classes []time.Duration) []float64 { + percents := make([]float64, len(classes)) + total := 100 / duration.Seconds() + for i, v := range classes { + percents[i] = v.Seconds() * total + } + return percents +} + +func parseFormTime( + rw http.ResponseWriter, + req *http.Request, + field string, + def time.Time, +) (time.Time, bool) { + f := req.FormValue(field) + if f == "" { + return def.UTC(), true + } + v, err := time.Parse(common.TimeFormat, f) + if err != nil { + http.Error( + rw, fmt.Sprintf("Invalid format for '%s': %v.", field, err), + http.StatusBadRequest, + ) + return time.Time{}, false + } + return v.UTC(), true +} + +func parseFormInt( + rw http.ResponseWriter, + req *http.Request, + field string, + def int, +) (int, bool) { + f := req.FormValue(field) + if f == "" { + return def, true + } + v, err := strconv.Atoi(f) + if err != nil { + http.Error( + rw, fmt.Sprintf("Invalid format for '%s': %v.", field, err), + http.StatusBadRequest, + ) + return 0, false + } + return v, true +} + +func intervalMode(mode string) int { + switch strings.ToLower(mode) { + case "monthly": + return 0 + case "quarterly": + return 1 + case "yearly": + return 2 + default: + return 0 + } +} + +func loadDepthValues( + ctx context.Context, + conn *sql.Conn, + bottleneck string, + los int, + from, to time.Time, +) (availMeasurements, error) { + + rows, err := conn.QueryContext( + ctx, selectAvailableDepthSQL, bottleneck, los, from, to) + if err != nil { + return nil, err + } + defer rows.Close() + + var ms availMeasurements + + for rows.Next() { + var m availMeasurement + if err := rows.Scan( + &m.when, + &m.depth, + &m.width, + &m.value, + ); err != nil { + return nil, err + } + m.when = m.when.UTC() + ms = append(ms, m) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return ms, nil +} + +func loadLDCReferenceValue( + ctx context.Context, + conn *sql.Conn, + bottleneck string, +) ([]float64, error) { + var value float64 + err := conn.QueryRowContext(ctx, selectGaugeLDCSQL, bottleneck).Scan(&value) + switch { + case err == sql.ErrNoRows: + return nil, nil + case err != nil: + return nil, err + } + return []float64{value}, nil +} + +func breaksToReferenceValue(breaks string) []float64 { + parts := strings.Split(breaks, ",") + var values []float64 + + for _, part := range parts { + part = strings.TrimSpace(part) + if v, err := strconv.ParseFloat(part, 64); err == nil { + values = append(values, v) + } + } + + sort.Float64s(values) + + // dedup + for i := 1; i < len(values); { + if values[i-1] == values[i] { + copy(values[i:], values[i+1:]) + values = values[:len(values)-1] + } else { + i++ + } + } + return values +} + +func bottleneckAvailabilty(rw http.ResponseWriter, req *http.Request) { + + mode := intervalMode(req.FormValue("mode")) + bn := mux.Vars(req)["objnam"] + + if bn == "" { + http.Error( + rw, + "Missing objnam of bottleneck", + http.StatusBadRequest, + ) + return + } + + from, ok := parseFormTime(rw, req, "from", time.Now().AddDate(-1, 0, 0)) + if !ok { + return + } + + to, ok := parseFormTime(rw, req, "to", from.AddDate(1, 0, 0)) + if !ok { + return + } + + if to.Before(from) { + to, from = from, to + } + + los, ok := parseFormInt(rw, req, "los", 1) + if !ok { + return + } + + conn := middleware.GetDBConn(req) + ctx := req.Context() + + ldcRefs, err := loadLDCReferenceValue(ctx, conn, bn) + if err != nil { + http.Error( + rw, + fmt.Sprintf("Internal server error: %v", err), + http.StatusInternalServerError, + ) + return + } + + if len(ldcRefs) == 0 { + http.Error( + rw, + "No gauge reference values found for bottleneck", + http.StatusNotFound, + ) + return + } + + var breaks []float64 + if b := req.FormValue("breaks"); b != "" { + breaks = breaksToReferenceValue(b) + } else { + breaks = afdRefs + } + + log.Printf("info: time interval: (%v - %v)\n", from, to) + + var ms availMeasurements + if ms, err = loadDepthValues(ctx, conn, bn, los, from, to); err != nil { + return + } + + if len(ms) == 0 { + http.Error( + rw, + "No available fairway depth values found", + http.StatusNotFound, + ) + return + } + + rw.Header().Add("Content-Type", "text/csv") + + out := csv.NewWriter(rw) + + record := make([]string, 1+2+len(breaks)+1) + record[0] = "#time" + record[1] = fmt.Sprintf("# < LDC (%.1f) [h]", ldcRefs[0]) + record[2] = fmt.Sprintf("# >= LDC (%.1f) [h]", ldcRefs[0]) + for i, v := range breaks { + if i == 0 { + record[3] = fmt.Sprintf("#d < %.1f [%%]", v) + } + record[i+4] = fmt.Sprintf("#d >= %.1f [%%]", v) + } + + if err := out.Write(record); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + return + } + + interval := intervals[mode](from, to) + + for pfrom, pto, label := interval(); label != ""; pfrom, pto, label = interval() { + lnwl := ms.classify( + pfrom, pto, + ldcRefs, + (*availMeasurement).getValue, + ) + + afd := ms.classify( + pfrom, pto, + breaks, + (*availMeasurement).getDepth, + ) + + duration := pto.Sub(pfrom) + lnwlPercents := durationsToPercentage(duration, lnwl) + afdPercents := durationsToPercentage(duration, afd) + + record[0] = label + for i, v := range lnwlPercents { + record[1+i] = fmt.Sprintf("%.3f", v) + } + for i, v := range afdPercents { + record[3+i] = fmt.Sprintf("%.3f", v) + } + + if err := out.Write(record); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + return + } + } + + out.Flush() + if err := out.Error(); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + } + return +} + +func bottleneckAvailableFairwayDepth(rw http.ResponseWriter, req *http.Request) { + + mode := intervalMode(req.FormValue("mode")) + + bn := mux.Vars(req)["objnam"] + if bn == "" { + http.Error( + rw, "Missing objnam of bottleneck", + http.StatusBadRequest) + return + } + + from, ok := parseFormTime(rw, req, "from", time.Now().AddDate(-1, 0, 0)) + if !ok { + return + } + + to, ok := parseFormTime(rw, req, "to", from.AddDate(1, 0, 0)) + if !ok { + return + } + + if to.Before(from) { + to, from = from, to + } + + los, ok := parseFormInt(rw, req, "los", 1) + if !ok { + return + } + + conn := middleware.GetDBConn(req) + ctx := req.Context() + + var limiting string + err := conn.QueryRowContext(ctx, selectLimitingSQL, bn).Scan(&limiting) + switch { + case err == sql.ErrNoRows: + http.Error( + rw, fmt.Sprintf("Unknown limitation for %s.", bn), + http.StatusNotFound) + return + case err != nil: + http.Error( + rw, fmt.Sprintf("DB error: %v.", err), + http.StatusInternalServerError) + return + } + + access := limitingFactor(limiting) + + log.Printf("info: time interval: (%v - %v)\n", from, to) + + // load the measurements + ms, err := loadDepthValues(ctx, conn, bn, los, from, to) + if err != nil { + http.Error( + rw, fmt.Sprintf("Loading measurements failed: %v.", err), + http.StatusInternalServerError) + return + } + + ldcRefs, err := loadLDCReferenceValue(ctx, conn, bn) + if err != nil { + http.Error( + rw, fmt.Sprintf("Loading LDC failed: %v.", err), + http.StatusInternalServerError) + return + } + if len(ldcRefs) == 0 { + http.Error(rw, "No LDC found", http.StatusNotFound) + return + } + + var breaks []float64 + if b := req.FormValue("breaks"); b != "" { + breaks = breaksToReferenceValue(b) + } else { + breaks = afdRefs + } + + rw.Header().Add("Content-Type", "text/csv") + + out := csv.NewWriter(rw) + + // label, ldc, classes + record := make([]string, 1+2+len(breaks)+1) + record[0] = "#time" + record[1] = fmt.Sprintf("# < LDC (%.1f) [h]", ldcRefs[0]) + record[2] = fmt.Sprintf("# >= LDC (%.1f) [h]", ldcRefs[0]) + for i, v := range breaks { + if i == 0 { + record[3] = fmt.Sprintf("# < %.1f [h]", v) + } + record[i+4] = fmt.Sprintf("# >= %.1f [h]", v) + } + + if err := out.Write(record); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + return + } + + //log.Println(len(ms)) + //for i := range ms { + // log.Println(ms[i].when, ms[i].depth) + //} + + log.Printf("info: measurements: %d\n", len(ms)) + if len(ms) > 1 { + log.Printf("info: first: %v\n", ms[0].when) + log.Printf("info: last: %v\n", ms[len(ms)-1].when) + log.Printf("info: interval: %.2f [h]\n", ms[len(ms)-1].when.Sub(ms[0].when).Hours()) + } + + interval := intervals[mode](from, to) + + for pfrom, pto, label := interval(); label != ""; pfrom, pto, label = interval() { + + ldc := ms.classify( + pfrom, pto, + ldcRefs, + access, + ) + + ranges := ms.classify( + pfrom, pto, + breaks, + access, + ) + + record[0] = label + for i, v := range ldc { + record[i+1] = fmt.Sprintf("%.3f", v.Hours()) + } + + for i, d := range ranges { + record[3+i] = fmt.Sprintf("%.3f", d.Hours()) + } + + if err := out.Write(record); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + return + } + } + + out.Flush() + if err := out.Error(); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + } +} + +var intervals = []func(time.Time, time.Time) func() (time.Time, time.Time, string){ + monthly, + quarterly, + yearly, +} + +func monthly(from, to time.Time) func() (time.Time, time.Time, string) { + pfrom := from + return func() (time.Time, time.Time, string) { + if pfrom.After(to) { + return time.Time{}, time.Time{}, "" + } + f := pfrom + pfrom = pfrom.AddDate(0, 1, 0) + label := fmt.Sprintf("%02d-%d", f.Month(), f.Year()) + return f, f.AddDate(0, 1, 0).Add(-time.Nanosecond), label + } +} + +func quarterly(from, to time.Time) func() (time.Time, time.Time, string) { + pfrom := from + return func() (time.Time, time.Time, string) { + if pfrom.After(to) { + return time.Time{}, time.Time{}, "" + } + f := pfrom + pfrom = pfrom.AddDate(0, 3, 0) + label := fmt.Sprintf("Q%d-%d", (int(f.Month())-1)/3+1, f.Year()) + return f, f.AddDate(0, 3, 0).Add(-time.Nanosecond), label + } +} + +func yearly(from, to time.Time) func() (time.Time, time.Time, string) { + pfrom := from + return func() (time.Time, time.Time, string) { + if pfrom.After(to) { + return time.Time{}, time.Time{}, "" + } + f := pfrom + pfrom = pfrom.AddDate(1, 0, 0) + label := fmt.Sprintf("%d", f.Year()) + return f, f.AddDate(1, 0, 0).Add(-time.Nanosecond), label + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/controllers/common.go Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,126 @@ +// 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) 2019 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package controllers + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/jackc/pgx/pgtype" +) + +type ( + filterNode interface { + serialize(*strings.Builder, *[]interface{}) + } + + filterTerm struct { + format string + args []interface{} + } + + filterNot struct { + filterNode + } + + filterAnd []filterNode + filterOr []filterNode +) + +func (ft *filterTerm) serialize(stmt *strings.Builder, args *[]interface{}) { + indices := make([]interface{}, len(ft.args)) + for i := range indices { + indices[i] = len(*args) + i + 1 + } + fmt.Fprintf(stmt, ft.format, indices...) + *args = append(*args, (*ft).args...) +} + +func buildFilterTerm(format string, args ...interface{}) *filterTerm { + return &filterTerm{format: format, args: args} +} + +func (fa filterAnd) serialize(stmt *strings.Builder, args *[]interface{}) { + for i, node := range fa { + if i > 0 { + stmt.WriteString(" AND ") + } + stmt.WriteByte('(') + node.serialize(stmt, args) + stmt.WriteByte(')') + } +} + +func (fo filterOr) serialize(stmt *strings.Builder, args *[]interface{}) { + for i, node := range fo { + if i > 0 { + stmt.WriteString(" OR ") + } + stmt.WriteByte('(') + node.serialize(stmt, args) + stmt.WriteByte(')') + } +} + +func (fn *filterNot) serialize(stmt *strings.Builder, args *[]interface{}) { + stmt.WriteString("NOT (") + fn.filterNode.serialize(stmt, args) + stmt.WriteByte(')') +} + +func toInt8Array(txt string) *pgtype.Int8Array { + parts := strings.Split(txt, ",") + var ints []int64 + for _, part := range parts { + part = strings.TrimSpace(part) + v, err := strconv.ParseInt(part, 10, 64) + if err != nil { + continue + } + ints = append(ints, v) + } + var ia pgtype.Int8Array + if err := ia.Set(ints); err != nil { + log.Printf("warn: %v\n", err) + return nil + } + return &ia +} + +func toTextArray(txt string, allowed []string) *pgtype.TextArray { + parts := strings.Split(txt, ",") + var accepted []string + for _, part := range parts { + if part = strings.ToLower(strings.TrimSpace(part)); len(part) == 0 { + continue + } + for _, a := range allowed { + if part == a { + accepted = append(accepted, part) + break + } + } + } + if len(accepted) == 0 { + return nil + } + var ta pgtype.TextArray + if err := ta.Set(accepted); err != nil { + log.Printf("warn: %v\n", err) + return nil + } + return &ta +}
--- a/pkg/controllers/cross.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/cross.go Mon Jun 03 10:19:18 2019 +0200 @@ -21,7 +21,6 @@ "net/http" "time" - "gemma.intevation.de/gemma/pkg/common" "gemma.intevation.de/gemma/pkg/models" "gemma.intevation.de/gemma/pkg/octree" ) @@ -167,13 +166,6 @@ Type: "MultiLineString", Coordinates: joined, }, - Properties: map[string]interface{}{ - "waterlevel": map[string]interface{}{ - // TODO: Fetch values from database. - "value": float64(50), - "when": start.Format(common.TimeFormat), - }, - }, }, }
--- a/pkg/controllers/gauges.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/gauges.go Mon Jun 03 10:19:18 2019 +0200 @@ -14,16 +14,20 @@ package controllers import ( + "context" "database/sql" "encoding/csv" "fmt" "log" + "math" "net/http" "sort" "strconv" + "strings" "time" "github.com/gorilla/mux" + "github.com/jackc/pgx/pgtype" "gonum.org/v1/gonum/stat" "gemma.intevation.de/gemma/pkg/common" @@ -34,26 +38,39 @@ const ( selectPredictedObserveredSQL = ` SELECT - a.measure_date AS measure_date, - a.water_level AS predicted, - b.water_level AS observed -FROM waterway.gauge_measurements a JOIN waterway.gauge_measurements b - ON a.fk_gauge_id = b.fk_gauge_id AND - a.measure_date = b.measure_date AND - a.predicted AND NOT b.predicted + measure_date, + date_issue, + predicted, + water_level +FROM ( + SELECT + location, + measure_date, + date_issue, + false AS predicted, + water_level + FROM waterway.gauge_measurements + UNION ALL + SELECT + location, + measure_date, + date_issue, + true AS predicted, + water_level + FROM waterway.gauge_predictions +) AS gmp WHERE - a.fk_gauge_id = ( + location = ( $1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int ) AND - a.measure_date BETWEEN - $6::timestamp AND $6::timestamp - '72hours'::interval -ORDER BY a.measure_date + measure_date BETWEEN + $6::timestamp - '72hours'::interval AND $6::timestamp +ORDER BY measure_date, date_issue ` - selectWaterlevelsSQL = ` SELECT measure_date, @@ -61,26 +78,84 @@ value_min, value_max, predicted +FROM ( + SELECT + location, + measure_date, + date_issue, + water_level, + NULL AS value_min, + NULL AS value_max, + false AS predicted + FROM waterway.gauge_measurements + UNION ALL + SELECT + location, + measure_date, + date_issue, + water_level, + lower(conf_interval) AS value_min, + upper(conf_interval) AS value_max, + true AS predicted + FROM waterway.gauge_predictions +) AS gmp +WHERE +` + + selectAllWaterlevelsMeasuredRangeSQL = ` +SELECT + min(measure_date), + max(measure_date) FROM waterway.gauge_measurements WHERE + location = ( + $1::char(2), + $2::char(3), + $3::char(5), + $4::char(5), + $5::int + )::isrs + AND staging_done ` - selectWaterlevelsMeasuredSQL = ` + + selectAllWaterlevelsMeasuredSQL = ` +SELECT + extract(day from measure_date)::varchar || ':' || + extract(month from measure_date)::varchar AS day_month, + percentile_disc(0.25) within group (order by water_level) AS q25, + percentile_disc(0.5) within group (order by water_level) AS median, + percentile_disc(0.75) within group (order by water_level) AS q75, + avg(water_level) AS mean, + min(water_level) AS min, + max(water_level) AS max +FROM waterway.gauge_measurements +WHERE + location = ( + $1::char(2), + $2::char(3), + $3::char(5), + $4::char(5), + $5::int + )::isrs + AND staging_done +GROUP BY extract(day from measure_date)::varchar || ':' || + extract(month from measure_date)::varchar; +` + selectYearWaterlevelsMeasuredSQL = ` SELECT measure_date, water_level FROM waterway.gauge_measurements WHERE - NOT predicted - AND fk_gauge_id = ( + location = ( $1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int - ) - AND measure_date BETWEEN - $6::timestamp with time zone AND - $7::timestamp with time zone + )::isrs + AND staging_done + AND measure_date BETWEEN $6 AND $7 ORDER BY measure_date ` ) @@ -103,7 +178,100 @@ return "f" } -func averageWaterlevels(rw http.ResponseWriter, req *http.Request) { +func yearWaterlevels(rw http.ResponseWriter, req *http.Request) { + + gauge := mux.Vars(req)["gauge"] + + isrs, err := models.IsrsFromString(gauge) + if err != nil { + http.Error( + rw, fmt.Sprintf("error: Invalid ISRS code: %v", err), + http.StatusBadRequest) + return + } + + year, _ := strconv.Atoi(mux.Vars(req)["year"]) + + conn := middleware.GetDBConn(req) + + ctx := req.Context() + + begin := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC) + end := time.Date(year+1, time.January, 1, 0, 0, 0, 0, time.UTC).Add(-time.Microsecond) + + log.Printf("info: begin %s\n", begin) + log.Printf("info: end %s\n", end) + + rows, err := conn.QueryContext( + ctx, + selectYearWaterlevelsMeasuredSQL, + isrs.CountryCode, + isrs.LoCode, + isrs.FairwaySection, + isrs.Orc, + isrs.Hectometre, + begin, + end, + ) + if err != nil { + http.Error( + rw, fmt.Sprintf("error: %v", err), + http.StatusInternalServerError) + return + } + defer rows.Close() + + var values []float64 + + lastDay, lastMonth := -1, -1 + + write := func() error { + var err error + if len(values) > 0 { + mean := stat.Mean(values, nil) + _, err = fmt.Fprintf( + rw, "%02d-%02d,%s\n", lastDay, lastMonth, + float64format(mean)) + values = values[:0] + } + return err + } + + for rows.Next() { + var when time.Time + var value float64 + if err := rows.Scan(&when, &value); err != nil { + log.Printf("error: %v", err) + // Too late for an HTTP error code. + return + } + when = when.UTC() + day, month := when.Day(), int(when.Month()) + if day != lastDay || month != lastMonth { + if err := write(); err != nil { + log.Printf("error: %v", err) + // Too late for an HTTP error code. + return + } + lastDay, lastMonth = day, month + } + values = append(values, value) + } + + if err := rows.Err(); err != nil { + log.Printf("error: %v", err) + // Too late for an HTTP error code. + return + } + + if err := write(); err != nil { + log.Printf("error: %v", err) + // Too late for an HTTP error code. + } +} + +func longtermWaterlevels(rw http.ResponseWriter, req *http.Request) { + gauge := mux.Vars(req)["gauge"] isrs, err := models.IsrsFromString(gauge) @@ -114,52 +282,41 @@ return } - var from, to time.Time - - if t := req.FormValue("to"); t != "" { - var err error - if to, err = time.ParseInLocation(common.DateFormat, t, time.UTC); err != nil { - http.Error( - rw, fmt.Sprintf("error: bad from date: %v", err), - http.StatusBadRequest) - return - } - } else { - y, m, d := time.Now().Date() - to = time.Date(y, m, d, 0, 0, 0, 0, time.UTC) - } - - if f := req.FormValue("from"); f != "" { - var err error - if from, err = time.ParseInLocation(common.DateFormat, f, time.UTC); err != nil { - http.Error( - rw, fmt.Sprintf("error: bad from date: %v", err), - http.StatusBadRequest) - return - } - } else { - from = to.AddDate(-1, 0, 0) - } - - to = to.AddDate(0, 0, 1).Add(-time.Nanosecond) - - if to.Before(from) { - from, to = to, from - } - conn := middleware.GetDBConn(req) ctx := req.Context() - rows, err := conn.QueryContext( + var begin, end pgtype.Timestamp + + err = conn.QueryRowContext( ctx, - selectWaterlevelsMeasuredSQL, + selectAllWaterlevelsMeasuredRangeSQL, isrs.CountryCode, isrs.LoCode, isrs.FairwaySection, isrs.Orc, isrs.Hectometre, - from, to, + ).Scan(&begin, &end) + + switch { + case err == sql.ErrNoRows || begin.Status != pgtype.Present || end.Status != pgtype.Present: + http.NotFound(rw, req) + return + case err != nil: + http.Error( + rw, fmt.Sprintf("error: %v", err), + http.StatusInternalServerError) + return + } + + rows, err := conn.QueryContext( + ctx, + selectAllWaterlevelsMeasuredSQL, + isrs.CountryCode, + isrs.LoCode, + isrs.FairwaySection, + isrs.Orc, + isrs.Hectometre, ) if err != nil { http.Error( @@ -169,14 +326,84 @@ } defer rows.Close() + type result struct { + day int + month int + q25 float64 + median float64 + q75 float64 + mean float64 + min float64 + max float64 + } + + results := make([]result, 0, 366) + + start := time.Now() + + for rows.Next() { + var r result + var dayMonth string + if err := rows.Scan( + &dayMonth, + &r.q25, + &r.median, + &r.q75, + &r.mean, + &r.min, + &r.max, + ); err != nil { + http.Error( + rw, fmt.Sprintf("error: %v", err), + http.StatusInternalServerError) + } + parts := strings.SplitN(dayMonth, ":", 2) + r.day, _ = strconv.Atoi(parts[0]) + r.month, _ = strconv.Atoi(parts[1]) + results = append(results, r) + } + + if err := rows.Err(); err != nil { + http.Error( + rw, fmt.Sprintf("error: %v", err), + http.StatusInternalServerError) + return + } + + log.Printf("info: loading entries took %s\n", time.Since(start)) + + log.Printf("info: days found: %d\n", len(results)) + + sort.Slice(results, func(i, j int) bool { + if d := results[i].month - results[j].month; d != 0 { + return d < 0 + } + return results[i].day < results[j].day + }) + rw.Header().Add("Content-Type", "text/csv") out := csv.NewWriter(rw) - var last time.Time - var values []float64 + record := []string{ + fmt.Sprintf("#interval: %d-%d", + begin.Time.UTC().Year(), + end.Time.UTC().Year()), + "", + "", + "", + "", + "", + "", + } - record := []string{ + if err := out.Write(record); err != nil { + log.Printf("error: %v\n", err) + // Too late for an HTTP error code. + return + } + + record = []string{ "#date", "#min", "#max", @@ -192,62 +419,20 @@ return } - write := func() error { - if len(values) > 0 { - sort.Float64s(values) - // date - record[0] = last.Format(common.DateFormat) - // min - record[1] = float64format(values[0]) - // max - record[2] = float64format(values[len(values)-1]) - // mean - record[3] = float64format(stat.Mean(values, nil)) - // median - record[4] = float64format(values[len(values)/2]) - // Q25 - record[5] = float64format( - stat.Quantile(0.25, stat.Empirical, values, nil)) - // Q75 - record[6] = float64format( - stat.Quantile(0.75, stat.Empirical, values, nil)) - - err := out.Write(record) - values = values[:0] - return err - } - return nil - } - - for rows.Next() { - var ( - date time.Time - value float64 - ) - if err := rows.Scan(&date, &value); err != nil { + for i := range results { + r := &results[i] + record[0] = fmt.Sprintf("%02d-%02d", r.day, r.month) + record[1] = float64format(r.min) + record[2] = float64format(r.max) + record[3] = float64format(r.mean) + record[4] = float64format(r.median) + record[5] = float64format(r.q25) + record[6] = float64format(r.q75) + if err := out.Write(record); err != nil { log.Printf("error: %v\n", err) // Too late for an HTTP error code. return } - oy, om, od := last.Date() - ny, nm, nd := date.Date() - if oy != ny || om != nm || od != nd { - if err := write(); err != nil { - log.Printf("error: %v\n", err) - // Too late for an HTTP error code. - return - } - last = date - } else { - values = append(values, value) - } - } - write() - - if err := rows.Err(); err != nil { - log.Printf("error: %v", err) - // Too late for an HTTP error code. - return } out.Flush() @@ -256,7 +441,137 @@ // Too late for an HTTP error code. return } +} +func parseISRS(code string) (*models.Isrs, error) { + isrs, err := models.IsrsFromString(code) + if err != nil { + return nil, JSONError{ + Code: http.StatusBadRequest, + Message: fmt.Sprintf("error: Invalid ISRS code: %v", err), + } + } + return isrs, nil +} + +type observedPredictedValues struct { + when time.Time + observed float64 + predicted common.TimedValues +} + +func loadNashSutcliffeData( + ctx context.Context, + conn *sql.Conn, + gauge *models.Isrs, + when time.Time, +) ([]observedPredictedValues, error) { + + var rows *sql.Rows + var err error + if rows, err = conn.QueryContext( + ctx, + selectPredictedObserveredSQL, + gauge.CountryCode, + gauge.LoCode, + gauge.FairwaySection, + gauge.Orc, + gauge.Hectometre, + when, + ); err != nil { + return nil, err + } + defer rows.Close() + + acceptedDeltas := []time.Duration{ + -time.Hour * 24, + -time.Hour * 48, + -time.Hour * 72, + } + + isAccepted := func(observed, predicted time.Time) bool { + for _, delta := range acceptedDeltas { + t := observed.Add(delta) + d := predicted.Sub(t) + if -10*time.Millisecond < d && d < 10*time.Millisecond { + return true + } + } + return false + } + + var ( + hasCurrent bool + current observedPredictedValues + values []observedPredictedValues + ) + + for rows.Next() { + var ( + measureDate time.Time + issueDate time.Time + predicted bool + value float64 + ) + if err := rows.Scan( + &measureDate, + &issueDate, + &predicted, + &value, + ); err != nil { + return nil, err + } + measureDate = measureDate.UTC() + issueDate = issueDate.UTC() + + if hasCurrent { + if !current.when.Equal(measureDate) { + if !math.IsNaN(current.observed) && len(current.predicted) > 0 { + values = append(values, current) + } + current = observedPredictedValues{ + observed: math.NaN(), + when: measureDate, + } + } + } else { + hasCurrent = true + current = observedPredictedValues{ + observed: math.NaN(), + when: measureDate, + } + } + + if predicted { + if isAccepted(measureDate, issueDate) { + current.predicted = append( + current.predicted, + common.TimedValue{When: issueDate, Value: value}, + ) + } + } else { + current.observed = value + } + } + + if err := rows.Err(); err != nil { + return nil, err + } + + if hasCurrent && !math.IsNaN(current.observed) && len(current.predicted) > 0 { + values = append(values, current) + } + + // for i := range values { + // log.Printf("%v %f %d\n", values[i].when, values[i].observed, len(values[i].predicted)) + // if len(values[i].predicted) > 0 { + // for j := range values[i].predicted { + // log.Printf("\t%v %f\n", values[i].predicted[j].When, values[i].predicted[j].Value) + // } + // } + // } + + return values, nil } func nashSutcliffe( @@ -267,11 +582,7 @@ gauge := mux.Vars(req)["gauge"] var isrs *models.Isrs - if isrs, err = models.IsrsFromString(gauge); err != nil { - err = JSONError{ - Code: http.StatusBadRequest, - Message: fmt.Sprintf("error: Invalid ISRS code: %v", err), - } + if isrs, err = parseISRS(gauge); err != nil { return } @@ -287,40 +598,17 @@ } else { when = time.Now() } + when = when.UTC() ctx := req.Context() - var rows *sql.Rows - if rows, err = conn.QueryContext( - ctx, - selectPredictedObserveredSQL, - isrs.CountryCode, - isrs.LoCode, - isrs.FairwaySection, - isrs.Orc, - isrs.Hectometre, - when, - ); err != nil { + var values []observedPredictedValues + + if values, err = loadNashSutcliffeData(ctx, conn, isrs, when); err != nil { return } - defer rows.Close() - var measurements []common.NSMeasurement - - for rows.Next() { - var m common.NSMeasurement - if err = rows.Scan( - &m.When, - &m.Predicted, - &m.Observed, - ); err != nil { - return - } - measurements = append(measurements, m) - } - if err = rows.Err(); err != nil { - return - } + log.Printf("info: found %d value(s) for Nash Sutcliffe.\n", len(values)) type coeff struct { Value float64 `json:"value"` @@ -333,19 +621,31 @@ Coeffs []coeff `json:"coeffs"` } + var predicted, observed []float64 + cs := make([]coeff, 3) for i := range cs { cs[i].Hours = (i + 1) * 24 - cs[i].Value, cs[i].Samples = common.NashSutcliffe( - measurements, - when, - when.Add(time.Duration(-cs[i].Hours)*time.Hour), - ) + delta := -time.Duration(cs[i].Hours) * time.Hour + + for j := range values { + when := values[j].when.Add(delta) + if p, ok := values[j].predicted.Find(when); ok { + predicted = append(predicted, p) + observed = append(observed, values[j].observed) + } + } + + cs[i].Value = common.NashSutcliffe(predicted, observed) + cs[i].Samples = len(predicted) + + predicted = predicted[:0] + observed = observed[:0] } jr = JSONResult{ Result: &coeffs{ - When: models.ImportTime{when}, + When: models.ImportTime{Time: when}, Coeffs: cs, }, } @@ -363,17 +663,30 @@ return } - var fb filterBuilder - fb.stmt.WriteString(selectWaterlevelsSQL) - - fb.cond( - " fk_gauge_id = ($%d::char(2), $%d::char(3), $%d::char(5), $%d::char(5), $%d::int) ", - isrs.CountryCode, - isrs.LoCode, - isrs.FairwaySection, - isrs.Orc, - isrs.Hectometre, - ) + filters := filterAnd{ + buildFilterTerm( + "location = ($%d::char(2), $%d::char(3), $%d::char(5), $%d::char(5), $%d::int)", + isrs.CountryCode, + isrs.LoCode, + isrs.FairwaySection, + isrs.Orc, + isrs.Hectometre, + ), + &filterOr{ + &filterNot{&filterTerm{format: "predicted"}}, + buildFilterTerm( + `date_issue = ( + SELECT max(date_issue) + FROM waterway.gauge_measurements gm + WHERE location = ($%d::char(2), $%d::char(3), $%d::char(5), $%d::char(5), $%d::int))`, + isrs.CountryCode, + isrs.LoCode, + isrs.FairwaySection, + isrs.Orc, + isrs.Hectometre, + ), + }, + } if from := req.FormValue("from"); from != "" { fromTime, err := time.Parse(models.ImportTimeFormat, from) @@ -383,7 +696,7 @@ http.StatusBadRequest) return } - fb.cond("measure_date >= $%d", fromTime) + filters = append(filters, buildFilterTerm("measure_date >= $%d", fromTime)) } if to := req.FormValue("to"); to != "" { @@ -394,14 +707,20 @@ http.StatusBadRequest) return } - fb.cond("measure_date <= $%d", toTime) + filters = append(filters, buildFilterTerm("measure_date <= $%d", toTime)) } + var stmt strings.Builder + var args []interface{} + + stmt.WriteString(selectWaterlevelsSQL) + filters.serialize(&stmt, &args) + conn := middleware.GetDBConn(req) ctx := req.Context() - rows, err := conn.QueryContext(ctx, fb.stmt.String(), fb.args...) + rows, err := conn.QueryContext(ctx, stmt.String(), args...) if err != nil { http.Error( rw, fmt.Sprintf("error: %v", err),
--- a/pkg/controllers/importconfig.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/importconfig.go Mon Jun 03 10:19:18 2019 +0200 @@ -98,10 +98,17 @@ _, oldCron := pc.Attributes.Get("cron") session, _ := auth.GetSession(req) + // When a password is stored it doesn't get retransmitted to the client + // in order to prevent password leakage + // When the user changes the import configuration without a new password + // the old password should be conserved + oldPasswd, ok := pc.Attributes["password"] pc.User = session.User pc.Attributes = common.Attributes{} - pc.Attributes.Marshal(config) - + if ok == true { + pc.Attributes["password"] = oldPasswd + } + pc.Attributes.Marshal(config) // Marshal only overwrites keys present in config cron, newCron := pc.Attributes.Get("cron") var tx *sql.Tx @@ -177,9 +184,19 @@ return } - what := ctor() + // Remove `password` from the attributes to be delivered to the client. + // Even a priviledged user shall not be able to see the password. + // (See config.ListAllPersistentConfigurationsContext() for the other + // place where this is done.) + filteredAttributes := make(common.Attributes) + for key, value := range cfg.Attributes { + if key != "password" { + filteredAttributes[key] = value + } + } - if err = cfg.Attributes.Unmarshal(what); err != nil { + what := ctor() + if err = filteredAttributes.Unmarshal(what); err != nil { return }
--- a/pkg/controllers/importqueue.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/importqueue.go Mon Jun 03 10:19:18 2019 +0200 @@ -25,7 +25,6 @@ "time" "github.com/gorilla/mux" - "github.com/jackc/pgx/pgtype" "gemma.intevation.de/gemma/pkg/auth" "gemma.intevation.de/gemma/pkg/imports" @@ -65,8 +64,8 @@ SELECT enqueued FROM import.imports WHERE ` - selectImportSummaySQL = ` -SELECT summary FROM import.imports WHERE id = $1` + selectImportSummarySQL = ` +SELECT summary, enqueued FROM import.imports WHERE id = $1` selectHasNoRunningImportSQL = ` SELECT true FROM import.imports @@ -88,98 +87,22 @@ DELETE FROM import.imports WHERE id = $1` ) -func toInt8Array(txt string) *pgtype.Int8Array { - parts := strings.Split(txt, ",") - var ints []int64 - for _, part := range parts { - part = strings.TrimSpace(part) - v, err := strconv.ParseInt(part, 10, 64) - if err != nil { - continue - } - ints = append(ints, v) - } - var ia pgtype.Int8Array - if err := ia.Set(ints); err != nil { - log.Printf("warn: %v\n", err) - return nil - } - return &ia +type filledStmt struct { + stmt strings.Builder + args []interface{} } -func toTextArray(txt string, allowed []string) *pgtype.TextArray { - parts := strings.Split(txt, ",") - var accepted []string - for _, part := range parts { - if part = strings.ToLower(strings.TrimSpace(part)); len(part) == 0 { - continue - } - for _, a := range allowed { - if part == a { - accepted = append(accepted, part) - break - } - } - } - if len(accepted) == 0 { - return nil - } - var ta pgtype.TextArray - if err := ta.Set(accepted); err != nil { - log.Printf("warn: %v\n", err) - return nil - } - return &ta -} +func buildFilters(req *http.Request) (*filledStmt, *filledStmt, *filledStmt, error) { -type filterBuilder struct { - stmt strings.Builder - args []interface{} - hasCond bool -} - -func (fb *filterBuilder) arg(format string, v ...interface{}) { - indices := make([]interface{}, len(v)) - for i := range indices { - indices[i] = len(fb.args) + i + 1 - } - fmt.Fprintf(&fb.stmt, format, indices...) - fb.args = append(fb.args, v...) -} - -func (fb *filterBuilder) cond(format string, v ...interface{}) { - if fb.hasCond { - fb.stmt.WriteString(" AND ") - } else { - fb.hasCond = true - } - fb.arg(format, v...) -} - -func buildFilters(req *http.Request) (l, b, a *filterBuilder, err error) { - - l = new(filterBuilder) - a = new(filterBuilder) - b = new(filterBuilder) + var l, a, b filterAnd var noBefore, noAfter bool - var counting bool - - switch count := strings.ToLower(req.FormValue("count")); count { - case "1", "t", "true": - counting = true - l.stmt.WriteString(selectImportsCountSQL) - default: - l.stmt.WriteString(selectImportsSQL) - } - a.stmt.WriteString(selectAfterSQL) - b.stmt.WriteString(selectBeforeSQL) - - cond := func(format string, v ...interface{}) { - l.cond(format, v...) - a.cond(format, v...) - b.cond(format, v...) + cond := func(format string, args ...interface{}) { + term := &filterTerm{format: format, args: args} + l = append(l, term) + a = append(l, term) + b = append(b, term) } if query := req.FormValue("query"); query != "" { @@ -205,23 +128,23 @@ } if from := req.FormValue("from"); from != "" { - var fromTime time.Time - if fromTime, err = time.Parse(models.ImportTimeFormat, from); err != nil { - return + fromTime, err := time.Parse(models.ImportTimeFormat, from) + if err != nil { + return nil, nil, nil, err } - l.cond(" enqueued >= $%d ", fromTime) - b.cond(" enqueued < $%d", fromTime) + l = append(l, buildFilterTerm("enqueued >= $%d", fromTime)) + b = append(b, buildFilterTerm("enqueued < $%d", fromTime)) } else { noBefore = true } if to := req.FormValue("to"); to != "" { - var toTime time.Time - if toTime, err = time.Parse(models.ImportTimeFormat, to); err != nil { - return + toTime, err := time.Parse(models.ImportTimeFormat, to) + if err != nil { + return nil, nil, nil, err } - l.cond(" enqueued <= $%d ", toTime) - a.cond(" enqueued > $%d", toTime) + l = append(l, buildFilterTerm("enqueued <= $%d", toTime)) + a = append(a, buildFilterTerm("enqueued > $%d", toTime)) } else { noAfter = true } @@ -231,32 +154,58 @@ cond(" id IN (SELECT id FROM warned) ") } - if !l.hasCond { - l.stmt.WriteString(" TRUE ") + fl := &filledStmt{} + fa := &filledStmt{} + fb := &filledStmt{} + + fa.stmt.WriteString(selectAfterSQL) + fb.stmt.WriteString(selectBeforeSQL) + + var counting bool + + switch count := strings.ToLower(req.FormValue("count")); count { + case "1", "t", "true": + counting = true + fl.stmt.WriteString(selectImportsCountSQL) + default: + fl.stmt.WriteString(selectImportsSQL) } - if !b.hasCond { - b.stmt.WriteString(" TRUE ") + + if len(l) == 0 { + fl.stmt.WriteString(" TRUE ") + } else { + l.serialize(&fl.stmt, &fl.args) } - if !a.hasCond { - a.stmt.WriteString(" TRUE ") + + if len(b) == 0 { + fb.stmt.WriteString(" TRUE ") + } else { + b.serialize(&fb.stmt, &fb.args) + } + + if len(a) == 0 { + fa.stmt.WriteString(" TRUE ") + } else { + a.serialize(&fa.stmt, &fa.args) } if !counting { - l.stmt.WriteString(" ORDER BY enqueued DESC ") - a.stmt.WriteString(" ORDER BY enqueued LIMIT 1") - b.stmt.WriteString(" ORDER BY enqueued DESC LIMIT 1") + fl.stmt.WriteString(" ORDER BY enqueued DESC ") + fa.stmt.WriteString(" ORDER BY enqueued LIMIT 1") + fb.stmt.WriteString(" ORDER BY enqueued DESC LIMIT 1") } if noBefore { - b = nil + fb = nil } if noAfter { - a = nil + fa = nil } - return + + return fl, fb, fa, nil } -func neighbored(ctx context.Context, conn *sql.Conn, fb *filterBuilder) *models.ImportTime { +func neighbored(ctx context.Context, conn *sql.Conn, fb *filledStmt) *models.ImportTime { var when time.Time err := conn.QueryRowContext(ctx, fb.stmt.String(), fb.args...).Scan(&when) @@ -267,7 +216,7 @@ log.Printf("warn: %v\n", err) return nil } - return &models.ImportTime{when} + return &models.ImportTime{Time: when} } func listImports( @@ -276,7 +225,7 @@ conn *sql.Conn, ) (jr JSONResult, err error) { - var list, before, after *filterBuilder + var list, before, after *filledStmt if list, before, after, err = buildFilters(req); err != nil { return @@ -330,7 +279,7 @@ if signer.Valid { it.Signer = signer.String } - it.Enqueued = models.ImportTime{enqueued} + it.Enqueued = models.ImportTime{Time: enqueued} imports = append(imports, &it) } @@ -374,7 +323,11 @@ // Check if he have such a import job first. var summary sql.NullString - err = conn.QueryRowContext(ctx, selectImportSummaySQL, id).Scan(&summary) + var enqueued time.Time + err = conn.QueryRowContext(ctx, selectImportSummarySQL, id).Scan( + &summary, + &enqueued, + ) switch { case err == sql.ErrNoRows: err = JSONError{ @@ -418,11 +371,13 @@ jr = JSONResult{ Result: struct { - Summary interface{} `json:"summary,omitempty"` - Entries []*models.ImportLogEntry `json:"entries"` + Enqueued models.ImportTime `json:"enqueued"` + Summary interface{} `json:"summary,omitempty"` + Entries []*models.ImportLogEntry `json:"entries"` }{ - Summary: sum, - Entries: entries, + Enqueued: models.ImportTime{Time: enqueued}, + Summary: sum, + Entries: entries, }, } return @@ -573,12 +528,18 @@ err = tx.QueryRowContext(ctx, isPendingSQL, id).Scan(&pending, &kind) switch { case err == sql.ErrNoRows: - err = fmt.Errorf("cannot find import #%d", id) + err = JSONError{ + Code: http.StatusNotFound, + Message: fmt.Sprintf("cannot find import #%d", id), + } return case err != nil: return case !pending: - err = fmt.Errorf("import %d is not pending", id) + err = JSONError{ + Code: http.StatusConflict, + Message: fmt.Sprintf("import #%d is not pending", id), + } return }
--- a/pkg/controllers/printtemplates.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/printtemplates.go Mon Jun 03 10:19:18 2019 +0200 @@ -18,6 +18,7 @@ "database/sql" "encoding/json" "net/http" + "strings" "time" "github.com/gorilla/mux" @@ -32,62 +33,93 @@ listPrintTemplatesSQL = ` SELECT template_name, + template_type::varchar, date_info, country -FROM - users.templates -ORDER BY date_info DESC` +FROM users.templates +WHERE +` hasPrintTemplateSQL = ` -SELECT true FROM users.templates WHERE template_name = $1` +SELECT true FROM users.templates +WHERE template_name = $1 AND template_type = $2::template_types` deletePrintTemplateSQL = ` -DELETE FROM users.templates WHERE template_name = $1` +DELETE FROM users.templates +WHERE template_name = $1 AND template_type = $2::template_types` selectPrintTemplateSQL = ` -SELECT template_data FROM users.templates WHERE template_name = $1` +SELECT template_data FROM users.templates +WHERE template_name = $1 AND template_type = $2::template_types` insertPrintTemplateSQL = ` -INSERT INTO users.templates (template_name, template_data, country) +INSERT INTO users.templates (template_name, template_type, template_data, country) SELECT $1, - $2, + $2::template_types, + $3, CASE WHEN pg_has_role('sys_admin', 'MEMBER') THEN NULL ELSE users.current_user_country() END` updatePrintTemplateSQL = ` -UPDATE user.templates template_data = $2 WHERE template_name = $1` +UPDATE user.templates template_data = $2 +WHERE template_name = $1 AND template_type = $2::template_types` ) +var templateTypes = []string{"map", "diagram", "report"} + func listPrintTemplates( _ interface{}, req *http.Request, conn *sql.Conn, ) (jr JSONResult, err error) { + ts := mux.Vars(req)["type"] + if ts == "" { + if ts = req.FormValue("types"); ts == "" { + ts = strings.Join(templateTypes, ",") + } + } + + types := toTextArray(ts, templateTypes) + filter := buildFilterTerm("template_type = ANY($%d) ", types) + + var stmt strings.Builder + var args []interface{} + + stmt.WriteString(listPrintTemplatesSQL) + filter.serialize(&stmt, &args) + stmt.WriteString(" ORDER BY date_info DESC") + + var rows *sql.Rows + if rows, err = conn.QueryContext(req.Context(), stmt.String(), args...); err != nil { + return + } + defer rows.Close() + type template struct { Name string `json:"name"` + Type string `json:"type"` Time models.Time `json:"time"` Country *string `json:"country,omitempty"` } - var rows *sql.Rows - if rows, err = conn.QueryContext(req.Context(), listPrintTemplatesSQL); err != nil { - return - } - defer rows.Close() - templates := []*template{} for rows.Next() { var tmpl template var w time.Time var country sql.NullString - if err = rows.Scan(&tmpl.Name, &w, &country); err != nil { + if err = rows.Scan( + &tmpl.Name, + &tmpl.Type, + &w, + &country, + ); err != nil { return } - tmpl.Time = models.Time{w} + tmpl.Time = models.Time{Time: w} if country.Valid { tmpl.Country = &country.String } @@ -104,11 +136,12 @@ conn *sql.Conn, ) (jr JSONResult, err error) { - ctx := req.Context() - name := mux.Vars(req)["name"] + vars := mux.Vars(req) + name, typ := vars["name"], vars["type"] + ctx := req.Context() var data pgtype.Bytea - err = conn.QueryRowContext(ctx, selectPrintTemplateSQL, name).Scan(&data) + err = conn.QueryRowContext(ctx, selectPrintTemplateSQL, name, typ).Scan(&data) switch { case err == sql.ErrNoRows: @@ -136,8 +169,9 @@ conn *sql.Conn, ) (jr JSONResult, err error) { - ctx := req.Context() - name := mux.Vars(req)["name"] + vars := mux.Vars(req) + name, typ := vars["name"], vars["type"] + in := input.(*json.RawMessage) if name == "" { @@ -154,6 +188,8 @@ } return } + + ctx := req.Context() var tx *sql.Tx if tx, err = conn.BeginTx(ctx, nil); err != nil { return @@ -161,7 +197,7 @@ defer tx.Rollback() var dummy bool - err = tx.QueryRowContext(ctx, hasPrintTemplateSQL, name).Scan(&dummy) + err = tx.QueryRowContext(ctx, hasPrintTemplateSQL, name, typ).Scan(&dummy) switch { case err == sql.ErrNoRows: @@ -177,7 +213,7 @@ } data := pgtype.Bytea{Bytes: *in, Status: pgtype.Present} - if _, err = tx.ExecContext(ctx, insertPrintTemplateSQL, name, &data); err != nil { + if _, err = tx.ExecContext(ctx, insertPrintTemplateSQL, name, typ, &data); err != nil { return } @@ -199,9 +235,10 @@ conn *sql.Conn, ) (jr JSONResult, err error) { + vars := mux.Vars(req) + name, typ := vars["name"], vars["type"] + ctx := req.Context() - name := mux.Vars(req)["name"] - var tx *sql.Tx if tx, err = conn.BeginTx(ctx, nil); err != nil { return @@ -209,7 +246,7 @@ defer tx.Rollback() var dummy bool - err = tx.QueryRowContext(ctx, hasPrintTemplateSQL, name).Scan(&dummy) + err = tx.QueryRowContext(ctx, hasPrintTemplateSQL, name, typ).Scan(&dummy) switch { case err == sql.ErrNoRows: @@ -228,7 +265,7 @@ return } - if _, err = tx.ExecContext(ctx, deletePrintTemplateSQL, name); err != nil { + if _, err = tx.ExecContext(ctx, deletePrintTemplateSQL, name, typ); err != nil { return } @@ -251,8 +288,9 @@ conn *sql.Conn, ) (jr JSONResult, err error) { - ctx := req.Context() - name := mux.Vars(req)["name"] + vars := mux.Vars(req) + name, typ := vars["name"], vars["type"] + in := input.(*json.RawMessage) if name == "" { @@ -269,6 +307,8 @@ } return } + + ctx := req.Context() var tx *sql.Tx if tx, err = conn.BeginTx(ctx, nil); err != nil { return @@ -276,7 +316,7 @@ defer tx.Rollback() var dummy bool - err = tx.QueryRowContext(ctx, hasPrintTemplateSQL, name).Scan(&dummy) + err = tx.QueryRowContext(ctx, hasPrintTemplateSQL, name, typ).Scan(&dummy) switch { case err == sql.ErrNoRows: @@ -296,7 +336,7 @@ } data := pgtype.Bytea{Bytes: *in, Status: pgtype.Present} - if _, err = tx.ExecContext(ctx, updatePrintTemplateSQL, name, &data); err != nil { + if _, err = tx.ExecContext(ctx, updatePrintTemplateSQL, name, typ, &data); err != nil { return }
--- a/pkg/controllers/proxy.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/proxy.go Mon Jun 03 10:19:18 2019 +0200 @@ -27,7 +27,6 @@ "net/url" "regexp" "strings" - "time" "github.com/gorilla/mux" "golang.org/x/net/html/charset" @@ -118,7 +117,7 @@ ) { switch enc := h.Get("Content-Encoding"); { case strings.Contains(enc, "gzip"): - log.Println("info: gzip compression") + //log.Println("info: gzip compression") return func(r io.Reader) (io.ReadCloser, error) { return gzip.NewReader(r) }, @@ -126,7 +125,7 @@ return gzip.NewWriter(w), nil } case strings.Contains(enc, "deflate"): - log.Println("info: deflate compression") + //log.Println("info: deflate compression") return func(r io.Reader) (io.ReadCloser, error) { return flate.NewReader(r), nil }, @@ -134,7 +133,7 @@ return flate.NewWriter(w, flate.DefaultCompression) } default: - log.Println("info: no content compression") + //log.Println("info: no content compression") return func(r io.Reader) (io.ReadCloser, error) { if r2, ok := r.(io.ReadCloser); ok { return r2, nil @@ -177,13 +176,13 @@ } go func(force io.ReadCloser) { - start := time.Now() + //start := time.Now() defer func() { //r.Close() w.Close() pw.Close() force.Close() - log.Printf("info: rewrite took %s\n", time.Since(start)) + //log.Printf("info: rewrite took %s\n", time.Since(start)) }() if err := rewrite(suffix, w, r); err != nil { log.Printf("error: rewrite failed: %v\n", err)
--- a/pkg/controllers/routes.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/routes.go Mon Jun 03 10:19:18 2019 +0200 @@ -74,6 +74,11 @@ })).Methods(http.MethodGet) // System Settings + api.Handle("/system/config", any(&JSONHandler{ + Handle: getSystemConfig, + NoConn: true, + })).Methods(http.MethodGet) + api.Handle("/system/style/{feature}/{attr}", any(&JSONHandler{ Handle: getFeatureStyle, })).Methods(http.MethodGet) @@ -96,25 +101,31 @@ }).Methods(http.MethodGet) // Print templates - api.Handle("/templates/print", any(&JSONHandler{ + api.Handle("/templates", any(&JSONHandler{ Handle: listPrintTemplates, })).Methods(http.MethodGet) - api.Handle("/templates/print/{name}", any(&JSONHandler{ + tTypes := "{type:" + strings.Join(templateTypes, "|") + "}" + + api.Handle("/templates/"+tTypes, any(&JSONHandler{ + Handle: listPrintTemplates, + })).Methods(http.MethodGet) + + api.Handle("/templates/"+tTypes+"/{name}", any(&JSONHandler{ Handle: fetchPrintTemplate, })).Methods(http.MethodGet) - api.Handle("/templates/print/{name}", waterwayAdmin(&JSONHandler{ + api.Handle("/templates/"+tTypes+"/{name}", waterwayAdmin(&JSONHandler{ Input: func(*http.Request) interface{} { return &json.RawMessage{} }, Handle: createPrintTemplate, Limit: maxPrintTemplateSize, })).Methods(http.MethodPost) - api.Handle("/templates/print/{name}", waterwayAdmin(&JSONHandler{ + api.Handle("/templates/"+tTypes+"/{name}", waterwayAdmin(&JSONHandler{ Handle: deletePrintTemplate, })).Methods(http.MethodDelete) - api.Handle("/templates/print/{name}", waterwayAdmin(&JSONHandler{ + api.Handle("/templates/"+tTypes+"/{name}", waterwayAdmin(&JSONHandler{ Input: func(*http.Request) interface{} { return &json.RawMessage{} }, Handle: updatePrintTemplate, Limit: maxPrintTemplateSize, @@ -229,6 +240,7 @@ kinds := strings.Join([]string{ "bn", "gm", "fa", "wx", "wa", "wg", "dmv", "fd", "dma", + "sec", }, "|") api.Handle("/imports/{kind:"+kinds+"}", waterwayAdmin(&JSONHandler{ @@ -299,11 +311,26 @@ // Handler to serve data to the client. + api.Handle("/data/{kind:stretch|section}/availability/{name}", any( + middleware.DBConn(http.HandlerFunc(stretchAvailabilty)))).Methods(http.MethodGet) + + api.Handle("/data/{kind:stretch|section}/fairway-depth/{name}", any( + middleware.DBConn(http.HandlerFunc(stretchAvailableFairwayDepth)))).Methods(http.MethodGet) + + api.Handle("/data/bottleneck/fairway-depth/{objnam}", any( + middleware.DBConn(http.HandlerFunc(bottleneckAvailableFairwayDepth)))).Methods(http.MethodGet) + + api.Handle("/data/bottleneck/availability/{objnam}", any( + middleware.DBConn(http.HandlerFunc(bottleneckAvailabilty)))).Methods(http.MethodGet) + api.Handle("/data/waterlevels/{gauge}", any( middleware.DBConn(http.HandlerFunc(waterlevels)))).Methods(http.MethodGet) - api.Handle("/data/average-waterlevels/{gauge}", any( - middleware.DBConn(http.HandlerFunc(averageWaterlevels)))).Methods(http.MethodGet) + api.Handle("/data/longterm-waterlevels/{gauge}", any( + middleware.DBConn(http.HandlerFunc(longtermWaterlevels)))).Methods(http.MethodGet) + + api.Handle("/data/year-waterlevels/{gauge}/{year:[0-9]+}", any( + middleware.DBConn(http.HandlerFunc(yearWaterlevels)))).Methods(http.MethodGet) api.Handle("/data/nash-sutcliffe/{gauge}", any(&JSONHandler{ Handle: nashSutcliffe,
--- a/pkg/controllers/srimports.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/srimports.go Mon Jun 03 10:19:18 2019 +0200 @@ -126,7 +126,7 @@ session, _ := auth.GetSession(req) - sendEmail := req.FormValue("email") != "" + sendEmail := req.FormValue("send-email") != "" jobID, err := imports.AddJob( imports.SRJobKind,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/controllers/stretches.go Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,654 @@ +// 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) 2099 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package controllers + +import ( + "context" + "database/sql" + "encoding/csv" + "fmt" + "log" + "net/http" + "runtime" + "strings" + "sync" + "time" + + "gemma.intevation.de/gemma/pkg/middleware" + "github.com/gorilla/mux" +) + +const ( + selectSectionBottlenecks = ` +SELECT + distinct(b.objnam), + b.limiting +FROM waterway.sections s, waterway.bottlenecks b +WHERE ST_Intersects(b.area, s.area) AND s.name = $1` + + selectStretchBottlenecks = ` +SELECT + distinct(b.objnam), + b.limiting +FROM waterway.stretches s, waterway.bottlenecks b +WHERE ST_Intersects(b.area, s.area) AND s.name = $1` +) + +type ( + stretchBottleneck struct { + name string + limiting string + } + + stretchBottlenecks []stretchBottleneck + + fullStretchBottleneck struct { + *stretchBottleneck + measurements availMeasurements + ldc []float64 + breaks []float64 + access func(*availMeasurement) float64 + } +) + +func (bns stretchBottlenecks) contains(limiting string) bool { + for i := range bns { + if bns[i].limiting == limiting { + return true + } + } + return false +} + +func loadFullStretchBottleneck( + ctx context.Context, + conn *sql.Conn, + bn *stretchBottleneck, + los int, + from, to time.Time, + depthbreaks, widthbreaks []float64, +) (*fullStretchBottleneck, error) { + measurements, err := loadDepthValues(ctx, conn, bn.name, los, from, to) + if err != nil { + return nil, err + } + ldc, err := loadLDCReferenceValue(ctx, conn, bn.name) + if err != nil { + return nil, err + } + + var access func(*availMeasurement) float64 + var breaks []float64 + + switch bn.limiting { + case "width": + access = (*availMeasurement).getWidth + breaks = widthbreaks + case "depth": + access = (*availMeasurement).getDepth + breaks = depthbreaks + default: + log.Printf( + "warn: unknown limitation '%s'. default to 'depth'.\n", + bn.limiting) + access = (*availMeasurement).getDepth + breaks = depthbreaks + } + + return &fullStretchBottleneck{ + stretchBottleneck: bn, + measurements: measurements, + ldc: ldc, + breaks: breaks, + access: access, + }, nil +} + +func loadStretchBottlenecks( + ctx context.Context, + conn *sql.Conn, + stretch bool, + name string, +) (stretchBottlenecks, error) { + var sql string + if stretch { + sql = selectStretchBottlenecks + } else { + sql = selectSectionBottlenecks + } + + rows, err := conn.QueryContext(ctx, sql, name) + if err != nil { + return nil, err + } + defer rows.Close() + + var bns stretchBottlenecks + + for rows.Next() { + var bn stretchBottleneck + if err := rows.Scan( + &bn.name, + &bn.limiting, + ); err != nil { + return nil, err + } + bns = append(bns, bn) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return bns, nil +} + +func stretchAvailableFairwayDepth(rw http.ResponseWriter, req *http.Request) { + + vars := mux.Vars(req) + stretch := vars["kind"] == "stretch" + name := vars["name"] + mode := intervalMode(req.FormValue("mode")) + + depthbreaks, widthbreaks := afdRefs, afdRefs + + from, ok := parseFormTime(rw, req, "from", time.Now().AddDate(-1, 0, 0)) + if !ok { + return + } + + to, ok := parseFormTime(rw, req, "to", from.AddDate(1, 0, 0)) + if !ok { + return + } + + if to.Before(from) { + to, from = from, to + } + + los, ok := parseFormInt(rw, req, "los", 1) + if !ok { + return + } + + conn := middleware.GetDBConn(req) + ctx := req.Context() + + bns, err := loadStretchBottlenecks(ctx, conn, stretch, name) + if err != nil { + http.Error( + rw, fmt.Sprintf("DB error: %v.", err), + http.StatusInternalServerError) + return + } + + if len(bns) == 0 { + http.Error(rw, "No bottlenecks found.", http.StatusNotFound) + return + } + + if b := req.FormValue("depthbreaks"); b != "" { + depthbreaks = breaksToReferenceValue(b) + } + + if b := req.FormValue("widthbreaks"); b != "" { + widthbreaks = breaksToReferenceValue(b) + } + + useDepth, useWidth := bns.contains("depth"), bns.contains("width") + + if useDepth && useWidth && len(widthbreaks) != len(depthbreaks) { + http.Error( + rw, + fmt.Sprintf("class breaks lengths differ: %d != %d", + len(widthbreaks), len(depthbreaks)), + http.StatusBadRequest, + ) + return + } + + log.Printf("info: time interval: (%v - %v)\n", from, to) + + var loaded []*fullStretchBottleneck + var errors []error + + for i := range bns { + l, err := loadFullStretchBottleneck( + ctx, + conn, + &bns[i], + los, + from, to, + depthbreaks, widthbreaks, + ) + if err != nil { + log.Printf("error: %v\n", err) + errors = append(errors, err) + continue + } + loaded = append(loaded, l) + } + + if len(loaded) == 0 { + http.Error( + rw, + fmt.Sprintf("No bottleneck loaded: %v", joinErrors(errors)), + http.StatusInternalServerError, + ) + return + } + + n := runtime.NumCPU() / 2 + if n == 0 { + n = 1 + } + + type result struct { + label string + from time.Time + to time.Time + ldc []time.Duration + breaks []time.Duration + } + + jobCh := make(chan *result) + + var wg sync.WaitGroup + + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for res := range jobCh { + + var ldc, breaks []time.Duration + + for _, bn := range loaded { + l := bn.measurements.classify( + res.from, res.to, + bn.ldc, + bn.access, + ) + b := bn.measurements.classify( + res.from, res.to, + bn.breaks, + bn.access, + ) + + if ldc == nil { + ldc, breaks = l, b + } else { + for i, v := range l { + ldc[i] += v + } + for i, v := range b { + breaks[i] += v + } + } + } + + res.ldc = ldc + res.breaks = breaks + } + }() + } + + var results []*result + + interval := intervals[mode](from, to) + + var breaks []float64 + + if useDepth { + breaks = depthbreaks + } else { + breaks = widthbreaks + } + + for pfrom, pto, label := interval(); label != ""; pfrom, pto, label = interval() { + + res := &result{ + label: label, + from: pfrom, + to: pto, + } + results = append(results, res) + jobCh <- res + } + + close(jobCh) + wg.Wait() + + rw.Header().Add("Content-Type", "text/csv") + + out := csv.NewWriter(rw) + + // label, lnwl, classes + record := make([]string, 1+2+len(breaks)+1) + record[0] = "# time" + record[1] = "# < LDC [h]" + record[2] = "# >= LDC [h]" + for i, v := range breaks { + if useDepth && useWidth { + if i == 0 { + record[3] = "# < break_1 [h]" + } + record[i+4] = fmt.Sprintf("# >= break_%d", i+1) + } else { + if i == 0 { + record[3] = fmt.Sprintf("# < %.1f [h]", v) + } + record[i+4] = fmt.Sprintf("# >= %.1f [h]", v) + } + } + + if err := out.Write(record); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + return + } + + // Normalize to look like as we have only one bottleneck. + scale := 1 / float64(len(loaded)) + + empty := fmt.Sprintf("%.3f", 0.0) + for i := range record[1:] { + record[i+1] = empty + } + + for _, r := range results { + record[0] = r.label + for i, v := range r.ldc { + record[1+i] = fmt.Sprintf("%.3f", v.Hours()*scale) + } + + for i, d := range r.breaks { + record[3+i] = fmt.Sprintf("%.3f", d.Hours()*scale) + } + + if err := out.Write(record); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + return + } + } + + out.Flush() + if err := out.Error(); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + } +} + +func joinErrors(errors []error) string { + var b strings.Builder + for _, err := range errors { + if b.Len() > 0 { + b.WriteString(", ") + } + b.WriteString(err.Error()) + } + return b.String() +} + +func stretchAvailabilty(rw http.ResponseWriter, req *http.Request) { + + vars := mux.Vars(req) + stretch := vars["kind"] == "stretch" + name := vars["name"] + mode := intervalMode(req.FormValue("mode")) + + if name == "" { + http.Error( + rw, + fmt.Sprintf("Missing %s name", vars["kind"]), + http.StatusBadRequest, + ) + return + } + + from, ok := parseFormTime(rw, req, "from", time.Now().AddDate(-1, 0, 0)) + if !ok { + return + } + + to, ok := parseFormTime(rw, req, "to", from.AddDate(1, 0, 0)) + if !ok { + return + } + + if to.Before(from) { + to, from = from, to + } + + los, ok := parseFormInt(rw, req, "los", 1) + if !ok { + return + } + + depthbreaks, widthbreaks := afdRefs, afdRefs + + if b := req.FormValue("depthbreaks"); b != "" { + depthbreaks = breaksToReferenceValue(b) + } + + if b := req.FormValue("widthbreaks"); b != "" { + widthbreaks = breaksToReferenceValue(b) + } + + conn := middleware.GetDBConn(req) + ctx := req.Context() + + bns, err := loadStretchBottlenecks(ctx, conn, stretch, name) + if err != nil { + http.Error( + rw, fmt.Sprintf("DB error: %v.", err), + http.StatusInternalServerError) + return + } + + if len(bns) == 0 { + http.Error( + rw, + "No bottlenecks found.", + http.StatusNotFound, + ) + return + } + + useDepth, useWidth := bns.contains("depth"), bns.contains("width") + + if useDepth && useWidth && len(widthbreaks) != len(depthbreaks) { + http.Error( + rw, + fmt.Sprintf("class breaks lengths differ: %d != %d", + len(widthbreaks), len(depthbreaks)), + http.StatusBadRequest, + ) + return + } + + log.Printf("info: time interval: (%v - %v)\n", from, to) + + var loaded []*fullStretchBottleneck + var errors []error + + for i := range bns { + l, err := loadFullStretchBottleneck( + ctx, + conn, + &bns[i], + los, + from, to, + depthbreaks, widthbreaks, + ) + if err != nil { + log.Printf("error: %v\n", err) + errors = append(errors, err) + continue + } + loaded = append(loaded, l) + } + + if len(loaded) == 0 { + http.Error( + rw, + fmt.Sprintf("No bottleneck loaded: %v", joinErrors(errors)), + http.StatusInternalServerError, + ) + return + } + + n := runtime.NumCPU() / 2 + if n == 0 { + n = 1 + } + + type result struct { + label string + from time.Time + to time.Time + ldc []float64 + breaks []float64 + } + + jobCh := make(chan *result) + + var wg sync.WaitGroup + + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for res := range jobCh { + var ldc, breaks []time.Duration + + for _, bn := range loaded { + l := bn.measurements.classify( + from, to, + bn.ldc, + (*availMeasurement).getValue, + ) + + b := bn.measurements.classify( + from, to, + bn.breaks, + bn.access, + ) + + if ldc == nil { + ldc, breaks = l, b + } else { + for i, v := range l { + ldc[i] += v + } + for i, v := range b { + breaks[i] += v + } + } + } + + duration := res.to.Sub(res.from) * time.Duration(len(loaded)) + + res.ldc = durationsToPercentage(duration, ldc) + res.breaks = durationsToPercentage(duration, breaks) + } + }() + } + + var results []*result + + interval := intervals[mode](from, to) + + var breaks []float64 + + if useDepth { + breaks = depthbreaks + } else { + breaks = widthbreaks + } + + for pfrom, pto, label := interval(); label != ""; pfrom, pto, label = interval() { + + res := &result{ + label: label, + from: pfrom, + to: pto, + } + results = append(results, res) + + jobCh <- res + } + + close(jobCh) + wg.Wait() + + rw.Header().Add("Content-Type", "text/csv") + + out := csv.NewWriter(rw) + + // label, lnwl, classes + record := make([]string, 1+2+len(breaks)+1) + record[0] = "# time" + record[1] = "# < LDC [%%]" + record[2] = "# >= LDC [%%]" + for i, v := range breaks { + if useDepth && useWidth { + if i == 0 { + record[3] = "# < break_1 [%%]" + } + record[i+4] = fmt.Sprintf("# >= break_%d [%%]", i+1) + } else { + if i == 0 { + record[3] = fmt.Sprintf("# < %.3f [%%]", v) + } + record[i+4] = fmt.Sprintf("# >= %.3f [%%]", v) + } + } + + if err := out.Write(record); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + return + } + + empty := fmt.Sprintf("%.3f", 0.0) + for i := range record[1:] { + record[i+1] = empty + } + + for _, res := range results { + record[0] = res.label + + for i, v := range res.ldc { + record[1+i] = fmt.Sprintf("%.3f", v) + } + + for i, v := range res.breaks { + record[3+i] = fmt.Sprintf("%.3f", v) + } + + if err := out.Write(record); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + return + } + } + + out.Flush() + if err := out.Error(); err != nil { + // Too late for HTTP status message. + log.Printf("error: %v\n", err) + } +}
--- a/pkg/controllers/surveys.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/surveys.go Mon Jun 03 10:19:18 2019 +0200 @@ -29,19 +29,15 @@ s.bottleneck_id, s.date_info::text, s.depth_reference, - bg.objname AS gauge_objname, + g.objname AS gauge_objname, r.value AS waterlevel_value -FROM - ( - ( SELECT * FROM waterway.bottlenecks AS b, waterway.gauges AS g - WHERE b.fk_g_fid = g.location - ) AS bg - JOIN waterway.sounding_results AS s - ON bg.id = s.bottleneck_id - ) -LEFT JOIN waterway.gauges_reference_water_levels AS r -ON s.depth_reference = r.depth_reference AND bg.location = r.gauge_id -WHERE bg.objnam=$1` +FROM waterway.bottlenecks AS b + JOIN waterway.gauges AS g + ON b.gauge_location = g.location AND b.gauge_validity = g.validity + JOIN waterway.sounding_results AS s ON b.id = s.bottleneck_id + LEFT JOIN waterway.gauges_reference_water_levels AS r + USING (depth_reference, location, validity) +WHERE b.objnam = $1` ) func listSurveys(
--- a/pkg/controllers/system.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/system.go Mon Jun 03 10:19:18 2019 +0200 @@ -14,12 +14,14 @@ package controllers import ( + "bytes" "database/sql" "fmt" "io/ioutil" "net/http" "strings" + "gemma.intevation.de/gemma/pkg/config" "gemma.intevation.de/gemma/pkg/models" "github.com/gorilla/mux" ) @@ -78,6 +80,26 @@ return } +func getSystemConfig( + _ interface{}, req *http.Request, + _ *sql.Conn, +) (jr JSONResult, err error) { + + cfg := config.PublishedConfig() + if cfg == "" { + jr = JSONResult{Result: strings.NewReader("{}")} + return + } + + var data []byte + if data, err = ioutil.ReadFile(cfg); err != nil { + return + } + + jr = JSONResult{Result: bytes.NewReader(data)} + return +} + // Map/Feature style end points func getFeatureStyle(
--- a/pkg/controllers/uploadedimports.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/uploadedimports.go Mon Jun 03 10:19:18 2019 +0200 @@ -178,7 +178,7 @@ session, _ := auth.GetSession(req) - sendEmail := req.FormValue("email") != "" + sendEmail := req.FormValue("send-email") != "" jobID, err := imports.AddJob( kind,
--- a/pkg/controllers/user.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/controllers/user.go Mon Jun 03 10:19:18 2019 +0200 @@ -403,10 +403,10 @@ var bodyTmpl *template.Template if userData.Role == "sys_admin" { - subject = "Sysadmin Notification TEST" + subject = "Gemma: Sysadmin Notification TEST" bodyTmpl = testSysadminNotifyMailTmpl } else if userData.Role == "waterway_admin" { - subject = "Waterway Admin Notification TEST" + subject = "Gemma: Waterway Admin Notification TEST" bodyTmpl = testWWAdminNotifyMailTmpl } else { err = JSONError{
--- a/pkg/geoserver/boot.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/geoserver/boot.go Mon Jun 03 10:19:18 2019 +0200 @@ -24,6 +24,8 @@ "net/url" "strings" + "golang.org/x/net/html/charset" + "gemma.intevation.de/gemma/pkg/config" "gemma.intevation.de/gemma/pkg/models" ) @@ -172,6 +174,7 @@ {"Primary key metadata table", primaryKeyMetadataTbl}, {"Session startup SQL", startupSQL}, {"Session close-up SQL", closeupSQL}, + {"validate connections", true}, {"Estimated extends", false}, }, }, @@ -261,11 +264,20 @@ hasFeature = func(string) bool { return false } } + var already []string + + defer func() { + if len(already) > 0 { + log.Printf("info: already having featuretypes: %s\n", + strings.Join(already, ", ")) + } + }() + for i := range tables { table := tables[i].Name if hasFeature(table) { - log.Printf("info: featuretype %s already exists.\n", table) + already = append(already, table) continue } @@ -495,6 +507,8 @@ // isSymbologyEncoding tries to figure out if its plain SLD or SE. func isSymbologyEncoding(data string) bool { decoder := xml.NewDecoder(strings.NewReader(data)) + decoder.CharsetReader = charset.NewReaderLabel + for { tok, err := decoder.Token() switch { @@ -524,10 +538,19 @@ models.IntWMS, models.IntWithStyle)) + var already []string + + defer func() { + if len(already) > 0 { + log.Printf("info: already having styles: %s\n", + strings.Join(already, ", ")) + } + }() + for i := range entries { entry := &entries[i] if stls.hasStyle(entry.Name) { - log.Printf("warn: already has style for %s\n", entry.Name) + already = append(already, entry.Name) continue } if err := updateStyle(entry, true); err != nil {
--- a/pkg/imports/agm.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/agm.go Mon Jun 03 10:19:18 2019 +0200 @@ -20,6 +20,7 @@ "database/sql" "encoding/csv" "encoding/json" + "errors" "fmt" "io" "math" @@ -55,10 +56,10 @@ func (agmJobCreator) Create() Job { return new(ApprovedGaugeMeasurements) } -func (agmJobCreator) Depends() []string { - return []string{ - "gauges", - "gauge_measurements", +func (agmJobCreator) Depends() [2][]string { + return [2][]string{ + {"gauge_measurements"}, + {"gauges"}, } } @@ -75,8 +76,8 @@ SELECT o.id AS id FROM waterway.gauge_measurements o JOIN waterway.gauge_measurements n - ON n.fk_gauge_id = o.fk_gauge_id AND n.measure_date = o.measure_date - WHERE n.id IN (SELECT key FROM staged) + USING (location, measure_date) + WHERE n.id IN (SELECT key FROM staged) AND o.id NOT IN (SELECT key FROM staged) ) DELETE FROM waterway.gauge_measurements WHERE id IN (SELECT id from to_delete)` @@ -119,38 +120,23 @@ } type agmLine struct { - CountryCode string `json:"country-code"` - Sender string `json:"sender"` - LanguageCode string `json:"language-code"` - DateIssue timetz `json:"date-issue"` - ReferenceCode string `json:"reference-code"` - WaterLevel float64 `json:"water-level"` - Predicted bool `json:"predicted"` - ValueMin *float64 `json:"value-min"` - ValueMax *float64 `json:"value-max"` - DateInfo timetz `json:"date-info"` - SourceOrganization string `json:"source-organization"` + CountryCode string `json:"country-code"` + Sender string `json:"sender"` + LanguageCode string `json:"language-code"` + DateIssue timetz `json:"date-issue"` + ReferenceCode string `json:"reference-code"` + WaterLevel float64 `json:"water-level"` + DateInfo timetz `json:"date-info"` + SourceOrganization string `json:"source-organization"` } func (a *agmLine) hasDiff(b *agmLine) bool { const eps = 0.00001 - fdiff := func(x, y *float64) bool { - if x == nil && y == nil { - return false - } - if (x == nil && y != nil) || (x != nil && y == nil) { - return true - } - return math.Abs(*x-*y) > eps - } return a.CountryCode != b.CountryCode || a.Sender != b.Sender || a.LanguageCode != b.LanguageCode || a.ReferenceCode != b.ReferenceCode || math.Abs(a.WaterLevel-b.WaterLevel) > eps || - a.Predicted != b.Predicted || - fdiff(a.ValueMin, b.ValueMin) || - fdiff(a.ValueMax, b.ValueMax) || a.SourceOrganization != b.SourceOrganization } @@ -170,19 +156,20 @@ date_issue, reference_code, water_level, - predicted, - value_min, - value_max, date_info, source_organization FROM waterway.gauge_measurements WHERE - fk_gauge_id = ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int) AND - measure_date = $6 AND staging_done` + location + = ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int) + AND measure_date = $6 + AND staging_done +` agmInsertSQL = ` INSERT INTO waterway.gauge_measurements ( - fk_gauge_id, + location, + validity, measure_date, country_code, sender, @@ -190,15 +177,17 @@ date_issue, reference_code, water_level, - predicted, - value_min, - value_max, date_info, source_organization, - is_waterlevel, staging_done -) VALUES( +) VALUES ( ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int), + COALESCE( + (SELECT validity FROM waterway.gauges + WHERE location + = ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int) + AND validity @> CAST($6 AS timestamp with time zone)), + tstzrange(NULL, NULL)), $6, $7, $8, @@ -208,10 +197,6 @@ $12, $13, $14, - $15, - $16, - $17, - true, false ) RETURNING id` @@ -223,6 +208,8 @@ ` ) +var errContinue = errors.New("continue") + // Do executes the actual approved gauge measurements import. func (agm *ApprovedGaugeMeasurements) Do( ctx context.Context, @@ -290,28 +277,25 @@ return nil, fmt.Errorf("Missing columns: %s", strings.Join(missing, ", ")) } - tx, err := conn.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - gaugeCheckStmt, err := tx.PrepareContext(ctx, agmGaugeCheckSQL) + gaugeCheckStmt, err := conn.PrepareContext(ctx, agmGaugeCheckSQL) if err != nil { return nil, err } defer gaugeCheckStmt.Close() - selectStmt, err := tx.PrepareContext(ctx, agmSelectSQL) + + selectStmt, err := conn.PrepareContext(ctx, agmSelectSQL) if err != nil { return nil, err } defer selectStmt.Close() - insertStmt, err := tx.PrepareContext(ctx, agmInsertSQL) + + insertStmt, err := conn.PrepareContext(ctx, agmInsertSQL) if err != nil { return nil, err } defer insertStmt.Close() - trackStmt, err := tx.PrepareContext(ctx, trackImportSQL) + + trackStmt, err := conn.PrepareContext(ctx, trackImportSQL) if err != nil { return nil, err } @@ -321,12 +305,24 @@ checkedGauges := map[models.Isrs]bool{} + warnLimiter := misc.WarningLimiter{Log: feedback.Warn, MaxWarnings: 100} + warn := warnLimiter.Warn + defer warnLimiter.Close() + lines: - for line := 1; ; line++ { + for line, ignored := 1, 0; ; line++ { row, err := r.Read() switch { case err == io.EOF || len(row) == 0: + feedback.Info("Read %d entries in CSV file", line-1) + if ignored > 0 { + feedback.Info("%d entries ignored", ignored) + } + if ignored == line-1 { + return nil, UnchangedError("No entries imported") + } + feedback.Info("Imported %d entries with changes", len(entries)) break lines case err != nil: return nil, fmt.Errorf("CSV parsing failed: %v", err) @@ -340,7 +336,8 @@ if exists, found := checkedGauges[*gid]; found { if !exists { - feedback.Warn("Ignoring data for unknown gauge %s", gid.String()) + // Just ignore the line since we have already warned + ignored++ continue lines } } else { // not found in gauge cache @@ -356,7 +353,8 @@ } checkedGauges[*gid] = exists if !exists { - feedback.Warn("Ignoring data for unknown gauge %s", gid.String()) + warn("Ignoring data for unknown gauge %s", gid.String()) + ignored++ continue lines } } @@ -374,9 +372,6 @@ oldDateIssue time.Time oldReferenceCode string oldValue float64 - oldPredicted bool - oldValueMin sql.NullFloat64 - oldValueMax sql.NullFloat64 oldDateInfo time.Time oldSourceOrganization string ) @@ -397,9 +392,6 @@ &oldDateIssue, &oldReferenceCode, &oldValue, - &oldPredicted, - &oldValueMin, - &oldValueMax, &oldDateInfo, &oldSourceOrganization, ) @@ -425,48 +417,55 @@ } newValue := value - newPredicted := false - - newValueMin := sql.NullFloat64{ - Float64: 0, - Valid: true, - } - newValueMax := sql.NullFloat64{ - Float64: 0, - Valid: true, - } - newDateInfo := newDateIssue newSourceOrganization := newSender - var newID int64 + switch err := func() error { + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + var newID int64 - if err := insertStmt.QueryRowContext( - ctx, - gid.CountryCode, - gid.LoCode, - gid.FairwaySection, - gid.Orc, - gid.Hectometre, - md, - newCountryCode, - newSender, - newLanguageCode, - newDateIssue, - newReferenceCode, - newValue, - newPredicted, - newValueMin, - newValueMax, - newDateInfo, - newSourceOrganization, - ).Scan(&newID); err != nil { - return nil, err - } - if _, err := trackStmt.ExecContext( - ctx, importID, "waterway.gauge_measurements", newID, - ); err != nil { + if err := tx.StmtContext(ctx, insertStmt).QueryRowContext( + ctx, + gid.CountryCode, + gid.LoCode, + gid.FairwaySection, + gid.Orc, + gid.Hectometre, + md, + newCountryCode, + newSender, + newLanguageCode, + newDateIssue, + newReferenceCode, + newValue, + newDateInfo, + newSourceOrganization, + ).Scan(&newID); err != nil { + warn(handleError(err).Error()) + ignored++ + return errContinue + } + + if _, err := tx.StmtContext(ctx, trackStmt).ExecContext( + ctx, importID, "waterway.gauge_measurements", newID, + ); err != nil { + return err + } + + if err = tx.Commit(); err != nil { + err = fmt.Errorf("Commit failed: %v", err) + } + return err + }(); { + case err == errContinue: + continue lines + case err != nil: return nil, err } @@ -477,9 +476,6 @@ newDateIssue, newReferenceCode, newValue, - newPredicted, - newValueMin, - newValueMax, newDateInfo, newSourceOrganization, ) @@ -499,9 +495,6 @@ oldDateIssue, oldReferenceCode, oldValue, - oldPredicted, - oldValueMin, - oldValueMax, oldDateInfo, oldSourceOrganization, ) @@ -514,10 +507,6 @@ entries = append(entries, ase) } - if err := tx.Commit(); err != nil { - return nil, fmt.Errorf("Commit failed: %v", err) - } - feedback.Info("Importing approved gauge measurements took %s", time.Since(start)) @@ -531,19 +520,9 @@ dateIssue time.Time, referenceCode string, waterLevel float64, - predicted bool, - valueMin sql.NullFloat64, - valueMax sql.NullFloat64, dateInfo time.Time, sourceOrganization string, ) *agmLine { - nilFloat := func(v sql.NullFloat64) *float64 { - var p *float64 - if v.Valid { - p = &v.Float64 - } - return p - } return &agmLine{ CountryCode: countryCode, Sender: sender, @@ -551,9 +530,6 @@ DateIssue: timetz{dateIssue}, ReferenceCode: referenceCode, WaterLevel: waterLevel, - Predicted: predicted, - ValueMin: nilFloat(valueMin), - ValueMax: nilFloat(valueMax), DateInfo: timetz{dateInfo}, SourceOrganization: sourceOrganization, }
--- a/pkg/imports/bn.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/bn.go Mon Jun 03 10:19:18 2019 +0200 @@ -20,6 +20,7 @@ "errors" "regexp" "strconv" + "strings" "time" "gemma.intevation.de/gemma/pkg/soap/ifbn" @@ -45,9 +46,15 @@ SELECT true FROM waterway.bottlenecks WHERE bottleneck_id = $1` insertBottleneckSQL = ` +WITH +bounds (b) AS (VALUES (isrs_fromText($5)), (isrs_fromText($6))), +r AS (SELECT isrsrange( + (SELECT b FROM bounds ORDER BY b USING <~ FETCH FIRST ROW ONLY), + (SELECT b FROM bounds ORDER BY b USING >~ FETCH FIRST ROW ONLY)) AS r) INSERT INTO waterway.bottlenecks ( bottleneck_id, - fk_g_fid, + gauge_location, + gauge_validity, objnam, nobjnm, stretch, @@ -59,16 +66,18 @@ limiting, date_info, source_organization -) VALUES( +) VALUES ( $1, isrs_fromText($2), + COALESCE( + (SELECT validity FROM waterway.gauges + WHERE location = isrs_fromText($2) AND NOT erased), + tstzrange(NULL, NULL)), $3, $4, - isrsrange(least(isrs_fromText($5), isrs_fromText($6)), - greatest(isrs_fromText($5), isrs_fromText($6))), + (SELECT r FROM r), ISRSrange_area( - ISRSrange_axis(isrsrange(least(isrs_fromText($5), isrs_fromText($6)), - greatest(isrs_fromText($5), isrs_fromText($6))), + ISRSrange_axis((SELECT r FROM r), $14), (SELECT ST_Collect(CAST(area AS geometry)) FROM waterway.waterway_area)), @@ -81,6 +90,15 @@ $13 ) RETURNING id` + + insertBottleneckMaterialSQL = ` +INSERT INTO waterway.bottlenecks_riverbed_materials ( + bottleneck_id, + riverbed +) VALUES ( + $1, + $2 +)` ) type bnJobCreator struct{} @@ -95,10 +113,10 @@ func (bnJobCreator) Create() Job { return new(Bottleneck) } -func (bnJobCreator) Depends() []string { - return []string{ - "gauges", - "bottlenecks", +func (bnJobCreator) Depends() [2][]string { + return [2][]string{ + {"bottlenecks", "bottlenecks_riverbed_materials"}, + {"gauges", "distance_marks_virtual", "waterway_axis", "waterway_area"}, } } @@ -134,14 +152,6 @@ return &m[1], &m[2] } -func revisitingTime(s string) int { - v, err := strconv.Atoi(s) - if err != nil { - v = 0 - } - return v -} - // Do executes the actual bottleneck import. func (bn *Bottleneck) Do( ctx context.Context, @@ -161,7 +171,8 @@ } if resp.Export_bn_by_isrsResult == nil { - return nil, errors.New("no Bottlenecks found") + return nil, errors.New( + "The requested service returned no bottlenecks") } return resp.Export_bn_by_isrsResult.BottleNeckType, nil @@ -187,7 +198,7 @@ feedback.Info("Found %d bottlenecks for import", len(bns)) - var hasStmt, insertStmt, trackStmt *sql.Stmt + var hasStmt, insertStmt, insertMaterialStmt, trackStmt *sql.Stmt for _, x := range []struct { sql string @@ -195,6 +206,7 @@ }{ {hasBottleneckSQL, &hasStmt}, {insertBottleneckSQL, &insertStmt}, + {insertBottleneckMaterialSQL, &insertMaterialStmt}, {trackImportSQL, &trackStmt}, } { var err error @@ -211,7 +223,7 @@ for _, bn := range bns { if err := storeBottleneck( ctx, importID, conn, feedback, bn, &nids, tolerance, - hasStmt, insertStmt, trackStmt); err != nil { + hasStmt, insertStmt, insertMaterialStmt, trackStmt); err != nil { return nil, err } } @@ -237,7 +249,7 @@ bn *ifbn.BottleNeckType, nids *[]string, tolerance float64, - hasStmt, insertStmt, trackStmt *sql.Stmt, + hasStmt, insertStmt, insertMaterialStmt, trackStmt *sql.Stmt, ) error { tx, err := conn.BeginTx(ctx, nil) @@ -247,7 +259,8 @@ defer tx.Rollback() var found bool - err = tx.Stmt(hasStmt).QueryRowContext(ctx, bn.Bottleneck_id).Scan(&found) + err = tx.StmtContext(ctx, hasStmt).QueryRowContext(ctx, + bn.Bottleneck_id).Scan(&found) switch { case err == sql.ErrNoRows: // This is good. @@ -261,6 +274,19 @@ rb, lb := splitRBLB(bn.Rb_lb) + var revisitingTime *int + if bn.Revisiting_time != nil && + len(strings.TrimSpace(*bn.Revisiting_time)) > 0 { + i, err := strconv.Atoi(*bn.Revisiting_time) + if err != nil { + feedback.Warn( + "Cannot convert given revisiting time '%s' to number of months", + *bn.Revisiting_time) + } else { + revisitingTime = &i + } + } + var limiting, country string if bn.Limiting_factor != nil { @@ -273,7 +299,7 @@ var nid int64 - err = tx.Stmt(insertStmt).QueryRowContext( + err = tx.StmtContext(ctx, insertStmt).QueryRowContext( ctx, bn.Bottleneck_id, bn.Fk_g_fid, @@ -283,7 +309,7 @@ rb, lb, country, - revisitingTime(bn.Revisiting_time), + revisitingTime, limiting, bn.Date_Info, bn.Source, @@ -295,7 +321,23 @@ return nil } - if _, err := tx.Stmt(trackStmt).ExecContext( + if bn.Riverbed != nil { + for _, material := range bn.Riverbed.Material { + if material != nil { + mat := string(*material) + if _, err := tx.StmtContext(ctx, + insertMaterialStmt).ExecContext( + ctx, nid, material); err != nil { + feedback.Warn( + "Failed to insert riverbed material '%s' for bottleneck '%s'.", + mat, bn.OBJNAM) + feedback.Warn(handleError(err).Error()) + } + } + } + } + + if _, err := tx.StmtContext(ctx, trackStmt).ExecContext( ctx, importID, "waterway.bottlenecks", nid, ); err != nil { return err
--- a/pkg/imports/config.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/config.go Mon Jun 03 10:19:18 2019 +0200 @@ -269,7 +269,12 @@ if pc.Attributes == nil { pc.Attributes = common.Attributes{} } - pc.Attributes.Set(k.String, v.String) + // Prevent sending the `password` back to the client. + // (See importconfig.infoImportConfig() for the other place + // where this is done.) + if k.String != "password" { + pc.Attributes.Set(k.String, v.String) + } } }
--- a/pkg/imports/dma.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/dma.go Mon Jun 03 10:19:18 2019 +0200 @@ -56,9 +56,10 @@ func (dmaJobCreator) Create() Job { return new(DistanceMarksAshore) } -func (dmaJobCreator) Depends() []string { - return []string{ - "distance_marks", +func (dmaJobCreator) Depends() [2][]string { + return [2][]string{ + {"distance_marks"}, + {}, } } @@ -77,36 +78,28 @@ const ( deleteDistanceMarksSQL = ` WITH resp AS ( - SELECT best_utm(area::geometry) AS t, - ST_Transform(area::geometry, best_utm(area::geometry)) AS a - FROM users.responsibility_areas - WHERE country = users.current_user_country() + SELECT users.current_user_area_utm() AS a ) DELETE FROM waterway.distance_marks -WHERE ST_Covers( - (SELECT a FROM resp), - ST_Transform(geom::geometry, (SELECT t FROM resp))) +WHERE pg_has_role('sys_admin', 'MEMBER') + OR ST_Covers((SELECT a FROM resp), + ST_Transform(geom::geometry, (SELECT ST_SRID(a) FROM resp))) ` insertDistanceMarksSQL = ` WITH resp AS ( - SELECT best_utm(area::geometry) AS t, - ST_Transform(area::geometry, best_utm(area::geometry)) AS a - FROM users.responsibility_areas - WHERE country = users.current_user_country() + SELECT users.current_user_area_utm() AS a ) INSERT INTO waterway.distance_marks (geom, catdis) -SELECT ST_Transform(clipped.geom, 4326)::geography, $3 FROM ( - SELECT (ST_Dump( - ST_Intersection( - (SELECT a FROM resp), - ST_Transform( - ST_GeomFromWKB($1, $2::integer), - (SELECT t FROM resp) - ) - ) - )).geom AS geom - ) AS clipped - WHERE clipped.geom IS NOT NULL +SELECT ST_Transform(new_dma, 4326), $3 + FROM (SELECT + CASE WHEN pg_has_role('sys_admin', 'MEMBER') + THEN dma + ELSE ST_Intersection((SELECT a FROM resp), + ST_Transform(dma, (SELECT ST_SRID(a) FROM resp))) + END AS new_dma + FROM ST_GeomFromWKB($1, $2::integer) AS dma (dma)) AS new_dma + WHERE NOT ST_IsEmpty(new_dma) +RETURNING id ` ) @@ -169,6 +162,7 @@ unsupported = stringCounter{} missingProperties int badProperties int + outside int features int ) @@ -211,13 +205,19 @@ if err := json.Unmarshal(*feature.Geometry.Coordinates, &p); err != nil { return err } - if _, err := insertStmt.ExecContext( + var dmaid int64 + err := insertStmt.QueryRowContext( ctx, p.asWKB(), epsg, props.HydroCatdis, - ); err != nil { - feedback.Error("error: %s", err) + ).Scan(&dmaid) + switch { + case err == sql.ErrNoRows: + outside++ + // ignore -> filtered by responsibility area + continue + case err != nil: return err } features++ @@ -242,6 +242,10 @@ feedback.Warn("Unsupported types found: %s", unsupported) } + if outside > 0 { + feedback.Info("Features outside responsibility area: %d", outside) + } + if features == 0 { err := errors.New("No features found") feedback.Error("%v", err)
--- a/pkg/imports/dmv.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/dmv.go Mon Jun 03 10:19:18 2019 +0200 @@ -48,9 +48,10 @@ func (dmvJobCreator) Create() Job { return new(DistanceMarksVirtual) } -func (dmvJobCreator) Depends() []string { - return []string{ - "distance_marks_virtual", +func (dmvJobCreator) Depends() [2][]string { + return [2][]string{ + {"distance_marks_virtual"}, + {}, } } @@ -86,15 +87,9 @@ start := time.Now() - tx, err := conn.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - responseData, err := getRisData( - tx, + responseData, _, err := getRisData( ctx, + conn, feedback, dmv.Username, dmv.Password, @@ -105,6 +100,12 @@ return nil, err } + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + insertStmt, err := tx.PrepareContext(ctx, insertDistanceMarksVirtualSQL) if err != nil { return nil, err
--- a/pkg/imports/email.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/email.go Mon Jun 03 10:19:18 2019 +0200 @@ -28,7 +28,7 @@ const ( selectEmailSQL = `SELECT email_address FROM users.list_users WHERE username = $1` - importNotificationMailSubject = `import notification mail` + importNotificationMailSubject = `Gemma: import notification mail` ) var ( @@ -38,15 +38,15 @@ a {{ .Description }} import on server {{ .Server }} triggered this email notification. -{{ if eq .State "accepted" }}The imported data were successfully integrated into the database.{{ end -}} -{{ if eq .State "unchanged" }}The import has not changed any data in the database.{{ end -}} -{{ if eq .State "failed" }}The import failed for some reasons.{{ end -}} +{{ if eq .State "accepted" }}The imported data were successfully integrated into the database. {{ end -}} +{{ if eq .State "unchanged" }}The import has not changed any data in the database. {{ end -}} +{{ if eq .State "failed" }}The import failed for some reasons. {{ end -}} {{ if eq .State "pending" }}The imported data could be integrated into the database -but your final decision is needed.{{ end -}} +but your final decision is needed. {{ end -}} Please follow this link to have a closer look at the details: -{{ .Server }}/#/{{ if eq .State "pending" }}review{{ else }}importqueue{{ end }}/{{ .ID }} +{{ .Server }}/#/imports/overview/{{ .ID }} Best regards Your service team`))
--- a/pkg/imports/erdms.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/erdms.go Mon Jun 03 10:19:18 2019 +0200 @@ -18,24 +18,59 @@ "context" "database/sql" "fmt" + "log" "strings" "gemma.intevation.de/gemma/pkg/soap" "gemma.intevation.de/gemma/pkg/soap/erdms" ) -const selectUserCountriesSQL = `SELECT DISTINCT country FROM users.list_users` +const ( + selectUserCountriesSQL = ` +SELECT DISTINCT country FROM users.list_users +WHERE country <> '--' +` +) + +func userCountries(ctx context.Context, conn *sql.Conn) ([]string, error) { + rows, err := conn.QueryContext(ctx, selectUserCountriesSQL) + if err != nil { + return nil, err + } + defer rows.Close() + + var countries []string + + for rows.Next() { + var country string + if err = rows.Scan(&country); err != nil { + return nil, err + } + countries = append(countries, country) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return countries, nil +} func getRisData( - tx *sql.Tx, ctx context.Context, + conn *sql.Conn, feedback Feedback, username string, password string, URL string, insecure bool, funcode string, -) ([]*erdms.GetRisDataXMLResponse, error) { +) ([]*erdms.GetRisDataXMLResponse, []string, error) { + + countries, err := userCountries(ctx, conn) + if err != nil { + return nil, nil, err + } var auth *soap.BasicAuth if username != "" { @@ -47,21 +82,10 @@ client := erdms.NewRefService(URL, insecure, auth) - rows, err := tx.QueryContext(ctx, selectUserCountriesSQL) - if err != nil { - return nil, err - } - defer rows.Close() + var responseData []*erdms.GetRisDataXMLResponse + for _, country := range countries { - var country string - var countries []string - var responseData []*erdms.GetRisDataXMLResponse - for rows.Next() { - err = rows.Scan(&country) - if err != nil { - return nil, err - } - countries = append(countries, country) + feedback.Info("Request RIS index for country %s", country) request := &erdms.GetRisDataXML{ GetRisDataXMLType: &erdms.GetRisDataXMLType{ @@ -70,13 +94,26 @@ }, } + const maxTries = 3 + + tries := 0 + + again: data, err := client.GetRisDataXML(request) if err != nil { - return nil, fmt.Errorf("Error requesting ERDMS service: %v", err) + if t, ok := err.(interface{ Timeout() bool }); ok && t.Timeout() && tries < maxTries { + log.Println("warn: ERDMS SOAP request timed out. Trying again.") + tries++ + goto again + } + return nil, nil, fmt.Errorf( + "Error requesting ERDMS service: %v", err) } responseData = append(responseData, data) } + feedback.Info("Import data for countries: %s.", strings.Join(countries, ", ")) - return responseData, nil + + return responseData, countries, nil }
--- a/pkg/imports/errors.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/errors.go Mon Jun 03 10:19:18 2019 +0200 @@ -29,13 +29,41 @@ // Handle PostgreSQL error codes const ( - noDataFound = "P0002" + notNullViolation = "23502" + foreignKeyViolation = "23503" + noDataFound = "P0002" ) type dbError pgx.PgError func (err dbError) Error() string { switch err.Code { + case notNullViolation: + switch err.SchemaName { + case "waterway": + switch err.TableName { + case "gauges": + switch err.ColumnName { + case "objname": + return "Missing objname" + case "geom": + return "Missing lat/lon" + case "zero_point": + return "Missing zeropoint" + } + } + } + case foreignKeyViolation: + switch err.SchemaName { + case "waterway": + switch err.TableName { + case "gauge_measurements", "gauge_predictions", "bottlenecks": + switch err.ConstraintName { + case "gauge_key": + return "Referenced gauge with matching temporal validity not available" + } + } + } case noDataFound: // Most recent line from stacktrace contains name of failed function recent := strings.SplitN(err.Where, "\n", 1)[0] @@ -44,6 +72,8 @@ return "No distance mark found for at least one given ISRS Location Code" case strings.Contains(recent, "isrsrange_axis"): return "No contiguous axis found between given ISRS Location Codes" + case strings.Contains(recent, "isrsrange_area"): + return "No area around axis between given ISRS Location Codes" } } return "Unexpected database error: " + err.Message
--- a/pkg/imports/fa.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/fa.go Mon Jun 03 10:19:18 2019 +0200 @@ -161,14 +161,11 @@ func (faJobCreator) Create() Job { return new(FairwayAvailability) } -func (faJobCreator) Depends() []string { - return []string{ - "bottlenecks", - "fairway_availability", - "bottleneck_pdfs", - "effective_fairway_availability", - "fa_reference_values", - "levels_of_service", +func (faJobCreator) Depends() [2][]string { + return [2][]string{ + {"effective_fairway_availability", "fa_reference_values", + "bottleneck_pdfs", "fairway_availability"}, + {"bottlenecks", "levels_of_service"}, } }
--- a/pkg/imports/fd.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/fd.go Mon Jun 03 10:19:18 2019 +0200 @@ -80,9 +80,10 @@ func (fdJobCreator) Create() Job { return new(FairwayDimension) } -func (fdJobCreator) Depends() []string { - return []string{ - "fairway_dimensions", +func (fdJobCreator) Depends() [2][]string { + return [2][]string{ + {"fairway_dimensions"}, + {"level_of_service"}, } } @@ -139,15 +140,12 @@ deleteFairwayDimensionSQL = ` WITH resp AS ( - SELECT best_utm(area::geometry) AS t, - ST_Transform(area::geometry, best_utm(area::geometry)) AS a - FROM users.responsibility_areas - WHERE country = users.current_user_country() + SELECT users.current_user_area_utm() AS a ) DELETE FROM waterway.fairway_dimensions -WHERE ST_Covers( - (SELECT a FROM resp), - ST_Transform(area::geometry, (SELECT t FROM resp))) +WHERE (pg_has_role('sys_admin', 'MEMBER') + OR ST_Covers((SELECT a FROM resp), + ST_Transform(area::geometry, (SELECT ST_SRID(a) FROM resp)))) AND staging_done AND level_of_service = $1::smallint` @@ -155,24 +153,27 @@ // avoid errors due to reprojection. insertFairwayDimensionSQL = ` WITH resp AS ( - SELECT best_utm(area::geometry) AS t, - ST_Transform(area::geometry, best_utm(area::geometry)) AS a - FROM users.responsibility_areas - WHERE country = users.current_user_country() + SELECT users.current_user_area_utm() AS a ) -INSERT INTO waterway.fairway_dimensions (area, level_of_service, min_width, max_width, min_depth, date_info, source_organization) -SELECT ST_Transform(clipped.geom, 4326)::geography, $3, $4, $5, $6, $7, $8 FROM ( - SELECT (ST_Dump( - ST_Intersection( - (SELECT ST_Buffer(a, -0.0001) FROM resp), - ST_MakeValid(ST_Transform( - ST_GeomFromWKB($1, $2::integer), - (SELECT t FROM resp) - )) - ) - )).geom AS geom - ) AS clipped - WHERE clipped.geom IS NOT NULL +INSERT INTO waterway.fairway_dimensions ( + area, + level_of_service, + min_width, + max_width, + min_depth, + date_info, + source_organization) +SELECT ST_Transform(dmp.geom, 4326), $3, $4, $5, $6, $7, $8 + FROM ST_GeomFromWKB($1, $2::integer) AS new_fd (new_fd), + ST_Dump( + CASE WHEN pg_has_role('sys_admin', 'MEMBER') + THEN ST_MakeValid(ST_Transform( + new_fd, best_utm(ST_Transform(new_fd, 4326)))) + ELSE ST_CollectionExtract(ST_Intersection( + (SELECT ST_Buffer(a, -0.0001) FROM resp), + ST_MakeValid(ST_Transform(new_fd, (SELECT ST_SRID(a) FROM resp)))), + 3) + END) AS dmp RETURNING id, ST_X(ST_Centroid(area::geometry)), ST_Y(ST_Centroid(area::geometry)) @@ -259,6 +260,8 @@ feedback.Info("Using EPSG: %d", epsg) + savepoint := Savepoint(ctx, tx, "feature") + features: for _, feature := range rfc.Features { if feature.Geometry.Coordinates == nil { @@ -287,24 +290,28 @@ } var fdid int64 var lat, lon float64 - err = insertStmt.QueryRowContext( - ctx, - p.asWKB(), - epsg, - fd.LOS, - fd.MinWidth, - fd.MaxWidth, - fd.Depth, - dateInfo, - fd.SourceOrganization, - ).Scan(&fdid, &lat, &lon) + err = savepoint(func() error { + err := insertStmt.QueryRowContext( + ctx, + p.asWKB(), + epsg, + fd.LOS, + fd.MinWidth, + fd.MaxWidth, + fd.Depth, + dateInfo, + fd.SourceOrganization, + ).Scan(&fdid, &lat, &lon) + return err + }) switch { case err == sql.ErrNoRows: outside++ // ignore -> filtered by responsibility_areas continue features case err != nil: - return err + feedback.Warn(handleError(err).Error()) + continue features } // Store for potential later removal. if err = track(ctx, tx, importID, "waterway.fairway_dimensions", fdid); err != nil {
--- a/pkg/imports/gm.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/gm.go Mon Jun 03 10:19:18 2019 +0200 @@ -4,13 +4,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // License-Filename: LICENSES/AGPL-3.0.txt // -// Copyright (C) 2018 by via donau +// Copyright (C) 2018, 2019 by via donau // – Österreichische Wasserstraßen-Gesellschaft mbH // Software engineering by Intevation GmbH // // Author(s): // * Raimund Renkert <raimund.renkert@intevation.de> // * Sascha L. Teichmann <sascha.teichmann@intevation.de> +// * Tom Gottfried <tom.gottfried@intevation.de> package imports @@ -24,6 +25,7 @@ "gemma.intevation.de/gemma/pkg/models" "gemma.intevation.de/gemma/pkg/soap/nts" + "github.com/jackc/pgx/pgtype" ) // GaugeMeasurement is an import job to import @@ -48,14 +50,17 @@ (location).orc, (location).hectometre FROM waterway.gauges -WHERE (location).country_code = users.current_user_country()` +WHERE (location).country_code = users.current_user_country() + OR pg_has_role('sys_admin', 'MEMBER') +` - // TODO: Currently this statement updates existing data sets. In case we want - // 'historization' we need to develop an other mechanism to keep existing - // data. + // Note: we do not expect corrections of data through this service. So + // any constraint conflicts are triggered by redundant data which + // can be dropped. insertGMSQL = ` INSERT INTO waterway.gauge_measurements ( - fk_gauge_id, + location, + validity, measure_date, sender, language_code, @@ -63,15 +68,17 @@ date_issue, reference_code, water_level, - predicted, - is_waterlevel, - value_min, - value_max, date_info, source_organization, staging_done -) VALUES( +) VALUES ( ($1, $2, $3, $4, $5), + COALESCE( + (SELECT validity FROM waterway.gauges + WHERE location + = ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int) + AND validity @> CAST($6 AS timestamp with time zone)), + tstzrange(NULL, NULL)), $6, $7, $8, @@ -81,27 +88,47 @@ $12, $13, $14, - $15, - $16, - $17, - $18, - $19 + true ) -ON CONFLICT ON CONSTRAINT gauge_measurements_fk_gauge_id_measure_date_staging_done_key -DO UPDATE SET -country_code = EXCLUDED.country_code, -sender = EXCLUDED.sender, -language_code = EXCLUDED.language_code, -date_issue = EXCLUDED.date_issue, -reference_code= EXCLUDED.reference_code, -water_level = EXCLUDED.water_level, -predicted = EXCLUDED.predicted, -is_waterlevel = EXCLUDED.is_waterlevel, -value_min = EXCLUDED.value_min, -value_max = EXCLUDED.value_max, -date_info = EXCLUDED.date_info, -source_organization = EXCLUDED.source_organization -RETURNING id +ON CONFLICT DO NOTHING +RETURNING 1 +` + + insertGPSQL = ` +INSERT INTO waterway.gauge_predictions ( + location, + validity, + measure_date, + sender, + language_code, + country_code, + date_issue, + reference_code, + water_level, + conf_interval, + date_info, + source_organization +) VALUES( + ($1, $2, $3, $4, $5), + COALESCE( + (SELECT validity FROM waterway.gauges + WHERE location + = ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int) + AND validity @> CAST($6 AS timestamp with time zone)), + tstzrange(NULL, NULL)), + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15 +) +ON CONFLICT DO NOTHING +RETURNING 1 ` ) @@ -115,10 +142,10 @@ func (gmJobCreator) Create() Job { return new(GaugeMeasurement) } -func (gmJobCreator) Depends() []string { - return []string{ - "gauges", - "gauge_measurements", +func (gmJobCreator) Depends() [2][]string { + return [2][]string{ + {"gauge_measurements"}, + {"gauges"}, } } @@ -177,9 +204,9 @@ ) } -func loadGauges(ctx context.Context, tx *sql.Tx) ([]string, error) { +func loadGauges(ctx context.Context, conn *sql.Conn) ([]string, error) { - rows, err := tx.QueryContext(ctx, listGaugesSQL) + rows, err := conn.QueryContext(ctx, listGaugesSQL) if err != nil { return nil, err } @@ -205,6 +232,11 @@ return nil, err } + if len(gauges) == 0 { + return nil, UnchangedError( + "No gauges for which measurements can be imported in database") + } + sort.Strings(gauges) return gauges, nil @@ -220,37 +252,24 @@ start := time.Now() - tx, err := conn.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - // Get available gauges from database for use as filter in SOAP request - gauges, err := loadGauges(ctx, tx) + // Get gauges from database, for which user is allowed to import data + gauges, err := loadGauges(ctx, conn) if err != nil { return nil, err } - // TODO get date_issue for selected gauges - gids, err := doForGM(ctx, gauges, fetch, tx, feedback) + gids, err := doForGM(ctx, gauges, fetch, conn, feedback) if err != nil { - feedback.Error("Error processing %d gauges: %v", len(gauges), err) + feedback.Error("Error processing gauges: %v", err) return nil, err } if len(gids) == 0 { - feedback.Info("No new gauge measurements found") return nil, UnchangedError("No new gauge measurements found") } - if err = tx.Commit(); err != nil { - feedback.Info( - "Importing gauge measurements failed after %s", time.Since(start)) - return nil, err - } feedback.Info( - "Importing gauge measurements successfully took %s", time.Since(start)) + "Importing gauge measurements took %s", time.Since(start)) // TODO: needs to be filled more useful. summary := struct { @@ -262,7 +281,7 @@ } // rescale returns a scaling function to bring the unit all to cm. -func rescale(unit string) (func(float32) float32, error) { +func rescale(unit string) (func(*float32), error) { var scale float32 @@ -283,7 +302,11 @@ return nil, fmt.Errorf("unknown unit '%s'", unit) } - fn := func(x float32) float32 { return scale * x } + fn := func(x *float32) { + if x != nil { + *x *= scale + } + } return fn, nil } @@ -291,17 +314,23 @@ ctx context.Context, gauges []string, fetch func() ([]*nts.RIS_Message_Type, error), - tx *sql.Tx, + conn *sql.Conn, feedback Feedback, ) ([]string, error) { - insertStmt, err := tx.PrepareContext(ctx, insertGMSQL) + insertGPStmt, err := conn.PrepareContext(ctx, insertGPSQL) if err != nil { return nil, err } - defer insertStmt.Close() + defer insertGPStmt.Close() - // lookup to see if we have gauges in the database. + insertGMStmt, err := conn.PrepareContext(ctx, insertGMSQL) + if err != nil { + return nil, err + } + defer insertGMStmt.Close() + + // lookup to see if data can be imported for gauge isKnown := func(s string) bool { idx := sort.SearchStrings(gauges, s) return idx < len(gauges) && gauges[idx] == s @@ -314,7 +343,7 @@ var gids []string for _, msg := range result { - var gid int64 + var dummy int for _, wrm := range msg.Wrm { curr := string(*wrm.Geo_object.Id) currIsrs, err := models.IsrsFromString(curr) @@ -322,9 +351,9 @@ feedback.Warn("Invalid ISRS code %v", err) continue } - feedback.Info("Found measurements for %s", curr) + feedback.Info("Found measurements/predictions for %s", curr) if !isKnown(curr) { - feedback.Warn("Gauge '%s' is not in database.", curr) + feedback.Warn("Cannot import data for %s", curr) continue } @@ -335,6 +364,8 @@ } else { referenceCode = string(*wrm.Reference_code) } + + newM, newP := 0, 0 for _, measure := range wrm.Measure { var unit string if measure.Unit == nil { @@ -347,35 +378,93 @@ if err != nil { return nil, err } - isWaterlevel := *measure.Measure_code == nts.Measure_code_enumWAL - err = insertStmt.QueryRowContext( - ctx, - currIsrs.CountryCode, - currIsrs.LoCode, - currIsrs.FairwaySection, - currIsrs.Orc, - currIsrs.Hectometre, - measure.Measuredate, - msg.Identification.From, - msg.Identification.Language_code, - msg.Identification.Country_code, - msg.Identification.Date_issue, - referenceCode, - convert(measure.Value), - measure.Predicted, - isWaterlevel, - convert(measure.Value_min), - convert(measure.Value_max), - msg.Identification.Date_issue, - msg.Identification.Originator, - true, // staging_done - ).Scan(&gid) - if err != nil { - return nil, err + convert(measure.Value) + convert(measure.Value_min) + convert(measure.Value_max) + + if *measure.Measure_code != nts.Measure_code_enumWAL { + feedback.Warn("Ignored message with measure_code %s", + *measure.Measure_code) + continue + } + + if measure.Predicted { + var confInterval pgtype.Numrange + if measure.Value_min != nil && measure.Value_max != nil { + var valueMin, valueMax pgtype.Numeric + valueMin.Set(measure.Value_min) + valueMax.Set(measure.Value_max) + confInterval = pgtype.Numrange{ + Lower: valueMin, + Upper: valueMax, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Inclusive, + Status: pgtype.Present, + } + } + err = insertGPStmt.QueryRowContext( + ctx, + currIsrs.CountryCode, + currIsrs.LoCode, + currIsrs.FairwaySection, + currIsrs.Orc, + currIsrs.Hectometre, + measure.Measuredate, + msg.Identification.From, + msg.Identification.Language_code, + msg.Identification.Country_code, + msg.Identification.Date_issue, + referenceCode, + measure.Value, + &confInterval, + msg.Identification.Date_issue, + msg.Identification.Originator, + ).Scan(&dummy) + switch { + case err == sql.ErrNoRows: + // thats expected, nothing to do + case err != nil: + feedback.Warn(handleError(err).Error()) + default: + newP++ + } + } else { + if measure.Value == nil { + feedback.Info("Missing value at %s. Ignored", + measure.Measuredate.Format(time.RFC3339)) + continue + } + err = insertGMStmt.QueryRowContext( + ctx, + currIsrs.CountryCode, + currIsrs.LoCode, + currIsrs.FairwaySection, + currIsrs.Orc, + currIsrs.Hectometre, + measure.Measuredate, + msg.Identification.From, + msg.Identification.Language_code, + msg.Identification.Country_code, + msg.Identification.Date_issue, + referenceCode, + measure.Value, + msg.Identification.Date_issue, + msg.Identification.Originator, + ).Scan(&dummy) + switch { + case err == sql.ErrNoRows: + // thats expected, nothing to do + case err != nil: + feedback.Warn(handleError(err).Error()) + default: + newM++ + } } } feedback.Info("Inserted %d measurements for %s", - len(wrm.Measure), curr) + newM, curr) + feedback.Info("Inserted %d predictions for %s", + newP, curr) gids = append(gids, curr) } }
--- a/pkg/imports/modelconvert.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/modelconvert.go Mon Jun 03 10:19:18 2019 +0200 @@ -28,6 +28,7 @@ FDJobKind: func() interface{} { return new(models.FairwayDimensionImport) }, DMAJobKind: func() interface{} { return new(models.DistanceMarksAshoreImport) }, STJobKind: func() interface{} { return new(models.StretchImport) }, + SECJobKind: func() interface{} { return new(models.SectionImport) }, } func ImportModelForJobKind(kind JobKind) func() interface{} { @@ -144,6 +145,20 @@ Countries: sti.Countries, } }, + + SECJobKind: func(input interface{}) interface{} { + seci := input.(*models.SectionImport) + return &Section{ + Name: seci.Name, + From: seci.From, + To: seci.To, + Tolerance: seci.Tolerance, + ObjNam: seci.ObjNam, + NObjNam: seci.NObjNam, + Source: seci.Source, + Date: seci.Date, + } + }, } func nilString(s *string) string {
--- a/pkg/imports/queue.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/queue.go Mon Jun 03 10:19:18 2019 +0200 @@ -20,6 +20,7 @@ "fmt" "log" "runtime/debug" + "sort" "strings" "sync" "time" @@ -75,10 +76,12 @@ Description() string // Create build the actual job. Create() Job - // Depends returns a list of ressources locked by this type of import. + // Depends returns two lists of ressources locked by this type of import. // Imports are run concurrently if they have disjoint sets // of dependencies. - Depends() []string + // The first list are locked exclusively. + // The second allows multiple read users but only one writing one. + Depends() [2][]string // StageDone is called if an import is positively reviewed // (state = accepted). This can be used to finalize the imported // data to move it e.g from the staging area. @@ -99,19 +102,22 @@ } ) -const pollDuration = time.Second * 10 +const ( + pollDuration = time.Second * 10 + runExclusive = -66666 +) type importQueue struct { signalChan chan struct{} creatorsMu sync.Mutex creators map[JobKind]JobCreator - usedDeps map[string]struct{} + usedDeps map[string]int } var iqueue = importQueue{ signalChan: make(chan struct{}), creators: map[JobKind]JobCreator{}, - usedDeps: map[string]struct{}{}, + usedDeps: map[string]int{}, } var ( @@ -220,6 +226,15 @@ return iqueue.importKindNames() } +// LogImportKindNames logs a list of importer types registered +// to the global import queue. +func LogImportKindNames() { + kinds := ImportKindNames() + sort.Strings(kinds) + log.Printf("info: registered import kinds: %s", + strings.Join(kinds, ", ")) +} + // HasImportKindName checks if the import queue supports a given kind. func HasImportKindName(kind string) bool { return iqueue.hasImportKindName(kind) @@ -236,7 +251,6 @@ // This a good candidate to be called in a init function for // a particular JobCreator. func RegisterJobCreator(kind JobKind, jc JobCreator) { - log.Printf("info: register import job creator for kind '%s'\n", kind) iqueue.registerJobCreator(kind, jc) } @@ -304,6 +318,30 @@ return d } +func (q *importQueue) lockDependencies(jc JobCreator) { + deps := jc.Depends() + q.creatorsMu.Lock() + defer q.creatorsMu.Unlock() + for _, d := range deps[0] { + q.usedDeps[d] = runExclusive + } + for _, d := range deps[1] { + q.usedDeps[d]++ + } +} + +func (q *importQueue) unlockDependencies(jc JobCreator) { + deps := jc.Depends() + q.creatorsMu.Lock() + defer q.creatorsMu.Unlock() + for _, d := range deps[0] { + q.usedDeps[d] = 0 + } + for _, d := range deps[1] { + q.usedDeps[d]-- + } +} + func (q *importQueue) jobCreator(kind JobKind) JobCreator { q.creatorsMu.Lock() defer q.creatorsMu.Unlock() @@ -436,8 +474,14 @@ q.creatorsMu.Lock() nextCreator: for kind, jc := range q.creators { - for _, d := range jc.Depends() { - if _, found := q.usedDeps[d]; found { + deps := jc.Depends() + for _, d := range deps[0] { + if q.usedDeps[d] != 0 { + continue nextCreator + } + } + for _, d := range deps[1] { + if q.usedDeps[d] == runExclusive { continue nextCreator } } @@ -488,6 +532,30 @@ return &ji, nil } +func tryHardToStoreState(ctx context.Context, fn func(*sql.Conn) error) error { + // As it is important to keep the persistent model + // in sync with the in-memory model try harder to store + // the state. + const maxTries = 10 + var sleep = time.Second + + for try := 1; ; try++ { + var err error + if err = auth.RunAs(ctx, queueUser, fn); err == nil || try == maxTries { + return err + } + log.Printf("warn: [try %d/%d] Storing state failed: %v (try again in %s).\n", + try, maxTries, err, sleep) + + time.Sleep(sleep) + if sleep < time.Minute { + if sleep *= 2; sleep > time.Minute { + sleep = time.Minute + } + } + } +} + func updateStateSummary( ctx context.Context, id int64, @@ -502,7 +570,8 @@ } s = sql.NullString{String: b.String(), Valid: true} } - return auth.RunAs(ctx, queueUser, func(conn *sql.Conn) error { + + return tryHardToStoreState(ctx, func(conn *sql.Conn) error { _, err := conn.ExecContext(ctx, updateStateSummarySQL, state, s, id) return err }) @@ -510,7 +579,7 @@ func errorAndFail(id int64, format string, args ...interface{}) error { ctx := context.Background() - err := auth.RunAs(ctx, queueUser, func(conn *sql.Conn) error { + return tryHardToStoreState(ctx, func(conn *sql.Conn) error { tx, err := conn.BeginTx(ctx, nil) if err != nil { return err @@ -528,7 +597,6 @@ } return err }) - return err } func (q *importQueue) importLoop() { @@ -565,21 +633,13 @@ } // Lock dependencies. - q.creatorsMu.Lock() - for _, d := range jc.Depends() { - q.usedDeps[d] = struct{}{} - } - q.creatorsMu.Unlock() + q.lockDependencies(jc) go func(jc JobCreator, idj *idJob) { // Unlock the dependencies. defer func() { - q.creatorsMu.Lock() - for _, d := range jc.Depends() { - delete(q.usedDeps, d) - } - q.creatorsMu.Unlock() + q.unlockDependencies(jc) select { case q.signalChan <- struct{}{}: default: @@ -619,7 +679,7 @@ } var errCleanup error - if retry { // cleanup debris + if !retry { // cleanup debris if errCleanup = survive(job.CleanUp)(); errCleanup != nil { feedback.Error("error cleanup: %v", errCleanup) }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/imports/sec.go Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,200 @@ +// 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,2019 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package imports + +import ( + "context" + "database/sql" + "time" + + "gemma.intevation.de/gemma/pkg/models" +) + +type Section struct { + Name string `json:"name"` + From models.Isrs `json:"from"` + To models.Isrs `json:"to"` + Tolerance float32 `json:"tolerance"` + ObjNam string `json:"objnam"` + NObjNam *string `json:"nobjnam"` + Source string `json:"source-organization"` + Date models.Date `json:"date-info"` +} + +const SECJobKind JobKind = "sec" + +type secJobCreator struct{} + +func init() { + RegisterJobCreator(SECJobKind, secJobCreator{}) +} + +func (secJobCreator) Description() string { return "section" } + +func (secJobCreator) AutoAccept() bool { return false } + +func (secJobCreator) Create() Job { return new(Section) } + +func (secJobCreator) Depends() [2][]string { + return [2][]string{ + {"sections"}, + {"distance_marks_virtual", "waterway_axis", "waterway_area"}, + } +} + +const ( + secDeleteSQL = ` +DELETE FROM waterway.sections WHERE +staging_done AND name = ( + SELECT name + FROM waterway.sections WHERE + id = ( + SELECT key from import.track_imports + WHERE import_id = $1 AND + relation = 'waterway.sections'::regclass) + AND NOT staging_done +)` + + secStageDoneSQL = ` +UPDATE waterway.sections SET staging_done = true +WHERE id IN ( + SELECT key from import.track_imports + WHERE import_id = $1 AND + relation = 'waterway.sections'::regclass)` + + secInsertSQL = ` +WITH +bounds (b) AS (VALUES ( + ($1::char(2), + $2::char(3), + $3::char(5), + $4::char(5), + $5::int)::isrs + ), ( + ($6::char(2), + $7::char(3), + $8::char(5), + $9::char(5), + $10::int)::isrs)), +r AS (SELECT isrsrange( + (SELECT b FROM bounds ORDER BY b USING <~ FETCH FIRST ROW ONLY), + (SELECT b FROM bounds ORDER BY b USING >~ FETCH FIRST ROW ONLY)) AS r), +axs AS ( + SELECT ISRSrange_axis((SELECT r FROM r), $16::double precision) AS axs) +INSERT INTO waterway.sections ( + name, + section, + area, + objnam, + nobjnam, + date_info, + source_organization +) VALUES ( + $11, + (SELECT r FROM r), + ISRSrange_area( + (SELECT axs FROM axs), + (SELECT ST_Collect(CAST(area AS geometry)) + FROM waterway.waterway_area)), + $12, + $13, + $14, + $15) +RETURNING id` +) + +// StageDone moves the imported section out of the staging area. +func (secJobCreator) StageDone( + ctx context.Context, + tx *sql.Tx, + id int64, +) error { + if _, err := tx.ExecContext(ctx, secDeleteSQL, id); err != nil { + return err + } + _, err := tx.ExecContext(ctx, secStageDoneSQL, id) + return err +} + +// CleanUp of a section import is a NOP. +func (*Section) CleanUp() error { return nil } + +// Do executes the actual section import. +func (sec *Section) Do( + ctx context.Context, + importID int64, + conn *sql.Conn, + feedback Feedback, +) (interface{}, error) { + + start := time.Now() + + if sec.Date.Time.IsZero() { + sec.Date = models.Date{Time: start} + } + + feedback.Info("Storing section '%s'", sec.Name) + + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + var nobjnm sql.NullString + if sec.NObjNam != nil { + nobjnm = sql.NullString{String: *sec.NObjNam, Valid: true} + } + + feedback.Info("Section from %s to %s.", sec.From.String(), sec.To.String()) + feedback.Info("Tolerance used to snap waterway axis: %g", sec.Tolerance) + + var id int64 + if err := tx.QueryRowContext( + ctx, + secInsertSQL, + sec.From.CountryCode, + sec.From.LoCode, + sec.From.FairwaySection, + sec.From.Orc, + sec.From.Hectometre, + sec.To.CountryCode, + sec.To.LoCode, + sec.To.FairwaySection, + sec.To.Orc, + sec.To.Hectometre, + sec.Name, + sec.ObjNam, + nobjnm, + sec.Date.Time, + sec.Source, + sec.Tolerance, + ).Scan(&id); err != nil { + return nil, handleError(err) + } + + if err := track(ctx, tx, importID, "waterway.sections", id); err != nil { + return nil, err + } + + feedback.Info("Storing section '%s' took %s", sec.Name, time.Since(start)) + if err := tx.Commit(); err != nil { + return nil, err + } + feedback.Info("Import of section was successful") + + summary := sec // to provide full data for review + + return summary, nil +}
--- a/pkg/imports/sr.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/sr.go Mon Jun 03 10:19:18 2019 +0200 @@ -35,6 +35,7 @@ shp "github.com/jonas-p/go-shp" "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/misc" "gemma.intevation.de/gemma/pkg/models" "gemma.intevation.de/gemma/pkg/octree" ) @@ -79,11 +80,10 @@ func (srJobCreator) Create() Job { return new(SoundingResult) } -func (srJobCreator) Depends() []string { - return []string{ - "sounding_results", - "sounding_results_contour_lines", - "bottlenecks", +func (srJobCreator) Depends() [2][]string { + return [2][]string{ + {"sounding_results", "sounding_results_contour_lines"}, + {"bottlenecks"}, } } @@ -124,8 +124,8 @@ id, ST_X(ST_Centroid(area::geometry)), ST_Y(ST_Centroid(area::geometry)), - best_utm(area::geometry), - ST_AsBinary(ST_Transform(area::geometry, best_utm(area::geometry))) + best_utm(area), + ST_AsBinary(ST_Transform(area::geometry, best_utm(area))) ` reprojectPointsSQL = ` @@ -161,6 +161,17 @@ FROM waterway.sounding_results sr WHERE id = $1 ` + + selectGaugeLDCSQL = ` +SELECT + grwl.value, + grwl.depth_reference +FROM waterway.gauges_reference_water_levels grwl + JOIN waterway.bottlenecks bns + ON grwl.location = bns.gauge_location + AND grwl.validity = bns.gauge_validity +WHERE bns.objnam = $1 AND grwl.depth_reference like 'LDC%' +` ) // Do executes the actual sounding result import. @@ -190,6 +201,31 @@ return nil, err } + feedback.Info("Bottleneck: %s", m.Bottleneck) + feedback.Info("Survey date: %s", m.Date.Format(common.DateFormat)) + + var xform vertexTransform + + if m.DepthReference == "ZPG" { + feedback.Info("Found ZPG as reference system -> translating Z values to LDC") + var ldc float64 + var depthReference string + err := conn.QueryRowContext(ctx, selectGaugeLDCSQL, m.Bottleneck).Scan( + &ldc, + &depthReference, + ) + switch { + case err == sql.ErrNoRows: + return nil, errors.New("Cannot load LDC value") + case err != nil: + return nil, err + } + xform = func(v octree.Vertex) octree.Vertex { + return octree.Vertex{X: v.X, Y: v.Y, Z: ldc - v.Z} + } + m.DepthReference = depthReference + } + if err := m.Validate(ctx, conn); err != nil { return nil, common.ToError(err) } @@ -205,7 +241,7 @@ return nil, errors.New("Cannot find any *.xyz or *.txt file") } - xyz, err := loadXYZ(xyzf, feedback) + xyz, err := loadXYZ(xyzf, feedback, xform) if err != nil { return nil, err } @@ -422,24 +458,30 @@ return &m, nil } -func loadXYZReader(r io.Reader, feedback Feedback) (octree.MultiPointZ, error) { +type vertexTransform func(octree.Vertex) octree.Vertex + +func loadXYZReader(r io.Reader, feedback Feedback, xform vertexTransform) (octree.MultiPointZ, error) { mpz := make(octree.MultiPointZ, 0, 250000) s := bufio.NewScanner(r) var hasNegZ bool + warnLimiter := misc.WarningLimiter{Log: feedback.Warn, MaxWarnings: 100} + warn := warnLimiter.Warn + defer warnLimiter.Close() + for line := 1; s.Scan(); line++ { text := s.Text() var p octree.Vertex // fmt.Sscanf(text, "%f,%f,%f") is 4 times slower. idx := strings.IndexByte(text, ',') if idx == -1 { - feedback.Warn("format error in line %d", line) + warn("format error in line %d", line) continue } var err error if p.X, err = strconv.ParseFloat(text[:idx], 64); err != nil { - feedback.Warn("format error in line %d: %v", line, err) + warn("format error in line %d: %v", line, err) continue } text = text[idx+1:] @@ -448,21 +490,24 @@ continue } if p.Y, err = strconv.ParseFloat(text[:idx], 64); err != nil { - feedback.Warn("format error in line %d: %v", line, err) + warn("format error in line %d: %v", line, err) continue } text = text[idx+1:] if p.Z, err = strconv.ParseFloat(text, 64); err != nil { - feedback.Warn("format error in line %d: %v", line, err) + warn("format error in line %d: %v", line, err) continue } if p.Z < 0 { p.Z = -p.Z if !hasNegZ { hasNegZ = true - feedback.Warn("Negative Z value found: Using -Z") + warn("Negative Z value found: Using -Z") } } + if xform != nil { + p = xform(p) + } mpz = append(mpz, p) } @@ -473,13 +518,13 @@ return mpz, nil } -func loadXYZ(f *zip.File, feedback Feedback) (octree.MultiPointZ, error) { +func loadXYZ(f *zip.File, feedback Feedback, xform vertexTransform) (octree.MultiPointZ, error) { r, err := f.Open() if err != nil { return nil, err } defer r.Close() - return loadXYZReader(r, feedback) + return loadXYZReader(r, feedback, xform) } func loadBoundary(z *zip.ReadCloser) (polygonSlice, error) {
--- a/pkg/imports/st.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/st.go Mon Jun 03 10:19:18 2019 +0200 @@ -48,9 +48,10 @@ func (stJobCreator) Create() Job { return new(Stretch) } -func (stJobCreator) Depends() []string { - return []string{ - "stretches", +func (stJobCreator) Depends() [2][]string { + return [2][]string{ + {"stretches", "stretch_countries"}, + {"distance_marks_virtual", "waterway_axis", "waterway_area"}, } } @@ -75,30 +76,24 @@ relation = 'waterway.stretches'::regclass)` stInsertSQL = ` -WITH r AS ( - SELECT isrsrange( - least(($1::char(2), - $2::char(3), - $3::char(5), - $4::char(5), - $5::int)::isrs, - ($6::char(2), - $7::char(3), - $8::char(5), - $9::char(5), - $10::int)::isrs), - greatest(($1::char(2), - $2::char(3), - $3::char(5), - $4::char(5), - $5::int)::isrs, - ($6::char(2), - $7::char(3), - $8::char(5), - $9::char(5), - $10::int)::isrs) - ) AS r -) +WITH +bounds (b) AS (VALUES ( + ($1::char(2), + $2::char(3), + $3::char(5), + $4::char(5), + $5::int)::isrs + ), ( + ($6::char(2), + $7::char(3), + $8::char(5), + $9::char(5), + $10::int)::isrs)), +r AS (SELECT isrsrange( + (SELECT b FROM bounds ORDER BY b USING <~ FETCH FIRST ROW ONLY), + (SELECT b FROM bounds ORDER BY b USING >~ FETCH FIRST ROW ONLY)) AS r), +axs AS ( + SELECT ISRSrange_axis((SELECT r FROM r), $16::double precision) AS axs) INSERT INTO waterway.stretches ( name, stretch, @@ -110,10 +105,10 @@ ) VALUES ( $11, (SELECT r FROM r), - ISRSrange_area( - ISRSrange_axis((SELECT r FROM r), $16::double precision), - (SELECT ST_Collect(CAST(area AS geometry)) - FROM waterway.waterway_area)), + ST_Transform(ISRSrange_area( + (SELECT axs FROM axs), + (SELECT ST_Buffer(axs, 10000) FROM axs)), + 4326), $12, $13, $14, @@ -176,6 +171,7 @@ if err != nil { return nil, err } + defer insertCountryStmt.Close() var nobjnm sql.NullString if st.NObjNam != nil { @@ -228,11 +224,7 @@ } feedback.Info("Import of stretch was successful") - summary := struct { - Stretch string `json:"stretch"` - }{ - Stretch: st.Name, - } + summary := st // provide full information for summary - return &summary, nil + return summary, nil }
--- a/pkg/imports/ubn.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/ubn.go Mon Jun 03 10:19:18 2019 +0200 @@ -43,7 +43,7 @@ func (ubnJobCreator) Create() Job { return new(UploadedBottleneck) } -func (ubnJobCreator) Depends() []string { +func (ubnJobCreator) Depends() [2][]string { // Same as normal bottleneck import. return bnJobCreator{}.Depends() }
--- a/pkg/imports/ufa.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/ufa.go Mon Jun 03 10:19:18 2019 +0200 @@ -42,7 +42,7 @@ func (ufaJobCreator) Create() Job { return new(UploadedFairwayAvailability) } -func (ufaJobCreator) Depends() []string { +func (ufaJobCreator) Depends() [2][]string { // Same as faJobCreator return faJobCreator{}.Depends() }
--- a/pkg/imports/ugm.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/ugm.go Mon Jun 03 10:19:18 2019 +0200 @@ -38,7 +38,7 @@ func (ugmJobCreator) Create() Job { return new(UploadedGaugeMeasurement) } -func (ugmJobCreator) Depends() []string { return gmJobCreator{}.Depends() } +func (ugmJobCreator) Depends() [2][]string { return gmJobCreator{}.Depends() } func (ugmJobCreator) AutoAccept() bool { return true }
--- a/pkg/imports/wa.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/wa.go Mon Jun 03 10:19:18 2019 +0200 @@ -59,9 +59,10 @@ func (waJobCreator) Create() Job { return new(WaterwayArea) } -func (waJobCreator) Depends() []string { - return []string{ - "waterway_area", +func (waJobCreator) Depends() [2][]string { + return [2][]string{ + {"waterway_area"}, + {}, } } @@ -81,35 +82,29 @@ const ( deleteWaterwayAreaSQL = ` WITH resp AS ( - SELECT best_utm(area::geometry) AS t, - ST_Transform(area::geometry, best_utm(area::geometry)) AS a - FROM users.responsibility_areas - WHERE country = users.current_user_country() + SELECT users.current_user_area_utm() AS a ) DELETE FROM waterway.waterway_area WHERE pg_has_role('sys_admin', 'MEMBER') OR ST_Covers((SELECT a FROM resp), - ST_Transform(area::geometry, (SELECT t FROM resp))) + ST_Transform(area::geometry, (SELECT ST_SRID(a) FROM resp))) ` insertWaterwayAreaSQL = ` WITH resp AS ( - SELECT best_utm(area::geometry) AS t, - ST_Transform(area::geometry, best_utm(area::geometry)) AS a - FROM users.responsibility_areas - WHERE country = users.current_user_country() + SELECT users.current_user_area_utm() AS a ) INSERT INTO waterway.waterway_area (area, catccl, dirimp) -SELECT ST_Transform(clipped.geom, 4326)::geography, $3, $4 - FROM resp, - ST_CollectionExtract(ST_MakeValid(ST_Transform( - ST_GeomFromWKB($1, $2::integer), t)), 3) AS new_area (new_area), - LATERAL (SELECT (ST_Dump( +SELECT dmp.geom, $3, $4 + FROM ST_GeomFromWKB($1, $2::integer) AS new_area (new_area), + ST_Dump(ST_Transform(ST_CollectionExtract( CASE WHEN pg_has_role('sys_admin', 'MEMBER') - THEN new_area - ELSE ST_Intersection(a, new_area) - END - )).geom AS geom - ) AS clipped + THEN ST_MakeValid(ST_Transform(new_area, + best_utm(ST_Transform(new_area, 4326)))) + ELSE ST_Intersection((SELECT a FROM resp), + ST_MakeValid(ST_Transform(new_area, (SELECT ST_SRID(a) FROM resp)))) + END, + 3), 4326)) AS dmp +RETURNING id ` ) @@ -176,6 +171,7 @@ unsupported = stringCounter{} missingProperties int badProperties int + outside int features int ) @@ -234,18 +230,24 @@ if err := json.Unmarshal(*feature.Geometry.Coordinates, &p); err != nil { return err } - if err := savepoint(func() error { - _, err := insertStmt.ExecContext( + var waid int64 + err := savepoint(func() error { + err := insertStmt.QueryRowContext( ctx, p.asWKB(), epsg, catccl, dirimp, - ) + ).Scan(&waid) return err - }); err != nil { + }) + switch { + case err == sql.ErrNoRows: + outside++ + // ignore -> filtered by responsibility_areas + case err != nil: feedback.Warn(handleError(err).Error()) - } else { + default: features++ } default: @@ -269,6 +271,10 @@ feedback.Warn("Unsupported types found: %s", unsupported) } + if outside > 0 { + feedback.Info("Features outside responsibility area: %d", outside) + } + if features == 0 { err := errors.New("No features found") feedback.Error("%v", err)
--- a/pkg/imports/wg.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/wg.go Mon Jun 03 10:19:18 2019 +0200 @@ -17,7 +17,6 @@ import ( "context" "database/sql" - "errors" "time" "github.com/jackc/pgx/pgtype" @@ -52,10 +51,10 @@ func (wgJobCreator) Create() Job { return new(WaterwayGauge) } -func (wgJobCreator) Depends() []string { - return []string{ - "gauges", - "gauges_reference_water_levels", +func (wgJobCreator) Depends() [2][]string { + return [2][]string{ + {"gauges_reference_water_levels", "gauges"}, + {"depth_references"}, } } @@ -66,14 +65,28 @@ func (*WaterwayGauge) CleanUp() error { return nil } const ( - hasGaugeSQL = ` -SELECT true -FROM waterway.gauges -WHERE location = ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int)` + eraseObsoleteGaugesSQL = ` +UPDATE waterway.gauges SET erased = true +WHERE NOT erased + AND (location).country_code = ANY($1) + AND isrs_astext(location) <> ALL($2) +RETURNING isrs_astext(location) +` - deleteReferenceWaterLevelsSQL = ` -DELETE FROM waterway.gauges_reference_water_levels -WHERE gauge_id = ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int)` + eraseGaugeSQL = ` +WITH upd AS ( + UPDATE waterway.gauges SET + erased = true + WHERE isrs_astext(location) = $1 + AND NOT erased + -- Don't touch old entry if validity did not change: will be updated + AND validity <> $2 + RETURNING 1 +) +-- Decide whether a new version will be INSERTed +SELECT EXISTS(SELECT 1 FROM upd) + OR NOT EXISTS(SELECT 1 FROM waterway.gauges WHERE isrs_astext(location) = $1) +` insertGaugeSQL = ` INSERT INTO waterway.gauges ( @@ -86,7 +99,8 @@ zero_point, geodref, date_info, - source_organization + source_organization, + lastupdate ) VALUES ( ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int), $6, @@ -97,31 +111,69 @@ $12, $13, $14, - $15 -) ON CONFLICT (location) DO UPDATE SET + $15, + $16 +) +` + + moveGMSQL = ` +UPDATE waterway.gauge_measurements +-- Associate measurements to matching gauge version +SET validity = $2 +WHERE isrs_astext(location) = $1 + AND measure_date <@ CAST($2 AS tstzrange) +` + + fixValiditySQL = ` +UPDATE waterway.gauges SET + -- Set enddate of old entry to new startdate in case of overlap: + validity = validity - $2 +WHERE isrs_astext(location) = $1 + AND validity && $2 + AND erased +` + + updateGaugeSQL = ` +UPDATE waterway.gauges SET objname = $6, - geom = ST_SetSRID(ST_MakePoint($7, $8), 4326)::geography, + geom = ST_SetSRID(ST_MakePoint($7, $8), 4326), applicability_from_km = $9, applicability_to_km = $10, - validity = $11, - zero_point = $12, - geodref = $13, - date_info = $14, - source_organization = $15 + zero_point = $11, + geodref = $12, + date_info = $13, + source_organization = $14, + lastupdate = $15 +WHERE location = ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int) + AND NOT erased + AND $15 > lastupdate +RETURNING 1 ` + + deleteReferenceWaterLevelsSQL = ` +DELETE FROM waterway.gauges_reference_water_levels +WHERE isrs_astext(location) = $1 + AND validity = $2 + AND depth_reference <> ALL($3) +RETURNING depth_reference +` + isNtSDepthRefSQL = ` SELECT EXISTS(SELECT 1 FROM depth_references WHERE depth_reference = $1)` insertReferenceWaterLevelsSQL = ` INSERT INTO waterway.gauges_reference_water_levels ( - gauge_id, + location, + validity, depth_reference, value ) VALUES ( ($1::char(2), $2::char(3), $3::char(5), $4::char(5), $5::int), $6, - $7 -) + $7, + $8 +) ON CONFLICT (location, validity, depth_reference) DO UPDATE SET + value = EXCLUDED.value ` ) @@ -134,15 +186,9 @@ start := time.Now() - tx, err := conn.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - responseData, err := getRisData( - tx, + responseData, countries, err := getRisData( ctx, + conn, feedback, wg.Username, wg.Password, @@ -153,273 +199,353 @@ return nil, err } - hasGaugeStmt, err := tx.PrepareContext(ctx, hasGaugeSQL) - if err != nil { - return nil, err - } - defer hasGaugeStmt.Close() - - var ignored int - - type idxCode struct { - jdx int - idx int - code *models.Isrs + var eraseGaugeStmt, insertStmt, moveGMStmt, fixValidityStmt, updateStmt, + deleteReferenceWaterLevelsStmt, + isNtSDepthRefStmt, insertWaterLevelStmt *sql.Stmt + for _, x := range []struct { + sql string + stmt **sql.Stmt + }{ + {eraseGaugeSQL, &eraseGaugeStmt}, + {insertGaugeSQL, &insertStmt}, + {moveGMSQL, &moveGMStmt}, + {fixValiditySQL, &fixValidityStmt}, + {updateGaugeSQL, &updateStmt}, + {deleteReferenceWaterLevelsSQL, &deleteReferenceWaterLevelsStmt}, + {isNtSDepthRefSQL, &isNtSDepthRefStmt}, + {insertReferenceWaterLevelsSQL, &insertWaterLevelStmt}, + } { + var err error + if *x.stmt, err = conn.PrepareContext(ctx, x.sql); err != nil { + return nil, err + } + defer (*x.stmt).Close() } - var news, olds []idxCode + var gauges []string + var unchanged int - for j, data := range responseData { - for i, dr := range data.RisdataReturn { - if dr.RisidxCode == nil { - ignored++ + for _, data := range responseData { + for _, dr := range data.RisdataReturn { + + isrs := string(*dr.RisidxCode) + code, err := models.IsrsFromString(isrs) + if err != nil { + feedback.Warn("Invalid ISRS code '%s': %v", isrs, err) continue } - code, err := models.IsrsFromString(string(*dr.RisidxCode)) - if err != nil { - feedback.Warn("invalid ISRS code %v", err) - ignored++ - continue - } + gauges = append(gauges, isrs) + feedback.Info("Processing %s", code) - if dr.Objname.Loc == nil { - feedback.Warn("missing objname: %s", code) - ignored++ - continue - } - - if dr.Lat == nil || dr.Lon == nil { - feedback.Warn("missing lat/lon: %s", code) - ignored++ - continue - } - - if dr.Zeropoint == nil { - feedback.Warn("missing zeropoint: %s", code) - ignored++ + // We need a valid, non-empty time range to identify gauge versions + if dr.Enddate != nil && dr.Startdate != nil && + !time.Time(*dr.Enddate).After(time.Time(*dr.Startdate)) { + feedback.Warn("End date not after start date") + unchanged++ continue } - var dummy bool - err = hasGaugeStmt.QueryRowContext(ctx, - code.CountryCode, - code.LoCode, - code.FairwaySection, - code.Orc, - code.Hectometre, - ).Scan(&dummy) - switch { - case err == sql.ErrNoRows: - news = append(news, idxCode{jdx: j, idx: i, code: code}) - case err != nil: - return nil, err - case !dummy: - return nil, errors.New("Unexpected result") - default: - olds = append(olds, idxCode{jdx: j, idx: i, code: code}) + var from, to sql.NullInt64 + + if dr.Applicabilityfromkm != nil { + from = sql.NullInt64{ + Int64: int64(*dr.Applicabilityfromkm), + Valid: true, + } + } + if dr.Applicabilitytokm != nil { + to = sql.NullInt64{ + Int64: int64(*dr.Applicabilitytokm), + Valid: true, + } + } + + var tfrom, tto, dateInfo pgtype.Timestamptz + + if dr.Startdate != nil { + tfrom = pgtype.Timestamptz{ + Time: time.Time(*dr.Startdate), + Status: pgtype.Present, + } + } else { + tfrom = pgtype.Timestamptz{ + Status: pgtype.Null, + } + } + + if dr.Enddate != nil { + tto = pgtype.Timestamptz{ + Time: time.Time(*dr.Enddate), + Status: pgtype.Present, + } + } else { + tto = pgtype.Timestamptz{ + Status: pgtype.Null, + } } - } - } - feedback.Info("ignored gauges: %d", ignored) - feedback.Info("new gauges: %d", len(news)) - feedback.Info("update gauges: %d", len(olds)) - if len(news) == 0 && len(olds) == 0 { - return nil, UnchangedError("nothing to do") - } + validity := pgtype.Tstzrange{ + Lower: tfrom, + Upper: tto, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + } - // delete reference water leves of the old. - if len(olds) > 0 { - deleteReferenceWaterLevelsStmt, err := tx.PrepareContext( - ctx, deleteReferenceWaterLevelsSQL) - if err != nil { - return nil, err - } - defer deleteReferenceWaterLevelsStmt.Close() - for i := range olds { - code := olds[i].code - if _, err := deleteReferenceWaterLevelsStmt.ExecContext(ctx, - code.CountryCode, - code.LoCode, - code.FairwaySection, - code.Orc, - code.Hectometre, - ); err != nil { + if dr.Infodate != nil { + dateInfo = pgtype.Timestamptz{ + Time: time.Time(*dr.Infodate), + Status: pgtype.Present, + } + } else { + dateInfo = pgtype.Timestamptz{ + Status: pgtype.Null, + } + } + + var geodref sql.NullString + if dr.Geodref != nil { + geodref = sql.NullString{ + String: string(*dr.Geodref), + Valid: true, + } + } + + var source sql.NullString + if dr.Source != nil { + source = sql.NullString{ + String: string(*dr.Source), + Valid: true, + } + } + + tx, err := conn.BeginTx(ctx, nil) + if err != nil { return nil, err } - } - // treat them as new - news = append(news, olds...) - } - - insertStmt, err := tx.PrepareContext(ctx, insertGaugeSQL) - if err != nil { - return nil, err - } - defer insertStmt.Close() - - insertWaterLevelStmt, err := tx.PrepareContext( - ctx, insertReferenceWaterLevelsSQL) - if err != nil { - return nil, err - } - defer insertWaterLevelStmt.Close() - - isNtSDepthRefStmt, err := tx.PrepareContext(ctx, isNtSDepthRefSQL) - if err != nil { - return nil, err - } - defer isNtSDepthRefStmt.Close() - - // insert/update the gauges - for i := range news { - ic := &news[i] - dr := responseData[ic.jdx].RisdataReturn[ic.idx] - - feedback.Info("insert/update %s", ic.code) - - var from, to sql.NullInt64 - - if dr.Applicabilityfromkm != nil { - from = sql.NullInt64{ - Int64: int64(*dr.Applicabilityfromkm), - Valid: true, - } - } - if dr.Applicabilitytokm != nil { - to = sql.NullInt64{ - Int64: int64(*dr.Applicabilitytokm), - Valid: true, - } - } - - var tfrom, tto, dateInfo pgtype.Timestamptz - - if dr.Startdate != nil { - tfrom = pgtype.Timestamptz{ - Time: time.Time(*dr.Startdate), - Status: pgtype.Present, - } - } else { - tfrom = pgtype.Timestamptz{ - Status: pgtype.Null, - } - } + defer tx.Rollback() - if dr.Enddate != nil { - tto = pgtype.Timestamptz{ - Time: time.Time(*dr.Enddate), - Status: pgtype.Present, - } - } else { - tto = pgtype.Timestamptz{ - Status: pgtype.Null, - } - } - - validity := pgtype.Tstzrange{ - Lower: tfrom, - Upper: tto, - LowerType: pgtype.Inclusive, - UpperType: pgtype.Inclusive, - Status: pgtype.Present, - } - - if dr.Infodate != nil { - dateInfo = pgtype.Timestamptz{ - Time: time.Time(*dr.Infodate), - Status: pgtype.Present, - } - } else { - dateInfo = pgtype.Timestamptz{ - Status: pgtype.Null, - } - } - - var geodref sql.NullString - if dr.Geodref != nil { - geodref = sql.NullString{ - String: string(*dr.Geodref), - Valid: true, - } - } + // Mark old entry of gauge as erased, if applicable + var isNew bool + err = tx.StmtContext(ctx, eraseGaugeStmt).QueryRowContext(ctx, + code.String(), + validity, + ).Scan(&isNew) + switch { + case err != nil: + feedback.Warn(handleError(err).Error()) + if err2 := tx.Rollback(); err2 != nil { + return nil, err2 + } + unchanged++ + continue + case isNew: + // insert gauge version entry + if _, err = tx.StmtContext(ctx, insertStmt).ExecContext(ctx, + code.CountryCode, + code.LoCode, + code.FairwaySection, + code.Orc, + code.Hectometre, + dr.Objname.Loc, + dr.Lon, dr.Lat, + from, + to, + &validity, + dr.Zeropoint, + geodref, + &dateInfo, + source, + time.Time(*dr.Lastupdate), + ); err != nil { + feedback.Warn(handleError(err).Error()) + if err2 := tx.Rollback(); err2 != nil { + return nil, err2 + } + unchanged++ + continue + } + // Move gauge measurements to new matching gauge version, + // if applicable + if _, err = tx.StmtContext(ctx, moveGMStmt).ExecContext(ctx, + code.String(), + &validity, + ); err != nil { + feedback.Warn(handleError(err).Error()) + if err2 := tx.Rollback(); err2 != nil { + return nil, err2 + } + unchanged++ + continue + } + // Set end of validity of old version to start of new version + // in case of overlap + if _, err = tx.StmtContext(ctx, fixValidityStmt).ExecContext( + ctx, + code.String(), + &validity, + ); err != nil { + feedback.Warn(handleError(err).Error()) + if err2 := tx.Rollback(); err2 != nil { + return nil, err2 + } + unchanged++ + continue + } + feedback.Info("insert new version") + case !isNew: + // try to update + var dummy int + err2 := tx.StmtContext(ctx, updateStmt).QueryRowContext(ctx, + code.CountryCode, + code.LoCode, + code.FairwaySection, + code.Orc, + code.Hectometre, + dr.Objname.Loc, + dr.Lon, dr.Lat, + from, + to, + dr.Zeropoint, + geodref, + &dateInfo, + source, + time.Time(*dr.Lastupdate), + ).Scan(&dummy) + switch { + case err2 == sql.ErrNoRows: + feedback.Info("unchanged") + if err3 := tx.Rollback(); err3 != nil { + return nil, err3 + } + unchanged++ + continue + case err2 != nil: + feedback.Warn(handleError(err2).Error()) + if err3 := tx.Rollback(); err3 != nil { + return nil, err3 + } + unchanged++ + continue + default: + feedback.Info("update") + } - var source sql.NullString - if dr.Source != nil { - source = sql.NullString{ - String: string(*dr.Source), - Valid: true, - } - } - - if _, err := insertStmt.ExecContext(ctx, - ic.code.CountryCode, - ic.code.LoCode, - ic.code.FairwaySection, - ic.code.Orc, - ic.code.Hectometre, - string(*dr.Objname.Loc), - float64(*dr.Lon), float64(*dr.Lat), - from, - to, - &validity, - float64(*dr.Zeropoint), - geodref, - &dateInfo, - source, - ); err != nil { - return nil, err - } - - for _, wl := range []struct { - level **erdms.RisreflevelcodeType - value **erdms.RisreflevelvalueType - }{ - {&dr.Reflevel1code, &dr.Reflevel1value}, - {&dr.Reflevel2code, &dr.Reflevel2value}, - {&dr.Reflevel3code, &dr.Reflevel3value}, - } { - if *wl.level == nil || *wl.value == nil { - continue + // Remove obsolete reference water levels + var currLevels pgtype.VarcharArray + currLevels.Set([]string{ + string(*dr.Reflevel1code), + string(*dr.Reflevel2code), + string(*dr.Reflevel3code), + }) + rwls, err := tx.StmtContext(ctx, + deleteReferenceWaterLevelsStmt).QueryContext(ctx, + code.String(), + &validity, + &currLevels, + ) + if err != nil { + return nil, err + } + defer rwls.Close() + for rwls.Next() { + var delRef string + if err := rwls.Scan(&delRef); err != nil { + return nil, err + } + feedback.Warn("Removed reference water level %s from %s", + delRef, code) + } + if err := rwls.Err(); err != nil { + return nil, err + } } - var isNtSDepthRef bool - if err := isNtSDepthRefStmt.QueryRowContext( - ctx, - string(**wl.level), - ).Scan( - &isNtSDepthRef, - ); err != nil { - return nil, err - } - if !isNtSDepthRef { - feedback.Warn( - "Reference level code '%s' is not in line with the NtS reference_code table", - string(**wl.level)) + // "Upsert" reference water levels + for _, wl := range []struct { + level **erdms.RisreflevelcodeType + value **erdms.RisreflevelvalueType + }{ + {&dr.Reflevel1code, &dr.Reflevel1value}, + {&dr.Reflevel2code, &dr.Reflevel2value}, + {&dr.Reflevel3code, &dr.Reflevel3value}, + } { + if *wl.level == nil || *wl.value == nil { + continue + } + + var isNtSDepthRef bool + if err := tx.StmtContext( + ctx, isNtSDepthRefStmt).QueryRowContext(ctx, + string(**wl.level), + ).Scan( + &isNtSDepthRef, + ); err != nil { + return nil, err + } + if !isNtSDepthRef { + feedback.Warn( + "Reference level code '%s' is not in line "+ + "with the NtS reference_code table", + string(**wl.level)) + } + + if _, err := tx.StmtContext( + ctx, insertWaterLevelStmt).ExecContext(ctx, + code.CountryCode, + code.LoCode, + code.FairwaySection, + code.Orc, + code.Hectometre, + &validity, + string(**wl.level), + int64(**wl.value), + ); err != nil { + feedback.Warn(handleError(err).Error()) + tx.Rollback() + continue + } } - if _, err := insertWaterLevelStmt.ExecContext( - ctx, - ic.code.CountryCode, - ic.code.LoCode, - ic.code.FairwaySection, - ic.code.Orc, - ic.code.Hectometre, - string(**wl.level), - int64(**wl.value), - ); err != nil { + if err = tx.Commit(); err != nil { return nil, err } } } - if err = tx.Commit(); err == nil { - feedback.Info("Refreshing gauges successfully took %s.", - time.Since(start)) - } else { - feedback.Error("Refreshing gauges failed after %s.", - time.Since(start)) + if len(gauges) == 0 { + return nil, UnchangedError("No gauges returned from ERDMS") + } + + var pgCountries, pgGauges pgtype.VarcharArray + pgCountries.Set(countries) + pgGauges.Set(gauges) + obsGauges, err := conn.QueryContext(ctx, + eraseObsoleteGaugesSQL, + &pgCountries, + &pgGauges) + if err != nil { + return nil, err } + defer obsGauges.Close() + for obsGauges.Next() { + var isrs string + if err := obsGauges.Scan(&isrs); err != nil { + return nil, err + } + feedback.Info("Erased %s", isrs) + unchanged-- + } + if err := obsGauges.Err(); err != nil { + return nil, err + } + + if unchanged == len(gauges) { + return nil, UnchangedError("All gauges unchanged") + } + + feedback.Info("Importing gauges took %s", + time.Since(start)) return nil, err }
--- a/pkg/imports/wp.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/wp.go Mon Jun 03 10:19:18 2019 +0200 @@ -72,9 +72,10 @@ return "waterway profiles" } -func (wpJobCreator) Depends() []string { - return []string{ - "waterway_profiles", +func (wpJobCreator) Depends() [2][]string { + return [2][]string{ + {"waterway_profiles"}, + {"distance_marks_virtual"}, } }
--- a/pkg/imports/wx.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/imports/wx.go Mon Jun 03 10:19:18 2019 +0200 @@ -58,9 +58,10 @@ func (wxJobCreator) Create() Job { return new(WaterwayAxis) } -func (wxJobCreator) Depends() []string { - return []string{ - "waterway_axis", +func (wxJobCreator) Depends() [2][]string { + return [2][]string{ + {"waterway_axis"}, + {}, } } @@ -80,43 +81,29 @@ const ( deleteWaterwayAxisSQL = ` WITH resp AS ( - SELECT best_utm(area::geometry) AS t, - ST_Transform(area::geometry, best_utm(area::geometry)) AS a - FROM users.responsibility_areas - WHERE country = users.current_user_country() + SELECT users.current_user_area_utm() AS a ) DELETE FROM waterway.waterway_axis WHERE pg_has_role('sys_admin', 'MEMBER') OR ST_Covers((SELECT a FROM resp), - ST_Transform(wtwaxs::geometry, (SELECT t FROM resp))) -` - - checkCrossingAxisSQL = ` -SELECT ST_AsText(ST_Intersection(new_line.wtwaxs, axis.wtwaxs)) - FROM waterway.waterway_axis AS axis, waterway.waterway_axis AS new_line - WHERE new_line.id = $1 AND axis.id <> $1 - AND ST_Crosses(new_line.wtwaxs::geometry, axis.wtwaxs::geometry) + ST_Transform(wtwaxs::geometry, (SELECT ST_SRID(a) FROM resp))) ` insertWaterwayAxisSQL = ` WITH resp AS ( - SELECT best_utm(area::geometry) AS t, - ST_Transform(area::geometry, best_utm(area::geometry)) AS a - FROM users.responsibility_areas - WHERE country = users.current_user_country() + SELECT users.current_user_area_utm() AS a ) INSERT INTO waterway.waterway_axis (wtwaxs, objnam, nobjnam) -SELECT ST_Transform(clipped.geom, 4326)::geography, $3, $4 - FROM resp, - ST_Node(ST_Transform( - ST_GeomFromWKB($1, $2::integer), t)) AS new_line (new_line), - LATERAL (SELECT (ST_Dump( +SELECT dmp.geom, $3, $4 + FROM ST_GeomFromWKB($1, $2::integer) AS new_line (new_line), + ST_Dump(ST_Transform(ST_CollectionExtract( CASE WHEN pg_has_role('sys_admin', 'MEMBER') - THEN new_line - ELSE ST_Intersection(a, new_line) - END - )).geom AS geom - ) AS clipped + THEN ST_Node(ST_Transform(new_line, + best_utm(ST_Transform(new_line, 4326)))) + ELSE ST_Intersection((SELECT a FROM resp), + ST_Node(ST_Transform(new_line, (SELECT ST_SRID(a) FROM resp)))) + END, + 2), 4326)) AS dmp RETURNING id ` ) @@ -175,12 +162,6 @@ } defer insertStmt.Close() - checkCrossingStmt, err := tx.PrepareContext(ctx, checkCrossingAxisSQL) - if err != nil { - return nil, err - } - defer checkCrossingStmt.Close() - // Delete the old features. if _, err := tx.ExecContext(ctx, deleteWaterwayAxisSQL); err != nil { return nil, err @@ -190,6 +171,7 @@ unsupported = stringCounter{} missingProperties int badProperties int + outside int features int ) @@ -248,11 +230,11 @@ epsg, props, nobjnam, - checkCrossingStmt, + &outside, + &features, insertStmt); err != nil { return err } - features++ case "MultiLineString": var ls []lineSlice if err := json.Unmarshal(*feature.Geometry.Coordinates, &ls); err != nil { @@ -267,11 +249,11 @@ epsg, props, nobjnam, - checkCrossingStmt, + &outside, + &features, insertStmt); err != nil { return err } - features++ } default: unsupported[feature.Geometry.Type]++ @@ -294,10 +276,12 @@ feedback.Warn("Unsupported types found: %s", unsupported) } + if outside > 0 { + feedback.Info("Features outside responsibility area: %d", outside) + } + if features == 0 { - err := errors.New("No features found") - feedback.Error("%v", err) - return nil, err + return nil, errors.New("No features found") } if err = tx.Commit(); err == nil { @@ -316,10 +300,11 @@ epsg int, props waterwayAxisProperties, nobjnam sql.NullString, - checkCrossingStmt, insertStmt *sql.Stmt, + outside, features *int, + insertStmt *sql.Stmt, ) error { var id int - if err := savepoint(func() error { + err := savepoint(func() error { err := insertStmt.QueryRowContext( ctx, l.asWKB(), @@ -328,22 +313,16 @@ nobjnam, ).Scan(&id) return err - }); err != nil { + }) + switch { + case err == sql.ErrNoRows: + *outside++ + // ignore -> filtered by responsibility_areas + return nil + case err != nil: feedback.Warn(handleError(err).Error()) - } - - var crossing string - switch err := checkCrossingStmt.QueryRowContext( - ctx, - id, - ).Scan(&crossing); { - case err != nil && err != sql.ErrNoRows: - return err - case err == nil: - feedback.Warn( - "Linestring %d crosses previously imported linestring near %s. "+ - "Finding a contiguous axis may not work here", - id, crossing) + default: + *features++ } return nil }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/misc/warn.go Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,32 @@ +// 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) 2019 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package misc + +type WarningLimiter struct { + Log func(format string, args ...interface{}) + MaxWarnings int + Warnings int +} + +func (w *WarningLimiter) Warn(format string, args ...interface{}) { + if w.Warnings++; w.Warnings <= w.MaxWarnings { + w.Log(format, args...) + } +} + +func (w *WarningLimiter) Close() { + if w.Warnings > w.MaxWarnings { + w.Log("Too many warnings. %d ignored.", w.Warnings-w.MaxWarnings) + } +}
--- a/pkg/models/cross.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/models/cross.go Mon Jun 03 10:19:18 2019 +0200 @@ -22,6 +22,7 @@ "math" "time" + "gemma.intevation.de/gemma/pkg/common" "gemma.intevation.de/gemma/pkg/wkb" ) @@ -103,7 +104,7 @@ if err := json.Unmarshal(data, &s); err != nil { return err } - t, err := time.Parse("2006-01-02", s) + t, err := time.Parse(common.DateFormat, s) if err != nil { return err }
--- a/pkg/models/importbase.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/models/importbase.go Mon Jun 03 10:19:18 2019 +0200 @@ -91,8 +91,7 @@ } func (cd *ConfigDuration) MarshalJSON() ([]byte, error) { - s := cd.Duration.String() - return json.Marshal([]byte(s)) + return json.Marshal(cd.Duration.String()) } func (ct *ConfigTime) UnmarshalJSON(data []byte) error {
--- a/pkg/models/imports.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/models/imports.go Mon Jun 03 10:19:18 2019 +0200 @@ -107,6 +107,19 @@ Date Date `json:"date-info"` Countries UniqueCountries `json:"countries"` } + + SectionImport struct { + EmailType + + Name string `json:"name"` + From Isrs `json:"from"` + To Isrs `json:"to"` + Tolerance float32 `json:"tolerance"` + ObjNam string `json:"objnam"` + NObjNam *string `json:"nobjnam"` + Source string `json:"source-organization"` + Date Date `json:"date-info"` + } ) func (cui *ConfigurableURLImport) MarshalAttributes(attrs common.Attributes) error { @@ -294,3 +307,69 @@ } return nil } + +func (seci *SectionImport) MarshalAttributes(attrs common.Attributes) error { + if err := seci.EmailType.MarshalAttributes(attrs); err != nil { + return err + } + attrs.Set("name", seci.Name) + attrs.Set("from", seci.From.String()) + attrs.Set("to", seci.To.String()) + attrs.Set("objnam", seci.ObjNam) + if seci.NObjNam != nil { + attrs.Set("nobjnam", *seci.NObjNam) + } + attrs.Set("source-organization", seci.Source) + attrs.SetDate("date-info", seci.Date.Time) + + return nil +} + +func (seci *SectionImport) UnmarshalAttributes(attrs common.Attributes) error { + if err := seci.EmailType.UnmarshalAttributes(attrs); err != nil { + return err + } + name, found := attrs.Get("name") + if !found { + return errors.New("missing 'name' attribute") + } + seci.Name = name + from, found := attrs.Get("from") + if !found { + return errors.New("missing 'from' attribute") + } + f, err := IsrsFromString(from) + if err != nil { + return err + } + seci.From = *f + to, found := attrs.Get("to") + if !found { + return errors.New("missing 'to' attribute") + } + t, err := IsrsFromString(to) + if err != nil { + return err + } + seci.To = *t + objnam, found := attrs.Get("objnam") + if !found { + return errors.New("missing 'objnam' attribute") + } + seci.ObjNam = objnam + nobjnam, found := attrs.Get("nobjnam") + if found { + seci.NObjNam = &nobjnam + } + source, found := attrs.Get("source-organization") + if !found { + return errors.New("missing 'source' attribute") + } + seci.Source = source + date, found := attrs.Date("date-info") + if !found { + return errors.New("missing 'date-info' attribute") + } + seci.Date = Date{date} + return nil +}
--- a/pkg/models/sr.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/models/sr.go Mon Jun 03 10:19:18 2019 +0200 @@ -36,7 +36,13 @@ const ( checkDepthReferenceSQL = ` -SELECT true FROM depth_references WHERE depth_reference = $1` +SELECT EXISTS(SELECT 1 + FROM waterway.bottlenecks bn + JOIN waterway.gauges g + ON bn.gauge_location = g.location AND bn.gauge_validity = g.validity + JOIN waterway.gauges_reference_water_levels rl USING (location, validity) + WHERE bn.objnam = $1 + AND rl.depth_reference = $2)` checkBottleneckSQL = ` SELECT true FROM waterway.bottlenecks WHERE objnam = $1` @@ -61,18 +67,6 @@ var b bool err := conn.QueryRowContext(ctx, - checkDepthReferenceSQL, - m.DepthReference).Scan(&b) - switch { - case err == sql.ErrNoRows: - errs = append(errs, fmt.Errorf("unknown depth reference '%s'", m.DepthReference)) - case err != nil: - errs = append(errs, err) - case !b: - errs = append(errs, errors.New("unexpected depth reference")) - } - - err = conn.QueryRowContext(ctx, checkBottleneckSQL, m.Bottleneck).Scan(&b) switch { @@ -85,6 +79,18 @@ } err = conn.QueryRowContext(ctx, + checkDepthReferenceSQL, + m.Bottleneck, + m.DepthReference).Scan(&b) + switch { + case !b: + errs = append(errs, + fmt.Errorf("unknown depth reference '%s'", m.DepthReference)) + case err != nil: + errs = append(errs, err) + } + + err = conn.QueryRowContext(ctx, checkBottleneckDateUniqueSQL, m.Bottleneck, m.Date.Time).Scan(&b) switch {
--- a/pkg/scheduler/scheduler.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/scheduler/scheduler.go Mon Jun 03 10:19:18 2019 +0200 @@ -15,6 +15,8 @@ import ( "log" + "sort" + "strings" "sync" "github.com/robfig/cron" @@ -78,6 +80,8 @@ cr := cron.New() + var numJobs int + for { var ba BoundAction ok, err := next(&ba) @@ -97,8 +101,11 @@ cfgID: ba.CfgID, } cr.Schedule(schedule, job) + numJobs++ } + log.Printf("info: booting %d scheduler jobs from database.\n", numJobs) + s.mu.Lock() defer s.mu.Unlock() @@ -288,8 +295,26 @@ return s.actions[name] } +func LogActionNames() { + names := global.actionNames() + sort.Strings(names) + log.Printf("info: actions registered to scheduler: %s\n", + strings.Join(names, ", ")) +} + +func (s *scheduler) actionNames() []string { + s.mu.Lock() + defer s.mu.Unlock() + names := make([]string, len(s.actions)) + var i int + for k := range s.actions { + names[i] = k + i++ + } + return names +} + func (s *scheduler) registerAction(name string, action Action) { - log.Printf("info: register action '%s' in scheduler.", name) s.mu.Lock() defer s.mu.Unlock() s.actions[name] = action
--- a/pkg/soap/erdms/service.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/soap/erdms/service.go Mon Jun 03 10:19:18 2019 +0200 @@ -118,6 +118,21 @@ // lastupdate, last modification date time of this record type LastupdateType time.Time +const LastupdateTypeFormat = "2006-01-02T15:04:05.999Z07:00" + +func (lut *LastupdateType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var value string + if err := d.DecodeElement(&value, &start); err != nil { + return err + } + t, err := time.Parse(LastupdateTypeFormat, value) + if err != nil { + return err + } + *lut = LastupdateType(t) + return nil +} + // one of the supported languages (basic and additional set), based on ISO 639-1 type LangType string
--- a/pkg/soap/ifbn/service.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/soap/ifbn/service.go Mon Jun 03 10:19:18 2019 +0200 @@ -77,7 +77,7 @@ Responsible_country *CountryCode `xml:"responsible_country,omitempty"` - Revisiting_time string `xml:"revisiting_time,omitempty"` + Revisiting_time *string `xml:"revisiting_time,omitempty"` SURTYP *SurtypEnum `xml:"SURTYP,omitempty"`
--- a/pkg/soap/nts/service.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/soap/nts/service.go Mon Jun 03 10:19:18 2019 +0200 @@ -1273,13 +1273,13 @@ Measure_code *Measure_code_enum `xml:"measure_code,omitempty"` // Measured or predicted value - Value float32 `xml:"value,omitempty"` + Value *float32 `xml:"value,omitempty"` // Lowest value of confidence interval - Value_min float32 `xml:"value_min,omitempty"` + Value_min *float32 `xml:"value_min,omitempty"` // Highest value of confidence interval - Value_max float32 `xml:"value_max,omitempty"` + Value_max *float32 `xml:"value_max,omitempty"` // Unit of the water related value Unit *Unit_enum `xml:"unit,omitempty"`
--- a/pkg/soap/soap.go Wed May 29 10:58:45 2019 +0200 +++ b/pkg/soap/soap.go Mon Jun 03 10:19:18 2019 +0200 @@ -15,6 +15,7 @@ import ( "bytes" + "context" "crypto/tls" "encoding/xml" "fmt" @@ -23,7 +24,10 @@ "math/rand" "net" "net/http" + "sync" "time" + + "gemma.intevation.de/gemma/pkg/config" ) const timeout = time.Duration(30 * time.Second) @@ -272,6 +276,21 @@ client := &http.Client{Transport: tr} + timeout := config.SOAPTimeout() + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + + var once sync.Once + + defer once.Do(cancel) + + req = req.WithContext(ctx) + + go func() { + defer once.Do(cancel) + <-ctx.Done() + }() + res, err := client.Do(req) if err != nil { return err
--- a/schema/auth.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/auth.sql Mon Jun 03 10:19:18 2019 +0200 @@ -4,7 +4,7 @@ -- SPDX-License-Identifier: AGPL-3.0-or-later -- License-Filename: LICENSES/AGPL-3.0.txt --- Copyright (C) 2018 by via donau +-- Copyright (C) 2018, 2019 by via donau -- – Österreichische Wasserstraßen-Gesellschaft mbH -- Software engineering by Intevation GmbH @@ -102,6 +102,8 @@ LOOP EXECUTE format('CREATE POLICY hide_staging ON waterway.%I ' 'FOR SELECT TO waterway_user USING (staging_done)', the_table); + EXECUTE format('CREATE POLICY sys_admin ON waterway.%I ' + 'FOR ALL TO sys_admin USING (true)', the_table); EXECUTE format('ALTER TABLE waterway.%I ENABLE ROW LEVEL SECURITY', the_table); END LOOP; @@ -112,10 +114,10 @@ -- RLS policies for templates -- CREATE POLICY select_templates ON users.templates FOR SELECT TO waterway_user - USING (country IS NULL OR country = users.current_user_country()); + USING (country IS NULL OR country = (SELECT country FROM users.list_users WHERE username = current_user)); CREATE POLICY user_templates ON users.templates FOR ALL TO waterway_admin - USING (country = users.current_user_country()); + USING (country = (SELECT country FROM users.list_users WHERE username = current_user)); CREATE POLICY admin_templates ON users.templates FOR ALL TO sys_admin USING (true); @@ -131,27 +133,25 @@ CREATE POLICY same_country ON waterway.gauge_measurements FOR ALL TO waterway_admin - USING ((fk_gauge_id).country_code = users.current_user_country()); + USING ((location).country_code + = (SELECT country FROM users.list_users WHERE username = current_user) + ); CREATE POLICY same_country ON waterway.waterway_profiles FOR ALL TO waterway_admin - USING ((location).country_code = users.current_user_country()); + USING ((location).country_code = (SELECT country FROM users.list_users WHERE username = current_user)); CREATE POLICY responsibility_area ON waterway.bottlenecks FOR ALL TO waterway_admin - USING (utm_covers(area)); + USING (users.utm_covers(area)); CREATE POLICY responsibility_area ON waterway.sounding_results FOR ALL TO waterway_admin - USING (utm_covers(area)); + USING (users.utm_covers(area)); CREATE POLICY responsibility_area ON waterway.fairway_dimensions FOR ALL TO waterway_admin - USING (utm_covers(area)); - -CREATE POLICY sys_admin ON waterway.stretches - FOR ALL TO sys_admin - USING (true); + USING (users.utm_covers(area)); -- -- RLS policies for imports and import config @@ -212,7 +212,7 @@ CREATE POLICY import_configuration_policy ON import.import_configuration FOR ALL TO waterway_admin USING ( - users.current_user_country() = ( + (SELECT country FROM users.list_users WHERE username = current_user) = ( SELECT country FROM users.list_users lu WHERE lu.username = import.import_configuration.username));
--- a/schema/auth_tests.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/auth_tests.sql Mon Jun 03 10:19:18 2019 +0200 @@ -15,6 +15,28 @@ -- pgTAP test script for privileges and RLS policies -- +CREATE FUNCTION test_privs() RETURNS SETOF TEXT AS +$$ +DECLARE the_schema CONSTANT varchar = 'waterway'; +DECLARE the_table varchar; +BEGIN + FOR the_table IN + SELECT table_name + FROM information_schema.tables + WHERE table_schema = the_schema + LOOP + RETURN NEXT table_privs_are( + the_schema, + the_table, + 'waterway_user', + ARRAY['SELECT'], + format('waterway_user can SELECT from %I.%I', + the_schema, the_table)); + END LOOP; +END; +$$ LANGUAGE plpgsql; +SELECT * FROM test_privs(); + -- -- Run tests as unprivileged user -- @@ -53,16 +75,18 @@ PREPARE bn_insert (varchar, geometry(MULTIPOLYGON, 4326)) AS INSERT INTO waterway.bottlenecks ( - bottleneck_id, fk_g_fid, stretch, area, rb, lb, responsible_country, + gauge_location, gauge_validity, + bottleneck_id, stretch, area, rb, lb, responsible_country, revisiting_time, limiting, source_organization) - VALUES ( + SELECT + location, validity, $1, - ('AT', 'XXX', '00001', 'G0001', 1)::isrs, isrsrange(('AT', 'XXX', '00001', '00000', 0)::isrs, ('AT', 'XXX', '00001', '00000', 2)::isrs), $2, 'AT', 'AT', 'AT', 1, 'depth', 'testorganization' - ); + FROM waterway.gauges + WHERE location = ('AT', 'XXX', '00001', 'G0001', 1)::isrs; SELECT lives_ok($$ EXECUTE bn_insert( 'test1',
--- a/schema/demo-data/published_services.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/demo-data/published_services.sql Mon Jun 03 10:19:18 2019 +0200 @@ -12,6 +12,7 @@ -- * Tom Gottfried <tom@intevation.de> INSERT INTO sys_admin.published_services (name) VALUES + ('waterway.sections_geoserver'), ('waterway.stretches_geoserver'), ('waterway.fairway_dimensions'), ('waterway.gauges_geoserver'), @@ -22,4 +23,5 @@ ('waterway.bottleneck_overview'), ('waterway.waterway_axis'), ('waterway.waterway_area'), - ('waterway.waterway_profiles') + ('waterway.waterway_profiles'), + ('waterway.sounding_differences')
--- a/schema/demo-data/responsibility_areas.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/demo-data/responsibility_areas.sql Mon Jun 03 10:19:18 2019 +0200 @@ -21,6 +21,7 @@ AT HR BG +RS \. COPY users.responsibility_areas (country, area) FROM stdin; @@ -30,6 +31,7 @@ AT 0106000020E61000000100000001030000000100000046040000C04A9D52EEF13040EAFA1051554D4840E86633EC4FF430408EFE99D958474840286AB342F5F230403A155330BD45484000FF1F3E68F23040BE56E1BA15454840F8725F4833EE304062C9DE1D9C43484038DAA08CF7E93040CEC6C3B57942484038728DDE0FE830408A033BAD44414840D0B6E56DB6E630403A28B3A4903F48407009A8C604E03040E6296E605B3C484018E343FD6CDD30407E106C43A23A4840A075570E5EDC304012F2820ECE38484050C79F8C17DA304046FBE4D7F831484050CA48376ED93040B6A2B4412A314840E8D9EFA7CBD830401A76D6E3C8304840709B628258D8304086E26EE033304840605F8CDE2FD830405241190BCC2E484040D7BF11FBD830403AB61EC5062E48401083C011F7DA3040864BC652A02D484000C38ADE1FE03040E6FCB4C1712D48401038156794E1304072A37FF1282D4840F0175C4847E4304042883D4A6B2C484090FFB9D7E5E630407EE7FA85712B484038728DDE0FE830400E7FBF986D2A4840E0B823DB7AE7304062A652F8DB2948406082267879E63040428B10511E294840A0829F8CFFE530408E94B85E7828484030BE83071CE730402279FDA2342848401831D022CCE730408EDAC235EF274840B8ECF0A7ABE830404E41E8F44A27484078DA19A17DE9304052A925FF8E264840A852C6E8CEE930400EF0BBFBF9254840204DEDA7A7EA3040FA197FF138254840A037F4E19CEC30403A71AC6ACD24484058689FEFF0EE30406E3107FA7124484018C3C2AEC5F130401A7CE5D79523484000B329155CF330407A2DD44667234840E86633EC4FF430403672C6D2F5224840D8E6F6E18CF43040227A56956722484058AD50D434F43040B21E9282F0204840E86633EC4FF43040A6CF6DE05320484050342AB24EF830402EE59002BB1B48407029F14486F9304096E0E6576A194840C0EAEA0A8DF93040966B71FDA3164840685A156760FB30409EB33473A514484058D39318D50131407A2C973C4712484068EC07F3590531403275D1A98E114840E0BEE5D0730931408EB6077A58114840C09E2CB2260C314002AC5C4FC410484048C8FD11FD0F314092B58AC16D0E48402025146734113140769F3710B00D4840F040B8D789143140E6BCAB6A7E0C484050595A48EB113140FE16828E690B484098AAB09D981031409629D2A91D0A484088352615240F31400A9C3510BC074840A8B727782510314036036EE08507484038920E2D371331409AFC0A97AA064840086171F6D61131409A3FE7F490044840902086A4B61731402E6E7EF17C03484030092FB2FE1F31400AC73CCA7F02484060822678F9253140928F4F5BB20048402072550EEA1531402EC35DCF2DFC474018529CEF9C1831404A8DEC9120FB47408898B9D7811831406215B95E55FA474018E2F87E431731400640087A88F9474008AEB29D8C163140CE0A23626FF84740783836EC0B163140729D9A5938F84740B0D09B52AA13314096B166A6ACF7474048124B37FA1231401A3A46844FF74740885308F33D1331404E1CD446C1F64740A07FBE1183143140565E963C7FF64740A0EF6182DC153140E202BDFB59F64740E071DCF963163140A2F33DCA18F64740E0B8EB0A551531407EBCC96FD1F3474020CE6BBCD91331407AA23B2D24F24740B0DEF67E4F113140FE3D4821D9F04740C05236891A0D3140BACAE8F4BAEF474058FC27783D043140E61ED3C610EF4740685B60E509013140F2F424FF7FEE4740309F4D371E013140AA850DB41AED4740C804AA63BB0231408A4F236287EC4740E04E42601A083140C6AC3C2DB1EB4740787038261D0A3140468E3ECA2EEB4740F01BC011930C31405E1A68A6CAE84740200410CA3D0E314016137937FBE74740E072EFA7670A31406E2DAAEA8AE6474048AC95B5BF0A31405A8B640957E447407862DDF9770C3140FA640040BFE14740B81660E5F10C31407E74371091DD4740D0421604370E314086E404DD46DC4740F8EBE66D62103140F21DC0184DDB474000D6FB7E4B133140FA45DF9DAFDA474000914A9A070E314056AF34F3DBD94740685A156760FB30409A63CC0C04D9474000FD894115E73040A6A747A14CD74740A06050715FDD30400E9F2BB9E6D7474070D41667A4D930409A1E4C3E41DB474010367F6A41D6304086B5842B49DA4740F870917B3AD13040DE267F7195D74740B8CA69BC65CE3040067FB124A4D64740B0DDAB0026CC30402656A29375D64740708B3989BABD3040D20CDC003AD74740F828FF1BFABA30407EE548A1CBD7474070C1DD961EB830409247852BCCD84740E8D6D65C29B63040FA1B31D61EDA4740E822CD8535B53040F68BD44678DB4740A0F12F4FD5B33040EA1DABEA9EDC474000DB72F69AB03040DE25267F62DD4740E0BE1DA1199C30401657AE8714E047403076F1A75B913040FEF2B85E89E047400052641FF78730409607ADEA19DF4740B0E4F0446E8630409AAE6943DDDD474040237E6AE1863040A6629D762DDC4740C030E76D7A853040B67EF98513DB4740A08716044F83304006A830565EDA4740B075608218793040CE0CC7D28BD84740505C73930D763040161AB7C19ED74740E8B229155C743040F688D02944D74740107F5C482B733040B6557A5459D74740E052A629E67130402AE6D4C6BFD74740981C22DB6A7030408A4D717D61D84740E07508F3896E30403EB4963CBDD747408816F044A66A3040FAACBCFB9BD547402075368966683040D2753373A6D44740507E8ADE076D3040DE6F5C4FBFD34740E82318045F7B304096D6963CC9D14740B0D8F3447682304092908C6552D247404815F4E15093304052ABE557FECF474078857793C19B3040FA6567A67BD04740B042D5BF56A130407A2DC6D29DCF4740C00F5371CFA530404A8251F895CD4740E0B823DBFAA73040EE046FE0FFCA4740200593188DA63040461B6DE084C84740805734ECE3AA30406AAAC435B1C74740D831A28C6FAE30408E063FCA78C6474008929E8C6BB0304052871C28DEC4474038DBEB0A21B0304036BC188BE5C24740103AAB6367AD304002CD3D4A43C1474018D6334FF1A5304086B6CFA932C0474010EC1E3E08A3304042F1A23021BF47405898116710A430400A60973CF9B94740007A715481A03040DE4B5CBF07B94740F0B05B48E396304032081D287BB64740E82318045F7B30408676E9F436B247403849314F4D783040AE453573E8B347402835A48CE37430407A786309B7B44740D048D8F9AF7130405E3E31D66AB44740E86D7830186F3040CE2F53F8CBB24740D0A9D5BFBA6F3040D6044584E3AD4740286661822C6F3040FE8625FF82AD4740103AAB63E76D3040B6E75E6C49AD474078FEC6E8CA6C304042AEB85EF1AC4740D80DF544A26C30409229BD7B2FAC4740F0413B26596D30402A49392DB2AB474090B40E2D836E304056B071FD7BAB4740F065E86DA66F30406680D7E355AB47405824398956703040E6965AB214AB4740E04B61E51D783040668130D688A5474028F8530E267930406E3B505B6EA34740E04E0A9074773040322E8AC8B7A14740A830AF9DD47330404ADA2D3996A04740A80BB7D7DD6B30401A5967261F9F4740A80BB7D7DD6B304072786AC31B9F47402006DE96B66C3040A6406A43E09C4740D06FD65CC5683040A25B3C4A129A474050A1ECA7AB693040BEE31D45F5974740105A6482346B3040CAF0941F82974740D83C540E3E6D304006AB035D9197474050702FB2626F30401E9954957F974740808F6EF61A7130407602AAEAAB964740C81E60482F713040C231CD0C8C954740E86D7830186F3040C2CD01405D934740A0758FDE036F304052D13610A8924740004B16677472304072977DF1E1914740482E5F481B7B3040569013514C93474038BA1F3E507F3040926CB5C128934740E0CBF57E6F82304032C1C7D29A914740C06D37EC378130408EFB87481B904740E82318045F7B304056AA707D788D474008648A41F9753040C675178B538C4740703036894E743040EA003C2D758A4740E0E34D37367630408EE0AE87C4884740E82318045F7B304006A4C5D22D884740007C0B2D637E3040AECACC0CA8874740F0F82578497F30409A44B9DEFD86474020012F4F417E30405ECB6D604A864740E82318045F7B3040FE63D1A9A8854740885BA129EA6F304072C5AB6A118447402006DE96B66C3040FACC281C15834740D03A7DCD0A743040C286BA5EC6824740B8CECD85B1773040FA66A4B05B824740E82318045F7B3040C6514E5B3381474040D5F144827C30406A14759AD07F4740680DD4BFAA773040B6A91C286A7F4740F0D58BDEFF7030406AE82262637F474068C7D75CBD6C3040DA49E8F41D7F474068C7D75CBD6C30404269EB911A7F474058D7F7E1A06B30409AACC5D2C07E4740E8203789626A30400AF3E2BAA57E4740B82689DE0F693040761042E7C57E47400841F0A7AF6730400ECD67A61F7F474060F66582446330406A7E41E742804740C829DAF9D75D30405ECCC6527D80474050595A486B533040421DF5680E8047408044FB1BDE493040A66B8DE5B6804740A8EC800760463040125BF6688D804740E04DF7E1F0433040B2AD6409237F474050121367D44230408252190B327D4740406E2915C440304082EAF02E9C7C474068CAB8D7393E3040EA69A1136B7C4740F8A3540EA23B3040BE10E557A87B474060F201B9F83A30406E79AE87E07A474008B1CBE82E3B304042CE247F2A7A4740C0EAEA0A0D3B30407A0BAF87637947401080A7C65439304082951FC574784740D09270F68E373040F218188BFC7747408855D6BF36323040AE81E1BA34774740105A6482B42B30408692067A92754740103960E5BD2830406682973C857447406065BE74741F3040B2D278B72C704740A88009902C1C3040CE2C4FDB176F474048AE2BB2121830402A04B95E6F6E4740C0DD162F8D0D3040466671B04B6C4740E0E62EB232083040BEC7F905356B474090A4E0E747073040669AEE17216B4740C0C44A53A4F92F401633F3CB3D6A4740B06C572AA0F62F40FAF8C098F1694740F09254C7A6F12F4052CDE7740A694740D092DBB220F22F409222717DC268474050F58E3467F42F405EA339AD79684740A0722294D3F42F407214A6B0916747404098B4F347F12F40AE3925FF97634740802FEAE624F12F407E899DD534634740802081C07CF02F40EE462A1C59614740A0341D20E9F02F4036DDEB111B5F4740305D29CEC8F62F401E4ED346F95B4740F8F095B5D7003040D6BC88C8C65A474048CEAC00BA03304082E548A1CB584740F8E851EE47043040DACCC136D955474000BF55713F043040AAB5465DDB554740907FF1FDEEFE2F403EFEE8F4EC574740A0B5950B53F92F403EB63AAD995847401097893487E42F404ACC953C3C594740C09B8797D0C12F40E66F8D65405C4740D046E58994B32F401ACD9102BC5C4740B05A3A7C58A52F40C2E8C5D2855C4740B022DF63B8912F4072CE725A295B4740605F38DFA1822F407AE533731D5A4740407DE2EC15752F40AE6A5795FB59474040A864D8DF4D2F40FEA6911FFC5A4740B080B5909E452F40C230741AD95B474000914E2A08442F407E0767A6EA59474060AA4AB615442F402A0C50DB45584740C0CF630104412F406EC42FD626574740C0C66A65553E2F408EE38A6FCE564740207EB456B93B2F403A895E6C78564740103257011C352F40A2B334732556474000242B31622D2F40C20B52F885564740100820947B222F403A705A327F56474000012A6B87172F406A32593200564740C0ED1683B20F2F401A0F23E2F8544740B0282994AB0A2F40E273B95E26534740301B09D4650A2F40D2B39B7EE852474060B6CEA107092F40922431D6B1514740E0BF531223072F40D6B7E63DF750474090D119BDBF052F402630F6686E5047409032B05639FC2E4022A0C335244F4740605625CEE0EC2E404ED27AD4AC4E4740C0F5DE154AE12E406637FA05EC4F4740C044EE3DEADE2E406EEF66EF47504740606C6E75CED52E406631F985A8514740B01CA5A8B9C62E40FAF73D4AA2524740B0572CA162AA2E406E3F9CD160524740C0AA336BE7682E404A23198BC9514740B07FC45B59582E4026A88A11185247407041DC1410362E403A3EE6B2BA524740406CEED1E32B2E40D662B8F3EA524740C038EB60B81F2E40AEF039AD24534740A00CA5E23E022E40AA84DE1D8451474080CED11532EF2D40FAF8963CD54C474080B7BB675AE52D407EBD2262444F474020A42F6BFFDD2D40623DE657814F4740C09C5A97C9D62D40922A72DEB84E4740D06FB1B9A2CB2D407685A9CD824D47406020CF96C2C92D40B20342B3774D474040635B640DC12D4016A86826444D474060A9980BDBB92D40FA60BF186B4D4740207C1E5A66B32D40725BFB05F24C474080DCC2CAD3AA2D40D64D2262CD4A4740700899A801A52D404A854E5BA5484740503BC4AC08A12D405A3DF6399246474060FA6CD8479D2D400E16616C9C4447407042D125E4972D403E70F0BA7B42474070C4FA71C1932D405ECE6AC3D9404740E0F3E1ECE5902D40F6449BD96A404740F0F14BF092852D402E4B0040863F4740E0F5805D73782D404ACDE0BA253F4740A0301A5AE6732D40CE9911D1B43F47409013B256616B2D40BA13FEA20A3F4740F038CBA14F682D402A999AD96E3E4740B0E05E64C5652D40F2B13A2D903D4740E01B639ECA5F2D408E72BDFB503C4740903C0EE6235C2D402EE0D346BC3A4740F0B8A7ED57582D400A24516ADA3A4740707DEB6050552D40C29E9D76F23A474020984453FC522D407EBFB85ED73A4740D09AA345B8482D403EB4963CFD3847400049F49A6D432D40521A30D664384740609334083E3E2D40EAFE741A213847408004643B21332D4076BC9F13F5374740D0C169D8272E2D40CEFF79549B374740500D07AC96262D405AAAA193B9354740903D59644D222D403A95606C3F334740B0724EA5CC1F2D409E8E546D24324740006E24318A1D2D4076297EF12431474050561C5AA6142D40CAC26D6077304740B019ED60E00D2D4016C4707DB1314740405FBFCA1B082D40C27F7CD4E233474090AFCAA11F012D40F2DE0FB48C35474040EE980B73EF2C4076A4CA6FD2344740A04E3D7CE0E62C4022A10EB40D354740A0EADA6FD1DF2C403206CAFE9D354740D00934080ED72C409A9549215136474040A2128332D42C40C609AE87E9364740F02FB8908ED22C40DEE6751AA2374740B0452A6B1FD02C403ADE59323C384740609AB11CACCA2C401A77216272384740205613E66BB92C40F6224CBECA374740801A13CA117C2C400A96472A18384740D0079E0BBB4C2C40E6DC41E753384740A08D9BE2BD4B2C4026A4C6D55F38474060431B5A46462C40369AF3CBA1384740408A21F77C292C40A61860ECEB3C47405058B256F9212C40D2E378B7923D474000C324F79C192C40BE9887C8003E4740200F2D089E102C40626080F10A3E474060511DCEA3072C4032D47C90C33D474010CB8ED15DFF2B4072A68AC8813D474010B30DE6F3F62B409620DB80AF3D4740F03D42191FC82B40FEF6004082414740D0F3D878ABB82B404EB20EB4F341474030E3562A70972B402A304367024147408033620174902B40924F4604FF404740401E72AFA36E2B406AD1983C6A424740606C3F19E3662B40220EFB058742474090D1518DE55E2B40CE49BE9841424740809514BD2B572B40926D1C2865424740604E8C9734192B4052871C28DE45474080C77AE974032B40628578B701464740405B64758AFF2A40AAB03ECA7A464740104DBF904AF82A407ABB4DDBE6474740B0DB48F0BEF42A40A68C52F8224847402038C16786D52A40627F7737BE474740D0294E2A24BF2A400EE687C86B4847405077E826F78A2A40B6EEB8DE7F46474060E0B6F353762A409A8E0B97AD464740D0609B6E886B2A407AE1C8EF6C47474000C3653BFD4A2A406A0E9002E04A4740708BADB906212A40C2795F6C8C4C4740B0C27C862BA92940BE5EA293084E47407091D6DB108C294016C614EE4D5147405081B5533B7F2940FE7A84280C524740404D38A5D07A29406279929F4D52474040734319CB76294082A0ACEA3552474050CE875D976E29406631F985A8514740507C5F9EC6692940EA9501C0A151474060C626F710652940F6AFA430FD514740609A82C0C05B29409ED0AAEA3353474080AF2880BD5729402AEC84FF83534740404373E9D4562940B2D1492196534740E078FC9A893D29404267559507544740A0A6F8D4BE1F294046AF03DD5A534740E00B92343B182940AECC62097B53474030DA14BDC30F2940AE6BB0872E54474060950B49F1FF2840DE76D2A908564740206662A173F02840AEECC3938056474050C70A4929E428402AEE79B7DF564740D0A8FE715DCF2840D29A7FF155584740F064D94FA3BD28408EF74621075B4740406CA645F4B328402E4DA4B0225F474060922ACE74AF2840B65D11D1EF6147406075C2CAEFA628408E05DF1DE162474060E678E94C9C284046409D7621634740C0D43B7C9C912840B63C68A6D6634740B01435A5B08C2840EEB0CC0C6F644740802A2094C7892840EE714DBEEF6447408035590190882840B684EC91CD65474080D14819AB882840EAB141E77467474040B7E14F0B8A28407AC2C335F0684740501FF5FDF28B28400E39DF1DD3694740708781C0608D2840D22BEF2EC06A4740F096AF1C388D28400E3B5932536C4740E0B2657582882840D29C077A1F6F4740201391D1698028404E25D2291470474040FF4453E06A284062F1EF2E35704740804F502AE4632840B6ACFDA2A670474070F275E9C46028401EE25BCF45714740800D48F0F65D2840F28BCD8C1372474070D026CE24582840627DC552187347406098ECC36D5228407A0C087A9673474000A116205D4628402A07A84DF5734740404A805DF74028404A5FC5D2557447403013F8FD7A4828406A34FDA29C7547409095EB898848284076044E5BC8764740402475E9FC442840329C973CFE7747401054D578A74128407E7B67266B794740F05FF237883E2840A26A2D395F7C4740A0163B424F3C284056173373D57D4740E02C9F45EC3828404269EB911A7F4740D0EC2C08523E2840FAD2299C58814740201DEF60545D2840D670AE874D84474000A2F1FD3A6828401EAB6589D38647402004A72F6568284032734CD99388474020B1A6458C6828403E1DFC22338A4740C022C8A17B5C2840EEF3C452E88A4740406DF0C02B442840AED47027178A4740E09E070F843B284096B2A3B0CC89474080644E53A8072840FA894CBE2E85474040D80E2025E32740EA4F1A8BE2844740E0EDDEB28CCC2740D29A717D8C834740D066CD04CDB62740C62D77B789814740800E936E20A52740B2AD6409237F4740B09C0172E58D2740D671077A807E47403050B2F33B8827406E3CA94DE17D4740D0C5D6152E7E27402E9B5378797C4740D083CEDB4078274006CA4E5B3D7C474050F979E9AC6E27403A25AF87DC7C4740102828CE1C5E2740326B8F02F77E4740D015F93760542740B2AD6409237F474060C3DE4F8350274046897A540B7F4740C070C467A64C27404269EB911A7F474060806BD8034C2740B2AD6409237F4740D0F3A91CC04B2740B2AD6409237F4740F0243EDF65312740421DF5680E804740A02881233A252740B613E974DC7F4740104C7D865B162740DA49E8F41D7F4740B0C989FA4D162740DA49E8F41D7F474080C4A2E2321627404269EB911A7F47402042AF56251627404269EB911A7F474000878F9754112740B2D3DF1DA97E4740D03467D8830C27404ACF842B827E4740A06A19BDDB0727407E50606CA77E4740C09A6B75920327404269EB911A7F4740C0BA2494DF0027406AC44BBEB97F4740404E1CF7E8FD2640264D8F8234804740107E7C8693FA26406E7AF9058A80474050618D9794F1264026ECDC00E88047401097B89072EC2640BA394F5BB4804740A03A498DE5E726403A74A9CD1C804740A0FC4319FBE32640DA49E8F41D7F4740107A508DEDE326404269EB911A7F47408045E0B284D22640B23A931F397C4740005AA0E2DAC22640FA8930D65B7C474030A7B856EDB22640E2BB5932B07D474010E118BDABA0264046D1289C5E7E474010734319CB7C26407282B324587D4740608201D55659264046B7AF875F7B474020DE37422F5026402ABEA0136F7A4740B0D29C0B0F2F26400221D2A9CA74474080EED94FD32A2640CA1168A63773474090C5548D6D252640AAD43156B76E47407021040FCC1B2640DE696943C56A474030A470AF5F1B264046AFE7F4476A47400012D6B2F41B26405E7DBE98F3684740102CC4670E1B2640DE09E6574F684740F0B4FB71D518264082BE9082E567474070FFB4F32B132640EAB141E7746747405024DC150E112640C65299D91C674740C09A6B7592052640E2BA0040BD63474080639CA86DFE2540267C0F3472624740B03BFB3720F72540FA8A7B54456247402097B89072EE2540C251392DC562474040E5B4569DDC254016862E3912634740803B8B97D4D32540B2A16209DC624740E07EC704BDC225408E716409DE6147406037E689A8BD25406A0E741ACD61474020682A6B6BB8254096DDD4C62C62474000D8FC37B0AF2540D2BE482176634740F0F899A815AB2540B235DD1DDF634740C0C0E689D8A525404A16DA80E263474080AEB7F31B9C2540563C55956863474060E9C90415972540C680CE0C71634740A0FD970B5F882540F291DC80E0644740A05298D171822540EAF6C1B537654740D03B744CA6772540AEB49BD9A1644740E076FF71257225409AAD1051AA644740409C18BD136F25405AF53056C96547402004B35675712540BA5CCD0C6B66474070F133DFD57C2540D69D759A00684740805EB590527F25406A149182E3684740D01BBB2D597A25401A53828E2E6A474030C930A5305325407E07751A346E4740609C21314E4B25404A471A8B8F6E4740209447B641422540D6044584636E4740C04FC0CA2F0E2540CA2D7E71EE6B474020B42094F7F824402E4795BC556C474060F7E3EC59E82440CEC2828EA56E474020021D5A22E7244032E742E760714740602AB0F37BED2440E61F1E45BA754740105F7E86BBEA2440E2BCB224E3774740A0020F832EE62440CE3242E7D1784740E0CD9EA8C5D42440EEB9DF1D307B474040BC613B15CA2440665165A6217E4740706AB290CAC424404269EB911A7F4740706AB290CAC424400ECD67A61F7F4740E07EC704BDC424400ECD67A61F7F4740109F80230AC22440E2B558B26C7F474090375F9E2EBF24404663FF3F857F4740A0E939A560BC2440E2B558B26C7F4740400D360882AD2440E6B71151F77D4740601B39A598A02440429720C56E7B474040EA3442A7972440723716EE7E78474080F47B8663972440CA9D3DCA1A76474050F350C7A28A2440326D178B00764740206E5C01B08024402ED7E3D772764740405BC22D6178244092E12A1C2F764740909C91D1997024401EC92D39F073474050B203AC2A6E24408EBA4F5B517247400035704CBE6D24406EC3E4573D714740404FD7155E6C24402A4EE1BA42704740101724F720672440CA2EF34BF46E474050F49B45CC50244032762A9C016D4740508AF29A914324402E2E91825C6C4740E074B993184024409E332258626C4740C014A545FC382440521A3E4A6E6C4740F0FBEAC3DD222440760F05DDA56D474000F086975417244006445ECFCA6E474090537C238A03244006AC5C4F0472474010C6B656C5CC2340BA5B90020B75474080477F2312C02340A269E2BAB5764740A00009E68FB923404ED6C96F4A784740209DEA26B7B823405216D3C67D794740700F45195BBA2340FA1B31D6DE7A474000F0F637A0BB23403AB9299CDF7D47403062D815BEBD2340CE0B6EE0187F47403062D815BEBD23409607ADEAD97F47409077D1DBC8BB2340C29D44843F8047402098030F9CB82340264D8F823480474060A32EA570B623401AEB3DCA85804740C0345E6449B723400AE9842BFB814740D0D8D7158E56234052D144843187474030397C86FB4D2340D6C14C3E6A874740B06A895D2733234066C3DD9DD8864740F0482C6B932923408E72AF87478747402030F060B437234002415AB2568A4740700A5E01403B23408ED97E71AA8D4740A0BD956E1036234072E3731AEE90474040C1D8B2E42923401EC789C8D393474040EA3442A71A23406A34FDA25C954740B06741B6991A23403A4ADF1D7996474050AF2231AE1F234056F06CE06598474090319DA8352023402A28828E4F994740E05C0849D11B23401EC47737169B4740601FF5FDF2162340A688188B339C4740A025397CAC1423401E7FF768539D4740D091C8041D18234072786AC31B9F4740D06F79E97C0F2340521A4CBE77A04740600E23CED40A23406AEA9C76A3A14740807196D1491B2340D67B579561A64740D028FA37C02C23402A4F3AADF5A9474040130172B52E23404E68D8E3D6AA4740A07054C75A312340EA8C115118AD474080D8EE60BC3323403270291C3EAE474050F7EC6094472340FECF1EC57FB2474010D1EFC38D4C23409EFF2A9C71B44740504E8323FA4C2340BA80B224DEB9474020E544B6513E2340F667FDA20EBC4740E03D7AE9442B2340121B024088BD474000D5B490221C23409E6D1C2865C14740003CE27E2A1B2340E625D07E29C24740B0FA99844F18234082A1180A6CC4474000FA54C78A1923407E2B30D64AC44740603D33BEB81B234016751FED30C44740801AF79AA9392340D20E72FDCCC24740E0ADE589785A234036BC188BE5C24740400DCFDB70682340EA8CF56805C44740303A5764D96F23406E77FCA2FAC5474080F9F2FD327623406E82A5B04EC84740F073AE565D81234012C76DE080CA4740B0A036DFC58823409E2142E72BCB4740405A802372902340127D1B2851CB4740A0963608B297234082E70FB4DFCA474040C429CEAC9D2340262FABEAC4C9474020B9774C5E9F2340E6FFB12441C84740403776E95C9E23400A80035DB2C64740B0B8852352A023405261A11358C54740B0A6F8D4BEAA23407E298C656EC44740E085D478DFAE234046867DF17BC44740C0C27C862BB52340AE44CE0C2CC54740A034E54FC3B72340CADEAD874AC5474030FC5A6429C723402E4AA74D53C4474060D3FED466D62340CA0E5DCF1EC44740407833A520DE2340B6CC9AD960C4474000B0F49A51E42340361E40E737C547405018815DBFE523405A1D65091AC34740C0EC239417EB2340128C1A8BE7C14740A097AAB97EF12340BA1130D6D1C0474000F900D526F6234056F82DB918BF474000F900D526F623408648D0A9B5BE474090E9D2784FF62340CA0FB6C151BE4740806106AC1AF723402A97F66892BD4740702B629EB6FC23408ABE89C800BD47405005DE15360124405617337355BD4740C0CA4419C305244016F5862B02BE4740C0705D3B950B2440CA0D125175BE47400064FE71C524244016F6DF1D35B8474000A771E944292440AE81E1BAB4B6474030D6DF4FE326244076C0FC225CB54740105C9D0B3F1B244072A4CA6FD2B34740F08EF0FDDA20244056B4EAF4B5B24740C00B92343B222440AE5CBF9861B14740A00B1920B52224403251246201B0474000D74A8D75252440DED8DD1DC8AE4740801945F06E2A244076C5B224F6AD4740E085D478DF2D2440F260AAEAFCAD4740E05AE2ECC931244016F0EC117BAE474010151E5A823824403EB61EC506AF4740506D8A970C3B2440BAAA6E60F8AE474020BF394257402440D2BE3AAD6CAE4740801C8D97FC4224401A107CD46BAE474040116201284524408E69B1A4B4AE4740D0AC33DF3D4B244066EF4BBED8AF474040C08D34BB6124402E0B1A8B8AB04740305A1083266B24406A813790ADAF474090F678AFC76A2440FA88C96F5FAD4740204DC8048561244076A09002A3A84740C028930BAF6224403629E9F4CBA7474080B554C7F26224408A72AF8707A74740B0D4A2A8AD612440A61A0B172DA64740006821F7305F2440A220F768C2A54740402BF2FD6A5824406EC14E5B6AA54740B03B04AC5A562440D292CC8C2CA54740B085CB04A55124407E75828EBAA3474020FFD4B2944F24405E7ACFA9EDA24740D0F89034DB512440E6000B17B4A24740E0506628935924407E54C033DDA24740D0B88E978C7A244096D52FD68CA3474040334AF0B68524403E1CB1A449A44740C02C2FA5A09C2440F2B589C8ADA6474080CB1FF7A0A62440168779B73BA84740605BD415D6AF24400ECD593256AA4740F0DF5D9E36BB24401A32AF0739AE4740A0514D5348BE24403EB61EC506AF4740700CF4FD92C324401682C3B5A1AF4740F08B0F835ECE2440DE46DD1D45B04740E060A4E2C2D22440DE075ECFC5B04740E07D0CE647DB244042FF2CB9B1B24740402D88FABDE22440DE76D2A948B54740D0F7D51566E72440FED0694329B84740B0F2EEFD4AE72440F6D526FFE7BA4740801716202DE524401E17CF8C7DBC474020B5DBB26CE224408E966A439EBD474040E0465308DE2440262D15EE71BE4740A0BD8CFAD5D62440FAD82A1C1CBF474040734C8D05DC24406A0D458436C4474080FAAD1CA8DC24401ABE929F65C54740E010122045D924409A20E23AD4C6474070B98934D3D424400245B7C13DC84740105B528D15D42440EAB1178B58C94740C07D93D1C1DB2440167FD4C6DBC94740301DEF6054E42440BEEBD0A91ECA474030F2FCD43EE824406258CD8C61CA474040EC316B0BEA24408ED8099724CA4740C0B7DCEC51EC2440B21B80F1F2C8474090C7835DAFEB2440AE5AFF3F72C84740B05A02AC32E924406EA820C5D4C74740B0F2EEFD4AE72440AACC707D04C74740A06F094931E8244072978B65EBC54740904F502AE4EA2440BA80A4B054C5474050B11F5A12EF24407AE525FFD3C44740204C0DE60FF72440428AC5D234C4474090551220DD0C2540CA34D8E3A4C34740C09C0172E5122540AED42A9CD2C34740904C6FAF67192540464B4684B5C4474010E6FFD4C623254066798BE528C74740F09ADB15DE2A2540EE41741AFFC74740700A5E0140372540AE807A54F8C74740801B4219D37A2540CA34D8E3A4C34740400B306BE37C25409AF5BE983DC34740206975E9947E25404E1E6A4394C2474060C1E1261F812540AEA3067AF8C147404096768683852540720440E7BEC1474020FF3BDFA5942540EA44717D0EC2474020966D1249A92540AEEFD246A8C34740503B1BF788B0254046ACF80502C447403087FF37A0B725407ADF4EDBECC347406032788613BD25402E06561567C34740A048AAE2D2C82540128C1A8BE7C14740F078FC9A89C425401E4D7A5406C14740102A9E0B07B425406E36A8CD1DBF474000EB0F2085B72540F2CD6CE019BE4740E050845DDFD12540AEA603DD07BC4740003FC5676EEB254096FBB85E5CB7474060AC3842F7EC25404E424F5BC7B64740E09C0AE61FED2540F68C2D39EBB54740402A37DFF5EB25406216125108B54740F04EE5EC51EA25409EFF2A9C71B44740B0591E5A1AE92540021F2E396EB44740F091D17857EE2540FAADEB91B2B2474060C9A0457CF525404AD72262FDB14740F070CDDBE02A26407E5C4684DBB14740B00FBE2DE13426400E87807160B24740409305AC52562640061789484EB64740301C0B0F3C632640AA5FAE87E7B64740708CF837306E26407206F2CB24B64740309C18BD136D26403E8F741AAAB24740E006B4905A7926405E0FB1246DB24740201878E984842640E64752F84AB347402040195AD28B2640CE797B549FB44740B07C485398922640BEC7EB912BB64740507F498D7D9C2640121C5B32BBB74740701EB3F303A626402A72D4463FB84740303C548D3DBC26402AC2FD2256B84740E04EEE608CC426404268A013F1B84740C02383C0F0C826402ABA583236BA474060397C86FBC62640226E62092ABB4740D0F113206DC12640AE1A045DC8BB474020B56B1221BB26404AF7B8DE12BC4740A0A3175A42D32640CA6F39ADC7C04740F0B4629EE6DE2640F68EC3353EC147409097AAB97EEB26404AB20EB4F3C047401026027215F726401657AE8754C04740E00FC7A11B0F2740D634024001C14740B09E976E381A2740A687BF98C0C14740803333A588232740BA367C5481C34740800E936E202627406AE7C96FB0C4474080E887FA252A27407A08CE0CA7C747407040020FA42D2740AEFBF02E02C9474000AFA0A8ED3B274032E453F81ACB4740708FD9B2AC3D2740E2DEF3CB79CB474000AC4F8D25472740665C00406CCB4740B037CF3E7A5D27405286C335ABCA4740C0E2C5042D8727405A160B97A3CA4740701EB3F303A42740968ECC0CA3C947404070629E4EA9274066EE0040EFC94740503A37A570AE2740FEF44E5B1CCC4740F05D5C3B35B4274042AEB85EB1CC4740701716202DDF2740D2BE3AAD2CCE47404000875DCF542840528699D94ECD4740D0C56675E2582840C258B64173CD4740A0ACB1567D67284042B5045D9ED04740E091D17857692840FE19717D6FD1474050144CF0DE69284052ABBBFBA1D24740D0192531066928404E68CA6F0DD64740A0B1986E986728404290D4C6C1D64740A0D638A500652840A61B48210DD74740E0746975D2602840E294939FC0D74740902736A5105D2840220C3BAD97D8474010B5DBB26C5B2840D2E203DD4CD9474030CAE24F6B5A28408295F56858DA4740F069B7F3836228404AB367A6E6DA4740D0564653D86E28409EFEA74DA2DC474020AECE3E4A7328402E7BAF871ADD4740B05D4A53C07A28407EC460ECA7DD4740403EFC71057C28401A1516EEB2DD474090E59D0B6F7A28404A87EB111DDD474060EBEF601C792840FA878C65BFDB474040F1BACA4F7728400AF41151BCDA4740B0A30EE6077428409E17D6E3F7D9474090AEC0675672284096433D4A13D94740C093EE6024752840EE446AC3A9D7474070DBCFDB387A2840BE62F12EE6D64740C0223842C77F28407A0F0C970AD74740B029B556358B2840F2CEC5D20CD8474020A470AF5F962840926D004052D84740406CA645F4B32840CA7C949F41D74740D079E0ECA1BC2840EAD655957ED74740908E97A8BDD028409247852BCCD84740703D219427D928404271CF0C85D8474020A5BB2D89DB2840DA044C3EC8D7474080E25564E1E02840F2B44CBE0DD5474080C7835DAFE32840CA2D9A5901D44740A0A65F01D0E728404265B1242BD34740A0E6FA710DF72840FE3D482119D1474080A0BDCA3FFE284056F522E27FD04740802A2094C708294002F6AE0774D0474050414419931329400E105278CFD0474010DC31A5901B2940BE5877B768D14740E0FB68AD7220294042FE8F3C25D2474090D02F425F322940C2530040D9D44740B07967D81B3C29406AEAAAEAACD54740D0A8FE715D4E29408A05ED916AD64740E0D7950B9F6029408A05ED916AD6474070A5A4E25A7D2940B6F1AE872AD5474030B8C5A123862940824CFCA25BD5474060FD1EF7D8802940AE8B3F4A1FD34740A0B1080FE4882940D67DED9174D1474030079D452C96294086476943F9CF4740902D683B55A02940D2BC963C50CE4740203776E95C922940C6EC458424CD4740007DC1671E8C2940FE1C830E2DCA4740D05C0849D18E2940AACC707D04C7474000594CF0769B2940CADEAD874AC54740A0EE526459A329402AE269430FC54740C0407B232AA82940422BB7C1C4C44740E0921CF780AC2940BAEECD0C2EC44740408C8897E4C32940AAAFF34BD1BF4740807A42B6F9DC294046B1AE079CBC4740A0449EA895E229403A2753F838BC4740E0963F7CECE629407A36D2297ABC474000E4F93728EB2940326FEC11DEBC47403021946E80F029407E30E6D7E4BC474080E19A456CF5294076C6198B72BC4740E048BCCA47F829400EA81B28F0BB474080288A9774FB2940364F727D9BBB474070EDFE71F5002A40D29F277FA6BB474000C86C1281042A40367920452CBC47409017AFF31B132A40FAD82A1C1CBF47401027DD4FF3122A40B6C996BC2CC04740D02C2FA5A0112A404AB20EB4F3C0474080BAD4B2FC0F2A40322CE6579BC14740E0DE09ACD20E2A4072A531D64EC24740A061764C660E2A405A1D65091AC34740A061764C660E2A40BA80A4B054C5474040D9306BAB0F2A4002AE0E34AAC54740C01644B689142A4002843610BDC74740A01BB2B91E152A401AA7087AECC74740902BD23E02142A4086E69AD9D9C74740401736DF95132A409A272E39C1CA47401094D715F6142A405286C335ABCA474060BB865D371D2A40C62D694380CC4740703333A5881D2A40D6BE4FDBDACC474040660D839E232A40328EFF3F64CF4740E0DDC7A1E3242A4006B0B95EABCF474020CEA71C00262A40F2FD299C37D1474040DD5C6451262A400ACF198BC5D24740E0DDC7A1E3242A40EEF56F7D69D447405018E889D0202A405684115145D647408079F737D0162A40CEE4A74D29D94740C03C4F2A84102A401AC61BA872DA47408014BC902A0A2A409A1E4C3E41DB47401065B9903A022A40F68BD44678DB4740C0F0C8A143F5294016A68CE581DA474050CE875D97ED294086B5842B49DA4740E0733EB611D12940CA83E09D2EDB474060DEB056B5C829407AC02D399DDC474050E611BD3BD2294062842D3918DF4740808201D556D22940463F289C1BDF474020D0ADB99ED5294026773D4A05E04740404DC80485D629408A716409DEE04740A0D4A2A8ADD62940FE848F02B0E14740E0BF9B0BAFD72940C2603F4A80E24740A0A4C9047DDA29408E713AAD81E3474020AB7D2382FB29406AF3BDFB6DEC47407043E38920F92940CA0CD546D5EC47402037064911EE29408230097A9CEF4740F08489FAB5DC29409221347362F64740C0623ADF15D4294026524584CEF74740A01A673BF5CF29409AFBA3302EF84740B003CAA1A3C529402278963CF8F94740600226CE5CB9294052ABD7E334FB474010BA522A3CB529400AEE10D138FC4740B05718BD7BB22940CE5178B78FFD474060CCD1554DB229402AAD8DEBBEFD474050EAA4E2F2B029406680E5571FFF4740E0C7639E46A9294022C204DDFA01484050D2B3563D8529407A9AA4B04D084840B0027F237A802940F28EDF1D91094840503D1820ED7B2940BA59D0A91B0B484070CBAF56557929406ECB9E76CB0C4840904DB1B9567A294082935F6C850E4840A0AA8BFA757D2940964EF4CB700F484070F837DFBD802940A6FA820EA10F4840A05FE04F138429405A3989489A0F4840E0BCBA90328729407A934A3ED70F4840B0E45B01808E294022539E76C111484010D44953909029406AD031D62D1248406075C2CAEFA42940969549219114484020AD0AAC9AAB29405AA2FCA2D915484050C4996EF8B429409A85FF3F51184840F08EE789A0B9294082DDA3B02B19484000341C5A5AC12940924AB3A4DC194840E06F825DB7DD29409E8E04DDC81A484010FD354207DD2940EE4BB6C1161B484050AC3842F7E42940DABFA113291C4840E0638ED179F529409E01D6C6B21D484000A2619E86082A40A6FBBF18C120484080C910E6C7102A40921F828EBC214840F0F89034DB452A40B638F6684125484080DF74E9648C2A409ED0A3304F27484020382894979D2A40D24FC6D2E92848408078ACB9A6CF2A4086E26EE033304840C0AF7B86CBD62A40829AAB6A32324840D0028897B4D92A40C209AE87E9344840F08489FAB5DA2A401AE102DD92354840406A30080ADD2A4036DE59323C364840C0CCE38950DF2A40A6D1F5681D37484070D1D8785FE02A4006D8DF9D72384840F0D1CAA16BDF2A40829EFA05103C484070D1D8785FE02A405AD2C7D2403D4840B0245E9ECEE22A40EE76D9636D3E4840406674AFAFE92A40860927FFD9404840A0CE00721DEB2A40DE48A4301942484060DE20F700EA2A40623F830E39434840000FECC33DE52A40169E03DD74444840601FFE712DE32A403E4530D6C346484010B28A97A4E12A401EA11C2857474840F039DE4F53E12A407A9AA4B0CD474840F01E0C4921E42A40D6BF7E71B1484840C06666D8BBE82A40869647A166494840B0BF929774F92A406E9A7A54714A4840C06741B6990A2B40869B198BD34A48403097C104AD3F2B40B615949F5D48484080C8C5679E482B40A61AE1BA9047484060DB5F3BED502B40C2CB6B438A46484070857B23C2582B405A3F6EE08A44484020A15764BD6D2B40DADEC2B5F8424840609137DFD96E2B4096AFB4C1C6424840A0EDA6E266732B40E2BD045D31464840204048B6BD772B40D2BB4BBEA6474840602081C07C792B40727E5DCF9547484090AFCAA11F842B40E6D7929FDE4748402075AAB932882B4092B447212948484080596D756E8C2B40167FD4C6DB484840C0ACF29ADD8E2B40EE4A79B77649484050561C5AA6972B40C6C6CA6F9E4C4840F09EE126D39A2B4052B5198B4C4E4840E025518D699C2B405E3DBCFB245048409020F160C89C2B406AA812514B5248409040AA7F159A2B40869967A66D564840C0A34F2A689B2B409E835BCFF4574840E0CB69AF3BA22B40CA7AC6D2085948401021E389D49B2B407E73C2358B594840A0C8764C4A992B4006D2DE1DEF5948404086F5FDD6932B406242959FB65B4840E028A2A831912B408AE0BCFB8D5B4840809B75AF5B922B40EAAF818EC55C4840105E04ACA6A12B401E5823621A62484080094B533CB62B403E200EB430614840901B7AE9F8BF2B40DA2C727D4F60484090B0152049C92B405E18A84D1B5F484060ADC40481D42B40964DB7C1905D484010B30DE6F3F62B4012EADD1D6E5A4840E08C895D73FB2B4046B7AF879F594840F0A9F160F8032C40926B5CCF75574840C0F79D4540072C40AE35EB916856484090D2FD0ED8092C404E1CD44601564840E04374AF630D2C402EC21251C4554840901E5B6475102C4056826DE068554840C081000FC8112C4012EB3610A15448404019FB37D4102C40F2EF832BD4534840F033542A800E2C40CA55A4B0B553484020C7D278030C2C40D2738F02CA534840F0595F9E7A0A2C407A76BF989A534840A084D8150A072C4022E5741A68524840709AC30421042C403E4522623A524840209BB52D2D032C408E4A828EDB514840D00250C78E052C40A21E45841C504840007A18BDC7072C4062A5F905694F4840200F2D089E102C40625BD1A9954D484000DFDA4FE7142C40921F741AF34C484090AA532A50262C4066D32E39BD4B484060FE31A5DC6E2C40A63D89C8634A484040CBFE71A9A12C40B2618AC869474840608B054995A62C40AA3A707D8147484060FC02D59AAA2C40CA7B2D39C5474840507C976EECB42C404EFE0B9724494840E040BC678ACF2C409AB7606C0B4B4840A077990BA3D72C408297BC7B6C4C484030010AAC1EE32C40366FE55779514840C0197DC094EA2C401EC789C853524840101B2194DBF62C407AA4F4CBEE4F4840E03E8D97480B2D408291BBFB284E484010EF73E950112D403AB47A54EA4D48409011B52DFD182D407233B224334E4840C0ED3FB655282D40FA85F6686C4F4840601A4F2A38302D404E65DB80874F484000FF030F80332D4092DC97BC0C4F484060EF5C9E22342D402252299C3B4E48400009592A30342D403EFA842B614D484050E4AA1CD4352D409691D029D74C4840F03BAC1CCC392D40F6D06289C44C4840F0E5C704A1412D4026BEAE87384D4840604D7075F6442D40824F0E34194D48402012E54F774A2D404A8C852BE44B484060D3CF787B4D2D40C277BBFBAF4A484060ADC40481512D40FED4D4C6D9494840705C4EF0EA592D408EB0EA11C249484080E5DC28DD612D40E205E293134B4840C059EFFD2E642D409EB1741A764B4840C0344FC7C6662D40EE6F9BD9C94E4840103F960B83662D408670E874B352484080BBBE2D5D682D40F6FA2C3928564840004BFA370C722D40BA7E0EB4C1584840E01ED478FB8C2D40CE531C28AC5C4840B0903C42938F2D40F6F8A4B01E5F4840F0C30849B5942D409A47A1139F60484050B3F69AC5962D40B21891022D624840A00BEAC3C9992D401EE8559564634840B047279483A12D40BAA2D7E3E1634840401133A53CBC2D403628741A46634840207C8EFAB1D62D40E6FCC2357B614840907E162011E12D40C25E9BD9A3614840403E2BCEF0E62D40BAA2D7E3E163484020B74253D4E52D40769F1B281D644840A0EB97D18DE32D400E36C6D2F06448405074CFDB54E12D400ECA40E7F365484060F2CD7853E02D408E22717DC2664840C083FD372CE12D4096F60917A7674840A04E3D7CE0E42D40B6A76A4344694840F0504C8DB9EF2D40324F56954871484060D33F19C7EF2D4062A64484D274484030F8663BA9ED2D40727B52F8BC784840506B2C6BDFED2D40B6D727FF617C484040B22994DBF42D40D6916CE0147F484060B2A2A861F42D40A2AD3AADC67F484060B2A2A861F42D409A47A1135F8048405093CDDB2CF52D403A22B224CD804840A00528CED0F62D40DEE07B54038148408099DFB208022E40FE3A3D4A4081484080DA2CCE801E2E40120D5CCFA47F484020A0037259462E40165366A61B7F484090ED36421B4A2E403A47B85E4D7D4840204B03AC464C2E40B278F805907B484000D83D7C104C2E40FACF3AADD279484060EEA17FAD482E4006861251FF774840C0146D75D6522E40964199D9F6774840306210E6E3792E40DE8CD980B2794840A0BF633B897C2E40A2AE939FF979484080743F7CA0832E405A7B289C607B484060698DFA51852E401A597C540D7C484000E1471997862E40CAC3BF98C57D484090392D6BA7882E40E2C2BA5E4B7E4840900E3BDF918C2E40562038104D7E4840A080A3A8298F2E40CAC3BF98C57D4840C05B7C8647912E40AEAD6409237D484000B4E8C3D1932E4006D1AF87D87C484000B2C267CAAB2E40BAD13BADCC7C48402022377C38B72E40BE174684437C4840C0A772AFD3CF2E40EA4D5A32337A4840B0B085C094E62E4066591FC5EF78484060A3667596F12E403604ABEAE5774840D09BEEC3E10A2F4066EC4E5B49744840D0A0FBA29F162F404E1C01C888734840103257011C352F404E8988C89471484080894F8DD95C2F4006F6C335E26D4840107CB72D55682F408EB736106F6D4840B07DD41522742F408EB736106F6D4840C09F2331C27C2F4006F6C335E26D484070E3CF3EF68E2F4016C0289C786F4840B0A34F2A68932F40FE5F7B54A66F484000F886FA11A32F406EBDF805A86F4840B0CD869708A62F40BEC25832496F484020547DE918A82F4052D9F02EB66D484080ACE926A3AA2F406A5F198B0E6D48408004643B21AE2F401682D8E3CF6C48402099865DEBB72F402EC2F668B16C484000CCD94F87BD2F40220AA5B0046C4840109C00AC56C12F40EA8A6DE0BB6B4840D06BAEF39FC52F404E8AE1BAC76B48409033FBD462C02F40BE5588C8A26A484000617586D7C62F4046B64821E36A484080B2AB1C9CCC2F40223A54F8D86A48409082D2786BD02F4082B865A6456A4840D0044DF0F2D02F40426EC435EC68484060A98F97A0D92F40F26864094B6948406089D67853DC2F405ECE717DBE6848406089D67853DC2F40CA575695DB674840C07E162011DD2F40EA1BF9053967484020DF1704470830402EE437100861484010CE6BBCD915304086B42B39165F484008BD9018812D3040123133738E5F4840C0327D6A4D513040B6A778B7CD5D4840103534EC975130403A23E1BAE35D484028B1D45CE95630405A649E76275E4840584F6C594C5A30407A2C89C83D5D48403803FD1BBA5B3040D24D30D6165D4840D091EDA7BF5F30409A864ABE7A5D4840C8761BA18D623040E6E4ED91585E4840D86001568B6F3040468F828EB365484038B88941FD7330408A04862BAE664840E82318045F7B3040BEC4E0BA5266484050816B59047E3040628CD9E35C664840E8D5C3AEA5823040168331D602674840807C6482008530409A3C14EE1D67484080D700565B8730403A215932DA6648408036A0EFB88B3040822E4921ED654840984433EC039B3040DAD8C1357564484058076A59C09F30404201F4CB45644840B0517B6AA5A430405A871C289E634840300A7A30A8A63040D296299C1362484030BE83079CA730403A6C04DD3C604840C0A3A32976A93040063D0B17B95E4840B8804160D2AC3040F67FD246F15D4840E8912578E5B03040D6251151B45D484078EF580EA2BA304072C6EF2ED65D4840187962828CBE3040220BE2BA645D484000D70E2DCFC73040FE5F6DE05C5B484078D84BD484CC3040AAA603DDC75A484000E456AB70D13040A684D0A9FA5A484040FB6C5948DB3040DA9926FF225C4840B0D2314F7DDF30408ABA41E7075C4840A88F16678CE53040EEF4168B36594840801E800718E930404AD72262BD504840C04A9D52EEF13040EAFA1051554D4840 HR 0106000020E6100000190000000103000000010000004B05000020027ACDEA8330404641CC0C384047406838FE1B668530401E79F6E8CF3F474040F9D65C7590304062115C4F6E3D4740D858681FDF933040FAB14F5B3E3C47406087D5BF6E953040FAB14F5B3E3C47407077B53A8B9630400E12E874623D474058671CA121983040EA6F7837D23D4740D8881267249A3040F6AF96BCB33D474058F8C3AE719C304082919859313D4740A0AAE86D3EA33040926CD1A9BB3C4740D830570EC6A83040521BA5B06A3B474040FBA4296EAD3040866FA4B06E394740008F7CCD8EB130408E4D9BD9FD3647409095A08C5FB6304076A4CA6FD2344740687EC2AE2DBE304012C2B7DE2633474090DB9CEF4CC13040160EC335E13047407007DAF90BC33040160EC335E1304740786FEDA7F3C430405EA92CB9B332474020DF1704C7C63040025ED7E349324740889623DBAECD304012A4E1BAC0314740E847FD1BD2CE304056870EB49431474040F9D65CF5CF3040464DDC80483147402028BD110BD13040BEEEBF98E4304740B063A18C27D23040A2D6C09865304740180DEB0AD9D330408AD7C5D2DF2F4740E07F372689D430407E03180B4D30474068C3AB6317D530404A9142E7E230474068334FD470D63040160EC335E1304740A0CCFFB8B8D93040E668257FC02F474088E8B15602DC30406A0E1D63B02E474078A924DB8EDD3040926FB224F82D47407009A8C604E03040A6674CBEE22B4740F80E81072CDF3040CA7DD1A9E129474030B34A951AE13040126976A5F127474020DCFEB8A4E330400AD056956525474018083CC363E5304046D4178BA4244740103960E53DE730403EDCA74D16244740100FB9D7D1F03040F66A16EE3020474060005DE51DF73040CA723610171C4740C0EAEA0A8DF930407EC15CCFF31A4740285C324F2D0731408E4F3F4A1A17474050370B2D4B0931406218B6C124164740F0AEC54B900A3140B2899BD9C21447403891C3AE8D0D3140A2F039ADA4134740803F4CD4E8193140C256FDA268104740401C71F6BE1D31404E3AA3B0C20F4740807983078432314092FFE457820F4740C0569A52663531400EC3FBA2EB0E474058DE459AA33F3140C27B2D39450A4740F0D0DC960A4131404667630911094740E071A4293E443140DA94852BF704474068EC07F3D9443140E20453F8AC03474090D93EC39F4631406E7AF9058A014740F0A635899E4A314016A100400401474030BD00B9CC4E314026C33373D100474018E1AD001A513140D64FD446B3FF464030799A52324F314046AFF56891FF4640F0E4D085054C314092B6F9050FFF4640906D8F7B464A3140EED7A013E8FE4640906D8F7B464A3140FE41828E08FE46408806C74B084E3140D222F80592FD4640605B1A347E4E31409E1DDBD26DFD464020AABE740C5131401A30E8F4A4FC4640008A0556BF53314092B623626BFC4640F8A03BC37F563140FE41828E08FE464040D7BF11FB573140BE5A3710D8FB4640581B267895573140A2835BCFB4FA46408054537167583140822B3E4A54FA4640E0112AB2825D31404EA61A2876FA4640705102569F6331401282CA6F46FB4640D07C4D3752663140E242B124DFFA4640E8649DEFFC673140023B4BBEC9F8464038963A265D693140E6B86A432AF94640800B3EC3576A3140124F02DD8FF94640983F84A40E6B31404ECC953CFCF94640C0E8540E3A6D31404EA61A2876FA4640F8FE570E0E833140468C77B71AFA46403801671FE78D3140CE71C13551F94640A84FE56D52973140028741E7D5F746402015B39D70A33140561741E75EF34640E0BBCC8551A5314036227A5427F2464030BE83079CA53140BAF2077A1DF14640E06C2DB2EEA63140F264F2CB35EF4640C823E03339A7314016356DE0BDED46407051836998A73140E2C9A00D12ED4640401E07F311A83140322577B736EC46409883EA0A29AA3140BEED741ABBEB464060FF1167F4AF3140EE72983C99EB4640302DA42926CF314056F377B73EE84640581B267895D6314036465F6C5AE64640B8A10D2DA3DB31401207707D4FE34640E86207F3A9DB314056D0F905C8E24640983DEEA7BBDE3140D249BE9801E34640A8B2783030E03140E60453F8ECE34640485C3BC367E13140A2154E5BEEE4464010C4D55CC9E331400A0FD6E364E54640204417FAFEE731400A0FD6E364E5464098D2C1AEB1F931400A0FD6E364E54640F8E4A1299A14324056D0F905C8E246401806A6C69017324056D0F905C8E24640C029DAF9571A324006CB99D926E346407074641FC31E32407EC178B746E44640586122DB0221324042FC361087E446406875E76D1236324042FC361087E4464000451CA1D53A32407AC5C098FFE3464050B0314FB1423240E2D5D246EFE14640E0A14E7183483240663ADB80E8E14640F037DD96EE4C32400ECF0B173CE04640D00C72F65251324006F09F13A7DF46406057FC1BBE553240260D862B41E0464058ACCD85E55932404E46B32413E14640D07EAB63FF5D32400285B95E0CE14640388D5FE541623240A216A74D21DF464000D14C37566232400A36AAEA1DDF4640687A96B5076432407E9C5695F3DE464060A6D3BFC665324082BCE557E4DE464040D210CA856732407E9C5695F3DE464078C42EB266693240A216A74D21DF4640A04D46FD446E3240BACB4F5B77E0464040654ED4287732409E3D6DE050E44640787B1904577B32401636F02E4DE5464040C051D41481324082DF63095BE4464040AC95B5BF8732407E75828E3AE54640206D6EF6CE92324002D02C3989E84640309359ABE0953240CEC3DB80D8E94640B811B19D7C9B3240CE9302DDA7ED4640F86BE233459F32402A08014028EF4640A8668207A49F3240DA2678B730EF464070D9CE2254A0324052412E39FAEF46402090089018A132401A0C1FC584F14640A08F16670CA232408E2879B72AF24640800B3EC3D7A73240B2ECF1CB2BF44640409C6CBCA1AC32407E2C973C87F44640280EA6294EB932409A1CA11300F34640001B759369C332406A117FF125F1464028784FD488C632403690A3B000F14640C0969CEF34C932404E8F9E7686F146401848763058CA3240566337106BF24640104554717BCB3240623388C896F34640985EBA740CCE32408626ABEAF1F446409804691F5BD13240E273B95EE6F3464038D092208CD23240D6C39A1BEAF3464080E4AF000ED43240929230D6EEF3464018F84A9A6BD8324076C8A113FCF44640B8761BA18DDD324032FF178B83F5464048DE3C2669E03240FE1C8AC811F646402082AD63F3E23240A291014018F74640E090DAF9BBE63240B2ED66A631F74640F005658230E83240F24166A635F54640907CD422CCE33240968EE8F435F4464010C4D55CC9E132409AD6963C89F34640C0DE2E4F75DF324006800A1797F24640606FE433B9E43240C630828E22F14640C00327156CE73240167E900217F04640F005658230E832403A28900219EF4640E0D4406056E63240EEB0E8F441EE4640C0727F071CE332409AD3A74D03EE46406850B7D7F5DA324016356DE0BDED4640085B773038DA32403EDDF2CB3FEA4640482401B9B0D932409E64337380E946407831F1A7C3D832403A4DC098F5E8464000F3638250D83240A6B2CD0C69E8464008A76D5944D932402298178B9FE74640087C7BCD2EDD3240F66D05DD36E64640D0EBA52902DF3240BE3820C51DE54640708E237871E63240663ADB80E8E14640D00DF544A2E73240EE8D4E5BF8DF4640607BE133B1E832404EFDB95E16DC46404063B8D7D5EA32407E2DE2BA70DA4640607C2CB25AEE32403EB8DE1D76D94640F88FC74BB8F23240DAC442E794D84640A867055673F632401A0D6A436ED74640805285A4EEF73240F23FC23599D54640B89EF4E180F632404AAD5F6C7ED44640706C0C2D77EF3240121653F812D1464000D3E233A9EC324086DB0DB458D04640A0EBFDB890E93240F68625FF42CF4640207E11CA81E83240AE83939FDACC4640F005658230E83240DADD7EF173CA4640304A44FD50E73240A2A899D95AC9464058F314677CE932403A733BADBBC84640C8B0EBA797EE32402632A84DD4C546401069715914F13240A637818EFBC44640D82BA8C6D0F53240B21E7D5402C54640588652711FF93240C6309002ECC5464078EBBC74B0FB3240A25F76B701C7464088B83A2629003340FE1B2362D5C74640F82205565B023340D29013EE61C84640B085B8D7A1043340266E5495A0C84640E06A5FE5F5063340E6BB751A03C84640B05AC64B8C083340B2A47B54FEC64640A8D2F97E5709334066CD26FF54C64640E883D3BF7A0A3340F6CEB75EC3C5464008E93DC30B0D3340A637818EFBC44640287AE5D05B1133400261B85E46C44640A0902915101533408E07838EFDC34640C029DAF957183340A68DABEA55C346404891FB7E331B33409A05097A7DC1464020A37930C4133340EAFD1B28EEBE4640D81BD7CE96043340D6A72126B8BF464088DCE76D76023340662F3273D4BF4640C089547113FF32400218E9F4A5BC4640A0E799EFC40033401214CB6F49BA46400863DF50230333400E0E936967B94640381C392619083340BEEF0A178EB746408052BD749409334026A10EB40DB6464010B103B954073340EE8D5CCFC1B4464068BA9852D6F932400AC614EE8DB24640409F4D371EFD3240CE7E1C288BB04640306F7493ED0033404290CD0CDDAE4640107BF87E5F053340E6DEF3CBB9AD4640B8C2D9F9730A334052425DCF50AD4640080E36890215334052425DCF50AD4640E0B1A6C68C1733406EC6E1BACCAC4640A06DC74B6C1833400AF71C2895AB4640402C917BA2183340FA81929F20AA4640C0DDE3D04B19334056D14484F1A84640105432ECEF1F33402E50939F28A64640E06891187D29334012BFC1357CA44640D032B53AF35C3340DA97828EC69F464008C4D55CC9603340AAD26A43639D464090CFE033B5653340DE9F3CAD949C464090AF271568683340225AD546009A464040BFCE85C56733402297DA80FF964640F0EEA18ED8643340260079C8F7954640302697B5036433407AE830D6AC9546400859E133E54D334066CE8D655195464098A220DBA64C334026E9BCFBA0954640A0FE3689964A33406A5C1C28BF964640B0CA69BC654933401659606CFA96464040E5499A8B4733406E7CABEAAF964640403140C3974633403201BCFB1F964640102F0256D345334012CBD1A98C954640F0024C378E443340F6A9A3B039954640F82F1504D7393340EA91A4B0BA944640D0779EEF5C343340D651862BD9944640306C9318712F33402E29DB8082954640D00959ABB02C3340167FDB8080964640480335ECDF273340B281E1BAF4984640609E43FDD4243340063B4BBE89994640B05EF244321F33400261C6D20F994640D84B2915F820334016372D392D97464058A6D3BF46243340662D9C76C194464090AEDC963E2333405689B224B1924640F07372F6B61D3340F66F9BD94992464058CEE4D05F0F33409A68973CCC92464080BAD0227C0B33409EB8D5469191464000F47630D40B334082E1F9056E90464050226BBCDD0D33409A8BF905708F464020C6DBF9671033406234E1BA898E4640282EEFA74F1233405640B95EB48D4640A818F6E14414334002F3D4461C8C4640A090291510153340D6BF939FDF8A4640884433EC0316334002622D39CC874640386B10CA21183340F28A7B5405844640A06DC74B6C18334072785595AD824640F829530EDE173340AA898D65798146405880907BA61633406A7398D9EE7F464068D16DBCCD153340A66A2D395F7E4640C0BCDF33551633403AD4FBA2117D464080E013671C1933408E1F66A6297C46403864CB85591E3340FE1B31D6DE7C4640F0947693AD2033402AE41B28F57B4640384169BCB52133400E57929F017A464090D13E60E21F33405664828E1479464028BD3889F21C3340D225F568A178464000D3AA63831A3340D66BFF3F187846405044BAD77D163340F687707DAC754640D09D190423143340623388C896744640B8772E4F111133400A5C3373ED734640F09FF044560D3340561B89C817744640006026782D0833402E0504DD1876464078C9A529B6043340EA3D1EC57C76464008129A52CEFD3240BA0C818E1C7546408007DAF98BFE3240324A99D98972464058CC4ED40C0433400615DE1DCD6E4640B0A81AA145013340EADAABEA806E46406B34DF1694F93240C8F1C7D36E6E464090AEDC96BEE332405A3C717D3B6E4640280CD85CD5DB32407A3580F12B6F4640F86A1ECC95D732404E6F2B9C287046409804691F5BD13240FEF90B179B714640C099459A8BC83240D2532A9CF574464048370B2DCBC5324006A9741AA378464038682F4FA5C732402297B024E3794640286493B533CD32407E0BAF87637B46405066D122F8CD324036DAE7F4A67C464058AA378992CD32403ABBF768187D4640707183A446CC3240224ED346B97D4640E8C41767B8CB32406EEBF568167E4640403D3DC38FCB3240466915EE767E4640403D3DC38FCB324076A4E657257F464080C260E56DCB3240461D1FC56A7F464098811C3EB0CA32408AB4559532804640684DD65CF9C932408A6E4BBEBB80464020A1E333F1C832402E2B717D15814640F8B3AC632BC732404E8532735281464038C8E19606C032406288751A91814640A024EA6D02BD324022DF7A540982464088FEFEB8F0B93240F2D2299C58834640B8E99F8C63B032406AC8AF87458A464080BDE96D1EAF3240BE81198BDA8A4640F0C56B599CAD32408ADAC2352F8B4640109A2E4FDDAB32406677D2461E8B4640182A8BDE83AA32400E85D5469F8A464000F644FDCCA93240BAE613EEDF89464028BFCE8545A93240AAD25CCF19894640C8FB96527AA83240E6D479547C884640501E07F311A632407EDBF1CB05884640D8B823DBFAA33240AE3A9AD95D884640B0CBEC0A35A23240620FBF9836894640B8566282C0A032402E07B6C13E8A4640C02A2578019F32408672852BEB8B464080794B37DE9D3240CA2BEF2E808C464030503EC3EF9B32408A461EC5CF8C4640485BF0443E9A3240FA8C3BADB48C4640280B8DDEAB9632401E53828EEE8B464078A0499AF3943240FEF8C098B18B4640E8B05B48E39232408694B85EB88B4640381D84A4C28E3240C631DB80158C464028799A52B28C32401A314F5B218C464030E06282F08832401239DF1D938B4640B0C97FD4E888324062B23E13518B4640586586A4CE883240B68705DD6F8A4640C01B47FD0C8A32406233963CE088464000D0C9E8868A3240BAAA67A613874640F80DFEB85C8432404E6BD54626874640809FC64BA47D324022BCEE2E09884640F07E403A517B3240481FFDE09088464060A12478517732408E507C547A89464068B2051FAB73324066A49BEDDF8A4640F052A629E66D3240DE0A5B32158D4640C0BC17047B693240EED8077A648E4640707206F395643240162B39AD2F8F4640A8403FC3835232404668AE87BA8F4640B8701867B44E32403E0215EE5290464000ABD122104332404E23198B4993464010E905F3E53C32400A36B85E2794464040AC5DE5993632407AB9B024AF934640B81234ECCB333240CE28F2CB70924640E00DC6E8363332404A0110B4D8904640205CFA7E073332402E50AF873B8F46402830BD7448313240AE3D89C8E38D4640D0A145FDC82C3240C6E788C8E58C4640585FC4AE55273240F270F4CB7C8C4640C04C0E65C9243240AE250B6F818C4640B8566282C021324016EDE8F4868C46402014681FC71C3240AEA90EB4E08C4640A0C718A11D1732407EDA98D9D28D4640E06A5FE5F5083240E24F0C1759924640B8597BCDE204324032E742E720934640E8408007E40032408E0646841D93464090AF5FE50DFD3140DE2F7D546892464058600F2D7FED31408274299CC78B4640A0C7507143E93140BEA7949F608A4640C8A88A4111E73140822A0140F489464048EC97520EE031405ACDFCA278894640E0CE3D267DD83140CA7F9F765A88464070DFC8E8F2D53140C67AB85E3F884640809FFE1BCAD03140B2127B54FB8846404896AAC628CC31407EE4F6687D8A4640802F23DBCABD31402A9AD7E30E914640603911CAE9B63140FE3FEC9175934640E04DBF114BAF3140D64FE2BAFC94464020F250D4CCA63140FAC932732A954640006CE23345A13140267BBDFB23944640B810661FD39731409E1DD0A996904640680ABB748892314012F00140698F4640F870917BBA8F3140824F2362478F464078F239891E8A3140F28A6DE07B8F4640B8372CB2427D3140929389C8E18E4640B0CF18045B7B3140B213C6D2A48E4640687EC2AEAD7C31400ED0727D788F4640E0246382087F31408A6B40E72292464068E58ADEEB783140FE36E7F4FD904640C00AA429DA75314032DC99D9CC90464020F4E6D09F723140BA3AB6C13091464090D906F3797431408A6DE457FF91464070513A2645753140FA69AF87B4924640184F83A4FA743140B67EF2CB6E934640406403567F7331409AD6963C4994464040F708907C7131408272A113BE944640988435895270314042D722623D94464000D3E233A96F31405A5FEF2E72934640A85068BC216F3140AE84DE1D04934640B83300B99C66314012F0D7E30C924640C0E6BE11E7613140E275872BDF914640680C51715B5E31406AC9D0A992924640E865B09D005C31404E23198B4993464070022BB2965331409E8AA0133D95464030BD00B9CC4E31409A29EE91F095464068EC07F3D9443140A68B15EE42984640E0E0FC1BEE3F3140BE50E7F47694464000B21667D83E3140AA1AFDA2E393464060F45FE5A5323140EAFFCD0C94934640E8912578E52F3140AE84DE1D0493464018E323A6E71F3140266E3CAFA3954640680B06F33119314036D99C76BD96464068C50990440E3140EEFA2C39A899464050370B2D4B0931406E0FCD0C809D4640A8089E8CBB073140AAF641E70C9D4640C8B8B33AAF0331406661AF87219C4640408A469A1F023140FA6727FFAA9B46408804314F35033140C258939FBB9C4640A845EE0A7903314006F0C96F839D4640303E5071130331400AC6067A449E4640408A469A1F0231408EBE89C8409F4640182AC3AEA9FF304056CFAE879E9D4640B8FE77CDF6FC30403E49862B469E464090A19D8CD7F93040D69926FFA29F4640C89270F60EF630404AD1289C1EA0464060005DE51DF73040F21B077A429F4640C8BEAD00CEF730401ECBED915F9E46401004075683F23040EE69A1132B9E4640B8CD820708F130409E406A43E09E4640C00959AB30F130400AF9CE0CFB9F4640100FB9D7D1F03040FA8C4921FEA04640801FCB85C1EE3040860F1351AFA346407044F2A7A3EC304072A7D5466BA44640F087FFB8A0E53040E2BE727D52A2464048D1C54BDCE430409A5C87C8BBA14640006CE233C5E23040DE6B1B28ABA04640504D951819E0304096D82C399C9F4640B85DA7C608DE304062622462279F464078F51A049BDC3040E688919FB99D464050C79F8C17DA3040A6D6B2241C9D464060AB82073CD73040C6782262AC9C464040461804ABD430408A210A17C69B464030D18D7B36D33040763716EE3E9A4640201A62E565D330409621347322994640A012C44B00D33040AEABA4B03398464018BD00B9CCCF3040EE9AA94D32974640C0C18E7B4AC930409EDDF768E4964640D081FC7E479630405A5F0B17459C464080A5F8E1688730404EFF5DCF729C4640E82318045F7B3040063B4BBE89994640B8818CDEFB723040B2A46DE0F4924640E02912CA7D6F3040623DE6570191464068C7D75CBD6C304082E39D764A904640500D9CEF04663040EE6ADE1D8B8F464010F10556A3633040662EF568F48E46402089F2A7BB613040B67DA74DC58D464018C0E133C96130406EC6E1BA0C8D4640F0F657AB5062304056D2B95E378C464018C0E133C9613040FA5DBBFBB68A4640408C1467986030407E2E4921AD894640B8C9560E625B3040D652C33539864640106C14BA94583040009125B08F82464080B38BDE33573040D22F4584C280464040F9D65CF55030407638616C2880464080E95E4883473040E26B1B28EB804640A0CA31ECBF3D3040AE3D973C6D824640A0E67DCD8636304062E8F8050784464078E013671C1F30400ED1A1134F8C4640501396B5A31A304092B904DD678E4640F0D014673015304036E3FA0568934640F0FB06F3451130402E07A84DB595464080A64360120E30401E9DC6D29496464090DCE76D76083040561A5A3201974640D8B023783D0630401A3141E7979746406838FE1B66063040966D2A9C2E98464068EC07F359073040F24037101F9946402867E4D07B073040763716EE3E9A4640D8C52A153C053040C6E8EF2E629B4640A89F3F602A02304032006309ED9B4640700C2CCEB8FE2F404A25D9E3F89B4640308D0272F9F22F401EE6BF98919B46408023623BF9C12F403E40818ECE9B4640405A48534CA62F40EE6AD0A9019B464040CFCB42EA9A2F40C21C2F0A26994640A09D0BACAE952F405AAA8C654B98464060C9E189DC8F2F40BECB5DCF80954640C060639E628F2F4046D3DA8084944640C0C8764C4A912F40DE2503DDEA8C4640A051AE56118F2F405E42872BED8A4640A073D43E0E832F40A219963C678546408069060FD8822F4072547EF103834640409CE0ECED882F40D2C3E9F4217F4640E0B1D9B2F8862F40FE1B31D6DE7C46401033A27F457A2F40526626FFF0784640605DF705DD772F402A8ECCBB9B774640806DC26732762F40B641ED91AF764640D0E40372F1772F40E2E52A9C787346401017EC26FB7E2F404E2343E7E5704640C0249FE22E872F4096D9939F986E4640F076C7A1FF8B2F401215087A296C4640708C397C90892F40426D4F5BE6694640608946199F802F4012520DB42869464060FFE34F17762F408EBEA5B093684640F0B85F3BA16E2F40D62EEC91CF6646406040C1CA436F2F40EE4615EEAA6446404078FBD4FA742F403E1B51F871624640006AF837E4812F400AC8AAEAA05E464080D119BDBF842F40224ED346F95D4640E0C8EF60D0902F40626353F87D5C4640209EFDD4BA942F402610838E505B46405005A64510982F40EEFCD0A9045A464090C52531829C2F407A02AAEA2B59464080F794D105A42F40AE63F66860594640A0B79BA8F1A82F405640ABEAAA594640105C653B19B22F40B25D0A17CB594640C030FA9A7DB62F401279C5D20E5A4640D00021F74CBA2F4046B2F2CBE05A464060635B640DBD2F40BEC1D5E3F95B46401029B490A6C02F40CEBC88C8C65C4640006B43B60DC72F403AFC1A28B45C4640F0C2BDCA8BCA2F40BEC1D5E3F95B4640F02AD17873CC2F4066308B65C75A4640C0A772AFD3CD2F404224640973594640001FB4B992CF2F40620E828E56584640005DB92D7DD32F401A57BCFB9D57464080AF5A01D4D72F404AB8087AD2574640B0931DBD0FDC2F40EA92198B4058464020E637A5ECDF2F40C62F299C2F58464010ABAC7F6DE52F40E61D8F02CC5646407066BE674AE92F405A83C6D29B534640A025E1EC1DF02F408AB5929FD25146407072A97F4DF52F401680347333514640501D30A5B4FB2F40320215EED2504640F8C84CD4180430404A6466A68150464058E720DB3E073040DA4CF3CBF64F464070541BA1410930405E38299CC24E464010CCD5BF860A3040A68DC7D2284D4640E0BEE5D0730B304042FD818E704B4640E8024C370E0B30400E63A213524B464040DC6E59F00830408604862BEE4A464060DF9018CD07304096B8B95E3E494640C0DDDA5C91033040AAAEAF874C4746401025DC968E013040BA9FBE983F454640286A8D7BD2063040CA77C96F39434640787038261D0C3040AA1D087AFC4246406005D45C6D153040760888C877444640F0D15FE5D91A3040DE325ECF2444464010F36382D01D3040C69D4484BF424640709CAD00821F30409AD688C87F404640700C5171DB20304036779102FE3D46404049F97E2727304036B76943B0374640D0B33CC3DF283040D6755DCF423546406836304FED283040AEAA59324A33464010C9BC7464273040DA72448460324640F01FEC0A392530401E7A4821DE3146408876A28C872330402AC052F854314640B0FBC5AE65233040D20CD54655304640B0AFCF85592430403226EC91BC304640B8CBEC0A3527304012AAF768B230464038963A26DD2B3040A2A9E457443046403812DB962E3130402A9CA5B0072F4640B0741504EF3330402601A0130D2E46404805CD0FA53430404E8DB330DE2C4640E0765371B3343040CA516A43C62C464078B8025603343040EE69939FA12A46400887EC0A1D33304002F6A74D8F294640A060884185303040BACE3E4A7D274640406B489AC72F3040A63925FF17264640A89F0790043030407E551D2826244640F08995B5733230403EDFA4B025204640B81AC4AE3D3730404A1A30D6A41A464070E58ADE6B3B3040E6AF65A672184640C82857AB884630400EA23D4A24144640303F9BEF3C4A3040A639178BCE1146407801E033ED4F304052D8A5B0CC0C4640709A1704AF533040F28D6A438B0A464010A622DB9A583040921DB4C1C3084640488339267D5B3040C6EA852BF5074640F065B09D005E3040DE6E0A177107464050C8EA0AC1603040C212A5B057074640D0FF32EC6B673040E68CF56885074640F0649DEFFC693040020FD6E32407464070BF479A4B6D3040424AD1A92F05464018321BA1756E3040522038104D034640E0E202B98C703040864EBCFBCA014640E0DFB19DC47B30409E1E371053004640386E2915448030404AAD7B5411FF4540387A26153C8430405A61AF8761FD454060EBC2BE068730405EBB35CFE2FB4540205006F3498E30406A5C1C28FFF7454040476382D49A304072BD0D3456F3454070B2FF1B2AA330400ECD440428EF4540A8FD5BABB8AD3040F243188B9BEB454078DB641FA7AF3040C606B8DEBEE94540B019793094B03040C212A5B097E74540D8C2499ABFB2304006CFE1BADFE44540081894186DB63040E2D34304C1E245401059E133E5D23040BA9FCC0C89DA4540685A156760FB3040D2EBFA057BCB4540F8978FDECF073140124F099734C64540282494D0F30F31401E5E40378DC3454000319852A6153140D60597BCB1C1454098CE95B58B2431406E37EC91A2BE4540A844A38C4F3D31409EF3136E3CBD454060A8A18C3F453140760F0C974ABB454088B985A452493140328F5FECFBB7454080675DE58144314036B777B739AD45407836A0EF384A31402A50A8CDD6A6454000469FEF245431406659F56853A1454090161FA1915E31401EC48C65C49D454030CCDE33C1633140225245848E9C4540B0F72915746631403A4C6E60A79B454078EC7793256831405EAA8C654B9A454070D44E374A6A314072E830D6AC974540F0A94ED4406D3140368EEA1136954540107F5C482B713140628723E282934540A0FB530D6F73314092FBABA1F6924540A89BDB965E963140EABB751A838A4540A8ACFC3218993140E2F742DD548945408064449ADFA03140FAAA0A17F6854540105A6482B4A83140EAB9DF1D307F4540701B5E48BBA8314072E51E452F7F4540A0DC570EC2A83140EAD2E357297F454068524DD4C8A831407A7566A6277F4540100E6E59A8A93140DAC0FA059C7B45400893B13A6FA2314056D6F3CBA679454060DA6107A694314030B6D03596784540007D0EB280943140985ECF359E784540B0766307B68C3140F035248B477A4540D0636207D688314028D5CE35AE794540007D0EB20084314068557BE0BC7A4540206A0DB2A07F31400017268B6F7C4540007D0EB2007D3140E019CF35C67D4540007D0EB280793140C02CD03526804540805DB75C8B78314060BF248BB78145402096BA5CAB7A3140985ECF35DE824540007D0EB28080314060BF248BF78345406070B85CEB7C314088F7CE357A84454040570CB2C0783140C02CD035E6834540501910B210743140F880CF35EA824540007D0EB2006F314088F7CE353A82454090F30DB2D070314008E8CF358E82454090F30DB2D075314060BF248BF783454090F30DB25073314068557BE0FC83454090F30DB2D0703140D0B2CE35228445402096BA5CAB6E3140D0B2CE35A284454090896407166D314050A3CF35B6854540007D0EB2006F314040D2258B978645406070B85C6B6D3140D893D035CA87454090896407166D314040D2258B57884540501910B29067314040D2258B97864540B0E00CB2F05E314040877AE074894540F0506107F6503140D893D0354A904540206A0DB2202C3140E019CF3546974540D063620756233140C02CD035E6994540501910B2101F314088F7CE35BA9A45402096BA5C2B1D3140D893D0354A9B454090896407961B314088F7CE35FA9C45402096BA5CAB193140D048258BA79D45409089640716173140680AD0351A9E4540007D0EB280143140E019CF35469E454090F30DB2D012314030B6D035969E4540501910B21011314040D2258B579F45406070B85C6B0E3140B0107BE0E4A04540B0E00CB2F00D31400017268B6FA1454090896407160D3140680AD0351AA345402096BA5CAB0C314030B6D03596A34540805DB75C8B0B31401804258BCFA345404083B95C4B083140B0107BE024A44540B076630736073140D0B2CE3562A445409089640716EF3040403CCF35D2AF4540B0E00CB2F0E73040403CCF3592B1454090F30DB250E53040B0C5CF3582B24540F050610776E33040680AD0355AB34540B0E00CB270E1304050A3CF35F6B34540206A0DB2A0C03040204FD035B2B54540206A0DB2A0B13040680AD0351AB84540B076630736A0304088F7CE357AB945406070B85CEB883040D0B2CE35A2BF4540501910B29088304050A3CF35F6BF4540D06362075688304008E8CF358EC0454070060FB2B0873040A87A248B1FC1454090F30DB250863040D0B2CE3562C1454040570CB2C08430407871D0353EC14540B0E00CB270823040C02CD035A6C04540206A0DB220813040B0C5CF3582C04540B0E00CB270773040D0B2CE3562C14540B076630736643040D0B2CE3562C14540B076630736643040B0C5CF3542C2454090F30DB250693040F035248B47C245402096BA5C2B6F3040F8CB7AE0CCC24540B0766307B674304040877AE0B4C3454070060FB230793040D0B2CE35E2C44540B0E00CB2F0743040A87A248BDFC54540501910B2106F3040985ECF355EC64540B076630736563040F8CB7AE08CC6454070060FB23053304050A3CF3536C645406070B85C6B4E304088427AE01CC54540206A0DB2A04C3040D0B2CE35E2C44540F0506107F647304088427AE09CC44540D0636207563D3040C02CD03566C34540206A0DB2201E3040D0B2CE3522C34540206A0DB2201E3040B0C5CF3542C24540908964079626304040877AE074C14540B0766307362A3040E019CF3586C04540206A0DB2202C304028D5CE35AEBE4540E08F0FB2601C304028D5CE35AEBE4540F0506107F61C3040F8CB7AE04CBD4540B076630736193040204FD03572BD4540B0E00CB2F010304028D5CE35AEBE4540206A0DB2200A3040403CCF35D2BE454040570CB240073040680AD0355ABF45409089640796033040B0C5CF3582C04540007D0EB200023040B0C5CF3582C04540007D0EB200023040D0B2CE35A2BF4540C0E070B9D6FD2F4028D5CE356EC0454060EDC60E6CF22F40C02CD03566C04540C01F1F64C1EE2F40D0B2CE3562C14540402C75B956EE2F40F880CF352AC14540E00C1E6461EE2F40D893D035CAC04540C01F1F64C1ED2F40888D258B7FC0454040D41A6441EB2F40B0C5CF3582C045402013C90E2CEC2F4018B979E0ECC1454060C11964E1EE2F40F880CF352AC2454060C11964E1F22F40B0107BE024C2454020E71B64A1F72F40204FD035B2C24540402C75B956F92F407871D0357EC34540E0A1C20EECF72F40680AD0351AC44540E0A1C20EECF62F40E019CF35C6C4454080AE186481F92F40888D258BBFC54540A032206421F12F40403CCF3592C54540A032206421EE2F40985ECF355EC5454040D41A6441EB2F40D0B2CE35E2C44540800673B996E92F40D0B2CE3522C6454060EDC60E6CE52F40D0FD79E0C4C64540C01F1F64C1D92F40888D258B7FC7454040D41A6441E22F4028D5CE35AEC7454060EDC60E6CE32F40D893D0358AC84540E0A1C20EECDE2F4030B6D03556C9454000FA1C6401D62F40607479E054C9454000FA1C6401D62F40A87A248B1FCA4540E00C1E6461D82F40D0B2CE3562CA4540E0A1C20EECDA2F40D893D035CACA454040D41A6441DD2F40607479E014CB4540A032206421D82F40E019CF35C6CB454000FA1C6401D62F40204FD035F2CB4540C01F1F64C1D92F407871D035BECC454000FA1C6401D62F40985ECF359ECD4540C01F1F64C1D92F40403CCF3592CE454080AE186481D62F4088F7CE357AD04540E0A1C20EECDB2F4028D5CE352ED14540C01F1F64C1EE2F40985ECF351ED14540C01F1F64C1EE2F40403CCF3512D24540C01F1F64C1D72F4050A3CF35F6D2454040D41A6441CF2F40204FD035F2D24540E00C1E6461D52F40F880CF35AAD3454060EDC60E6CDA2F4030B6D03596D3454020E71B64A1DE2F4028D5CE352ED3454080AE186481E22F40204FD035F2D24540E0A1C20EECE72F401804258B4FD34540A032206421E72F40C02CD03526D4454020E71B64A1E22F40F035248B07D5454040D41A6441DD2F401804258B8FD54540402C75B956E42F4008E8CF350ED6454020E71B64A1E72F40B0C5CF3502D7454020E71B64A1E62F40D0B2CE3522D84540C01F1F64C1E02F40B0107BE024D94540A032206421E12F4068557BE0BCD64540C01F1F64C1DC2F4040D2258B97D6454040D41A6441D02F40B0C5CF3582D845402013C90E2CC92F40F880CF35AAD9454020E71B64A1C52F40B0107BE0A4D94540E0A1C20EECC02F401804258BCFD94540C0E070B9D6BD2F40B0C5CF3502DA454020E71B64A1BA2F40B0107BE0A4DA4540800673B996B32F40F880CF35AADC4540A032206421B02F4028D5CE356EDD4540E00C1E64619B2F40D893D0358ADF4540800673B996952F40403CCF3592E04540C01F1F64C18A2F40E019CF3546E1454020E71B64A1692F4060BF248BB7E1454020E71B64A1622F40C02CD03526E3454040D41A64415D2F40D893D0350AE64540A032206421512F40F8CB7AE00CE7454000BB6EB916442F4068557BE0BCE74540A0322064213C2F40F880CF35AAE9454060EDC60E6C4B2F4028D5CE356EE8454080AE186481532F401804258B0FE84540800673B9965B2F40F880CF35EAE74540800673B9965B2F40985ECF35DEE84540800673B996582F40985ECF351EE9454080AE186481562F4030B6D03516E94540E00C1E6461552F4040877AE0F4E8454060C11964E1542F40985ECF35DEE8454000BB6EB916472F40E019CF3506EB4540E0A1C20EEC252F40D0FD79E004EE454000FA1C6401182F40306B258B33F04540A032206421172F4030B6D03556F04540C01F1F64C10C2F40B0C5CF3542F3454040D41A6441062F40985ECF359EF44540C0E070B9D6FE2E400017268B2FF5454000FA1C6401F72E40B0107BE064F5454080AE186481F22E40C02CD035A6F5454020E71B64A1EE2E4088F7CE35FAF5454000FA1C6401E92E40D893D035CAF6454040D41A6441E62E40680AD0359AF745402013C90E2CE42E407871D0357EF8454020E71B64A1E02E4008E8CF358EF9454020E71B64A1C42E40F880CF35AAFF4540402C75B956B72E40D893D0354A03464060C11964E1AD2E40D893D0350A054640800673B996A32E40D893D035CA054640A0322064218E2E40D893D0358A0A464080AE1864818E2E4040877AE0F40A464060EDC60E6C772E4088F7CE357A0F4640A0C7C40EAC712E40607479E01410464000FA1C64016D2E40E019CF35C6104640E0A1C20EEC6A2E400017268BAF114640E0A1C20EEC6C2E4088F7CE35FA124640C0E070B9D6642E40F035248B0714464000FA1C64014A2E40D0FD79E004194640A032206421522E40D893D0354A1B4640E0A1C20EEC4D2E40985ECF351E1E4640402C75B956432E4040D2258B9720464080AE186481382E4030B6D035D6214640800673B9963E2E4018B979E02C23464000FA1C6401482E40D048258B27244640A0C7C40EAC502E40D0B2CE35E223464080AE186481542E400017268B6F21464060EDC60E6C5A2E407871D035FE1F4640402C75B956652E400017268BEF204640E00C1E64616A2E40F880CF35EA224640E0A1C20EEC5E2E40F035248B87244640C0E070B9D6622E4060BF248BB724464060EDC60E6C632E40204FD035F224464020E71B64A1632E4060BF248BF7244640E0A1C20EEC652E40F035248B87244640C01F1F64C1692E40F035248B8724464000FA1C6401672E40607479E01426464060EDC60E6C652E4088427AE09C264640C01F1F64C17E2E40607479E09423464060EDC60E6C892E40607479E014214640A0322064218F2E4088F7CE353A2046402013C90E2C972E4030B6D0351620464040D41A6441902E40F035248B87244640C01F1F64C1942E40D893D035CA24464000FA1C6401972E40C02CD0352625464080AE186481982E40D0B2CE35A225464000FA1C64019B2E40D893D0354A264640A032206421942E4028D5CE35AE26464060EDC60E6C8E2E40D048258BA7274640A0C7C40EAC892E40D0FD79E004294640C01F1F64C1852E40403CCF35922A464020E71B64A18F2E4018B979E0EC29464000FA1C64019E2E4030B6D0359627464080AE186481AC2E4088427AE09C2646402013C90E2CD12E40680AD035DA224640402C75B956D92E40F8CB7AE04C224640C0E070B9D6012F4028D5CE352E214640E0A1C20EEC0C2F40985ECF355E214640A0C7C40EAC0E2F40D893D035CA224640800673B996072F40C02CD035A623464000FA1C6401E62E40F035248B8724464060EDC60E6CE32E4018B979E0EC24464000FA1C6401D42E40D0FD79E08428464060C11964E1CF2E40D893D0350A294640A0C7C40EAC9D2E4088427AE05C2D464060EDC60E6C962E40204FD035B22E4640E00C1E64614F2E4068557BE0BC3B4640402C75B956372E40E019CF35C641464000FA1C6401292E40985ECF359E44464020E71B64A11A2E40403CCF35D245464080AE186481182E40204FD03572464640C0E070B9D6072E4040D2258BD7484640C01F1F64C1032E4018B979E0EC484640A0C7C40EAC002E40F8CB7AE00C4A464080AE186481CD2D4040D2258B5758464080AE186481C42D40A87A248B9F5C4640402C75B956CA2D4088F7CE353A5E4640A0C7C40EACC92D40A87A248B1F604640E00C1E6461C62D40680AD0355A62464080AE186481C42D4050A3CF35F664464040D41A6441C32D40D893D0350A6C464080AE186481C42D40D0FD79E0446E464080AE186481CF2D40680AD0355A76464060EDC60E6CD42D401804258B4F78464060C11964E1D72D4088F7CE35BA7A4640A032206421D42D40A87A248B9F7D464080AE186481B62D40D893D035CA89464020E71B64A1B42D4068557BE0FC8B4640E00C1E6461AF2D40B0C5CF35828D464020E71B64A1A72D4030B6D035968E46402013C90E2C882D40607479E094914640C0E070B9D6502D40F880CF352A99464020E71B64A1362D40F035248B479F464080AE1864812F2D4088F7CE35FA9F4640E0A1C20EEC262D40403CCF3592A14640A032206421232D40403CCF3552A3464000BB6EB9162A2D40E019CF3586A4464040D41A6441242D4030B6D03596A54640800673B996112D40E019CF3506A84640C01F1F64C11A2D40680AD0351AA64640E00C1E64611C2D40A87A248BDFA54640800673B9961A2D40A87A248BDFA446402013C90E2C162D40607479E054A44640E0A1C20EEC102D4030B6D03556A4464080AE1864810C2D4050A3CF35F6A44640E00C1E6461052D40985ECF355EA6464000FA1C6401CE2C4040D2258B57AB46402013C90E2CBA2C4088427AE01CAC464000BB6EB916B12C40B0C5CF3502AD4640C0E070B9D6A82C401804258B4FAD464080AE1864819F2C40F035248B07AC464060C11964E1942C40E019CF3506A9464040D41A64418B2C40D0B2CE3522A54640402C75B956842C4068557BE0BCA04640C0E070B9D67E2C40985ECF35DE9746402013C90E2C782C40C8777BE0C8934640C0E070B9D6772C4040D2258B9793464000BB6EB916772C40985ECF351E934640A0322064216B2C4018B979E02C904640C0E070B9D65B2C4060BF248B3791464000FA1C6401642C400017268B6F8F464060C11964E1622C40D0FD79E0848D4640E00C1E64615C2C4040877AE0B48B46402013C90E2C542C4088F7CE353A8A464000FA1C6401512C4028D5CE352E8B464000FA1C64014F2C40680AD0351A8A464060C11964E14E2C4050A3CF353689464000FA1C6401512C401804258BCF86464000FA1C64014F2C40D893D0350A87464080AE1864814B2C40607479E054874640A0C7C40EAC492C40680AD0359A874640A0C7C40EAC492C401804258BCF86464020E71B64A1512C40F880CF352A85464000BB6EB916552C4018B979E06C824640C0E070B9D6532C4040877AE0747F464080AE1864814D2C40985ECF351E7D464020E71B64A1472C40607479E0547C464080AE186481432C4018B979E06C7C4640800673B9963F2C40680AD035DA7C464000FA1C64013A2C40985ECF351E7D464060C11964E1332C4040877AE0B47C4640E0A1C20EEC2D2C4050A3CF35B67B46402013C90E2C292C40F035248B877A4640C01F1F64C1262C40D893D0358A79464040D41A6441232C40D893D0358A794640A0C7C40EAC242C40D893D0350A7C4640C01F1F64C1282C4018B979E02C7D4640E0A1C20EEC2A2C40B0C5CF35427E4640C01F1F64C1262C40985ECF359E804640E0A1C20EEC1B2C4088427AE05C824640E00C1E64611A2C40607479E0548346402013C90E2C1B2C407871D0353E844640800673B9961A2C40985ECF35DE84464020E71B64A1142C4068557BE0FC844640800673B996162C4088F7CE357A83464060EDC60E6C142C4040877AE034814640A0C7C40EAC162C4028D5CE352E80464000FA1C64011C2C4008E8CF358E7F4640C0E070B9D6202C4040877AE0747F464020E71B64A1242C40C02CD035267F4640C01F1F64C1262C40F880CF35EA7D46402013C90E2C212C40204FD035F27C464000FA1C64011A2C4028D5CE356E7A4640C01F1F64C1162C40F035248BC779464020E71B64A1142C40F880CF356A7A4640C01F1F64C1112C40F880CF356A7A4640C0E070B9D6132C40E019CF3586794640800673B996142C4040877AE07478464000FA1C6401142C40E019CF3546774640C01F1F64C1112C4008E8CF350E76464020E71B64A10D2C40F035248B4776464080AE1864810B2C4040877AE034764640800673B9960B2C4030B6D035D675464040D41A64410E2C4018B979E02C75464040D41A64410E2C4008E8CF354E74464020E71B64A10B2C40B0C5CF354274464000FA1C64010B2C40B0107BE024744640A0322064210B2C40680AD035DA73464080AE1864810A2C4040D2258B5773464060EDC60E6C052C40A87A248BDF73464000BB6EB916FA2B40F8CB7AE04C73464000FA1C6401F22B4040D2258B57734640A032206421F62B40B0C5CF35C2724640C01F1F64C1F82B400017268B2F72464060EDC60E6CFC2B4008E8CF35CE704640E0A1C20EECF82B4008E8CF35CE704640E0A1C20EECF82B40B0C5CF350270464060EDC60E6CFC2B40B0C5CF350270464060EDC60E6CFC2B4008E8CF350E6F4640C01F1F64C1F82B4088427AE09C6E464060EDC60E6CF62B4008E8CF350E6E464000FA1C6401F22B400017268B6F6C464000FA1C6401F32B4088F7CE357A6B464060C11964E1F32B40204FD035326B4640C0E070B9D6F32B40985ECF35DE6A464000FA1C6401F22B401804258BCF69464060EDC60E6CF72B4088427AE0DC694640800673B996FA2B4088427AE09C694640E0A1C20EECFC2B40E019CF350669464000FA1C6401002C40403CCF351268464080AE186481ED2B40F880CF35EA67464080AE186481E42B40F880CF352A68464000FA1C6401DD2B40E019CF350669464000FA1C6401DC2B407871D0357E684640E0A1C20EECDB2B40680AD0355A68464080AE186481D92B40403CCF3512684640E00C1E6461D62B40985ECF351E6A4640C01F1F64C1D12B4088427AE05C6A4640800673B996CC2B40985ECF351E6A4640C01F1F64C1C72B400017268BAF6A46402013C90E2CC42B401804258BCF694640A0C7C40EACC92B40B0107BE06469464000FA1C6401CF2B4088427AE09C684640800673B996D12B40F8CB7AE08C67464000BB6EB916CF2B407871D0353E664640A0C7C40EACD32B40C02CD0352664464000FA1C6401D62B40985ECF359E63464060EDC60E6CCE2B40985ECF35DE62464080AE186481CC2B4040D2258B17644640A0C7C40EACCC2B4028D5CE352E66464040D41A6441CB2B40403CCF351268464080AE186481C72B4030B6D03596684640402C75B956B82B400017268BAF694640C01F1F64C1B22B401804258BCF694640C0E070B9D6B32B407871D0353E6A46402013C90E2CB52B40B0107BE0246B464040D41A6441B62B4008E8CF358E6B464020E71B64A1B42B40985ECF359E6B4640E0A1C20EECB02B40D893D0358A6B464040D41A6441AF2B4008E8CF358E6B4640C01F1F64C1B22B400017268B6F6C464040D41A6441AF2B40B0107BE0646D4640A0C7C40EACA92B40C02CD035266D464020E71B64A1A12B40B0107BE0A46D464040D41A6441992B40F880CF35AA6E464000FA1C6401932B40B0C5CF3502704640C0E070B9D6962B4008E8CF35CE70464000BB6EB9169C2B400017268B6F6F464020E71B64A1A32B401804258B0F6F464060C11964E1AA2B40D893D035CA6F464040D41A6441AF2B4028D5CE35AE71464060EDC60E6CA12B4040D2258B1771464000BB6EB9169C2B40607479E054714640C0E070B9D6962B4008E8CF358E724640C0E070B9D6962B4040D2258B57734640E0A1C20EEC982B4040877AE0B4734640E00C1E64619B2B401804258B8F744640C01F1F64C19D2B4018B979E02C754640E0A1C20EEC972B40D0B2CE3562764640402C75B9568C2B40985ECF355E7B46402013C90E2C772B40680AD0355A7F4640C0E070B9D65C2B400017268BEF854640C01F1F64C1582B40D893D0358A864640402C75B956522B4030B6D035D687464020E71B64A1492B40E019CF350689464000BB6EB9163F2B40680AD0355A894640E00C1E6461412B40F880CF352A8A4640A032206421442B40204FD035728B464000BB6EB916462B4088F7CE35FA8B464020E71B64A1392B4030B6D035168F4640E0A1C20EEC462B40B0107BE0A4904640C0E070B9D6732B4060BF248B37914640C0E070B9D6692B401804258B0F92464000FA1C64015E2B40680AD0355A92464000BB6EB916462B4028D5CE352E924640402C75B956392B40B0C5CF358291464080AE186481332B40403CCF3592914640A032206421312B4088427AE09C924640A032206421322B4050A3CF35F693464080AE186481332B40403CCF351295464060C11964E1322B4018B979E0EC95464020E71B64A12D2B4050A3CF357696464000FA1C6401302B40607479E054974640402C75B956302B4088F7CE35FA974640E00C1E64612E2B40F8CB7AE08C984640A0322064212A2B40F880CF352A994640A0322064212A2B4050A3CF35F6994640402C75B9562E2B4028D5CE356E9A4640E00C1E6461302B4060BF248B379B464040D41A6441302B40B0C5CF35429C464020E71B64A12D2B40F035248B879D464000FA1C6401312B40D893D035CA9E4640C01F1F64C1302B40680AD0351AA046402013C90E2C2D2B4088F7CE353AA1464020E71B64A1262B40D048258BE7A14640402C75B956282B400017268BAFA24640A0322064212B2B4030B6D03556A34640A0322064212F2B4088F7CE35BAA34640800673B996342B4088F7CE35BAA34640800673B996342B40E019CF3586A44640E00C1E64612F2B4060BF248BF7A44640402C75B9562B2B40F035248B87A5464060EDC60E6C282B40E019CF3546A6464020E71B64A1262B4088F7CE353AA7464060C11964E12B2B40C02CD03566A74640E00C1E6461302B40403CCF35D2A7464000BB6EB916382B4088F7CE35FAA84640A032206421322B40D0FD79E084A94640E00C1E6461242B40680AD0359AA9464020E71B64A11F2B40E019CF35C6A94640E0A1C20EEC192B40D893D035CAAA4640A032206421152B4008E8CF354EAC4640C01F1F64C1142B40F880CF35AAAD4640A0322064211C2B4060BF248B37AE4640A0322064211C2B40D0FD79E004AF464000BB6EB916162B40C02CD03526B04640C0E070B9D6062B4030B6D035D6B7464060C11964E1072B40607479E014B8464080AE186481092B4088427AE05CB8464020E71B64A10A2B4040877AE0B4B846402013C90E2C082B40E019CF3546B9464000FA1C6401082B4088427AE05CB94640A032206421092B40403CCF3592B9464020E71B64A10A2B40F035248B87BA4640402C75B956022B400017268BEFBD4640C01F1F64C1002B40403CCF35D2BF464020E71B64A1032B40E019CF3586C14640C0E070B9D6062B40E019CF3586C14640C0E070B9D60B2B40F8CB7AE0CCC04640A032206421232B40403CCF35D2BE4640C0E070B9D62D2B40403CCF3592BE4640E0FE5B9E0E422B407A0D53F8BFBA464000918A8FE7512B408E36B88EE0BA464000372C31C2842B406AC7482149BB464020A8CB04F19C2B40EE447FF197B8464040D0E589C4A32B408E75BA5E60B7464070EDC6A1CFAB2B4016372D39EDB64640603E34422BC72B40CABDE1BA39B64640C080B5909ECC2B40BA172A9CF0B64640A05A31081ED12B40262569432DB84640F072A41C94D82B40EADA9D7677B9464020717EC08CF02B402E064F5B42BB4640A03AE8891CF72B40CE723610D7BC464070A1D07843EC2B40B65869431FBF464070A1D07843EC2B40F289067A7FC04640809B050F10EE2B4056649E7667C14640E07573AF9BF12B405A5EB224D2C14640203DBC8496F92B40EACA2B3A86C14640C089BF2D25072C404A63FF3F05C14640F033542A800E2C40DA4D30D656C046408070832346152C40B65869431FBF4640E0DF8CFA21222C40AAAE939F79BD464000BDFBD4922F2C40E6D2E357A9BC46409040CF43C63B2C40F219C2318BBC4640105EDA75113C2C40AE56F3778ABC4640B0262C6B473D2C401658077A87BC4640B090D515824A2C40C232188BF5BC464090894F8DD9622C409E1C939FF6BE464070851A20F96F2C405EFAE657A3BF46407082C904317B2C40B63F650926BF4640C023427C908F2C40FA8D78B794BD4640C08045F052A72C4006164584C9BC464080018C34DFBE2C40F69364092ABD4640F02FB8908ED22C40EADBE8F420BF4640804C2E6B07DC2C407E0A5695B0C0464080DEF19A15F02C40AAAD3AAD46C34640101B2194DBF62C402A760EB46EC54640109270758EF92C4086DF39AD3EC7464090F5B220A6FB2C401A302C1222CA4640E067E9260BFC2C40227D4584ADCA464020CAAA7F45FF2C404E6C20C54FCC46408099DFB208042D40C6795F6C8CCD464060F4D315F2102D405E43D2A916D0464030B461D857162D408E746FE076D14640108DF9FDBE1C2D402A75D1A90ED44640A04DF2FDB6202D4016060917D3D44640103FC66A60232D404213653206D54640D074283172292D40FA7FE0BA7AD54640C0A77B230E2F2D401ECD751AE9D44640D02E649E2A302D405A5FEF2EF2D24640002AF69A952F2D400E3CCE0C99D04640D0297D860F302D403A4E0B17DFCE4640D0DFB52DC5342D401A9DAAEA41CD4640D02B1383623B2D40BACDE5570ACC464020E85D01F44C2D40EA47606C94C94640E0ADB62D8D502D408E2CA5B050C94640C0036E31CD532D4096EE76E3FBC84640401A46B6FD532D40E66F7FF1F6C84640C0EF4553F4562D40AAF6337343C8464040EF532AE8572D40C2A0337345C7464060F51E941B562D40B21E7D5402C54640F07CF93744562D40962126FF58C44640500CBC2D6D602D40B21130D6D1C2464060F4D315F28F2D40B63F650926BF4640F076C7A1FF8F2D40B63F650926BF464060F9BA2D0D902D40EADBE8F420BF4640F07BAEB91A902D40B65869431FBF464020998FD125982D406685862B8BBB4640F039162079AD2D407A0D53F8BFBA464080B1F97161C32D401E9C6DE021BC4640C0C80F2039CD2D40B65869431FBF46403045384213CF2D40A61FACEAD8C1464000DAD37863D82D40125D9AD9E9C14640105416BD07E42D40BACDF3CB93C0464070803308DEEC2D40B65869431FBF46408012270466F42D406E5E40F5F0BE46406063F437FCF82D400E7CB4C1D4BE464040323742B3FE2D403E25AF875CBE4640F07918BDC7032E403E6F01408CBD46404063EBC3C11C2E4052AF1FC56DBD4640D0BE548FDB212E40E66038DCA9BC4640A00809494D472E40FEF60EB40BB746402007D515525E2E405E1EA2137AB64640E0560380A2732E4062E46295DDB74640806B9C0B2B902E401673D980B9B946405028D9B2C8A02E40E6B86A43AAB94640606013BD7FA62E40F6F29C76F6B94640B04EA4A8F1AA2E407E2DE2BAB0BA4640E0C45C3B19B42E40DE771D28F2BC4640001C6C1205B92E40E64D5A32B3BD464000526ED83F9F2E40DE474484C1C046409010D1DBE4972E40AAD11151F0C2464070C8FD37C4932E40BE144921B4C54640C0560E83B2902E40BA0AEB9109C94640E0A40AB679902E4016D8C2B726C94640A0FE1A5AAE8D2E406A7E5DCF95CA4640501FC6A1078A2E40A6AFFA05F6CB4640109D4B2A80892E40CABF694303CD46400076E34FE78F2E40AA1B569596CD4640C036556465932E409AD54BBE1FCE4640C09E68124D952E409A69C6D222CF4640400B71AF43982E405E43D2A916D04640606F4FF04AA72E409EBA79B7EDD04640A097E289A4AD2E408A54E01D86D14640E0CEB156C9B42E40E6666CE0F5D1464080DB07AC5EBF2E4002AC717DF2D1464060F6D9B290BC2E40F6FFCD0C14D3464010614C5334B42E407AC4593283D54640D01D96B737B32E4092FF4514B6D54640B04EA4A8F1AA2E4052B078B760D74640C0C734425BA92E40A68DB95E9FD74640E05F219473A72E40B2ED66A671D74640F027609E42A12E401611B22427D74640E042B990EE9E2E40E2B10917CFD64640E0CF6C753E9E2E40BE5B90024BD64640F0F49397209C2E40C69D52F808D64640502643B675952E40A6FBE2BA78D64640E060DCB2E8902E40368F4ABE0DD74640208195D1358E2E40926D2A9CAED746404039B45621892E4042D86DE066D946405079AD7F35842E402630125141DA4640D09471AF73802E40822B3E4A94DA4640C012704C727F2E407EE533731DDB4640C0336598F47F2E40B626CF8F5BDB4640D06F4A8D91822E40725333739ADC464010C2EB60E8862E40CAEF505B7DDD464010F025948B9B2E408694C6D281DF464060E0ACEE7CA92E404692567251E0464000957A23AEDB2E403E6C04DD3CE3464050CC36C808DC2E40329CB2DA49E34640204A488DD1E12E406285A2131EE4464030691D5A06E12E400E2F65A655E5464030FC229403DF2E401EE40DB4ABE6464070E6B0B972E12E4072755832DEE74640601904AC0EE72E4002F25F6C56E8464000D43142D3EC2E400E327EF137E8464010A85B64A5F22E401A565595E1E746408067701285F82E40F6DB04DDB3E746406018498D99FD2E40DE79B32405E8464050B7B2F31F072F400EAB505B65E94640700EC2CA0B0C2F402AE3DE1DD5E9464030B0889478192F409A06D8C36EE94640301AD615B22C2F401EF15A32DCE8464010D0DC158A402F40B6E8B75EFCE84640D0D7C4678A4A2F40028436107DE9464080DF33A504552F40623E3F4A74EA464090A908AC265A2F40324BF2CBBCEB464070315C64D5592F40E6BA1C2890ED464040EA6C12CD532F406A7D04DD22F04640808CA0A8A1512F40A26CDF1DC5F14640B0F99A6EA4532F40EAD952F84DF34640A056FC9A3D572F402A094CBED1F44640D0B34FF0E2592F40A2D9AF876BF64640F0947A23AE5A2F4022BB87C88CF84640A0D25C83CB592F408604256F7AFC4640404103D532592F40F6F3E7F41FFF4640A02CEE6040592F40CE57640925FF4640A001FCD42A5D2F40FAFA4821BB01474040C6F79A25632F40C2AC35734C03474090A6B7905E652F404E16DA80A204474090D8B525C1622F409EE8475154054740C083FD372C5E2F4086016DE08B06474070C948B6ED572F40A2FC1FC55807474050D8B69096492F40DE51949F62084740001E020F58432F409248EC9108094740E0AC2122493F2F408EE9FAF9C709474070B44A8D29352F400AADCA6FA50B4740401484C0042E2F40468AE1BA870E4740D096774C122E2F404E692362C011474030410C496D352F40F63B7A5460154740C0C67923E63E2F40C6604DBE89184740F04D7BF4CD402F40AAC20DA2FC184740506BBCCA93472F40FEA0BAFB941A474000F5A57F95522F400E3CC0988F1B4740D045D5931F5B2F40CEE92981731B474000FDFD71E17F2F40BA825695FA1A4740A0912094AB892F40329239AD131C4740202BB1B90A942F40D673ABEA1C1F47404076467F5B992F40CE86575AD21F4740606280862F9B2F40DA27B5C11020474000F39FE2F6A22F401AC18F02B5204740601568121DAB2F40665EB224122147405061AEAA36C22F40BEA8F30131214740C040EF9F04C32F403EDC2315322147403090E3EC75C42F40F6FE900234214740F0DCA97E7AD62F40E2733000EC224740208C50C7BEE52F40DAB85CCF60244740305D29CEC8F62F406615D546E8244740A0BF6315C2FC2F40864325D132254740200CB3B932FF2F40428C69435125474020027ACDEA0430402E02F9054026474088886182F8093040EAD7BCFB7A27474090BFDAB6C70C3040DA7786C28A284740E05DE80A690D3040DA992DB9C7284740D08E0C2D430F3040B2F7A84D892A4740205006F3490F304012313A2D332C474050E2A0EFB40E3040F2ACA013092E4740A81179CDD60E30400E1653F85230474010A06C0CB2163040EA4DD944C12F474010F21804271830404E38FF3FA62F474060D41667241B3040220A9082D62F47402073A08C931D30408A8F178B8C304740B85DA7C6881F304066A79D768531474098457E6AAD213040D64A10D14F324740F0635271D32430402E2FDC00863247404885CF2250273040AA60077A1A3247401086A18C732D3040028A537813304740F019F2441A31304026C052F8542F4740F0AA99526A353040F660AAEAFC2E474030546ABC9537304072A52362052F474040373A89B64030403E689959CC2F4740803555718F4630408E4922E2C32F4740F01D1E3EC047304052823CCA2730474048DACEEAA94730404A06BBF977304740F06184A45A47304056870EB494314740182EB7D729463040E601727D303247400866203ECC4130403EB5EF2E303347408037B39D3C4030406E8008FAD6334740187962828C3F3040EE1F3373E8344740582DE56D064030400A3B60ECF7364740687B1904D73F30408EE115EE00384740A0162815CC3C30401658077A873B4740D8EC2878D13B3040BA32035D473D474010FEBCB8F33B3040C699BAF3103E47400865D5BF223C30404ACDE0BA253F4740504A2861CC4230400A86AEDCBC4147403053570E92433040865379B709424740F8BB3C269D4B30408E05DF1D2143474000914A9A874F304052A8C552F743474050124B377A5430407A55F3CB89444740B8BE2E5B1C573040164FA1209C45474000E5A1291A583040D6E2F5680346474088C350D0EB593040BA09DE2D1C454740C059B39D085A30409EAB6CE00D454740A8491A049F5B3040D6E2F56803464740401F1AA1955E304062CFCA6F31454740B042D5BFD6613040BAF0717D0A454740981A8CDE1765304066EF59322245474020B99CEF006830409EAB6CE00D454740D81B5071476A30401236F02E8D444740F0ACF77E976E3040B2A3067AF8424740D094CE22BC703040B6E910516F42474070C09218F57230403E6EA84D5942474058BCF67E837830408E4BE9F497424740C0E74160367B3040B6E910516F42474068CAB8D7B97D3040AAD11151F041474020027ACDEA8330404641CC0C384047400103000000010000009C00000000F78F7BF66F32404A1E6A4394474540D82A5D48277132404A4842678145454098C2D085B96F324096B46AC36041454068749CEFE86F3240B6AEA8CD273F454028E4C64BBC71324092B166A62C3D4540588507F375743240F6B6DB007C3B4540901EB8D7BD7732405E161251083A454060CAB8D7B9793240767A077A9339454038D63CC32B7E3240B2A0FBA29F3845407002F3E1707F32403AB5EF2E3037454090896407167F3240D893D0354A354540F0506107767932407871D0357E384540007D0EB20076324030B6D03556394540F0506107766D3240E019CF35463A454040570CB2C0693240985ECF351E3B4540B0E00CB27069324028D5CE35EE3C45402096BA5C2B3D32401804258B8F474540F050610776383240D0B2CE35224A4540805DB75C8B38324008E8CF354E4B4540206A0DB220393240403CCF35124D4540B07663073639324028D5CE35AE4E454070060FB2B0373240A87A248B5F4F4540007D0EB2802A3240888D258B3F504540F0506107F61F3240B0C5CF358252454090F30DB2D01B3240985ECF35DE524540F05061077617324088F7CE35FA524540F050610776133240985ECF355E534540E08F0FB26010324018B979E02C54454070060FB2B00E32407871D0357E5545404083B95CCB1332407871D0357E55454090F30DB2500E324018B979E06C57454040570CB2400C3240C02CD03566584540501910B2100B324088427AE0DC59454040570CB24007324068557BE07C584540D0636207D60032400017268BAF594540501910B210F6314088427AE05C5D454090F30DB2D0E631407871D0353E604540F050610776E13140C02CD03566624540501910B290E4314088F7CE353A654540F050610776DD314088427AE01C65454040570CB2C0D53140403CCF3552674540007D0EB280CE314018B979E0EC67454090F30DB2D0CC314008E8CF35CE674540D0636207D6CA31407871D0357E674540206A0DB220C931401804258B0F674540B0766307B6C73140B0C5CF35026645409089640716C6314060BF248B37664540805DB75C8BC43140204FD035B2664540B0E00CB2F0C3314060BF248BF766454040570CB240C23140A87A248B5F67454070060FB230BC314050A3CF3536694540501910B290B83140B0C5CF35C2694540D063620756BD3140403CCF35126845402096BA5CABBF314030B6D035D6664540805DB75C0BC3314018B979E0AC634540805DB75C0BC3314088427AE01C63454090F30DB250C1314028D5CE35AE6245402096BA5CABBF3140403CCF35D26245402096BA5CABBF3140680AD0359A634540501910B210C03140403CCF3592644540B0766307B6BF314088F7CE353A654540501910B290BA3140888D258B7F664540B0766307B6AD314088F7CE357A6845402096BA5C2BA2314030B6D03556694540007D0EB28094314028D5CE35AE6B4540F050610776863140A87A248B9F6C454040570CB2C0703140D893D035CA6F4540D063620756713140E019CF350673454090F30DB2D06A3140D0B2CE35A2744540D0636207D659314068557BE0FC754540F0506107764531401804258B4F7B45404083B95CCB3C314030B6D035567C4540B0766307B6383140F880CF356A7D454040570CB2C0363140E019CF35C67D454070060FB2B03331407871D035FE7D45406070B85C6B183140B0C5CF35027E4540007D0EB20010314088F7CE35BA7E454090F30DB2D00B3140985ECF355E7F4540805DB75C0B07314040877AE0748045409089640796043140D0FD79E0C4814540B07663073607314040D2258B17834540B07663073607314060BF248BF783454090F30DB2D004314088427AE05C84454090F30DB250033140D0FD79E004854540501910B210023140F8CB7AE0CC8545406070B85C6B00314040D2258B978645406070B85CEB05314088F7CE353A86454040570CB2400E314040877AE0B483454040570CB2C012314040D2258B17834540206A0DB2A028314060BF248BF7834540007D0EB2803D3140F880CF35EA824540F0506107F6633140C02CD035267D4540007D0EB2006D31407871D035FE7A454040570CB24074314088427AE09C7845402096BA5CAB743140985ECF351E784540B0766307B6743140F880CF356A774540B0E00CB2F0743140D048258BA776454090F30DB2D075314068557BE0FC754540007D0EB200773140680AD035DA754540007D0EB2007A3140403CCF351276454040570CB2407B314068557BE0FC75454090F30DB2D07F314060BF248BB7744540F0506107F681314088427AE05C744540D0636207D68431407871D0353E7445404083B95CCB853140B0C5CF35827445402096BA5CAB85314050A3CF35B675454070060FB2B086314068557BE0FC754540B07663073688314030B6D035D6754540206A0DB2A08A314028D5CE352E7545406070B85CEB8B3140F035248B0775454090896407168F314040D2258BD7744540B0E00CB27091314088427AE05C744540B0E00CB270953140D893D0354A734540F0506107769F314030B6D0351672454040570CB240A4314050A3CF35367145409089640716A73140D893D035CA6F454070060FB2B0A83140D893D035CA6F4540F050610776AC3140B0C5CF35027045404083B95C4BB7314008E8CF350E6C454090F30DB2D0BD314018B979E06C6B4540007D0EB200B63140C02CD035A66D454070060FB2B0B33140F880CF352A6F454070060FB2B0B631407871D035BE7045409089640716B53140D893D0358A71454040570CB2C0AD31401804258B8F70454040570CB240A73140D893D0350A72454050479B527AAA314072E38848DC734540E0DD1BA1F1AD3140B6A46DE034754540E0DE661F9BB331401285F12EB2754540901EB8D73DB931404E65DB8047754540F0A5EA0AF5C33140A668AC6ABA734540D033705968C931400AAC86AB60734540B01660E571CB3140DE221BA8C9734540004554717BCD31409A90B6C16E744540B0DEF67ECFCF31409E006F6076744540C8FFFA1BC6D231403AFC289CFD72454028F53AC383D331409A6B6A43BF714540C8898C4139D331409E01C852A96E45401875FEB8C0D33140262FC018336D4540781CEA0AC5DB3140EAAE289C926845408801180493DE31409ED7D346E9674540085D0D2D8BE731405E10035DFB664540E062C6AEC9ED3140CE03C9EF3865454010A1AB63CBF83140424B2A9CA260454088DB9CEFCCFE314092E28AC8C65E4540980BAE63A30C32402AC65A327D5B4540E8CF9118613932402E124AA164504540B0F410CAD14132406EA07BD4B44E454030DE0456C34F3240D6C57FF1F44C4540680B06F3B15632401685D5461F4D454060F033EC7F593240725EE33A534C4540180C7F11AA5E324092A845B4854945404079D222D85E32400ACC0EB46C4945400088FFB8A0623240E242B8DE83484540482CC94BC86B32403248180B2548454000F78F7BF66F32404A1E6A43944745400103000000010000001600000070060FB2B0DB3040680AD0355A6445404083B95CCBEB304088F7CE357A6345409089640716EF304028D5CE35AE624540B0766307B6EF304040877AE0B4604540B0766307B6EC3040C02CD035E65E45404083B95CCBE73040F880CF35AA5D4540805DB75C8BE2304088427AE05C5D4540D0636207D6E13040B0C5CF35425E454040570CB2C0DF3040F880CF356A5E454070060FB2B0DB3040680AD0351A5F4540007D0EB200D93040C02CD035E65D45409089640796D530401804258B8F5D4540206A0DB220D2304060BF248B375E454090F30DB250CF304088F7CE35FA5F4540007D0EB280D23040D048258BA7604540F0506107F6D23040F8CB7AE0CC614540206A0DB2A0D1304088F7CE357A62454090F30DB250CF304088F7CE35BA614540501910B290CD304088F7CE35BA6145406070B85C6BCF30407871D0353E634540007D0EB200D4304050A3CF35F663454070060FB2B0DB3040680AD0355A6445400103000000010000001E00000070060FB2B0A83140680AD0351A5F45406070B85CEBAD314018B979E06C5D4540007D0EB200BB3140403CCF35525B4540B0766307B6BF3140D048258BE75845404083B95CCB62314028D5CE35AE62454070060FB2B05F3140F035248B47614540501910B2905A3140D0B2CE3522624540206A0DB2A0553140D0B2CE35E2634540E08F0FB2E052314088F7CE353A654540B0E00CB27057314008E8CF358E65454070060FB2B05F314068557BE0BC664540B0E00CB270643140985ECF359E6645402096BA5C2B63314028D5CE356E6645404083B95CCB613140A87A248B1F664540206A0DB2205F314088F7CE353A654540F0506107F6613140607479E05465454070060FB2B0643140D0FD79E0446545404083B95C4B67314050A3CF35F66445409089640796693140680AD0355A644540007D0EB2806B314088F7CE353A654540D0636207D66F314018B979E02C6445406070B85C6B7C3140985ECF351E644540501910B210863140C02CD035E6614540501910B21093314050A3CF35F6604540F0506107F698314088F7CE35FA5F45402096BA5C2B98314050A3CF35B65F4540B0E00CB2F0973140B0107BE0A45F4540B076630736973140680AD0351A5F4540B0E00CB2F0A53140F035248B875E454070060FB2B0A83140680AD0351A5F45400103000000010000002400000040570CB2C0213140680AD0351A7C4540501910B21026314028D5CE35EE794540501910B2102A3140B0C5CF358278454040570CB24033314068557BE0FC754540007D0EB280313140F035248B07754540206A0DB2A02C3140D0FD79E00476454070060FB230273140D048258BE7754540007D0EB2801B3140F035248B077545406070B85CEB03314088427AE0DC764540501910B210F93040985ECF359E764540501910B210E63040204FD035F2734540F0506107F6D93040D893D0354A734540E08F0FB260BD3040C02CD03566754540007D0EB200B130401804258BCF76454040570CB240AC3040A87A248B9F76454070060FB230AC304068557BE0FC7545404083B95CCBA730401804258B8F764540B0E00CB270A33040C02CD035A6774540B0E00CB2F0B43040C02CD035267B4540B0E00CB2F0B43040680AD0351A7C4540501910B210B03040F880CF35AA7C4540F0506107F6A53040D048258B277D4540B0766307B6A13040E019CF35C67D45409089640716A73040E019CF35067F454040570CB2C0AC304040D2258B177F4540501910B290B230407871D035BE7E4540B0E00CB270B8304088F7CE35BA7E45404083B95CCBBD3040D0FD79E0847D454040570CB2C0C43040204FD035727C4540F0506107F6D23040C02CD035267B4540B0E00CB2F0E03040C02CD035267B4540007D0EB280E430401804258B8F7B4540E08F0FB260EA304068557BE0BC7C4540B076630736083140E019CF35C67D4540B0E00CB2701C314028D5CE356E7B454040570CB2C0213140680AD0351A7C45400103000000010000002000000040570CB2C03D304050A3CF3536894540D0636207D63E3040E019CF35C6894540F0506107F63F3040F880CF352A8A4540E08F0FB2E040304060BF248BF789454040570CB240413040E019CF35C6884540805DB75C8B40304068557BE0FC874540D0636207D63E3040B0107BE064874540D0636207D63B304040D2258B9786454090F30DB2D0383040D048258B27854540D0636207D6353040B0C5CF3542844540501910B2101F304050A3CF35B681454090F30DB25016304060BF248B778145402096BA5C2B10304088F7CE353A824540206A0DB2A0153040403CCF35528345404083B95C4B163040F8CB7AE04C85454040570CB240143040403CCF35D286454040570CB2C011304040D2258B978645402096BA5C2B10304040D2258B97864540007D0EB2800C304040D2258B57884540501910B2900D30407871D035BE8845402096BA5CAB0E304068557BE0FC884540B0E00CB2F00F3040A87A248B1F89454040570CB2C011304050A3CF353689454070060FB2B017304040D2258B978A4540D063620756213040403CCF35528B4540501910B2902B304088F7CE353A8B4540206A0DB220333040F880CF352A8A4540D0636207D6313040E019CF3586894540E08F0FB26031304050A3CF353689454040570CB240373040D0B2CE35228A45406070B85C6B3A3040403CCF35128A454040570CB2C03D304050A3CF353689454001030000000100000038000000B0E00CB270B13040D893D0354A904540501910B290AB30401804258B0F90454090F30DB2508F3040F8CB7AE04C92454070060FB2B07930401804258B4F954540F0506107F671304088427AE0DC954540E08F0FB2606D3040E019CF3506974540007D0EB2806A3040E019CF3546974540206A0DB2205F3040E019CF3506994540B07663073660304040D2258B97994540E08F0FB260613040680AD035DA994540B076630736643040C02CD035E69945406070B85C6B67304068557BE03C99454090F30DB2506A3040D048258B679945409089640796703040680AD035DA9A454070060FB230723040C02CD035E6994540F0506107F672304028D5CE356E9A4540D0636207D6753040B0107BE0A49B4540D0636207D6753040680AD035DA9A454090896407167F3040F880CF35EA9B45406070B85CEB863040C02CD035269A4540501910B2108E304008E8CF350E98454090F30DB250953040C02CD0352698454090F30DB2508E3040680AD035DA9A4540007D0EB280873040985ECF355E9C454040570CB2408530401804258B4F9D4540F0506107F6883040C02CD035269D4540501910B2109030401804258B4F9D454090F30DB250953040B0107BE0A49B45404083B95C4B96304028D5CE35AE9B454090F30DB2D0983040D0B2CE35629C4540501910B2909A3040680AD0359A9C45404083B95CCB9D304088427AE09C9C4540E08F0FB260A53040C02CD035269C4540D0636207D6A83040B0107BE0A49B4540501910B290AA304068557BE0FC9A45402096BA5C2BAC304068557BE0FC99454040570CB240AE304028D5CE35EE984540B0E00CB270B13040C02CD03526984540501910B290B1304008E8CF358E974540B076630736B13040C02CD03566974540007D0EB280B03040C02CD0356697454070060FB2B0AF3040E019CF354697454070060FB2B0B4304088427AE0DC9545404083B95C4BBA3040403CCF351295454090F30DB250C0304088F7CE35BA944540206A0DB220D93040F035248BC79445404083B95C4BDD304088427AE09C954540E08F0FB2E0E03040607479E054944540501910B290E530401804258BCF934540B0E00CB270F53040B0C5CF358293454040570CB2C0043140F035248B07924540206A0DB2201C3140D048258B6792454040570CB2C027314050A3CF35F6914540007D0EB280313140D893D0354A904540D8AE0DB278F13040D893D0354A904540B0E00CB270B13040D893D0354A9045400103000000010000002900000070060FB230803040D0B2CE3562A44540007D0EB2006C3040C02CD035A6A84540E08F0FB2E067304030B6D03596AA4540B0E00CB2F069304040D2258B17AA4540E08F0FB2606B3040A87A248B9FA94540206A0DB2206D3040A87A248B9FA9454040570CB2406D304028D5CE356EAA4540007D0EB2006E304008E8CF358EAA454070060FB2306F304050A3CF3576AA4540908964079670304030B6D03596AA45402096BA5C2B6F3040B0C5CF3582AB4540D0636207566F304040D2258B57AC4540501910B21071304040877AE0F4AC4540908964071674304040877AE034AD4540501910B29070304088F7CE357AAF4540206A0DB2206D3040204FD03572B24540B0E00CB270723040D893D0358AB245409089640716743040204FD03572B24540B0E00CB2707E30400017268BEFB04540805DB75C0BB03040B0C5CF3542B0454090F30DB250CF304040877AE034AD454090F30DB250CF304030B6D03556AC454090F30DB2D0CB304030B6D03556AC4540805DB75C8BCE304030B6D035D6AB4540F0506107F6D03040F8CB7AE08CAB4540007D0EB200D33040C02CD035A6AB4540501910B290D4304030B6D03556AC4540E08F0FB2E0D63040D0B2CE35A2AB454070060FB2B0DB3040A87A248B9FA94540F050610776DE3040D893D0354AA9454070060FB2B0E03040C02CD03566A945409089640796E2304050A3CF3536A94540B0E00CB270E4304050A3CF35F6A745402096BA5C2BE13040A87A248B9FA5454090F30DB250DF304088427AE0DCA445404083B95C4BDD304030B6D03556A54540206A0DB2A0D830407871D035FEA245402096BA5C2BD03040E019CF3546A24540D0636207D6A03040B0C5CF35C2A14540E08F0FB260903040403CCF3592A2454070060FB230803040D0B2CE3562A4454001030000000100000015000000B07663073664304030B6D03556AC454040570CB24060304028D5CE35AEAA454040570CB240593040680AD0355AAB4540206A0DB220483040F035248B47AF4540F0506107763B304050A3CF3536B14540007D0EB200353040403CCF3592B14540D0636207D6343040C02CD03526B2454070060FB230353040403CCF3552B24540D0636207D6353040607479E054B245409089640796363040204FD03572B24540501910B210343040204FD03532B3454090F30DB250333040680AD0351AB44540206A0DB2A0343040680AD035DAB44540D063620756383040C02CD03526B545406070B85CEB46304088F7CE35BAB44540D0636207564C3040F880CF35AAB34540007D0EB2804D3040403CCF3592B14540B0E00CB270543040D0B2CE35E2B1454040570CB24059304018B979E06CB04540E08F0FB2E05D304088F7CE353AAE4540B07663073664304030B6D03556AC45400103000000010000000E000000800673B9965B2F40204FD035F2D2454000BB6EB9167B2F40403CCF3552D04540C0E070B9D65C2F4050A3CF35F6D1454000BB6EB916582F4040D2258B97D1454080AE186481532F40C02CD035A6D2454040D41A64413D2F40A87A248B9FD54540A032206421352F4088427AE05CD6454000FA1C6401392F401804258B4FD74540A0322064213B2F40D048258BA7D6454040D41A64413D2F4040877AE074D6454060C11964E14C2F40C02CD03526D64540C01F1F64C1522F4028D5CE352ED5454060C11964E1562F400017268BEFD34540800673B9965B2F40204FD035F2D2454001030000000100000010000000C01F1F64C1852E4068557BE0FCEE4540E00C1E6461882E40204FD035B2ED4540402C75B956AF2E40F880CF35EAE7454060EDC60E6CB52E40204FD03572E6454000BB6EB916BC2E40D0B2CE3522E54540800673B996CA2E40985ECF35DEE24540A032206421C62E400017268B6FE34540E0A1C20EECA72E4028D5CE35EEE545402013C90E2C9E2E40985ECF351EE74540A0C7C40EAC932E40F880CF35AAE94540C01F1F64C1852E407871D0357EEB4540E0A1C20EEC7E2E4018B979E0ACEC454080AE186481772E4088427AE05CEE454020E71B64A1742E4028D5CE35EEEF454040D41A64417B2E4068557BE0BCF04540C01F1F64C1852E4068557BE0FCEE45400103000000010000002400000000FA1C64019B2E40204FD035B2FB4540A032206421982E40204FD03532FD4540402C75B9568A2E40D893D0350A004640C01F1F64C1852E40F880CF356A01464080AE1864818F2E40D0B2CE3562014640C01F1F64C1972E4060BF248BB70046402013C90E2CB32E4060BF248BF7FC4540C0E070B9D6C12E40A87A248B5FFC454020E71B64A1C42E4028D5CE352EFC4540E0A1C20EECC52E40204FD03572FB4540E00C1E6461C72E40F8CB7AE04CF9454080AE186481C82E40680AD0359AF84540800673B996CB2E4050A3CF35F6F7454080AE186481D22E40F880CF356AF7454060EDC60E6CD62E4028D5CE35EEF6454080AE186481DA2E40E019CF3506F64540E0A1C20EECDE2E40B0107BE0A4F44540A0C7C40EACE22E40607479E014F34540E00C1E6461E42E40680AD0359AF1454020E71B64A1E02E40B0C5CF3502F2454060C11964E1DC2E40E019CF3546F24540E0A1C20EECD82E4028D5CE356EF2454080AE186481D42E4068557BE07CF2454060EDC60E6CD02E4040877AE0B4F2454020E71B64A1CE2E4088F7CE353AF34540800673B996CD2E401804258BCFF3454020E71B64A1CB2E4068557BE03CF4454080AE186481BE2E4088F7CE35FAF4454020E71B64A1B72E4028D5CE35AEF545402013C90E2CB32E4028D5CE35EEF6454060EDC60E6CB52E4088F7CE353AF74540A0C7C40EACB62E4088F7CE35BAF7454060C11964E1AF2E40E019CF3546F8454020E71B64A1A62E40C02CD03566F9454080AE1864819E2E4028D5CE35AEFA454000FA1C64019B2E40204FD035B2FB45400103000000010000001900000040D41A6441822E40D893D0354A024640C0E070B9D67B2E4008E8CF358E034640E0A1C20EEC762E4008E8CF354E054640402C75B956722E400017268B6F064640E0A1C20EEC6C2E40D893D035CA054640C01F1F64C1692E40D893D035CA054640A032206421692E40985ECF355E064640A032206421682E4060BF248BB7064640A032206421672E40D893D0350A074640E0A1C20EEC652E40F035248B87074640C0E070B9D6602E40985ECF351E06464060C11964E1562E40B0C5CF35820746402013C90E2C4C2E40E019CF35C6094640E00C1E64613E2E4008E8CF35CE0B4640800673B9962C2E4030B6D035D60F46402013C90E2C232E40C02CD0352611464040D41A6441242E40985ECF359E11464060EDC60E6C242E4088F7CE35BA11464060C11964E1242E40D0FD79E0C4114640A0C7C40EAC262E40E019CF3506124640800673B996242E4068557BE0BC12464000FA1C6401202E4088F7CE35BA144640800673B996732E4088F7CE357A08464080AE1864817E2E4088F7CE35FA05464040D41A6441822E40D893D0354A0246400103000000010000000C00000080AE186481AF2D4088F7CE35FA12464060EDC60E6CB32D4068557BE07C134640E0A1C20EECB62D4050A3CF357613464040D41A6441BA2D4050A3CF35F612464000FA1C6401BD2D4068557BE0FC114640402C75B956B62D40D0FD79E08411464020E71B64A1AB2D40D0B2CE3522124640C01F1F64C1A32D4030B6D0359613464000BB6EB916A52D4040D2258B9715464020E71B64A1A72D4050A3CF3536144640C01F1F64C1AB2D40607479E05413464080AE186481AF2D4088F7CE35FA1246400103000000010000003900000000FA1C64014A2E4008E8CF358EF9454000FA1C64016F2E4030B6D035D6F44540C01F1F64C1702E40680AD0355AF3454000BB6EB916642E4030B6D03596F2454080AE1864814A2E40B0C5CF3542F6454040D41A6441442E40F880CF35EAF5454080AE186481462E4068557BE07CF54540C0E070B9D6472E40985ECF35DEF4454000FA1C64014A2E4068557BE03CF4454060C11964E1562E4008E8CF358EF2454060C11964E1592E40D893D0350AF24540E0A1C20EEC5D2E40F035248B87F14540C0E070B9D6612E40680AD0359AF14540C01F1F64C1642E4040877AE074F14540E0A1C20EEC652E4030B6D03556F04540800673B996632E40204FD035B2EF454080AE1864815E2E40D0FD79E044F04540A0C7C40EAC592E400017268B2FF1454040D41A6441582E40680AD0359AF1454060C11964E1452E4030B6D03596F34540A0322064213E2E401804258BCFF44540800673B996362E4040877AE074F6454040D41A6441222E40680AD0351AFC4540E0A1C20EEC152E40E019CF35C6FE4540800673B996012E40204FD035B2064640402C75B956F72D4068557BE07C08464000FA1C6401E92D4018B979E0AC09464080AE186481BD2D40E019CF3506124640402C75B956BC2D4050A3CF3576134640C0E070B9D6B92D4088427AE09C14464000FA1C6401B32D4088F7CE357A16464060C11964E1B52D4040D2258B97164640A032206421B82D40F035248B8716464000FA1C6401BA2D4088F7CE357A16464000FA1C6401C42D40A87A248B5F144640C0E070B9D6E02D40E019CF354610464060EDC60E6CE32D40B0107BE0640F4640C01F1F64C1E52D4088F7CE353A0E4640A0C7C40EACE82D4050A3CF35360D464060C11964E1EC2D40F035248BC70C464060C11964E1F32D40A87A248B5F0C4640E00C1E6461F82D40B0107BE0A40B4640C0E070B9D6FC2D407871D0353E0B4640C01F1F64C1032E40D048258BE70B464060EDC60E6C012E40B0107BE0A40A464040D41A6441002E40F880CF352A0A4640E00C1E6461072E4088F7CE35FA09464020E71B64A10B2E407871D0353E094640A0C7C40EAC0D2E4088F7CE35FA07464040D41A64410E2E4088F7CE353A064640402C75B956102E40D048258BA70446402013C90E2C152E40D0B2CE35620346402013C90E2C2D2E4018B979E0ACFE4540C01F1F64C1322E4040D2258B17FE454040D41A64413A2E4018B979E0ECFD454060C11964E13B2E40D893D0354AFD454000FA1C64014A2E4008E8CF358EF945400103000000010000005F00000000FA1C6401C12D40C02CD035A6474640402C75B956BB2D407871D0357E48464060C11964E1852D4008E8CF354E544640C01F1F64C17C2D40680AD0351A57464020E71B64A1772D40B0C5CF35025A4640E0A1C20EEC792D40888D258BFF59464080AE1864817B2D4008E8CF35CE594640402C75B9568F2D40204FD035B2554640C01F1F64C1B02D40985ECF35DE4C464080AE186481C42D40204FD035324A464020E71B64A1C22D4088F7CE35FA4D464060C11964E1CE2D40607479E0144E4640E00C1E6461E02D40F8CB7AE00C4C4640C01F1F64C1EE2D40C02CD0356649464060EDC60E6CFA2D4060BF248BF745464000FA1C6401022E40985ECF359E44464000BB6EB916152E40D0FD79E08443464080AE1864811A2E40403CCF3512424640800673B9961F2E4040D2258B17404640A0C7C40EAC262E4050A3CF35F63D464020E71B64A11C2E4068557BE03C3E464060EDC60E6C0E2E40D048258B67404640C01F1F64C1032E40607479E09440464000FA1C6401062E40D048258BE741464040D41A6441072E40C02CD03566424640A032206421F22D40A87A248B9F44464060C11964E1E72D401804258B4F454640402C75B956E22D40403CCF351246464060EDC60E6CDB2D40204FD035B246464080AE186481D22D40204FD035B246464080AE186481D22D40403CCF35D2454640800673B996EE2D4008E8CF350E434640800673B996182E40888D258B3F38464080AE186481312E40B0107BE024354640E00C1E6461292E4040D2258B5736464040D41A6441212E4008E8CF350E384640A0322064211A2E40E019CF35463A464040D41A6441152E40B0C5CF35023D464020E71B64A12A2E40985ECF351E3B464060EDC60E6C5A2E4030B6D03516324640E0A1C20EEC652E40E019CF35C63046402013C90E2C6D2E40B0C5CF35422F4640800673B9967B2E400017268BEF2C464040D41A6441822E40E019CF35862B4640E00C1E6461782E40D893D0350A2B464060EDC60E6C6C2E4008E8CF354E2C4640E0A1C20EEC5F2E4008E8CF350E2E464080AE186481542E40E019CF35062F464080AE186481542E40C02CD035262E464020E71B64A15D2E4008E8CF35CE2D464020E71B64A1662E4068557BE0BC2C464080AE1864816E2E40F035248B472B464040D41A6441742E40E019CF35C6294640C01F1F64C1692E40E019CF35C629464020E71B64A16A2E4008E8CF350E29464000FA1C64016C2E40985ECF359E28464000BB6EB9166E2E401804258B4F284640C01F1F64C1702E40F035248B07284640C0E070B9D6652E40607479E094284640A0322064214F2E40985ECF355E2A464000FA1C6401432E40403CCF35922A464000FA1C6401432E40E019CF35C629464040D41A6441582E40607479E01427464040D41A6441582E40D893D0354A26464060EDC60E6C4C2E4008E8CF35CE264640E0A1C20EEC412E400017268BEF274640E0A1C20EEC382E40F8CB7AE08C29464080AE186481312E40E019CF35862B464080AE186481312E40403CCF35522C464080AE186481382E40403CCF35522C464080AE186481382E40E019CF35462D4640E00C1E6461342E4060BF248BB72D4640C01F1F64C1312E40888D258B3F2E4640A0C7C40EAC2D2E40C02CD035E62F464000BB6EB916332E40403CCF35D22F4640C01F1F64C1362E40E019CF350630464060EDC60E6C392E4030B6D0359630464000FA1C64013C2E401804258B8F314640E0A1C20EEC2E2E40985ECF351E33464080AE1864812A2E40B0107BE064334640A0C7C40EAC252E40B0C5CF3542334640800673B9961D2E4040877AE07432464000BB6EB916192E40D0FD79E08432464020E71B64A1132E40985ECF359E33464020E71B64A10A2E40D893D0350A374640E0A1C20EECEA2D4068557BE0BC3B464040D41A6441E62D40F880CF35EA3B464080AE186481CB2D4030B6D035D63E464080AE186481CB2D40A87A248B9F3F464060EDC60E6CCE2D4008E8CF35CE3F4640E00C1E6461D32D40680AD0355A404640402C75B956D62D40607479E09440464000FA1C6401D02D4018B979E0AC424640402C75B956C12D40403CCF359246464000FA1C6401C12D40C02CD035A647464001030000000100000025000000A032206421E42C40204FD0357248464020E71B64A10D2D4088F7CE357A404640800673B996112D4050A3CF35F63D4640A032206421062D4088F7CE35BA3D4640402C75B956F82C40607479E014404640E00C1E6461E72C40607479E014444640402C75B956E02C40204FD035F2444640402C75B956E02C40403CCF35D2454640A032206421EB2C40D0FD79E04445464020E71B64A1EE2C40204FD035F244464040D41A6441E02C40E019CF3546484640E00C1E6461D82C4040877AE07449464060C11964E1CE2C40204FD035324A464040D41A6441CB2C40680AD035DA484640E0A1C20EECC52C40C02CD03566484640C0E070B9D6BF2C40C02CD035A648464060C11964E1B92C40C02CD0356649464060C11964E1B92C40204FD035324A4640E00C1E6461BD2C40204FD035324A4640E00C1E6461BD2C4060BF248B374A4640C01F1F64C1BC2C40888D258B7F4A4640C01F1F64C1BA2C40F880CF352A4B464060C11964E1C32C4060BF248B374D464080AE186481C02C40888D258BBF4F464060C11964E1B22C40D0B2CE35E253464060EDC60E6CB02C40403CCF3592554640800673B996AC2C4008E8CF358E594640E0A1C20EECAB2C40888D258BBF5B4640A032206421B62C40B0C5CF35825A4640A032206421C32C4018B979E06C59464080AE186481CB2C40B0C5CF35C257464060C11964E1C72C40B0C5CF35C2544640402C75B956CA2C40D0B2CE35A2534640800673B996CC2C40680AD0359A51464060C11964E1CE2C40A87A248B1F4E4640E0A1C20EECD12C4018B979E0EC4C4640A032206421E42C40204FD035724846400103000000010000002D000000A0322064217B2D40985ECF359E634640A0322064217B2D40A87A248B9F624640E00C1E64617C2D40D0FD79E00462464000BB6EB916822D407871D035FE604640402C75B9567B2D4040D2258BD76146402013C90E2C732D40B0C5CF354262464080AE1864816B2D40D893D0354A624640A032206421662D40204FD035F2614640C0E070B9D6502D407871D0353E66464040D41A6441572D4030B6D0355666464020E71B64A15D2D407871D0353E664640C01F1F64C1632D40204FD035F2654640402C75B956692D40204FD03572654640402C75B956692D407871D0353E664640800673B996672D4018B979E0AC664640A032206421672D4040877AE0F466464000BB6EB916672D4030B6D03556674640A032206421662D40403CCF351268464080AE1864816E2D4008E8CF35CE66464020E71B64A1702D407871D0353E664640A032206421742D407871D0353E66464020E71B64A16D2D40B0C5CF35C2684640E00C1E6461692D4030B6D035D6694640402C75B956622D400017268BAF6A46402013C90E2C612D40680AD0351A6B4640A0322064215F2D400017268B6F6C464060C11964E1692D40D048258B276C4640A032206421742D4008E8CF358E6B4640C01F1F64C1722D4030B6D035166C464020E71B64A1702D40B0107BE0646D4640E00C1E64617A2D4068557BE03C6D4640A0C7C40EAC812D40D0FD79E0446C4640A032206421832D4060BF248BF76A4640A0322064217B2D401804258BCF694640A0322064217B2D40E019CF350669464000FA1C64017F2D40F8CB7AE08C684640A032206421932D40E019CF350665464080AE1864819F2D4068557BE0BC63464000BB6EB916A52D407871D035BE624640C0E070B9D6B32D40A87A248B5F5E4640C0E070B9D6B72D40403CCF35D25B464000FA1C6401B32D40B0C5CF35025A4640800673B996A42D407871D0353E5C4640C01F1F64C18B2D40403CCF3552614640A0322064217B2D40985ECF359E63464001030000000100000059000000A032206421072D40B0C5CF35C2544640A032206421042D40A87A248B9F534640E00C1E6461042D40680AD0359A524640A0C7C40EAC072D40204FD0353252464000BB6EB9160E2D4028D5CE35EE524640800673B996112D400017268B2F51464000FA1C64010E2D40E019CF354650464060EDC60E6C0A2D4008E8CF358E4F4640C0E070B9D6062D4050A3CF35364F46402013C90E2C012D40B0107BE0644F46402013C90E2CF22C40B0107BE0644F464000BB6EB916DB2C40D893D0350A54464000FA1C6401CA2C40403CCF35925A4640E00C1E6461CB2C4040877AE03460464000BB6EB916C92C40403CCF355260464020E71B64A1C72C40D0FD79E084604640402C75B956C62C407871D035BE604640E00C1E6461C42C407871D035FE604640C01F1F64C1BF2C407871D0357E60464080AE186481BC2C40F880CF356A61464080AE186481BA2C40A87A248B1F63464060C11964E1B92C4068557BE03C674640A032206421B92C40204FD03572684640800673B996B62C4028D5CE35EE6846402013C90E2CB12C40E019CF350669464060C11964E1A82C407871D035BE6A4640C0E070B9D6A02C40D048258B6772464020E71B64A1962C4008E8CF354E744640A032206421972C40A87A248BDF744640A0C7C40EAC972C4068557BE03C754640E00C1E64619A2C4008E8CF350E764640C01F1F64C1952C4088427AE05C784640E00C1E64619A2C40B0C5CF35C278464080AE186481A62C40A87A248BDF774640A032206421AA2C40403CCF3552774640402C75B956B32C40680AD035DA744640A032206421B82C4008E8CF354E74464000BB6EB916C12C407871D0353E744640402C75B956C62C40680AD0355A744640402C75B956C92C4040D2258B17754640E00C1E6461CB2C4018B979E0EC76464080AE186481C92C40C02CD035A67746402013C90E2CC72C40B0107BE0A4794640C0E070B9D6C72C4028D5CE35EE7A464060C11964E1CE2C40D893D0358A79464020E71B64A1D22C40F880CF356A7A464060EDC60E6CC82C4030B6D035167C464060C11964E1C02C40403CCF35527E464080AE186481B12C4030B6D0351686464080AE186481AE2C40985ECF351E8746402013C90E2C932C400017268BEF8C4640800673B9968E2C40B0C5CF35828E46402013C90E2C8E2C40D0B2CE3562904640C01F1F64C1932C40F8CB7AE00C934640E00C1E6461992C40F035248BC7944640800673B996A02C40403CCF3512964640E0A1C20EECA92C40B0C5CF3582964640E00C1E6461B62C40F880CF35AA95464060EDC60E6CBA2C4018B979E02C944640800673B996B82C4008E8CF350E8F464060C11964E1B92C4088F7CE35FA8B464040D41A6441BD2C40F880CF35AA89464080AE186481C62C40888D258BFF84464000BB6EB916D32C4060BF248B77804640C0E070B9D6D52C407871D035BE7F4640800673B996DC2C40B0C5CF35C27E4640E00C1E6461E82C4050A3CF35767E464020E71B64A1EE2C40F880CF35EA7D464060EDC60E6CF22C4040D2258B577C46402013C90E2CF12C40680AD0351A7A4640C0E070B9D6EC2C4008E8CF35CE774640E00C1E6461E72C4008E8CF350E764640A032206421EB2C4018B979E02C754640E00C1E6461E62C40204FD035F272464020E71B64A1E52C40985ECF351E714640E00C1E6461E72C40F880CF35EA6C464000FA1C6401EE2C40B0107BE02469464060C11964E1EE2C40D0FD79E004674640E00C1E6461E72C407871D0353E66464000FA1C6401EA2C40607479E0D464464040D41A6441F02C4040877AE0F4624640A032206421F22C40204FD035F261464020E71B64A1F22C40607479E09460464060EDC60E6CEF2C407871D035FE5E464020E71B64A1EE2C40F880CF356A5D4640C01F1F64C1F12C40985ECF359E5A464020E71B64A1F82C4030B6D03596584640A0C7C40EAC002D4008E8CF35CE564640A032206421072D40B0C5CF35C25446400103000000010000004A000000E00C1E6461722D40888D258BBF784640A0C7C40EAC692D4028D5CE352E794640C0E070B9D6502D40F880CF352A7C464000FA1C6401422D4040877AE0347D4640C01F1F64C13F2D40B0107BE0247E4640E00C1E64613F2D4028D5CE352E804640A0C7C40EAC402D40D0FD79E0C484464060C11964E13D2D4030B6D035D685464060C11964E1342D4068557BE0FC84464060C11964E1342D40985ECF351E84464060C11964E13B2D40D048258B2783464060EDC60E6C2E2D4088F7CE35FA824640E00C1E6461052D40985ECF351E84464080AE186481F82C40985ECF35DE844640800673B996EB2C4040877AE0B486464020E71B64A1E02C40E019CF350689464020E71B64A1D92C4028D5CE352E8B4640C01F1F64C1DD2C40D0FD79E0448C464000BB6EB916E22C40E019CF35C68C464000FA1C6401E72C4028D5CE35EE8C464060C11964E1EC2C4028D5CE35EE8C4640C0E070B9D6EE2C4088F7CE357A8D464080AE186481EE2C4028D5CE35AE8E4640A032206421EF2C40D0B2CE35E28F464060C11964E1F32C4028D5CE356E9046402013C90E2C072D40D0FD79E08490464060EDC60E6C0F2D40B0C5CF354291464060C11964E10A2D40F8CB7AE00C93464080AE186481112D40B0C5CF35C294464040D41A6441142D401804258BCF954640E00C1E6461152D40C02CD035E69646402013C90E2C142D40F880CF35EA974640E00C1E6461122D40680AD035DA98464060C11964E1122D40403CCF35D2994640800673B996182D40F880CF35EA9A464080AE186481192D4040D2258B579C464000FA1C6401192D4050A3CF35769D464060EDC60E6C162D4018B979E02C9E4640800673B996112D40D048258B679E464000FA1C6401192D4030B6D035D69F464060EDC60E6C202D4060BF248BF79F464000BB6EB916312D40D048258B679E4640C01F1F64C12B2D40204FD035729E464000BB6EB9162A2D40D048258B679E464000FA1C64012B2D4050A3CF35769D464020E71B64A12C2D4060BF248BB79C4640C01F1F64C12E2D40B0107BE0249C464000BB6EB916312D4050A3CF35B69B4640A0C7C40EAC382D40680AD0355A994640E00C1E64613F2D4050A3CF357696464060C11964E1312D4008E8CF354E95464020E71B64A1392D40D0B2CE3562944640800673B996492D40403CCF351294464020E71B64A1542D4050A3CF35B694464080AE186481572D4088427AE05C924640E00C1E6461512D40985ECF35DE8D464020E71B64A1542D4028D5CE352E8B46402013C90E2C5B2D40F880CF35EA894640402C75B956642D40C02CD03566894640A0322064217B2D40680AD0355A89464060C11964E17A2D4018B979E0AC85464080AE186481992D40F880CF35AA80464040D41A6441A12D40985ECF351E7D464000BB6EB9169E2D40985ECF351E7D46402013C90E2C982D40607479E0547C4640C01F1F64C1912D40403CCF35127C4640E00C1E64618B2D40607479E0547C4640800673B996852D40985ECF351E7D4640800673B9967E2D40F880CF352A7C4640800673B9967E2D40985ECF355E7B464080AE186481852D40B0C5CF35427A464080AE186481832D400017268B6F794640E0A1C20EEC7B2D4018B979E0EC784640E00C1E6461722D40888D258BBF78464001030000000100000015000000B0E00CB2704F30407871D035FEC04540805DB75C8B553040204FD03532C04540501910B2106030407871D035BEBE45406070B85CEB5E30407871D035FEBD4540D063620756553040985ECF359EBD454070060FB23048304040877AE074BE4540B0E00CB270443040403CCF3552BE454040570CB240423040B0107BE0E4BD454090896407163F3040A87A248B9FBD4540805DB75C0B3B3040680AD035DABD4540E08F0FB2E0373040888D258B3FBE4540206A0DB22036304088F7CE35BABE4540B07663073636304028D5CE356EBF4540805DB75C0B38304050A3CF35B6BF45402096BA5C2B393040F880CF35AABF4540E08F0FB2E03A3040B0C5CF3582BF4540D0636207563E3040680AD0355ABF4540501910B2904030407871D035FEBF4540007D0EB2803F30401804258B0FC1454070060FB2B0433040C02CD035A6C14540B0E00CB2704F30407871D035FEC045400103000000010000000E00000000FA1C6401852D40D0B2CE356225464000FA1C64018C2D40888D258B7F244640402C75B956952D40D0B2CE35E2224640402C75B956942D40F880CF352A22464000FA1C64018D2D40A87A248B1F2246402013C90E2C882D4018B979E0EC21464000BB6EB916852D40B0C5CF354221464040D41A64417F2D407871D0357E214640E0A1C20EEC792D40607479E0D4224640A0C7C40EAC7A2D4088427AE0DC234640C0E070B9D67F2D40985ECF35DE23464020E71B64A1812D40D893D0354A24464020E71B64A1812D40888D258B3F25464000FA1C6401852D40D0B2CE35622546400103000000010000000800000000BB6EB9163A2D40A87A248B9F2B464080AE186481492D40204FD035B229464020E71B64A1522D40B0C5CF3582274640800673B996502D40403CCF351227464000BB6EB916482D40F880CF35EA27464020E71B64A1322D40C02CD035662B4640A0C7C40EAC302D4028D5CE356E2C464000BB6EB9163A2D40A87A248B9F2B46400103000000010000001300000080AE186481982D4040D2258B5730464080AE1864819C2D40607479E0142F4640402C75B9569F2D4008E8CF35CE2C4640A0322064219A2D40F880CF35EA2B464000FA1C6401922D40C02CD035662C4640E00C1E64618C2D40985ECF351E2D46402013C90E2C832D4040D2258B972E464000FA1C6401842D40C02CD035A62F4640402C75B956892D4028D5CE352E31464060C11964E1862D40F880CF352A354640C01F1F64C1882D40607479E05435464000BB6EB9168B2D4068557BE07C354640C0E070B9D6902D40A87A248B9F354640E0A1C20EEC962D4008E8CF350E35464040D41A64419B2D40D048258BE73346402013C90E2C9C2D40D048258BA732464060EDC60E6C992D40985ECF359E314640E0A1C20EEC972D4008E8CF35CE30464080AE186481982D4040D2258B573046400103000000010000001600000000FA1C64017D2C40F880CF35AA54464000BB6EB916882C4088F7CE35FA564640800673B9968F2C4088F7CE35BA564640E00C1E64618F2C4088F7CE357A554640E00C1E64618C2C40204FD035B2544640800673B996852C40F880CF356A544640C0E070B9D6842C40888D258B3F544640C01F1F64C1832C401804258B0F54464080AE186481832C4040D2258B97534640402C75B956842C4018B979E0EC52464040D41A6441832C4088F7CE353A52464060EDC60E6C832C40D893D0358A51464020E71B64A1842C40F880CF352A51464000BB6EB916882C40F8CB7AE00C504640C0E070B9D68A2C40D048258B674E46402013C90E2C872C40F035248B474E4640A032206421812C4040877AE0F44E4640C01F1F64C17A2C4040D2258B574F464000BB6EB916772C40D0B2CE3522504640800673B996772C40B0C5CF354251464020E71B64A1782C4040877AE03452464000FA1C64017D2C40F880CF35AA544640 BG 0106000020E6100000010000000103000000010000006F030000486E45FD56553A408EFCD2C644DB444008572F4F7F4B3A4062146EE0EBDA444070589B5220463A40627C96BC81DB4440F0FDF0A7D1423A40D2548A488DDC44403000C8E8043C3A4012A2593277DF444088D9EA0AE7393A40720B9A59F5DF44405884A08C39363A400A3CC0980FE04440E805499A9D303A40D2C289480ADE4440F0236CBC97223A40860334F3DFDD444098A34F71BD1B3A408EDB299C2BDD4440F8F84160DC143A400AF3C6D212DB4440F0902EB2F4123A40BA3515EEC4DA44409019EDA735113A409242DD9DBBDA444068FAAD637D0F3A40568A12D188DA444000836C59BE0D3A40326FD029CBD9444038CFDB96500C3A409AD0AAEA33D84440309305F3270C3A406AC588C859D64440C81250D4DE0C3A404E365BCF89D4444068BC1267160E3A402EBC1F450AD3444028079EB53A0F3A409289F64667D244404821045621113A405E3E15EE57D1444030B5540EC8143A4056F67BD4B2D044402012B63A61183A402AB5BE186FD04440F071C011511B3A40262D077AE8CF4440C0908641831D3A40E6B703DDEDCE444020C70A90FE1E3A4086E69AD9D9CD4440A06DAB6359213A402608E55795CA4440D86A7BCD88213A407E73DE1D9EC94440F8AEE13323213A40C2CE68A699C74440F0A16A5996213A40B6F34484BDC64440B855FB1B04233A40623837104CC64440D02FF0A709273A405E3C7FF104C6444090E3806A77283A40321F99D96AC54440F8D0304FC3293A4042D3CC0CBBC34440780AD75C1B2A3A408E26C7D244C24440804E3DC3B5293A40F2B73BADD3C04440B05B2DB2C8283A406EA95DCF34BF444060792F4FCB253A403A94077A0CBE44406886A62958253A40E2DEE55730BD4440D0E3F97EFD273A408294AAEAEEBA4440305EE43313293A402EDB40E759BA444008ED4DD49E2C3A40760C087A16B94440E06481076A2D3A40A6D24E5B50B844408026F4E1F62C3A40DADA573248B74440003944FDAA2B3A40BEA025FFFBB6444068D3E7D00D2A3A40AA84DE1DC4B6444000184060B4283A40D64A25FFFDB54440F8FB22DBD8253A404EB05CCFCDB2444088651BA1E7213A40BA3523628EAF444088441704F11E3A403629E9F4CBAD444038A0B49D5A1D3A40AAAA3D4A77AD4440F8AD96B5791B3A408E4A9002A5AD4440A860A42998053A40625FFDA2BBAB44402806FA7E49023A402A049D761CAB4440A092DBF975FB3940E2B85CCF60A9444040CBAF9DAAF539408E256EE051A8444080708CDED5F23940C2AA910230A84440409B055665EF39407AE54F5B70A84440A0FF9DEFD2EB39405EA04ABE73A8444070CA0C9072E539404AB20EB433A7444008EBB7D7CBE139401633D7E3EAA64440B869B73AD9DC3940F26C9002B1A74440B037489A55D539402608E557D5AA444030B409909ECF39404E411251A7AB444098CCAB0000CD39404640731A45AB4440C0B74CD472C3394076A1E9F4D5A8444020A044FD8EBA3940FA40299C95A84440D0C22DB2ACB739400AC3091735A8444058A459AB86B439401A2DEB9155A74440B0342F4FB3B2394082949C76A5A644404054F644F4B03940BE7B115132A64440802BDBF911AE3940B23F3BAD09A6444038414DD4A2AB3940DED776B74BA64440F003B39D4AA63940C20B44847CA7444048213C26C7A339404E8AEF2ED1A74440E89F03B92E8D394016A1D6E367A84440D8BD7E6AB7893940C2E5C86FF6A7444008127E6ABB8739409699838EC0A64440F0292EB21086394076C6EF2E56A54440003D70F6D083394092907EF148A44440605AF97E4D813940E295DE1DEAA3444090AEF87E517F3940EAB3C96FFEA344409041FEB84E7D3940CA338D653BA444404829B000A67A3940B60C731A53A4444040AB4A37167439400A326209E5A34440D849931825493940AACDAD87A49E4440780FA28C234339401E345A327A9E444058CD9952363D39402A4EFDA2D59E4440C08A9FEF3C38394012A25932F79F4440F0059D52562D39406EC2A74D9DA5444068FA917B6A2839403674949F2EA7444030D9D08B622739401EDEA04E5DA7444048A23726D51D39408ABD30D60DA9444048B73EC3D31C39409AF5BE987DA94440800B3EC3D71A3940466D79B7C2AA4440D0540456131A394012C6226217AB444068675471C71839408623A01319AB4440E08F8F7B92143940466D79B7C2AA4440901EB8D7BDEA38404E40B95E74B14440F0BFA0EFE8E23840428AE1BA47B34440289F156778DF38401EECB95E70B3444050FBA429EEDC38402E0EED913DB34440687206F395D738403EB4A4B086B24440B8A10D2DA3CD38401E73E7F442B24440C029DAF9D7CC384016E9A0138EB04440A09C261588CD3840F25F5F6C53AE4440D07D60E555CB38404EF7BF9877AC444020367F6A41C638408A982A9C8DAC4440E878213EACC03840463F44846EAE444050D4DE96FEB73840A6D103DDA6B24440F084E66DFEB23840CAA04F5B58B4444038F99E8C4FAE38404AF2F4682FB544401034B19D48A938400E14AF8776B54440B8FEA62962A338400E14AF8776B54440402D14CAF1A438406AC3E457BDB64440C0080E2D079C3840E240F1CBAFB6444000B1931889983840FE611FC502B74440B01A8CDE979438403A93BCFB62B84440381DBC7468943840CA0FC435DBB84440285919040B9538402ABD559545BA4440B8566282C094384006F9B224E8BA444068E15EE5C5933840A2D3C33556BB444028F7D0BF56913840F68C2D39EBBB44407004C1AE69903840DA0A4DBE4BBC4440C01E2878098F38403A008D6509BD444010707ECDB68D38407A9B0B178ABD4440F8B68DDEA78C38407235727D22BE444098C14D37EA8B38409245E1BA2FBF44407088203E188B3840EA1F4F5BBBC24440B0A85271EB873840CE796DE015C64440B02CB2009A823840AA851B28E4C7444080751F3E387B38404AB4B224D0C644408074D4BF8E75384046DA2D3956C644409066126758703840EAD5FCA28BC344408896EB0A896C3840C65D505B3AC34440A8D65D4823673840021477B790C344405840560E326338401E5B20C569C3444088DF00B9985A384072C6198B72C24440D07C4D375258384012A22FD65AC2444078FC30EC77513840565FE1BAA8C24440401C71F63E4F3840FA41828E88C244402834213E944D3840AEA45F6C2BC24440F846EA6DCE4B3840A688188BF3C1444030DF4FD46C493840F645CA6F41C24440B8A0C2AEF9483840A61C939FF6C24440105A2CB28E4938402A717B540CC4444020DA68BC514938407AE4DA802AC5444038D6749351443840FAF8A4B05EC6444090429DEF3040384056825F6C1FC8444068751F3EB83B3840B2C3953CE9C74440B0AD01B9E0363840A2AFDE1D23C74440304CAC549C323840967FD0611BC64440F858A9633F3238405E3C7FF104C64440800B76937D2E384026093E4AC8C44440805285A46E2D384026539002F8C344407062A529522C384016E6953CF5C14440F86C6582942B3840160C11517BC1444070B94DD4AC29384012EC818E8AC1444060C96D5990283840A686741A17C24440081894186D273840167CB4C1D4C24440386C93187125384002161B286DC34440B8804160D21D3840B2EA3FE745C44440E06E8BDE9B13384072EC6A439CC44440A0491A041F0C3840329B3E4A4BC3444070857793C10C38409245E1BA2FBF444058F8C3AE710D3840A2673E4A59BC4440787DE7D04F0D38400E13569543BB4440F04E7A30C00B3840727E4F5B4CBA444068E758ABE4083840EA21D7E3C4B94440A08267BCD905384026C19D76FEB9444020CD203E30003840826E2FD668BB4440D08E0C2D43FF3740665C1C287FBA4440C099459A0BFE3740229F6A4331BA4440B0F2B29DA4FC37402ADF88C812BA4440E83E22DB36FB3740AE6768A6B5B9444080306E59F4F63740369704DD1BB8444040D63CC32BF3374056159D7602B8444028A060E521E737403276468454BB444078B0729311E53740C22F299C6FBB4440608371F622DE374056A9178B05B94440B8EF995202DA374042D3DA8044B8444068E9EEA7B7D43740DAB4DC1DC2B744404068671F4BCF37402EB898D986B74440B018F6E1C4CA37405A5903DD9CB744405887D5BFEEC637405E633710EBB6444018C0A96323C13740B6E75E6C49B34440206BD8F9FBBC3740E270CA6FE0B244403053570E92B43740AA850DB49AB34440F0ABE4D013AC37405EC413EE93B34440A02AE43321A73740FAAE4484E5B24440F8725F48B3A03740922FBEFB72B044400050B9F4F59F37402AE6719C44B0444070524DD4C89C37405AF834737DAF4440E8B2F14436943740B615949F9DAF444000B5F7E154833740928F41E7E8B24440E04B61E51D6A3740A28BF90530B34440D8C2499A3F653740AAF1929F97B24440B04F1D3E785D3740028D3BAD74B0444010C9BC74E4583740E6932C3984AF4440004835EC7753374086B6EB9145AF4440C8DDE3D0CB5037402E4BF2CB3CB0444050005471634E37404AFEF668B6B1444008F21804A74937408E6FB224F8B2444018E1AD001A453740A2AFD0A9D9B244400068B63A1F3F3740FA1ACA6FE2B1444070E4773068393740F26CACEA83B0444008CE33ECB3353740E6B703DD2DAF444050121367D43437409642004033AE4440505679CD6E34374032DAD980DDAB4440F031DA5C1533374082491B289FAA4440A08322DBCE303740AEF80140BCA94440C09EF4E1002E374092DB1B2822A94440B098C24B3C283740424241E77DA84440709B9A527E1D3740B2C539AD05A84440B005BD11BFEA36405E89C098FAAA4440404595B5DBF036400612EF2EC7AC44407883A9C6C8F13640CE7989C828AF4440904AF5E17CF036401679D346D8B14440B8137F6AF5EF36407A9D939F93B44440E04CAC63C7F036402E9247215DB544407822AC00BEF33640DE206209BFB64440485E099060F43640FE3D482159B7444030E65C480FF436401E598AC816B8444058FBDCF993F2364006A4B75E64B94440F0C136EC3BF23640FE241A8B03BA4440E03CE9B98FF1364012139241F8C24440B08809F369F13640CEE7963CEFC44440A0F3C54BA8F236409E91F3CB0EC7444040B28F7BDEF236406EE4B024CEC74440F0F8257849F23640EE1C52F8ABC84440285A9C52DAEF3640B2D0E2BA19CA444000216F5908EF36407AE525FFD3CA4440B8A3DBF99BEE3640FE432DB989CC4440A8239FEFD8EE3640421D115161CE44400050CE22A4EF3640C25B894826D04440404595B5DBF036409E8B0E349ED14440E0BC4FD420F2364036985DCF0ED24440784B406026F6364012F6ED917ED24440F03DD75C8DF7364026A0D863D2D24440E8B50A9058F83640EE64F2CB75D3444080E95E4803FA36409ECD919F51D54440101CEE6F45FC3640B29E856AAED644405029B000A6FF36408E4C26FFB7D84440C0C2D9F973023740424E27FFB1DB444068C50990440237404AA61A28B6DE444048C19C52BEFD364042FDA43068E1444080D457AB04FB3640FE85E8F4E2E14440A0177393F5F43640F645CA6F01E24440A832459A27F23640C29CCFA979E24440408EE23391F036409A449DF66AE34440303F631F17EB3640961F828E3CE8444080E926785DE83640BA9CD6639EEC44404838C64BC0E63640FACBCF2922EE44407004F97E8FE53640DE474B3EA6EE4440A86DFF1B92E236403E3F2F5640EF4440D828068AD0E1364052DC083091EF444060F314677CE136407A2C7B54B4EF444030F6BD11D3E0364092954267ACF04440C0AFA029EEE03640466E9AD98FF24440603F0B9088E0364076C5B22476F34440E8DCD022C8DD36409E44C09862F64440E074BD74E0DB3640E201482154F944406836304F6DDB3640FEB0EFAE66FC4440E0BBCC85D1DA36402EE28CE5C6FD4440C84ED2BFCED836404ECE247F2AFF44406856E96DBAD836401A32A1932FFF4440E0971F3E84D836403E01ED11E100454010B6D6A07AD836407EE588BCF20045400861A9C6FCD736405AF370FDD9014540906E12CA95D636409A8C4B3E7E0245405891FB7EB3D33640369552F835034540401C71F63ED23640A26288483F0345404047638254CE3640EE698CE5BC024540C89832EC87CC3640D62B12D1B702454030773C2685CA3640E2933AAD4D03454090BB1BA1A5C93640AAA6D9802B044540F040B8D709C93640EA1D818E02054540908FDE96E6C7364092BFFEA286054540F8A7800748C536401679E1BAA1054540906C44FD9CB93640926FD5C66F05454030C3FA7EEBB73640125952F8B0054540D811213EC8B636405664894839064540D05D1767D4B53640D630BA5EC8064540C09F3F60AAB43640FA88D7E328074540D8F2F3E104AD36409ED1FC22C207454050FD60A68EA036403271F6D1200A45408816F044269E364066A36309960A454020515171F38736402AC668A686104540B813B73A9B823640BA5EA29388124540E09C48B9C681364086F3890C1013454038BCB53AA37E3640B2D0D44610154540F01D1E3E407B36405EFADF9DBE184540408EE233917136400689E557721B4540F063527153583640F6FA1EC51E28454060AE6382385D3640928F25FF1529454028B3A229E26736402650939F28294540A87DF0448A6C3640FA6D05DDB6294540089434893E70364086B12ED6862B4540D0F01CA15174364066F233F339304540A03B20DB4278364092DDC65223324540A8102E4F2D7C3640568251F8D532454098B1F5E1607F3640FAEE4DDB1833454058DA102D43823640AAAC12D1D4334540807B51D4FC8436406AC140E7E035454060D3CBE87A883640167FC652923A45409843E86D5A8936408E91D7E33B3D4540D04BF14452883640B6AEA8CD273F4540D04BF14452883640BEEC22E22C3F45404861EA0A5D8636409AF8D029FB404540488933EC1B833640B6ABD5C674424540F01D1E3E407B3640FACBB3418F444540A0F27ACDFE6D3640D24CD7E32349454090029B52E26C36402A4AA74D534945400034B19DC86D3640EE1F5615E04B4540F0BB153BFA7036405EC0639602514540A8C2A129CE713640C2CA12515752454080F66EF6FE72364056F522E27F554540D886449A2B7136406E2D9C7641574540E009917BD67736405E3C6309F25B4540F01D1E3E407B3640A6CDAD87245D45404020D5BF8A7B3640B2CE4CBE865D4540A018BE119F7B3640EA64E457EC5D45404020D5BF8A7B364076C30040505E4540F01D1E3E407B3640CA9E9D76B25E4540E04DF7E17077364056633ECACF5F4540A078705900743640E2DD8C65BD61454058FA917BEA6D3640169D8E022F674540687D7730046D3640CA9C15EEA8674540503911CA696D36403ED4178B24684540B80470B8316E364096A13EF7956845406062A529D26F36405EF97837826945408042651F0B72364056CFB541436A4540F03DD75C8D783640FACF2C39896B4540F01D1E3E407B3640AAA451F8616C4540C03619043F7F36407E4C0A17A56E4540C05AC64B8C813640BE350E34606F4540E0733A2611853640B6A5C6D2676F4540B884A5299E893640CAE788C8256F45406026D8F9638B36404646A5B0896F4540E822560A778B364022769913946F4540606473F6CA8C364076281E454D704540B070E90A499036400E9F392D30714540006CE2334597364052CCA3B085714540D09270F68EAA36406E3252789B6F4540F8059D5256B23640AAAB9DF64E704540A02DC5AE1DBA364052CCA3B085714540A085B8D721BD3640D2DEC96FDD724540A021390655BD3640627993C30D73454048F0FB1BDABE36401A9C66267D744540F03F6D5960C33640229521E2B47A4540C02FD4BFF6C43640FAF440E7527C454038DE0456C3C63640D29A6AC3677D45402075FEB8C0C936402E72BF18117E4540C829DAF9D7D03640324AA093AE7E454098457E6A2DD43640EAD2E357297F4540E006783034D43640EAD2E357297F454010C871F63AD43640125663092B7F454010C871F63AD43640EAB9DF1D307F4540F01A05F39DD73640568114EEF5804540806331EC5BE236400E5952F8B084454080D3D45CB5E336403E00941FAE8545407004F97E8FE536405649B74107884540307CB39DD4E63640C27ACD8CED8845405025840700E93640725196BCA2894540A07725DB56ED36408E9E16EE628A4540789104F36DEF3640B2A2C2B5F38A4540689FC64BA4F4364042B816EEDB8D454068EC07F359F93640AEA154951292454060CC4ED40CFC3640CE39872B5A964540300012769FFB3640B2079808FA974540A8DE25DB3AFB36406EC2A09378994540A0846D59F8F636403ADE6E602A9A4540000B1D3E60EA36404E40ABEA2A9B454010FAB13AD3E536400EC15EEC339C4540F0A94ED440E236406689CE0C849D4540E0B823DB7ADB36403A2167A6E3A04540288162E549D53640D277DE9D27A345405891FB7EB3D33640D6DBBE9804A445406019C84BE8D23640D26F39AD07A54540005690181DD236403AD85F6C5DA745403063800730D13640AAC9573262A84540904CC3AEF5CD364042DD1C281CAA4540E09A3889A6BB36404E20156ED5B04540C8052F8D6EB93640AA771A6B6AB1454080F7B97428B83640DA06E9F4BFB14540906BF97EF3B33640729F371070B24540384A7CCD76B13640C67E1C288BB2454030C1648298AC36401A80347373B24540E8D6D65C29AA3640A6FEDF1DC8B24540D0F3EF60AFA83640E689BBA25DB345409071F34412A836404E958AC89BB34540000AD2BF36A53640BA56D346CCB54540502CC94B48A3364016654D3E93B6454000792AB2E6A03640EE6DCD0CD1B64540603EC0115F9B364022BA271CB5B64540309D7F6AA59836401E7FF0AEEEB64540E8B2F144369636405E1140679BB7454080A194189D923640EA94BDFB5CB9454040B45D48D7903640FE1AD12907BA45409858EF0A598836409EF914EE7FBB4540F0F37630D48436401E0EDF1DB4BC4540B009E90A65823640CADEA6CD25BF454070C4BE119B7D3640C68287AB3BC54540C8B2E8D07B7A36405A80A6CD94C74540A0A81AA1457A3640720D458436C84540C0E9D75C897A3640B6A61FC5DAC84540F01D1E3E407B3640160EBC7B7CC9454088E1CE85917B3640A28C6726D1C94540189BB19DAC7B36406EE510D125CA454088E1CE85917B36409ED4004076CA4540F01D1E3E407B36409A4071FDC4CA45407823F77E677A3640324DC09835CB454040E239C3237A364032274584AFCB45407823F77E677A36409E20CD0C26CC4540F01D1E3E407B3640BA3F57959CCC4540B8610B90547B3640769C4821AACC454030E4FE1B627B3640E2D93610BBCC4540B8610B90547B3640123D27FFCBCC4540F01D1E3E407B36408AB88F02E2CC4540503317044B793640EA4A79B776CE454000FF570E0E793640BEAA838E66D14540180A0A905C773640AEF64F5B16D34540B8ACB63AB7743640A66CDF1D05D4454078B94DD42C6D36405EC7170B88D5454008648A41F9693640366D41E79CD64540D87EE333A56736407A2E267FF5D745407050EFA79B653640CA0B52F885D94540A82999B5F76336401EEE648931DB4540C82CBB74D4623640FA84731ADDDC4540B8F5CBE8C6623640769BE1BAEDDD4540D0A9D5BFBA633640EA253BAD10E0454098681804776336408A2AFA850FE1454048F2C9E8D25C36404EDCD1A9F2E3454050152CB276593640DA7252F869E745404885CF22D05A364072C8C4B533EA4540006A8407185E36402AE3C9EF26ED4540C8080E2D87603640FEA4105117F14540507E8ADE076136400A5C3373EDF445408080C84BCC61364082788D6593F545405824398956643640CE53156E47F74540F0D58BDEFF6436402ADB40E7D9F74540408FF5E1946536409E673E4AD9F94540000AD2BFB66536404E0102408FFD454058C33BC34B6636405ECB66A625FF4540802016046B693640DA9926FFE20046409026489A2F6F364012F13E4AC90146401081964144773640CE75008241024640F01D1E3E407B36408E4488C87C024640A02B6782F08036408A24F9058C02464058BD79CDD283364072562262E0034640F8A78007C88536403E04C7D2B805464010C984A4BE8836408251ABEA50074640509840C3FB8D36407609FDA2FD07464050DDF1A73F933640E6681EC5DB07464040EE5CABCC973640028A4CBE2E084640B8FA4BD4D09A36409AB2CD0C290A46401085560ECA9A36405E83AAEA480B4640705102561F99364072E6707DFD0D4640B85B499ADB98364046FAAE873D0F464048D1C54B5C993640F2683AADAE104640B02E19A1019C36409A20E9F478144640003479CDA29B36400E7E66A67A14464070BEFC1B229B36403ABD7FF1E114464020BC459AD79A36409ED24E5B901546400841F0A72F9B3640E269852B5816464090BB5371CB9B36403229DB808216464000C520DBF29F3640469A1D283E18464058243989D6A33640FA3A4BBE891A4640C0C5F24416A63640EA92EF2E641B4640F0A0EE570FB136401224CD573D1D464050717A300CE83640EEB430D6BA0F4640A08322DB4EF1364076E2289C440E4640D0FD641FF3FC3640CA09BCFBB20D4640287E736320023740FA6A956DDB0C464050C6540EEE073740BA618AC8E90B464098ABFB1B420A3740DE280040FA07464008986082E40537402673F5680C044640D0FD641FF3FC364016CBC33543024640B04007F35DF73640822C89C8FD01464010F04A372EED3640BA3DC198C9004640C0E99F8CE3E73640F61B077A82004640C8E0C44BC8E2364006AABF984CFF4540C0C4A7C6ECDF3640322A26FF6BFC4540008DE6D0BBD936401AE4146ED0F24540E007C3AEDDD936403A2601C0EAEF454068E7907B0ADD3640E26DB1247EED4540A82109F385E33640C639807175EB4540C802146768EB36404AAD6DE0C7EA454060CA15FF730D374046F86285E1EB4540F0A94ED4C0213740260BE97489EC4540D08D89DE73293740364BF2CBBCED454050E97E076C323740FE66C0986EEE454008CCD5BF063C37401EA420454BF04540F03510143D53374002E419DB7BF1454028C445FD147C374096FBB1A4B7F0454078176A1FBB973740EE4904DD30EB4540B0D38A41F09E3740D6F15ED6C0EA4540D05A36ECD7A23740EE91B22484EA454080E95E4883B83740926CAE0744EC4540F89EA5C62CBE3740DE015695DDEB4540B085F0A7C7CC3740CEE2F568C3E8454040BB5B914C2638400A4CCDA99AE045405049314FCD283840A2406A4360E04540608D015332563840A22A62222FE14540301BE533B55B38404EF5FF3F48E14540A8656F592060384062844267C6E1454078C70F2D636E38404A66188BA7E54540C098FA1B62773840E64A48A1B5E64540107C43FD088038407EA1E9F455E64540A0C68C5069A938408A1CF95BB9E0454040F74060A2B43840DAC15AB233DF4540E878213EACC038400A86FD2291DE4540605CAB63B3F63840AE14180BF3DF454010083CC3E3143940BA0DCC0C06DC45406057C44B183639408A2DF7E81EDB4540A06B314F994039400215E5D731DA454060BF8E491049394016AF1BBE5ED845408800CD85E9493940727CABEA2FD84540B05EF244B2523940EA476726B9D5454010768107105C394006CEA4B0BFD34540B87A479A33673940A6B631D634D34540583A5C48136D3940EA889F13C3D34540E046B29DA87739406EE7A6CD78D54540904772D8817B394052E48836B9D54540B88F324F1F7D39403A274C3ED4D54540C0048E7BA88839405AA7884897D54540508DB39D7A8E3940B6141151CED545401050EA0A37933940E26B299CB4D6454020A212CA07983940623AFE2220D7454080693E26D39D39404EC9911F08D84540A021B53A4DA33940EE92198B40D94540F0EEAB004CA739400E7FB124A4DA454010C110CADFAB394006814067D2DB45401016DB79A2BB39408E9D230401DC4540D0C3783056BD3940BA0DCC0C06DC45406807BE11F9C73940CA9E9D76B2DD4540D00AC011EDCD3940A61403DD44E14540900A47FD66CE394022C348A1BFE1454008A5BB74DED639405ACE5595EBE44540201301B98ADE3940A21B3AAD83E64540780AD75C9BEA39403A6FDE9D14EC454010FAC4AEABEC3940760A4821E7ED4540D0984ED41AEF394056AA85AB66EF4540900A47FDE60D3A403E1D03DD97F7454050B160E5C70F3A40D645842B92F9454070B5CD224E143A40DA4E89C809FC4540B0104A37C01D3A400EA22FD6DAFF4540D83C678296263A4056ED6F7D96014640F094927B403B3A405A3C7FF184034640F06F2A157E4F3A402E9841E7BB064640D8356FF013553A40DACC2ED3070746406084EE41716A3A406A0A303B2A0846407010D1223A9D3A401606178BDC0A464040712678D3A53A40DEDD7EF1F30B4640E8EFF67EF5AA3A40F2F9C5D22B0C464030DA84A464AD3A406E5A862B6C0C4640C0DC60827CB23A40966FDC80940D464010886F596CB53A40CE07188BD60D46404842D022F2C03A40CE07188BD60D4640089B95B519CA3A405A3E15EED70E4640F08E281556E23A40BAC2583209144640B8BFDC9664003B40B61D404A2215464088812FB208073B40DA073473A916464050AB661FA9193B4002F867A67E12464048403A269F343B40E26ADE1D8B10464060C1C1AE0B3A3B40EAB878B7730F4640B07B76304A403B408E7742E7A90F4640A009A27CAE403B40A2A821EE8D0F4640D04DDBF9DD443B4062A20A17630E4640105F13CAA9433B4082E4F6687D0B46401064A6290C493B40063CB22446094640E8011DA177573B402A78B224CB064640481B0A90825A3B40D2C43473CB05464020E80E2D755F3B40DE03DE1DA70246409081382643623B40864C188BEE01464090A204F313933B40222B717D15024640A03DD2BF28A23B4006193473CF034640E03BEDA701A83B40FE42F7680E03464010CFD22216AD3B409E8C606C2CFF454058F32C4DB9AE3B4062DDA9675EFE4540F0BE0A2DC1B83B400ACBA74D70F945401810203E8EC93B403EBC26FFEEFA45404047479A41DB3B4002D4828E8BFE4540B0E7EDA77DE93B405ECB66A625FF4540B0E7EDA77DE93B40BAEA694322FF4540F065CC8593EF3B40E2B4226271FB4540C8E7257823FB3B400AAB3B2DB7EC4540907F6A59CA033C405AF377B73EEA454010812A15A4383C40D655C09888E14540F01D1E3E406F3C409A1532731BDE4540501910B210943C40D2B2CE35E2DE4540007D0EB280933C40A87A248B1FDD4540B0766307B6923C4030B6D03596CD4540D063620756933C40204FD035F2CB4540B0E00CB2F0953C4040877AE0B4C94540805DB75C8B983C40D048258B27C845404083B95C4B9A3C40888D258BBFC64540007D0EB2809A3C40D0B2CE35E2C44540B076630736983C40680AD0359AC145409089640716943C40D893D0358ABD4540B0E00CB2708F3C4040D2258B17BA4540501910B2908B3C40B0107BE0A4B84540F0506107F6883C40B0107BE024B8454090896407167D3C40E019CF3506B4454070060FB2B07A3C40D893D035CAB2454090F30DB250793C400017268B2FB14540206A0DB220793C4040877AE0F4AE4540206A0DB2A0753C4088F7CE35BAB04540206A0DB2206A3C4008E8CF350EB345402096BA5CAB673C40F8CB7AE0CCB3454090F30DB2D0643C40F035248B07B54540E08F0FB2605E3C4068557BE0FCB54540D063620756523C40403CCF35D2B6454090896407964C3C40D048258B67B64540F0506107F6423C40680AD0359AB4454030570CB2403E3C40204FD03532B44540501910B2902D3C4088F7CE357AB4454030570CB240283C40204FD03532B445406070B85C6B1E3C40985ECF351EB24540B0E00CB270173C4030B6D03596AE45400013650746163C4078DB79E078AD4540F0506107F6073C40985ECF35DE9F4540007D0EB280043C40D893D035CA9D4540805DB75C0B003C40403CCF35929C45409089640796EE3B40680AD035DA9A45402096BA5CABEB3B40985ECF351E9A4540B0E00CB2F0E93B40C02CD035E69945406070B85C6BE73B40C02CD035E69945406070B85C6BE73B40E019CF350699454090F30DB250F03B4040D2258B97964540501910B210F23B4088427AE09C95454030570CB2C0F13B4088427AE01C944540D0636207D6EC3B40D893D0358A8E454090F30DB250E93B4040D2258B57884540007D0EB280E73B40C02CD0352687454030570CB240E53B4008E8CF354E86454030570CB240E33B40B0107BE06485454070060FB230E23B4060BF248BF78345402096BA5CABE23B40680AD0351A814540007D0EB280E63B40888D258B3F7B45406070B85C6BE73B4088427AE09C784540007D0EB200E63B4040877AE0F46F45406070B85C6BE73B40D893D0350A6E45404083B95C4BE23B4040D2258B976C454070060FB230E23B4068557BE07C6A454030570CB240E43B4088F7CE35BA6745402096BA5CABE53B40680AD0355A6445402096BA5CABE53B4068557BE03C5E4540805DB75C0BE53B4030B6D035D65B45406070B85C6BE43B40204FD035F25A454090F30DB250D73B40F880CF35AA5A4540501910B290C93B40D893D0358A5B454030570CB240BE3B40D048258BA75B4540501910B290BB3B4040877AE0745B454030570CB2C0B93B400017268BAF5A4540501910B210B83B40D048258BE758454030570CB240B73B40D0FD79E084574540805DB75C8BB73B4030B6D03556564540E08F0FB260B93B40A87A248B5F55454090F30DB250BD3B40985ECF359E544540F050610776B63B4028D5CE352E54454090F30DB250AB3B40B0107BE064524540501910B290A53B407871D035FE514540E08F0FB2E0A03B40B0C5CF3582504540E08F0FB260A13B40C02CD035264D4540B076630736A43B40403CCF3592494540B0E00CB270A63B4028D5CE356E47454090F30DB250A23B40D048258B27484540F0506107768A3B40D0B2CE3562484540B0E00CB2F0823B40D893D035CA464540206A0DB2A07F3B40D048258B2744454090F30DB2D07D3B407871D035FE4045406070B85CEB7A3B401804258BCF3D4540D063620756763B40F880CF35AA3E4540B0E00CB2F0733B4040877AE0743D4540501910B210743B40F8CB7AE08C3B4540B076630736773B40403CCF35523A45404083B95C4B763B400017268B6F394540805DB75C0B763B40E019CF35C6384540B076630736773B40403CCF35D23645406070B85CEB773B40F035248BC73745404083B95C4B783B40204FD035F23745402096BA5C2B783B4050A3CF35F6374540B076630736773B407871D0357E384540007D0EB200793B40985ECF351E3B4540007D0EB200813B4088F7CE35FA374540206A0DB2A0833B40204FD035B237454030570CB2C0863B40D893D035CA38454090F30DB2D0883B40E019CF35463A4540E08F0FB2608B3B40D0B2CE35E23A4540E08F0FB2E08F3B40985ECF355E39454070060FB230923B400017268BAF3A4540E08F0FB260943B40E019CF35863A4540F050610776963B40D893D035CA394540F050610776983B40985ECF355E394540501910B2109C3B40888D258BBF39454090896407969E3B40680AD0355A3A4540206A0DB220A13B4040877AE0B43A454070060FB2B0A43B40403CCF35523A4540501910B290A43B40F880CF35AA3945404083B95C4BA43B40D893D0358A3945404083B95CCBA33B40403CCF3592394540B0E00CB2F0A23B40985ECF355E394540F050610776A43B40D048258B27374540F0506107F6A63B40403CCF3592354540B0E00CB2F0AA3B40B0C5CF3542354540501910B210B13B40403CCF35D2364540805DB75C8BB13B40985ECF359E354540F0506107F6B23B4088F7CE35FA344540B076630736B53B40607479E0D4344540501910B210B83B40607479E014354540501910B210B83B40A87A248B1F3445409089640796B63B4050A3CF35F633454070060FB2B0B23B40888D258B3F3345409089640796B43B40888D258BFF3145402096BA5CABB53B4028D5CE35AE314540501910B210B83B40888D258B7F314540206A0DB2A0B53B4088F7CE353A304540D063620756B53B4088F7CE357A2E4540E08F0FB2E0B63B40204FD035B22C454090F30DB2D0B93B40D0B2CE35622B4540805DB75C8BBD3B40D893D035CA2A4540805DB75C8BC03B4028D5CE35EE2A4540B0766307B6C33B4008E8CF354E2B45404083B95CCBC73B40D0B2CE35622B45404083B95CCBC73B4030B6D035962A45404083B95C4BC63B40D0B2CE35E229454030570CB240C63B4028D5CE352E294540F050610776C73B4068557BE07C28454070060FB2B0C93B4008E8CF35CE27454090F30DB250C33B4030B6D035D62545404083B95C4BC03B4050A3CF3576234540007D0EB280C13B4030B6D035162145404083B95CCBC73B40C02CD035261F45402096BA5CABC63B4008E8CF35CE1E4540501910B290C63B4018B979E0AC1E4540501910B210C63B40204FD035321E45404083B95C4BCF3B4040877AE0F41B4540B0766307B6D03B40D0B2CE35221B45406070B85CEBD13B40204FD035321A4540D0636207D6D43B4050A3CF35F61845404083B95C4BD83B40403CCF35D217454070060FB230DB3B4040877AE0341745402096BA5CABDA3B40E019CF35C6164540F0506107F6D93B40E019CF35C6154540B0E00CB270D93B40D0B2CE3562154540805DB75C0BE13B407871D035BE134540007D0EB200E33B4088427AE0DC1245406070B85C6BE73B4030B6D035560F454070060FB230F53B40403CCF35120C4540E08F0FB2E0F63B40607479E0D40A4540007D0EB200F93B40C02CD035A6094540501910B290FC3B4088F7CE353A094540007D0EB280FB3B40403CCF35D207454030570CB2C0FC3B40A87A248B9F064540B076630736FF3B40D0B2CE35A20545404083B95CCB013C40680AD035DA044540B0E00CB2F0013C4050A3CF3536044540501910B290013C40D893D0350A044540E08F0FB2E0003C40F035248B07044540805DB75C0B003C40C02CD035E6034540F0506107F6023C4068557BE0BC024540E08F0FB2E0043C40607479E014014540B076630736053C4008E8CF354EFF4440805DB75C8B033C40F035248BC7FD44404083B95C4B043C4068557BE07CFC4440C8E7257823FB3B401E5AD54640FD444040950B9046F73B4086535DCFB6FD4440685FA8C6C2EA3B405616E8F42BFD444090072EB244E73B40B62FFF3F93FD4440F8CAFEB87EE03B40B28842E7CFFE4440681861E52BDA3B40C62EE5D76AFF4440E871C011D1D73B40AAD0DB0075FF4440E0E8A8C6F2D23B404ECE247F2AFF4440A8EAB23AE7D13B4062813E4A52FF4440B044BF74E2D03B40A6B2F0AE60FF44406001C44BDACF3B4062813E4A52FF44406816CBE8D8CE3B404ECE247F2AFF44405859921811CE3B4002EDB024E1FD4440F05293B50DCE3B40BEA1707D25FC444080A627787FCD3B40B281F6E8E2FA4440E8B823DB7AD13B406AEC6389F7F9444020BDE4D0B9D03B40D603EC9130F94440B067213E86CD3B40D22967A6B6F84440004C7DCD30CA3B40D27B34F3A9F8444010F402B9B2C63B409A62818E1AF94440C8C3783056B93B405A16FD22DAFB44409868FC1BE4AF3B406EE21A28FBFB4440380C102DFB9B3B40720A4FDB0BFA4440308BAD005C9B3B405235170BC5F84440C05997B5759A3B403251FA0525F84440C8E9F3441C993B407AA23B2D24F8444090C0E6D02D973B400E3D2EB9B0F84440A80FFFB816953B4076C8B641AAF74440582EA08C7B923B40BA5A1B2805F74440E86F2A15FE8C3B4066A355954CF6444050A9D022568D3B4016E6A3B0FEF5444080DD16040D8E3B408A655495CDF54440909A4FD4D48E3B4086A0C118A4F54440C0139B52888F3B40A2006F6076F5444098DA6D59B68E3B40DADEB4412FF54440A8EA06F31F8D3B40D2724484E0F444402870A329848C3B405E190FB497F44440804A11CA0F903B40EE1AA01306F444405834469AE18B3B40FA624E5B59F3444090E1B29D7E883B40DA2AAB6A3BF44440301ECF226C823B40AE866D6072F74440C010BAD78B7E3B40DAB9AE07AFF84440209EAE00BC6B3B40565CF2CBA2FC4440589DDC9698653B40CAE8EF2EA2FE4440A85D8BDEF55F3B40A6ACEF2E1D014540705B7CCD1C553B40860927FF5907454080217D6A274E3B407E298C65EE0945407889BF74FA453B409AB8B95EBE0B45403021CC85FB3C3B400A5C2CB9880C454010646E5966373B40A6D0B85E3D0C4540B801AD002C343B407A992FD6470B4540883F68BC7B2E3B409E894EDB6E084540F08D1567D22D3B4032E2939FEB07454050CF4B379C2D3B40AEA3067A7807454040DF6BBC7F2C3B402E0AB3244E0745409855A7634B263B40C28416EEE907454038176EF690203B402632BD7B0208454030FB5071B51D3B40C28416EEE9074540E0644937C4193B402AE422E2190945409012A8636D153B40CE08717D090A454010485A48C5103B4066A36309960A45407079AA5F430C3B4056536E1C9D0A454070C54160EA0B3B406A131CA89D0A454018CB6E59CA053B4036FEC55275094540D0082A151A003B407A9A88C87A054540F83B257827FB3A40CA9911D134044540C0A6BC7498F73A4016F3E2BAA5034540A83D0A904EF53A40CE2A739A55024540C08C227837F33A404AB1C335CA004540B87005F35BF03A40A6B04C3E84FF4440989045FD22EE3A40DE6E035D4CFF4440F0441CA155E93A407A4BB8DE96FF444028E1C9E8ACE63A404ECE247F2AFF4440B07E8F7BECE33A400622325622FE444030D8EEA791E13A40DEDC2CB925FE4440B8FA5E4829DF3A40220A973C7BFE444040CA2C4F5BDC3A402A9ADE9D73FE444048B3F6E19AD93A4026524584CEFD444098BD8B8F80D63A4042E71FFED3FC4440C82FB8D7E3D33A4002F5551501FC44402009DBF9C5D13A408E64FBA29AFB44404869B29DF4CE3A4086093573E3FB4440005B773038CA3A40FA82E4D76EFD44407038E233D3C73A4002EDB024E1FD4440C05DFB7EC1C43A400E88CBEF89FD4440A0D78CDE39BF3A406A5A638974FB4440803B334F9BBC3A40BE839A59BFFA4440C8EA22DBB2B73A401AC5D02989FA444070396582A29F3A407239CF8C09FC4440486700B90E9B3A408A29C435D4FB444058122F4FE7963A4032E8A293B8FA444030F2A48C858F3A4042D3DA80C4F7444070B482A4A48D3A403E4B23E23DF744402010203E0E8C3A40CADCFBA2A4F6444008572F4FFF8A3A40063A156ECEF544408095BC74728B3A40C69E96BC4DF5444060319DEF8A8E3A403E64517893F4444020E8D65C4F8F3A402E2CC3B523F4444080290D2D198F3A409223BCFB6BF34440A0FA2678038E3A40DA52DF1D4CF24440D04374F6B88D3A4096494C3EA0F1444008C35F848A8D3A4056CE3FB0D9F04440503CD65C538D3A40B6CB41E7EDEF4440780DF0A73D8C3A403E9559B29AED4440C8E612CA1F8A3A405ACFB54143EB444008C68741AF863A40D2C27BD480E944409892DBF9757A3A404AD0E4D719E844407039658222603A403EFC361087E84440D02C0F2D8D553A40E23FB4C10FE54440B00EB43AED513A40B684D0A9FAE14440B05AAA63F9503A40F2609C7633DF4440F047E133BF523A40E672759AA1DC4440486E45FD56553A408EFCD2C644DB4440 +RS 0106000020E6100000010000000103000000010000000F00000066098E03C3023440A8EF44B543154740CD4B3B420C5A3440D0A9B4AE4C1647409D8444F618F33440E60CFD90BBCE46407933B169D1403540AD28A009C16E4640EB45198303423540D28393EE07624640DF755EA81A983540A88429DF7F2B464038BB9C46687B354036E4C1E293814540E03702C163C33340156514AA91884540BED5C61278063340AE47B39774624640DBB0F6DF1304334080D17F3C087B46408E3B731CAFCA32402121D92DFF9046408DE81EAA4DC5324026F1490FF1F0464037AAE3B4DBD13240F8C6C8B1AFF446408A347073D3DA324010CC6BE4B9F5464066098E03C3023440A8EF44B543154740 \. UPDATE users.responsibility_areas
--- a/schema/gemma.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/gemma.sql Mon Jun 03 10:19:18 2019 +0200 @@ -200,6 +200,11 @@ measure_type varchar PRIMARY KEY ); +CREATE TYPE template_types AS ENUM ( + 'map', + 'diagram', + 'report' +); -- Namespace for user management related data CREATE SCHEMA users @@ -211,10 +216,11 @@ CREATE TABLE templates ( template_name varchar NOT NULL, + template_type template_types NOT NULL DEFAULT 'map'::template_types, country char(2) REFERENCES countries, template_data bytea NOT NULL, date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE (template_name, country) + UNIQUE (template_name, template_type, country) ) CREATE TRIGGER templates_date_info BEFORE UPDATE ON templates FOR EACH ROW EXECUTE PROCEDURE update_date_info() @@ -267,80 +273,82 @@ CREATE TABLE gauges ( - location isrs PRIMARY KEY CHECK( + location isrs CHECK( (location).orc SIMILAR TO 'G[[:digit:]]{4}' AND CAST(substring((location).orc from 2 for 4) AS int) < 2048), objname varchar NOT NULL, geom geography(POINT, 4326) NOT NULL, applicability_from_km int8, applicability_to_km int8, - validity tstzrange, - -- pasted text from a more general specification is given - -- (a gauge is not a berth!) - -- TODO: Ranges need a joint exclusion constaint to prevent overlaps? + validity tstzrange NOT NULL, zero_point double precision NOT NULL, geodref varchar, - date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - source_organization varchar + date_info timestamp with time zone NOT NULL, + source_organization varchar, + lastupdate timestamp with time zone NOT NULL, + -- entry removed from external data source (RIS-Index)/historicised: + erased boolean NOT NULL DEFAULT false, + CHECK (erased OR NOT isempty(validity)), + PRIMARY KEY (location, validity), + EXCLUDE USING GiST (isrs_astext(location) WITH =, validity WITH &&) + DEFERRABLE INITIALLY DEFERRED ) - CREATE TRIGGER gauges_date_info BEFORE UPDATE ON gauges - FOR EACH ROW EXECUTE PROCEDURE update_date_info() + -- Allow only one non-erased entry per location + CREATE UNIQUE INDEX gauges_erased_unique_constraint + ON gauges (location) + WHERE NOT erased CREATE TABLE gauges_reference_water_levels ( - gauge_id isrs NOT NULL REFERENCES gauges, + location isrs NOT NULL, + validity tstzrange NOT NULL, + FOREIGN KEY (location, validity) REFERENCES gauges ON UPDATE CASCADE, -- Omit foreign key constraint to be able to store not NtS-compliant -- names, too: depth_reference varchar NOT NULL, -- REFERENCES depth_references, - PRIMARY KEY (gauge_id, depth_reference), + PRIMARY KEY (location, validity, depth_reference), value int NOT NULL ) - CREATE VIEW gauges_geoserver AS - SELECT - g.location, - isrs_asText(g.location) AS isrs_code, - g.objname, - g.geom, - g.applicability_from_km, - g.applicability_to_km, - g.validity, - g.zero_point, - g.geodref, - g.date_info, - g.source_organization, - json_strip_nulls(json_object_agg(coalesce(r.depth_reference,'empty'), - r.value)) - AS reference_water_levels - FROM gauges g LEFT JOIN LATERAL ( - SELECT gauge_id, depth_reference, value - FROM gauges_reference_water_levels - ) r ON r.gauge_id = g.location - GROUP BY g.location - CREATE TABLE gauge_measurements ( id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, - fk_gauge_id isrs NOT NULL REFERENCES gauges, + location isrs NOT NULL, + validity tstzrange NOT NULL, + CONSTRAINT gauge_key + FOREIGN KEY (location, validity) REFERENCES gauges + ON UPDATE CASCADE, measure_date timestamp with time zone NOT NULL, + CHECK (measure_date <@ validity), country_code char(2) NOT NULL REFERENCES countries, - -- TODO: add relations to stuff provided as enumerations - sender varchar NOT NULL, -- "from" attribute from DRC + sender varchar NOT NULL, -- "from" element from NtS response language_code varchar NOT NULL REFERENCES language_codes, date_issue timestamp with time zone NOT NULL, reference_code varchar(4) NOT NULL REFERENCES depth_references, water_level double precision NOT NULL, - predicted boolean NOT NULL, - is_waterlevel boolean NOT NULL, - -- XXX: "measure_code" if really only W or Q - -- XXX: Do we need "unit" attribute or can we normalise on import? - value_min double precision, -- XXX: NOT NULL if predicted? - value_max double precision, -- XXX: NOT NULL if predicted? - --- TODO: Add a double range type for checking? - date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - source_organization varchar NOT NULL, -- "originator" + date_info timestamp with time zone NOT NULL, + source_organization varchar NOT NULL, -- "originator" from NtS response staging_done boolean NOT NULL DEFAULT false, - -- So we can have a staged and - -- a non-staged fk_gauge_id/measure_date pair. - UNIQUE (fk_gauge_id, measure_date, staging_done) + UNIQUE (location, measure_date, staging_done) + ) + + CREATE TABLE gauge_predictions ( + location isrs NOT NULL, + validity tstzrange NOT NULL, + CONSTRAINT gauge_key + FOREIGN KEY (location, validity) REFERENCES gauges + ON UPDATE CASCADE, + measure_date timestamp with time zone NOT NULL, + CHECK (measure_date >= lower(validity)), + country_code char(2) NOT NULL REFERENCES countries, + sender varchar NOT NULL, -- "from" element from NtS response + language_code varchar NOT NULL REFERENCES language_codes, + date_issue timestamp with time zone NOT NULL, + reference_code varchar(4) NOT NULL REFERENCES depth_references, + water_level double precision NOT NULL, + conf_interval numrange + CHECK (conf_interval @> CAST(water_level AS numeric)), + date_info timestamp with time zone NOT NULL, + source_organization varchar NOT NULL, -- "originator" from NtS response + PRIMARY KEY (location, measure_date, date_issue) ) CREATE TABLE waterway_axis ( @@ -375,27 +383,6 @@ related_enc varchar(12) ) - -- A table to help geoserver serve the distance marks as WFS 1.1.0. - -- At least geoserver-2.13.2 does not serve type geography correctly - -- and does not serve the location_code as isrs type - CREATE VIEW distance_marks_geoserver AS - SELECT location_code, - isrs_asText(location_code) AS location, - geom::Geometry(POINT, 4326), - related_enc, - (location_code).hectometre - FROM distance_marks_virtual - - CREATE VIEW distance_marks_ashore_geoserver AS - SELECT id, - country, - geom::Geometry(POINT, 4326), - related_enc, - hectom, - catdis, - position_code - FROM distance_marks - -- We need to configure primary keys for the views used by -- geoserver for wfs, otherwise it will generate ids on the fly, -- which will change for the same feature... @@ -426,6 +413,9 @@ staging_done boolean NOT NULL DEFAULT false, UNIQUE(name, staging_done) ) + CREATE TRIGGER stretches_date_info + BEFORE UPDATE ON stretches + FOR EACH ROW EXECUTE PROCEDURE update_date_info() CREATE TABLE stretch_countries ( stretches_id int NOT NULL REFERENCES stretches(id) @@ -434,25 +424,21 @@ UNIQUE(stretches_id, country_code) ) - -- Published view for GeoServer - CREATE VIEW stretches_geoserver AS SELECT - id, - name, - (stretch).lower::varchar as lower, - (stretch).upper::varchar as upper, - area::Geometry(MULTIPOLYGON, 4326), - objnam, - nobjnam, - date_info, - source_organization, - (SELECT string_agg(country_code, ', ') - FROM stretch_countries - WHERE stretches_id = id) AS countries, - staging_done - FROM stretches - - - CREATE TRIGGER sections_stretches_date_info + -- Like stretches without the countries + CREATE TABLE sections ( + id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + name varchar NOT NULL, + section isrsrange NOT NULL, + area geography(MULTIPOLYGON, 4326) NOT NULL + CHECK(ST_IsValid(CAST(area AS geometry))), + objnam varchar NOT NULL, + nobjnam varchar, + date_info timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + source_organization varchar NOT NULL, + staging_done boolean NOT NULL DEFAULT false, + UNIQUE(name, staging_done) + ) + CREATE TRIGGER sections_date_info BEFORE UPDATE ON stretches FOR EACH ROW EXECUTE PROCEDURE update_date_info() @@ -500,14 +486,14 @@ -- -- Bottlenecks -- - -- XXX: Nullability differs between DRC (attributes marked "O") and WSDL - -- (minOccurs=0; nillable seems to be set arbitrarily as even bottleneck_id and - -- fk_g_fid (both mandatory, i.e. marked "M" in DRC) have nillable="true" in WSDL) CREATE TABLE bottlenecks ( id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, bottleneck_id varchar UNIQUE NOT NULL, - fk_g_fid isrs NOT NULL REFERENCES gauges, - -- XXX: DRC references "ch. 3.1.1", which does not exist in document. + gauge_location isrs NOT NULL, + gauge_validity tstzrange NOT NULL, + CONSTRAINT gauge_key + FOREIGN KEY (gauge_location, gauge_validity) REFERENCES gauges + ON UPDATE CASCADE, objnam varchar, nobjnm varchar, stretch isrsrange NOT NULL, @@ -516,7 +502,7 @@ rb char(2) REFERENCES countries, -- from rb_lb in interface lb char(2) REFERENCES countries, -- from rb_lb in interface responsible_country char(2) NOT NULL REFERENCES countries, - revisiting_time smallint NOT NULL, + revisiting_time smallint, limiting varchar NOT NULL REFERENCES limiting_factors, -- surtyp varchar NOT NULL REFERENCES survey_types, -- XXX: Also an attribut of sounding result? @@ -535,7 +521,8 @@ FOR EACH ROW EXECUTE PROCEDURE update_date_info() CREATE TABLE bottlenecks_riverbed_materials ( - bottleneck_id int NOT NULL REFERENCES bottlenecks(id), + bottleneck_id int NOT NULL REFERENCES bottlenecks(id) + ON DELETE CASCADE, riverbed varchar NOT NULL REFERENCES riverbed_materials, -- XXX: should be 'natsur' according to IENC Encoding Guide M.4.3 PRIMARY KEY (bottleneck_id, riverbed) @@ -565,17 +552,6 @@ -- CHECK(ST_IsSimple(CAST(lines AS geometry))), PRIMARY KEY (sounding_result_id, height) ) - -- A view to help geoserver serve contour lines. - -- At least geoserver-2.13.2 does not serve type geography correctly - CREATE VIEW sounding_results_contour_lines_geoserver AS - SELECT bottleneck_id, - date_info, - height, - CAST(lines AS geometry(multilinestring, 4326)) AS lines - FROM sounding_results_contour_lines cl - JOIN sounding_results sr - ON sr.id = cl.sounding_result_id - -- -- Fairway availability -- @@ -634,66 +610,6 @@ CHECK(measure_type = 'minimum guaranteed' OR value_lifetime IS NOT NULL) ) - - CREATE VIEW bottleneck_overview AS - SELECT - objnam AS name, - ST_Centroid(area)::Geometry(POINT, 4326) AS point, - (lower(stretch)).hectometre AS from, - (upper(stretch)).hectometre AS to, - sr.current::text - FROM bottlenecks bn LEFT JOIN ( - SELECT bottleneck_id, max(date_info) AS current FROM sounding_results - GROUP BY bottleneck_id) sr ON sr.bottleneck_id = bn.id - ORDER BY objnam - - -- Published view for GeoServer - CREATE VIEW bottlenecks_geoserver AS - WITH fairway_availability_latest AS ( - SELECT DISTINCT ON (bottleneck_id) bottleneck_id,date_info,critical - FROM fairway_availability - ORDER BY bottleneck_id, date_info DESC NULLS LAST), - gauge_measurements_waterlevel AS ( - SELECT DISTINCT ON (fk_gauge_id) - fk_gauge_id, measure_date, predicted, water_level - FROM gauge_measurements WHERE predicted ='false' - ORDER BY fk_gauge_id, measure_date DESC NULLS LAST) - SELECT - b.id, - b.bottleneck_id, - b.objnam, - b.nobjnm, - b.stretch, - b.area, - b.rb, - b.lb, - b.responsible_country, - b.revisiting_time, - b.limiting, - b.date_info, - b.source_organization, - g.location AS gauge_isrs_code, - g.objname AS gauge_objname, - json_strip_nulls(json_object_agg(coalesce(r.depth_reference,'empty'), - r.value)) - AS reference_water_levels, - fal.date_info AS fa_date_info, - fal.critical AS fa_critical, - gmw.water_level as gm_waterlevel - FROM bottlenecks b LEFT JOIN gauges g ON b.fk_g_fid = g.location - LEFT JOIN LATERAL ( - SELECT gauge_id,depth_reference,value - FROM gauges_reference_water_levels - ) r ON r.gauge_id = b.fk_g_fid - LEFT JOIN LATERAL ( - SELECT bottleneck_id,date_info,critical - FROM fairway_availability_latest - WHERE b.id=bottleneck_id) fal ON TRUE - LEFT JOIN LATERAL ( - SELECT water_level - FROM gauge_measurements_waterlevel - WHERE b.fk_g_fid=fk_gauge_id) gmw ON TRUE - GROUP BY b.id, g.location, fal.date_info, fal.critical, gmw.water_level; ; -- Configure primary keys for geoserver views @@ -702,7 +618,8 @@ ('waterway', 'distance_marks_geoserver', 'location_code'), ('waterway', 'distance_marks_ashore_geoserver', 'id'), ('waterway', 'bottlenecks_geoserver', 'id'), - ('waterway', 'stretches_geoserver', 'id'); + ('waterway', 'stretches_geoserver', 'id'), + ('waterway', 'sections_geoserver', 'id'); -- -- Import queue and respective logging @@ -825,18 +742,4 @@ ) ; -CREATE VIEW waterway.sounding_differences AS SELECT - sd.id AS id, - bn.objnam AS objnam, - srm.date_info AS minuend, - srs.date_info AS subtrahend, - sdcl.height AS height, - CAST(sdcl.lines AS geometry(multilinestring, 4326)) AS lines -FROM - caching.sounding_differences sd JOIN - caching.sounding_differences_contour_lines sdcl ON sd.id = sdcl.sounding_differences_id JOIN - waterway.sounding_results srm ON sd.minuend = srm.id JOIN - waterway.sounding_results srs ON sd.subtrahend = srs.id JOIN - waterway.bottlenecks bn ON srm.bottleneck_id = bn.id; - COMMIT;
--- a/schema/geo_functions.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/geo_functions.sql Mon Jun 03 10:19:18 2019 +0200 @@ -4,19 +4,25 @@ -- SPDX-License-Identifier: AGPL-3.0-or-later -- License-Filename: LICENSES/AGPL-3.0.txt --- Copyright (C) 2018 by via donau +-- Copyright (C) 2018, 2019 by via donau -- – Österreichische Wasserstraßen-Gesellschaft mbH -- Software engineering by Intevation GmbH -- Author(s): -- * Sascha L. Teichmann <sascha.teichmann@intevation.de> +-- * Tom Gottfried <tom@intevation.de> -CREATE OR REPLACE FUNCTION best_utm(g geometry) RETURNS integer AS +CREATE OR REPLACE FUNCTION best_utm(g geography) RETURNS integer AS $$ DECLARE center geometry; BEGIN - SELECT ST_Centroid(g) INTO center; + -- Centroid should be calculated on geography to get accurate results + -- from lon/lat coordinates, but the respective PostGIS function returns + -- POINT(-NaN NaN) for some invalid polygons, while the calculation on + -- geometry seems to give reasonable approximations in this context. + SELECT ST_Centroid(CAST(g AS geometry)) INTO center; + RETURN CASE WHEN ST_Y(center) > 0 THEN 32600 @@ -27,20 +33,3 @@ $$ LANGUAGE plpgsql IMMUTABLE; - -CREATE OR REPLACE FUNCTION utm_covers(g geography) RETURNS boolean AS -$$ -DECLARE - user_area geometry; - utm integer; -BEGIN - SELECT area::geometry FROM users.responsibility_areas INTO user_area - WHERE country = users.current_user_country(); - SELECT best_utm(user_area) INTO utm; - RETURN ST_Covers( - ST_Transform(user_area, utm), - ST_Transform(g::geometry, utm)); -END; -$$ -LANGUAGE plpgsql -STABLE;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/geoserver_views.sql Mon Jun 03 10:19:18 2019 +0200 @@ -0,0 +1,187 @@ +CREATE OR REPLACE VIEW waterway.gauges_geoserver AS + SELECT + g.location, + isrs_asText(g.location) AS isrs_code, + g.objname, + g.geom, + g.applicability_from_km, + g.applicability_to_km, + g.validity, + g.zero_point, + g.geodref, + g.date_info, + g.source_organization, + r.rwls AS reference_water_levels, + wl.measure_date AS gm_measuredate, + wl.water_level AS gm_waterlevel, + wl_14d.n AS gm_n_14d, + fca.forecast_accuracy_3d, + fca.forecast_accuracy_1d + FROM waterway.gauges g + LEFT JOIN (SELECT location, validity, + json_strip_nulls(json_object_agg( + coalesce(depth_reference, 'empty'), value)) AS rwls + FROM waterway.gauges_reference_water_levels + GROUP BY location, validity) AS r + USING (location, validity) + LEFT JOIN (SELECT DISTINCT ON (location) + location, + measure_date, + water_level + FROM waterway.gauge_measurements + ORDER BY location, measure_date DESC) AS wl + USING (location) + LEFT JOIN (SELECT location, count(water_level) AS n + FROM waterway.gauge_measurements + -- consider all measurements within 14 days plus a tolerance + WHERE measure_date + >= current_timestamp - '14 days 00:15'::interval + GROUP BY location) AS wl_14d + USING (location) + LEFT JOIN (SELECT location, + max(acc) FILTER (WHERE + measure_date <= current_timestamp + '1 day'::interval) + AS forecast_accuracy_1d, + max(acc) AS forecast_accuracy_3d + FROM waterway.gauge_predictions, + GREATEST(water_level - lower(conf_interval), + upper(conf_interval) - water_level) AS acc (acc) + WHERE measure_date + BETWEEN current_timestamp + AND current_timestamp + '3 days'::interval + GROUP BY location) AS fca + USING (location) + WHERE NOT g.erased; + +CREATE OR REPLACE VIEW waterway.distance_marks_geoserver AS + SELECT location_code, + isrs_asText(location_code) AS location, + geom::Geometry(POINT, 4326), + related_enc, + (location_code).hectometre + FROM waterway.distance_marks_virtual; + +CREATE OR REPLACE VIEW waterway.distance_marks_ashore_geoserver AS + SELECT id, + country, + geom::Geometry(POINT, 4326), + related_enc, + hectom, + catdis, + position_code + FROM waterway.distance_marks; + +CREATE OR REPLACE VIEW waterway.bottlenecks_geoserver AS + WITH + fairway_availability_latest AS ( + SELECT DISTINCT ON (bottleneck_id) bottleneck_id, date_info, critical + FROM waterway.fairway_availability + ORDER BY bottleneck_id, date_info DESC), + sounding_result_latest AS ( + SELECT DISTINCT ON (bottleneck_id) bottleneck_id, max(date_info) AS date_max + FROM waterway.sounding_results + GROUP BY bottleneck_id + ORDER BY bottleneck_id DESC) + SELECT + b.id, + b.bottleneck_id, + b.objnam, + b.nobjnm, + b.stretch, + b.area, + b.rb, + b.lb, + b.responsible_country, + b.revisiting_time, + b.limiting, + b.date_info, + b.source_organization, + g.location AS gauge_isrs_code, + g.objname AS gauge_objname, + g.reference_water_levels, + fal.date_info AS fa_date_info, + fal.critical AS fa_critical, + g.gm_measuredate, + g.gm_waterlevel, + g.gm_n_14d, + srl.date_max, + g.forecast_accuracy_3d, + g.forecast_accuracy_1d + FROM waterway.bottlenecks b + LEFT JOIN waterway.gauges_geoserver g + ON b.gauge_location = g.location AND b.gauge_validity = g.validity + LEFT JOIN fairway_availability_latest fal + ON b.id = fal.bottleneck_id + LEFT JOIN sounding_result_latest srl + ON b.id = srl.bottleneck_id; + +CREATE OR REPLACE VIEW waterway.stretches_geoserver AS + SELECT + id, + name, + (stretch).lower::varchar as lower, + (stretch).upper::varchar as upper, + area::Geometry(MULTIPOLYGON, 4326), + objnam, + nobjnam, + date_info, + source_organization, + (SELECT string_agg(country_code, ', ') + FROM waterway.stretch_countries + WHERE stretches_id = id) AS countries, + staging_done + FROM waterway.stretches; + +CREATE OR REPLACE VIEW waterway.sections_geoserver AS + SELECT + id, + name, + (section).lower::varchar as lower, + (section).upper::varchar as upper, + area::Geometry(MULTIPOLYGON, 4326), + objnam, + nobjnam, + date_info, + source_organization, + staging_done + FROM waterway.sections; + +CREATE OR REPLACE VIEW waterway.sounding_results_contour_lines_geoserver AS + SELECT bottleneck_id, + date_info, + height, + CAST(lines AS geometry(multilinestring, 4326)) AS lines + FROM waterway.sounding_results_contour_lines cl + JOIN waterway.sounding_results sr ON sr.id = cl.sounding_result_id; + +CREATE OR REPLACE VIEW waterway.bottleneck_overview AS + SELECT + objnam AS name, + ST_Centroid(area)::Geometry(POINT, 4326) AS point, + (lower(stretch)).hectometre AS from, + (upper(stretch)).hectometre AS to, + sr.current::text, + responsible_country + FROM waterway.bottlenecks bn LEFT JOIN ( + SELECT bottleneck_id, max(date_info) AS current + FROM waterway.sounding_results + GROUP BY bottleneck_id) sr ON sr.bottleneck_id = bn.id + ORDER BY objnam; + +CREATE OR REPLACE VIEW waterway.sounding_differences AS + SELECT + sd.id AS id, + bn.objnam AS objnam, + srm.date_info AS minuend, + srs.date_info AS subtrahend, + sdcl.height AS height, + CAST(sdcl.lines AS geometry(multilinestring, 4326)) AS lines + FROM caching.sounding_differences sd + JOIN caching.sounding_differences_contour_lines sdcl + ON sd.id = sdcl.sounding_differences_id + JOIN waterway.sounding_results srm + ON sd.minuend = srm.id + JOIN waterway.sounding_results srs + ON sd.subtrahend = srs.id + JOIN waterway.bottlenecks bn + ON srm.bottleneck_id = bn.id;
--- a/schema/install-db.sh Wed May 29 10:58:45 2019 +0200 +++ b/schema/install-db.sh Mon Jun 03 10:19:18 2019 +0200 @@ -130,6 +130,7 @@ -f "$BASEDIR/search_functions.sql" \ -f "$BASEDIR/geonames.sql" \ -f "$BASEDIR/manage_users.sql" \ + -f "$BASEDIR/geoserver_views.sql" \ -f "$BASEDIR/auth.sql" \ -f "$BASEDIR/isrs_functions.sql" \ -f "$BASEDIR/default_sysconfig.sql"
--- a/schema/isrs.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/isrs.sql Mon Jun 03 10:19:18 2019 +0200 @@ -47,10 +47,101 @@ CHECK (is_country((VALUE).country_code)) CHECK ((VALUE).hectometre BETWEEN 0 AND 99999); +CREATE FUNCTION isrs_cmp(a isrs, b isrs) RETURNS int +AS $$ + /* TODO: handle non-matching combinations of country_codes and + fairway_sections. Otherwise, this will give unexpected results if + both hectometre values do not refer to the same river. */ + SELECT (a).hectometre - (b).hectometre +$$ LANGUAGE sql + IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION isrslt(a isrs, b isrs) RETURNS boolean +AS $$ + SELECT isrs_cmp(a, b) < 0 +$$ LANGUAGE sql + IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION isrsle(a isrs, b isrs) RETURNS boolean +AS $$ + SELECT isrs_cmp(a, b) <= 0 +$$ LANGUAGE sql + IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION isrseq(a isrs, b isrs) RETURNS boolean +AS $$ + SELECT isrs_cmp(a, b) = 0 +$$ LANGUAGE sql + IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION isrsge(a isrs, b isrs) RETURNS boolean +AS $$ + SELECT isrs_cmp(a, b) >= 0 +$$ LANGUAGE sql + IMMUTABLE PARALLEL SAFE; + +CREATE FUNCTION isrsgt(a isrs, b isrs) RETURNS boolean +AS $$ + SELECT isrs_cmp(a, b) > 0 +$$ LANGUAGE sql + IMMUTABLE PARALLEL SAFE; + +CREATE OPERATOR <~ ( + leftarg = isrs, + rightarg = isrs, + function = isrslt +); + +CREATE OPERATOR <~= ( + leftarg = isrs, + rightarg = isrs, + function = isrsle +); + +CREATE OPERATOR ~= ( + leftarg = isrs, + rightarg = isrs, + function = isrseq, + commutator = ~= +); + +CREATE OPERATOR >~= ( + leftarg = isrs, + rightarg = isrs, + function = isrsge, + commutator = <~=, + negator = <~ +); + +CREATE OPERATOR >~ ( + leftarg = isrs, + rightarg = isrs, + function = isrsgt, + commutator = <~, + negator = <~= +); + +CREATE OPERATOR CLASS isrs_ops FOR TYPE isrs USING btree AS + OPERATOR 1 <~, + OPERATOR 2 <~=, + OPERATOR 3 ~=, + OPERATOR 4 >~=, + OPERATOR 5 >~, + FUNCTION 1 isrs_cmp; + +CREATE FUNCTION isrs_diff(a isrs, b isrs) RETURNS double precision +AS $$ + SELECT CAST(isrs_cmp(a, b) AS double precision) +$$ LANGUAGE sql + IMMUTABLE PARALLEL SAFE; + CREATE TYPE isrsrange AS RANGE ( - subtype = isrs + subtype = isrs, + subtype_opclass = isrs_ops, + subtype_diff = isrs_diff ); + -- -- Functions --
--- a/schema/isrs_functions.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/isrs_functions.sql Mon Jun 03 10:19:18 2019 +0200 @@ -54,24 +54,34 @@ -- in m, up to which linestrings will be connected at their boundary ) RETURNS geometry AS $$ +DECLARE z int; DECLARE result_geom geometry; BEGIN + -- Find best matchting UTM zone + z = best_utm(stretch); + + CREATE TEMP TABLE axis AS + SELECT id, wtwaxs, ST_Boundary(wtwaxs) AS bdr + FROM (SELECT id, ST_Transform(wtwaxs::geometry, z) AS wtwaxs + FROM waterway.waterway_axis) AS axs; + CREATE INDEX axs_bdr ON axis USING GiST (bdr); + ANALYZE axis; + WITH RECURSIVE - utm_zone AS ( - -- Find best matchting UTM zone - SELECT best_utm(stretch) AS z), - axis AS ( - SELECT id, ST_Transform(wtwaxs::geometry, z) AS wtwaxs - FROM waterway.waterway_axis, utm_zone), -- In order to guarantee the following ST_Covers to work, -- snap distance mark coordinates to axis - points AS ( + points0 AS ( SELECT ST_ClosestPoint( wtwaxs, ST_Transform(geom, z)) AS geom - FROM ST_Dump(ISRSrange_points(stretch)), utm_zone, ( + FROM ST_Dump(ISRSrange_points(stretch)), ( SELECT ST_Collect(wtwaxs) AS wtwaxs FROM axis) AS ax), + -- Ensure two distinct points on axis have been found + points AS ( + SELECT geom + FROM points0 + WHERE 2 = (SELECT count(DISTINCT geom) FROM points0)), axis_snapped AS ( -- Iteratively connect non-contiguous axis chunks -- to find the contiguous axis on which given distance marks lie @@ -91,19 +101,19 @@ UNION -- Fill eventual gap SELECT ST_ShortestLine( - ST_Boundary(refgeom), ST_Boundary(geom)) + ST_Boundary(refgeom), bdr) UNION -- Linestring to be added SELECT geom))) FROM axis_snapped AS axis_snapped (refids, refgeom), - axis AS axis (id, geom), + axis AS axis (id, geom, bdr), (SELECT ST_Collect(points.geom) AS pts FROM points) AS points WHERE id <> ALL(refids) AND ST_DWithin( - ST_Boundary(refgeom), ST_Boundary(geom), tolerance) + ST_Boundary(refgeom), bdr, tolerance) AND NOT ST_Covers(ST_Buffer(refgeom, 0.0001), points.pts) - ORDER BY ST_Distance(ST_Boundary(refgeom), ST_Boundary(geom)) + ORDER BY ST_Boundary(refgeom) <-> bdr FETCH FIRST ROW ONLY)), axis_segment AS ( -- Fetch end result from snapping @@ -119,8 +129,8 @@ -- end of the resulting linestring, that significantly differ from -- the direction of the input linestring due to finite precision -- of the calculation. The generated small segment of the - -- resulting line leads to unexpected results of the buffer with - -- endcap=flat in the CTE below. + -- resulting line would lead e.g. to unexpected results in an area + -- generated by ISRSrange_area(). SELECT ST_SimplifyPreserveTopology(ST_LineSubstring( axis_segment.line, min(fractions.f), max(fractions.f)), 0.0001) AS line @@ -130,11 +140,17 @@ FROM points) AS fractions GROUP BY axis_segment.line; + -- Drop temporary table to avoid side effects on PostgreSQL's MVCC, + -- because otherwise subsequent invocations of the function will not see + -- changes on the underlying waterway.waterway_axis that might have + -- occured. + DROP TABLE axis; + RETURN result_geom; END; $$ LANGUAGE plpgsql - STABLE PARALLEL SAFE; + PARALLEL RESTRICTED; -- Clip an area to a stretch given by a geometry representing an axis (e.g. -- the output of ISRSrange_axis()). @@ -149,16 +165,21 @@ area geometry ) RETURNS geometry AS $$ +DECLARE + area_subset geometry; + result_geom geometry; +BEGIN + -- In case area is a multipolygon, process the union of those + -- polygons, which intersect with the axis. The union is to avoid + -- problems with invalid/self-intersecting multipolygons + SELECT ST_Union(a_dmp.geom) + INTO STRICT area_subset + FROM (SELECT ST_MakeValid(ST_Transform(geom, ST_SRID(axis))) + FROM ST_Dump(area)) AS a_dmp (geom) + WHERE ST_Intersects(a_dmp.geom, axis) + HAVING ST_Union(a_dmp.geom) IS NOT NULL; + WITH - area_subset AS ( - -- In case area is a multipolygon, process the union of those - -- polygons, which intersect with the axis. The union is to avoid - -- problems with invalid/self-intersecting multipolygons - SELECT ST_Union(a_dmp.geom) AS area - FROM (SELECT ST_MakeValid(ST_Transform(geom, ST_SRID(axis))) - FROM ST_Dump(area)) AS a_dmp (geom) - WHERE ST_Intersects(a_dmp.geom, axis) - ), rotated_ends AS ( SELECT ST_Collect(ST_Scale( ST_Translate(e, @@ -169,18 +190,23 @@ FROM (VALUES (1), (-1)) AS idx (i)) AS ep, ST_Rotate(ST_PointN(axis, i*2), pi()/2, p1) AS ep2 (p2), ST_Makeline(p1, p2) AS e (e), - area_subset, - LATERAL (SELECT (ST_MaxDistance(p1, area) / ST_Length(e)) + LATERAL ( + SELECT (ST_MaxDistance(p1, area_subset) / ST_Length(e)) * 2) AS d (d)), range_area AS ( -- Split area by orthogonal lines at the ends of the clipped axis SELECT (ST_Dump(ST_CollectionExtract( - ST_Split(area, blade), 3))).geom - FROM area_subset, rotated_ends) + ST_Split(area_subset, blade), 3))).geom + FROM rotated_ends) -- From the polygons returned by the last CTE, select only those -- around the clipped axis SELECT ST_Multi(ST_Transform(ST_Union(range_area.geom), ST_SRID(area))) + INTO result_geom FROM range_area - WHERE ST_Intersects(ST_Buffer(range_area.geom, -0.0001), axis) + WHERE ST_Intersects(ST_Buffer(range_area.geom, -0.0001), axis); + + RETURN result_geom; +END; $$ - LANGUAGE sql; + LANGUAGE plpgsql + STABLE PARALLEL SAFE;
--- a/schema/isrs_tests.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/isrs_tests.sql Mon Jun 03 10:19:18 2019 +0200 @@ -15,6 +15,9 @@ -- pgTAP test script for ISRS location code types and functions -- +-- +-- Conversion from/to text +-- SELECT results_eq($$ SELECT isrs_fromText('DEBON03901G007906548') $$, @@ -34,6 +37,53 @@ , 'isrs_asText() is the inverse of isrs_fromText()'); +-- +-- Comparison operators +-- +SELECT ok( + isrs_fromText('DEBON03901G007906548') + <> isrs_fromText('DEXXX039010000006548'), + 'Different codes at equal hectometre do not equal by default'); + +SELECT ok( + isrs_fromText('DEBON03901G007906548') + ~= isrs_fromText('DEXXX039010000006548') + AND isrs_fromText('DEBON03901G007906548') + >~= isrs_fromText('DEXXX039010000006548') + AND isrs_fromText('DEBON03901G007906548') + <~= isrs_fromText('DEXXX039010000006548'), + 'isrs_ops: Different codes at equal hectometre compare as equal'); + +SELECT ok( + isrs_fromText('DEBON03901G007906549') + >~ isrs_fromText('DEXXX039010000006548') + AND isrs_fromText('DEBON03901G007906549') + >~= isrs_fromText('DEXXX039010000006548') + AND isrs_fromText('DEXXX039010000006547') + <~= isrs_fromText('DEBON03901G007906548') + AND isrs_fromText('DEXXX039010000006547') + <~ isrs_fromText('DEBON03901G007906548'), + 'isrs_ops: Ordering depends on hectometre'); + +SELECT ok( + isrsrange(isrs_fromText('DEXXX039010000006540'), + isrs_fromText('DEXXX039010000006560')) + @> isrs_fromText('ATXXX000000000006550') + AND isrs_fromText('DEXXX039010000006560') + <@ isrsrange(isrs_fromText('ATXXX000000000006550'), + isrs_fromText('ATXXX000000000006570')), + 'isrsrange: ''Contains'' depends on hectometre'); + +SELECT ok( + isrsrange(isrs_fromText('DEXXX039010000006540'), + isrs_fromText('DEXXX039010000006560')) + && isrsrange(isrs_fromText('ATXXX000000000006550'), + isrs_fromText('ATXXX000000000006570')), + 'isrsrange: Overlap depends on hectometre'); + +-- +-- Geometry processing +-- SELECT throws_ok($$ SELECT ISRSrange_points(isrsrange( ('AT', 'XXX', '00001', '00000', 0)::isrs, @@ -58,14 +108,21 @@ 5)), 'ISRSrange_axis returns a valid simple feature'); -SELECT ok( - ISRSrange_area(ISRSrange_axis(isrsrange( +SELECT throws_ok($$ + SELECT ISRSrange_area('LINESTRING(0 0, 1 1)', NULL) + $$, + 'P0002', NULL, + 'ISRSrange_area fails if no input area is given'); + +SELECT throws_ok($$ + SELECT ISRSrange_area(ISRSrange_axis(isrsrange( ('AT', 'XXX', '00001', '00000', 0)::isrs, ('AT', 'XXX', '00001', '00000', 1)::isrs), 5), - ST_SetSRID('POLYGON((0 1, 0 2, 1 2, 1 1, 0 1))'::geometry, 4326) - ) IS NULL, - 'ISRSrange_area returns NULL, if given area does not intersect with axis'); + ST_SetSRID('POLYGON((0 1, 0 2, 1 2, 1 1, 0 1))'::geometry, 4326)) + $$, + 'P0002', NULL, + 'ISRSrange_area fails, if given area does not intersect with axis'); SELECT results_eq($$ SELECT every(ST_DWithin(
--- a/schema/manage_users.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/manage_users.sql Mon Jun 03 10:19:18 2019 +0200 @@ -4,7 +4,7 @@ -- SPDX-License-Identifier: AGPL-3.0-or-later -- License-Filename: LICENSES/AGPL-3.0.txt --- Copyright (C) 2018 by via donau +-- Copyright (C) 2018, 2019 by via donau -- – Österreichische Wasserstraßen-Gesellschaft mbH -- Software engineering by Intevation GmbH @@ -68,6 +68,31 @@ STABLE PARALLEL SAFE; +CREATE OR REPLACE FUNCTION users.current_user_area_utm() + RETURNS geometry + AS $$ + DECLARE utm_area geometry; + BEGIN + SELECT ST_Transform(area::geometry, best_utm(area)) + INTO STRICT utm_area + FROM users.responsibility_areas + WHERE country = users.current_user_country(); + RETURN utm_area; + END; + $$ + LANGUAGE plpgsql + STABLE PARALLEL SAFE; + + +CREATE OR REPLACE FUNCTION users.utm_covers(g geography) RETURNS boolean AS + $$ + SELECT ST_Covers(a, ST_Transform(g::geometry, ST_SRID(a))) + FROM users.current_user_area_utm() AS a (a) + $$ + LANGUAGE SQL + STABLE PARALLEL SAFE; + + CREATE OR REPLACE FUNCTION internal.create_user() RETURNS trigger AS $$ BEGIN
--- a/schema/manage_users_tests.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/manage_users_tests.sql Mon Jun 03 10:19:18 2019 +0200 @@ -4,7 +4,7 @@ -- SPDX-License-Identifier: AGPL-3.0-or-later -- License-Filename: LICENSES/AGPL-3.0.txt --- Copyright (C) 2018 by via donau +-- Copyright (C) 2018, 2019 by via donau -- – Österreichische Wasserstraßen-Gesellschaft mbH -- Software engineering by Intevation GmbH @@ -17,10 +17,23 @@ SET search_path TO public, gemma, gemma_waterway, gemma_fairway; +SET SESSION AUTHORIZATION test_user_at; +-- +-- Utility functions +-- +SELECT results_eq($$ + SELECT ST_SRID(users.current_user_area_utm()) + $$, + $$ + SELECT best_utm(area) + FROM users.responsibility_areas + WHERE country = users.current_user_country() + $$, + 'Geometry has SRID corresponding to best_utm()'); + -- -- Role listing -- -SET SESSION AUTHORIZATION test_user_at; SELECT results_eq($$ SELECT username FROM users.list_users $$,
--- a/schema/run_tests.sh Wed May 29 10:58:45 2019 +0200 +++ b/schema/run_tests.sh Mon Jun 03 10:19:18 2019 +0200 @@ -28,7 +28,10 @@ -c 'SET client_min_messages TO WARNING' \ -c "DROP ROLE IF EXISTS $TEST_ROLES" \ -f tap_tests_data.sql \ - -c 'SELECT plan(63)' \ + -c "SELECT plan(70 + ( + SELECT count(*)::int + FROM information_schema.tables + WHERE table_schema = 'waterway'))" \ -f gemma_tests.sql \ -f isrs_tests.sql \ -f auth_tests.sql \
--- a/schema/search_functions.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/search_functions.sql Mon Jun 03 10:19:18 2019 +0200 @@ -69,12 +69,32 @@ END; $$; +CREATE OR REPLACE FUNCTION search_sections(search_string text) RETURNS jsonb + LANGUAGE plpgsql + AS $$ +DECLARE + _result jsonb; +BEGIN + SELECT COALESCE(json_agg(r),'[]') + INTO _result + FROM (SELECT objnam AS name, + ST_AsGeoJSON(ST_Centroid(area))::json AS geom, + 'section' AS type + FROM waterway.sections + WHERE objnam ILIKE '%' || search_string || '%' + OR nobjnam ILIKE '%' || search_string || '%' + ORDER BY name) r; + RETURN _result; +END; +$$; + CREATE OR REPLACE FUNCTION search_most(search_string text) RETURNS jsonb LANGUAGE plpgsql AS $$ BEGIN RETURN search_bottlenecks(search_string) || search_gauges(search_string) + || search_sections(search_string) || search_cities(search_string); END; $$;
--- a/schema/tap_tests_data.sql Wed May 29 10:58:45 2019 +0200 +++ b/schema/tap_tests_data.sql Mon Jun 03 10:19:18 2019 +0200 @@ -37,22 +37,30 @@ INSERT INTO limiting_factors VALUES ('depth'), ('width'); -INSERT INTO waterway.gauges ( - location, objname, geom, zero_point, source_organization) +WITH +gs AS ( + INSERT INTO waterway.gauges ( + location, + validity, + objname, + geom, + zero_point, + date_info, + source_organization, + lastupdate) VALUES ( ('AT', 'XXX', '00001', 'G0001', 1)::isrs, + tstzrange(current_timestamp - '1 day'::interval, current_timestamp), 'testgauge', ST_geomfromtext('POINT(0 0)', 4326), 0, - 'testorganization' - ); - -INSERT INTO waterway.bottlenecks ( - bottleneck_id, fk_g_fid, stretch, area, rb, lb, responsible_country, - revisiting_time, limiting, source_organization, staging_done) + current_timestamp, + 'testorganization', + current_timestamp) + RETURNING location, validity), +bns AS ( VALUES ( 'testbottleneck1', - ('AT', 'XXX', '00001', 'G0001', 1)::isrs, isrsrange(('AT', 'XXX', '00001', '00000', 0)::isrs, ('AT', 'XXX', '00001', '00000', 2)::isrs), ST_geomfromtext('MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)))', 4326), @@ -60,13 +68,17 @@ 1, 'depth', 'testorganization', false ), ( 'testbottleneck2', - ('AT', 'XXX', '00001', 'G0001', 1)::isrs, isrsrange(('AT', 'XXX', '00001', '00000', 0)::isrs, ('AT', 'XXX', '00001', '00000', 2)::isrs), ST_geomfromtext('MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)))', 4326), 'AT', 'AT', 'AT', 1, 'depth', 'testorganization', true - ); + )) +INSERT INTO waterway.bottlenecks ( + gauge_location, gauge_validity, + bottleneck_id, stretch, area, rb, lb, responsible_country, + revisiting_time, limiting, source_organization, staging_done) + SELECT * FROM gs, bns; INSERT INTO waterway.distance_marks_virtual VALUES ( ('AT', 'XXX', '00001', '00000', 0)::isrs,