view client/src/components/identify/Identify.vue @ 4279:30f26bf7ff24

Reordering of elements In order to improve user experience the configuration of avail, forceast vs. real and accuracy was changed in such a way, that it now mirrors the optics of the displayed triangle. The order in the identify tool was changed accordingly 1) avail 2) forcast vs. real 3) accuracy "Currency" was cleaned up to "recency"
author Thomas Junk <thomas.junk@intevation.de>
date Thu, 29 Aug 2019 15:04:02 +0200
parents 12398df5f414
children 81ab34bd2d0d
line wrap: on
line source

<template>
  <div
    :class="[
      'box ui-element rounded bg-white text-nowrap',
      { expanded: showIdentify }
    ]"
  >
    <div style="width: 18rem">
      <UIBoxHeader
        icon="info"
        :title="identifiedLabel"
        :closeCallback="close"
      />
      <div class="features">
        <div v-if="currentMeasurement">
          <small class="d-block bg-secondary text-light px-2 py-1">
            <translate> Measurement</translate>
          </small>
          <small class="d-flex justify-content-between px-2">
            <b>
              {{ currentMeasurement.quantity }}
            </b>
            {{ currentMeasurement.value }} {{ currentMeasurement.unitSymbol }}
          </small>
        </div>
        <div
          v-for="feature of filteredIdentifiedFeatures"
          :key="feature.getId()"
        >
          <small
            class="d-flex justify-content-between bg-secondary text-light px-2 py-1"
          >
            {{ featureLabel(feature) }}
            <a
              v-if="feature.getProperties().hasOwnProperty('bbox')"
              @click="zoomTo(feature)"
              class="btn btn-info btn-xs pointer rounded-0 zoom-btn"
            >
              <font-awesome-icon icon="crosshairs" />
            </a>
          </small>
          <div
            v-if="showBottleneckMeta(feature)"
            class="ml-2 mb-1 text-left d-flex flex-column"
          >
            <div>
              <font-awesome-icon
                icon="caret-up"
                fixed-width
                :style="{
                  color: recencyColor(feature),
                  'font-size': 'x-large'
                }"
              />
              <small class="my-auto">
                {{ recency(feature) }}
              </small>
            </div>
            <div>
              <small><translate>According gauge data:</translate></small>
            </div>
            <div>
              <font-awesome-icon
                icon="caret-up"
                fixed-width
                :style="{
                  color: gmAvailabilityColor(feature),
                  'font-size': 'x-large'
                }"
              />
              <small class="my-auto">
                {{ gmAvailability(feature) }}
              </small>
            </div>
            <div>
              <font-awesome-icon
                icon="caret-up"
                fixed-width
                :style="{
                  color: forecastVsRealityColor(feature),
                  'font-size': 'x-large'
                }"
              />
              <small class="my-auto">
                {{ forecastVsReality(feature) }}
              </small>
            </div>
            <div>
              <font-awesome-icon
                icon="caret-up"
                fixed-width
                :style="{
                  color: forecastAccuracyColor(feature),
                  'font-size': 'x-large'
                }"
              />
              <small class="my-auto">
                {{ forecastAccuracy(feature) }}
              </small>
            </div>
          </div>
          <div>
            <small
              v-for="prop in featureProps(feature)"
              :key="prop.key"
              v-if="prop.val"
              class="d-flex justify-content-between px-2"
            >
              <b>{{ prop.key }}</b>
              <span>{{ prop.val }}</span>
            </small>
          </div>
        </div>
        <div
          v-if="!currentMeasurement && !filteredIdentifiedFeatures.length"
          class="text-muted small text-center my-auto py-3 px-2"
        >
          <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 />
          %{ license } without warranty, see docs for details.
        </span>
        <br />
        <a href="https://hg.intevation.de/gemma/file/tip">
          <translate>source-code</translate>
        </a>
        {{ versionStr }} <br />© via donau. &#x24D4; Intevation. <br />
        <span v-translate="{ name: 'OpenSteetMap' }"
          >Some data ©
          <a href="https://www.openstreetmap.org/copyright">%{ name }</a>
          contributors.
        </span>
        <br />
        <span v-translate="{ geoLicense: 'CC-BY-4.0' }">
          Uses
          <a href="https://download.geonames.org/export/dump/readme.txt"
            >GeoNames</a
          >
          under %{ geoLicense }.
        </span>
        <translate>Generated PDFs use font: </translate>
        <a href="http://libertine-fonts.org">LinBiolinum</a>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.features {
  max-height: 19rem;
  overflow-y: auto;
  small {
    .zoom-btn {
      margin-top: -0.25rem;
      margin-right: -0.5rem;
      margin-bottom: -0.25rem;
      svg {
        vertical-align: middle;
      }
    }
    &:nth-child(even) {
      background: #f8f8f8;
    }
    &:hover {
      background: #eeeeee;
    }
  }
}

