changeset 3204:1253fe15e3e3

client: identify: implemented popup when clicking the map and the intention is not clear... ...because multiple features were identified. In that case no dialog will be opened automatically but a popup with possible options is shown.
author Markus Kottlaender <markus@intevation.de>
date Wed, 08 May 2019 17:10:17 +0200
parents cb67ee72485b
children bf571429515f
files client/src/components/App.vue client/src/components/Bottlenecks.vue client/src/components/fairway/Profiles.vue client/src/components/map/MapPopup.vue client/src/store/bottlenecks.js client/src/store/map.js
diffstat 6 files changed, 333 insertions(+), 93 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/App.vue	Wed May 08 16:19:31 2019 +0200
+++ b/client/src/components/App.vue	Wed May 08 17:10:17 2019 +0200
@@ -23,6 +23,7 @@
           <Toolbar v-if="isMapVisible" />
         </div>
       </div>
+      <MapPopup />
     </div>
     <router-view />
     <vue-snotify />
@@ -101,7 +102,8 @@
     Contextbox: () => import("./Contextbox"),
     Toolbar: () => import("./toolbar/Toolbar"),
     Popup: () => import("./Popup"),
-    Statistics: () => import("./Statistics")
+    Statistics: () => import("./Statistics"),
+    MapPopup: () => import("./map/MapPopup")
   }
 };
 </script>
