view client/src/components/identify/Identify.vue @ 4270:e4d6c6339cb4

identify_tool: accuracy legend improved
author Thomas Junk <thomas.junk@intevation.de>
date Tue, 27 Aug 2019 17:07:16 +0200
parents 837f90680d4c
children 12398df5f414
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: forecastAccuracyColor(feature),
                  'font-size': 'x-large'
                }"
              />
              <small class="my-auto">
                {{ forecastAccuracy(feature) }}
              </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>
            <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.surveyCurrency(feature)];
    },
    recencyColor(feature) {
      return recencyColorCodes[classifications.surveyCurrency(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>