.versioninfo {
  font-size: 60%;
  white-space: normal;
}
</style>

<script>
/* This is Free Software under GNU Affero General Public License v >= 3.0
 * without warranty, see README.md and license for details.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 * License-Filename: LICENSES/AGPL-3.0.txt
 *
 * Copyright (C) 2018, 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>
 * Markus Kottländer <markus.kottlaender@intevation.de>
 */
import { mapState, mapGetters } from "vuex";
import { formatter } from "./formatter";
import { getCenter } from "ol/extent";
import classifications from "../../lib/classifications";
import { styleFactory } from "@/components/map/styles";

const {
  recencyColorCodes,
  gmAvailabilityColorCodes,
  forecastAccuracyColorCodes,
  forecastVsRealityColorCodes
} = styleFactory();

export default {
  name: "identify",
  computed: {
    ...mapGetters("application", ["versionStr"]),
    ...mapState("application", ["showIdentify", "userManualUrl", "config"]),
    ...mapGetters("map", ["filteredIdentifiedFeatures"]),
    ...mapState("map", ["currentMeasurement"]),
    identifiedLabel() {
      return this.$gettext("Identified Features");
    },
    usermanualFilename() {
      return this.$gettext("User Manual");
    }
  },
  methods: {
    gmAvailability(feature) {
      const latestInHours = this.config.gm_latest_hours;
      const measurementsIn14D = this.config.gm_min_values_14d;
      const messagesPerState = {
        OK:
          this.$gettext("Avail: Last measurement <") +
          ` ${latestInHours} (${measurementsIn14D} in 14d)`,
        WARNING:
          this.$gettext("Avail: Below treshold") +
          `: ${measurementsIn14D} in 14d`,
        DANGER:
          this.$gettext("Avail: Latest measurement older than") +
          ` ${latestInHours} d`
      };
      return messagesPerState[[classifications.gmAvailability(feature)]];
    },
    gmAvailabilityColor(feature) {
      return gmAvailabilityColorCodes[classifications.gmAvailability(feature)];
    },
    forecastAccuracy(feature) {
      const offset24 = this.config.gm_forecast_offset_24h;
      const offset72 = this.config.gm_forecast_offset_72h;
      const messagesPerState = {
        OK:
          this.$gettext("Highest confidence") +
          ` <${offset24} cm/24h, <${offset72} cm/72h`,
        WARNING: this.$gettext("Confidence per 72h") + ` > ${offset72} cm`,
        DANGER: this.$gettext("Confidence per 24h") + ` > ${offset24} cm`
      };
      return messagesPerState[[classifications.forecastAccuracy(feature)]];
    },
    forecastAccuracyColor(feature) {
      return forecastAccuracyColorCodes[
        classifications.forecastAccuracy(feature)
      ];
    },
    forecastVsReality(feature) {
      const nsc24 = this.config.gm_forecast_vs_reality_nsc_24h;
      const nsc72 = this.config.gm_forecast_vs_reality_nsc_72h;
      const messagesPerState = {
        OK: this.$gettext("Nash-Sutcliffe") + `>${nsc24} /24h >${nsc72} / 72h`,
        WARNING: this.$gettext("Nash-Sutcliffe") + ` < ${nsc72}`,
        DANGER: this.$gettext("Nash-Sutcliffe") + ` < ${nsc24}`,
        NEUTRAL: this.$gettext("Nash-Sutcliffe not available")
      };
      return messagesPerState[[classifications.forecastVsReality(feature)]];
    },
    forecastVsRealityColor(feature) {
      return forecastVsRealityColorCodes[
        classifications.forecastVsReality(feature)
      ];
    },
    recency(feature) {
      const revisitingFactor = this.config.bn_revtime_multiplier;
      const messagesPerState = {
        OK: this.$gettext("Data within the revisiting time"),
        WARNING:
          this.$gettext("Data within revisiting treshold") +
          ` (${revisitingFactor})`,
        DANGER:
          this.$gettext("Data too old. Treshold:") + ` (${revisitingFactor})`
      };
      return messagesPerState[classifications.surveyRecency(feature)];
    },
    recencyColor(feature) {
      return recencyColorCodes[classifications.surveyRecency(feature)];
    },
    showBottleneckMeta(feature) {
      const result = /bottleneck/.test(feature.getId().toLowerCase());
      return result;
    },
    zoomTo(feature) {
      this.$store.dispatch("map/moveMap", {
        coordinates: getCenter(
          feature
            .getGeometry()
            .clone()
            .transform("EPSG:3857", "EPSG:4326")
            .getExtent()
        ),
        zoom: 17,
        preventZoomOut: true
      });
    },
    close() {
      this.$store.commit("application/showIdentify", false);
    },
    featureId(feature) {
      // cut away everything from the last . to the end
      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))) {
        return formatter[this.featureId(feature)].label;
      }
      return this.featureId(feature);
    },
    featureProps(feature) {
      let featureId = this.featureId(feature);

      // create array with {key, val} objects
      //   skip geometry here, because it is slightly more robust
      //   to get the name of the property to skip and we need a reference
      //   to `feature` for doing so.
      //   The geometry is not needed (and previous comments in the code
      //      mentioned a problem with it becoming cyclic when left in).
      let skipList = [feature.getGeometryName()];
      let propsArray = [];
      Object.keys(feature.getProperties()).forEach(key => {
        if (skipList.indexOf(key) === -1) {
          let val = feature.getProperties()[key];

          // if val is a valid json object string,
          // spread its values into the array
          let jsonObj = this.getObjectFromString(val);
          if (jsonObj) {
            Object.keys(jsonObj).forEach(key => {
              propsArray.push({ key, val: jsonObj[key] });
            });
          } else {
            // otherwise just put the key value pair into the array
            propsArray.push({ key, val });
          }
        }
      });

      // run general formatter
      propsArray = propsArray.map(formatter.all).filter(p => p);
      // run feature specific formatter
      if (
        formatter.hasOwnProperty(featureId) &&
        formatter[featureId].hasOwnProperty("props")
      ) {
        propsArray = propsArray.map(formatter[featureId].props).filter(p => p);
      }
      // remove underscores in labels that where not previously changed already
      propsArray = propsArray.map(prop => {
        return { key: prop.key.replace(/_/g, " "), val: prop.val };
      });

      return propsArray;
    },
    getObjectFromString(val) {
      // JSON.parse() accepts integers and null as valid json. So to be sure to
      // get an object, we cannot just try JSON.parse() but we need to check if
      // the given value is a string and starts with a {.
      if (
        Object.prototype.toString.call(val) === "[object String]" &&
        val[0] === "{"
      ) {
        try {
          return JSON.parse(val);
        } catch (e) {
          return null;
        }
      }
      return null;
    }
  }
};
</script>