--- a/client/src/components/Bottlenecks.vue	Wed May 08 16:19:31 2019 +0200
+++ b/client/src/components/Bottlenecks.vue	Wed May 08 17:10:17 2019 +0200
@@ -156,9 +156,6 @@
           bottleneck.properties.name
         )
         .then(() => {
-          this.$store.commit("bottlenecks/setFirstSurveySelected");
-        })
-        .then(() => {
           this.$store.dispatch("map/moveToFeauture", {
             feature: bottleneck,
             zoom: 17,
--- a/client/src/components/fairway/Profiles.vue	Wed May 08 16:19:31 2019 +0200
+++ b/client/src/components/fairway/Profiles.vue	Wed May 08 17:10:17 2019 +0200
@@ -342,11 +342,7 @@
         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: {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/map/MapPopup.vue	Wed May 08 17:10:17 2019 +0200
@@ -0,0 +1,243 @@
+<template>
+  <div class="map-popup rounded" ref="map-popup">
+    <UIBoxHeader :title="title" :closeCallback="close" small />
+    <div class="p-1 small text-nowrap">
+      <div
+        class="d-flex flex-nowrap justify-content-between align-items-center"
+        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"
+        >
+          <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/showStatistics", true);
+      this.close();
+    },
+    openFairwayAvailabilityForBottleneck(bottleneck) {
+      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("imports/selectedStretchId", stretch.getId());
+      this.$store.dispatch("map/moveToFeauture", {
+        feature: stretch,
+        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>
--- a/client/src/store/bottlenecks.js	Wed May 08 16:19:31 2019 +0200
+++ b/client/src/store/bottlenecks.js	Wed May 08 17:10:17 2019 +0200
@@ -73,9 +73,6 @@
               .clear();
           });
         }
-        if (name) {
-          commit("application/showProfiles", true, { root: true });
-        }
         commit("setSelectedBottleneck", name);
         if (name) {
           commit("surveysLoading", true);
@@ -90,6 +87,7 @@
                 a.date_info < b.date_info ? 1 : -1
               );
               commit("setSurveys", surveys);
+              commit("setFirstSurveySelected");
               resolve(response);
             })
             .catch(error => {
--- a/client/src/store/map.js	Wed May 08 16:19:31 2019 +0200
+++ b/client/src/store/map.js	Wed May 08 17:10:17 2019 +0200
@@ -18,13 +18,13 @@
 import { Stroke, Style, Fill, Circle } from "ol/style";
 import { fromLonLat } from "ol/proj";
 import { getLength, getArea } from "ol/sphere";
-import { getCenter } from "ol/extent";
 import { transformExtent } from "ol/proj";
 import bbox from "@turf/bbox";
 import app from "@/main";
 import { HTTP } from "@/lib/http";
 import Feature from "ol/Feature";
 import Point from "ol/geom/Point";
+import { toLonLat } from "ol/proj";
 
 // initial state
 const init = () => {
@@ -32,6 +32,7 @@
     openLayersMaps: [],
     syncedMaps: [],
     syncedView: null,
+    mapPopup: null,
     initialLoad: true,
     extent: {
       lat: 6155376,
@@ -39,6 +40,7 @@
       zoom: 11
     },
     identifiedFeatures: [], // map features identified by clicking on the map
+    identifiedCoordinates: null,
     currentMeasurement: null, // distance or area from line-/polygon-/cutTool
     lineToolEnabled: false,
     polygonToolEnabled: false,
@@ -86,6 +88,9 @@
     syncedView: (state, view) => {
       state.syncedView = view;
     },
+    mapPopup: (state, popup) => {
+      state.mapPopup = popup;
+    },
     setIdentifiedFeatures: (state, identifiedFeatures) => {
       state.identifiedFeatures = identifiedFeatures;
     },
@@ -94,6 +99,9 @@
         identifiedFeatures
       );
     },
+    identifiedCoordinates: (state, coordinates) => {
+      state.identifiedCoordinates = coordinates;
+    },
     setCurrentMeasurement: (state, measurement) => {
       state.currentMeasurement = measurement;
     },
@@ -286,6 +294,11 @@
     },
     initIdentifyTool({ state, rootState, commit, dispatch }, map) {
       map.on(["singleclick", "dblclick"], event => {
+        commit(
+          "identifiedCoordinates",
+          toLonLat(event.coordinate, map.getView().getProjection())
+        );
+        state.mapPopup.setPosition(undefined);
         if (
           state.lineToolEnabled ||
           state.polygonToolEnabled ||
@@ -296,93 +309,78 @@
         // checking our WFS layers
         var features = map.getFeaturesAtPixel(event.pixel, { hitTolerance: 7 });
         if (features) {
-          let identifiedFeatures = [];
+          let all = [];
+          let bottlenecks = [];
+          let gauges = [];
+          let stretches = [];
+          let sections = [];
 
           for (let feature of features) {
+            // avoid identifying the same feature twice
+            if (all.findIndex(f => f.getId() === feature.getId()) === -1)
+              all.push(feature);
+
             let id = feature.getId();
-            // avoid identifying the same feature twice
-            if (
-              identifiedFeatures.findIndex(
-                f => f.getId() === feature.getId()
-              ) === -1
-            ) {
-              identifiedFeatures.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");
-                });
-                dispatch("moveMap", {
-                  coordinates: getCenter(
-                    feature
-                      .getGeometry()
-                      .clone()
-                      .transform("EPSG:3857", "EPSG:4326")
-                      .getExtent()
-                  ),
-                  zoom: 17,
-                  preventZoomOut: true
-                });
-              }
-            }
-
+            // get selected bottleneck
+            if (/^bottlenecks/.test(id)) bottlenecks.push(feature);
             // get selected gauge
-            if (/^gauges/.test(id)) {
-              if (
-                rootState.gauges.selectedGaugeISRS !== feature.get("isrs_code")
-              ) {
-                dispatch("gauges/selectedGaugeISRS", feature.get("isrs_code"), {
-                  root: true
-                });
-                dispatch("moveMap", {
-                  coordinates: getCenter(
-                    feature
-                      .getGeometry()
-                      .clone()
-                      .transform("EPSG:3857", "EPSG:4326")
-                      .getExtent()
-                  ),
-                  zoom: 15,
-                  preventZoomOut: true
-                });
-              }
-            }
+            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 stretch
-            if (/^stretches/.test(id)) {
-              if (rootState.imports.selectedStretchId === feature.getId()) {
-                commit("imports/selectedStretchId", null, { root: true });
-              } else {
-                commit("imports/selectedStretchId", feature.getId(), {
-                  root: true
-                });
-                dispatch("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.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("application/showStatistics", false, { root: true });
+              commit("imports/selectedStretchId", null, {
+                root: true
+              });
+            } else {
+              commit("application/showStatistics", true, { root: true });
+              commit("imports/selectedStretchId", stretches[0].getId(), {
+                root: true
+              });
+              dispatch("moveToFeauture", { feature: stretches[0], zoom: 17 });
             }
           }
-
-          commit("setIdentifiedFeatures", identifiedFeatures);
         }
 
         // DEBUG output and example how to remove the GeometryName
@@ -531,7 +529,13 @@
       });
     },
     moveToFeauture({ dispatch }, { feature, zoom, preventZoomOut }) {
-      const boundingBox = bbox(feature.geometry);
+      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 }) {