changeset 3292:7a88b37bce8b

merge
author Thomas Junk <thomas.junk@intevation.de>
date Thu, 16 May 2019 14:15:45 +0200
parents 6996aa1d2df9 (current diff) 3ada3d0347bd (diff)
children 76f643d20f19
files
diffstat 5 files changed, 1068 insertions(+), 401 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/sections/SectionForm.vue	Thu May 16 14:15:45 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 mr-1">
+        <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	Thu May 16 14:15:45 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>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/stretches/StretchForm.vue	Thu May 16 14:15:45 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_Section_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>
--- a/client/src/components/stretches/Stretches.vue	Thu May 16 12:53:46 2019 +0200
+++ b/client/src/components/stretches/Stretches.vue	Thu May 16 14:15:45 2019 +0200
@@ -1,13 +1,10 @@
 <template>
   <div class="d-flex flex-column">
-    <UIBoxHeader
-      icon="road"
-      :title="defineStretchesLabel"
-      :closeCallback="$parent.close"
-    />
+    <UIBoxHeader icon="road" :title="title" :closeCallback="$parent.close" />
     <div class="position-relative">
       <UISpinnerOverlay v-if="loading" />
-      <div v-if="!edit">
+      <StretchForm v-if="showForm" :editStretch="editStretch" />
+      <div v-else>
         <UITableHeader
           :columns="[
             { id: 'properties.name', title: `${nameLabel}`, class: 'col-4' },
@@ -52,7 +49,10 @@
               </button>
               <button
                 class="btn btn-xs btn-dark mr-1"
-                @click="editStretch(stretch)"
+                @click="
+                  showForm = true;
+                  editStretch = stretch;
+                "
               >
                 <font-awesome-icon icon="pencil-alt" fixed-width />
               </button>
@@ -66,224 +66,17 @@
           </template>
         </UITableBody>
         <div class="text-right p-2 border-top">
-          <button :key="1" @click="startEdit()" class="btn btn-sm btn-info">
+          <button
+            @click="
+              showForm = true;
+              editStretch = null;
+            "
+            class="btn btn-sm btn-info"
+          >
             <translate>New stretch</translate>
           </button>
         </div>
       </div>
-      <div v-if="edit">
-        <div class="mx-2">
-          <div class="d-flex justify-content-between mt-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_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="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"
-                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>
-          <div class="d-flex justify-content-between mt-2">
-            <div class="text-left flex-fill mr-1">
-              <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"
-                  aria-label="startrhm"
-                  v-model="startrhm"
-                />
-                <span
-                  class="input-group-text position-absolute input-button"
-                  @click="togglePipette('start')"
-                  v-tooltip="pipetteTooltip"
-                >
-                  <font-awesome-icon
-                    :class="{ 'text-info': pipetteStart }"
-                    icon="crosshairs"
-                  />
-                </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="text-left flex-fill ml-1">
-              <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"
-                  aria-label="endrhm"
-                  v-model="endrhm"
-                />
-                <span
-                  class="input-group-text position-absolute input-button"
-                  @click="togglePipette('end')"
-                  v-tooltip="pipetteTooltip"
-                >
-                  <font-awesome-icon
-                    :class="{ 'text-info': pipetteEnd }"
-                    icon="crosshairs"
-                  />
-                </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 form-control-sm"
-                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 form-control-sm"
-                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 form-control-sm"
-                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 form-control-sm"
-                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 form-control-sm"
-                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="d-flex justify-content-between mt-2 p-2 border-top">
-          <button :key="2" @click="edit = false" class="btn btn-sm btn-warning">
-            Back
-          </button>
-          <button
-            @click="save"
-            type="submit"
-            class="shadow-sm btn btn-sm btn-info submit-button"
-          >
-            <translate>Submit</translate>
-          </button>
-        </div>
-      </div>
     </div>
   </div>
 </template>
@@ -317,45 +110,23 @@
 import { sortTable } from "@/lib/mixins";
 
 export default {
-  name: "importstretches",
   mixins: [sortTable],
+  components: {
+    StretchForm: () => import("./StretchForm")
+  },
   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,
-      loading: false
+      loading: false,
+      showForm: false,
+      editStretch: null
     };
   },
   computed: {
     ...mapState("application", ["searchQuery"]),
-    ...mapState("map", ["identifiedFeatures", "currentMeasurement"]),
     ...mapGetters("map", ["openLayersMap"]),
-    ...mapGetters("user", ["isSysAdmin"]),
     ...mapState("imports", ["stretches"]),
-    defineStretchesLabel() {
+    title() {
       return this.$gettext("Define Stretches");
     },
     nameLabel() {
@@ -369,24 +140,6 @@
     },
     reviewTooltip() {
       return this.$gettext("Review pending import");
-    },
-    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: {
@@ -438,19 +191,6 @@
           });
         });
     },
-    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.replace(/[,()]/g, "");
-      this.endrhm = properties.upper.replace(/[,()]/g, "");
-      this.editExistingStretch = true;
-    },
     deleteStretch(stretch) {
       this.$store.commit("application/popup", {
         icon: "trash",
@@ -489,123 +229,9 @@
         zoom: 17,
         preventZoomOut: true
       });
-    },
-    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.openLayersMap()
-        .getLayer("DISTANCEMARKSAXIS")
-        .setVisible(true);
-      if (t === "start") {
-        this.$store.commit("map/mapPopupEnabled", this.pipetteStart);
-        this.pipetteStart = !this.pipetteStart;
-        this.pipetteEnd = false;
-      } else {
-        this.$store.commit("map/mapPopupEnabled", this.pipetteEnd);
-        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.$store.dispatch("imports/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.loading = true;
     this.$store
       .dispatch("imports/loadStretches")
--- a/client/src/store/imports.js	Thu May 16 12:53:46 2019 +0200
+++ b/client/src/store/imports.js	Thu May 16 14:15:45 2019 +0200
@@ -33,6 +33,8 @@
     warning: false,
     stretches: [],
     selectedStretchId: null,
+    sections: [],
+    selectedSectionId: null,
     imports: [],
     reviewed: [],
     show: null,
@@ -46,19 +48,19 @@
   };
 };
 
-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 +134,12 @@
     selectedStretchId: (state, id) => {
       state.selectedStretchId = id;
     },
+    setSections: (state, sections) => {
+      state.sections = sections;
+    },
+    selectedSectionId: (state, id) => {
+      state.selectedSectionId = id;
+    },
     setReviewed: (state, reviewed) => {
       state.reviewed = reviewed;
     },
@@ -194,7 +202,7 @@
   actions: {
     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);
@@ -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 = "";