changeset 2486:bca9a7a89f28 octree-diff

Merged default into octree-diff branch.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Fri, 01 Mar 2019 18:28:50 +0100
parents 86173ac7f222 (current diff) 219c5b57ef5b (diff)
children bd46ffbb944e
files
diffstat 14 files changed, 323 insertions(+), 159 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/ImportSoundingresults.vue	Fri Mar 01 18:28:12 2019 +0100
+++ b/client/src/components/ImportSoundingresults.vue	Fri Mar 01 18:28:50 2019 +0100
@@ -111,7 +111,10 @@
             <a
               download="meta.json"
               :href="dataLink"
-              class="btn btn-outline-info"
+              :class="[
+                'btn btn-outline-info',
+                { disabled: !bottleneck || !importDate || !depthReference }
+              ]"
             >
               <translate>Download Meta.json</translate>
             </a>
@@ -323,16 +326,18 @@
       return this.$gettext("Confirm");
     },
     dataLink() {
-      return (
-        "data:text/json;charset=utf-8," +
-        encodeURIComponent(
-          JSON.stringify({
-            depthReference: this.depthReference,
-            bottleneck: this.bottleneck.properties.objnam,
-            date: this.importDate
-          })
-        )
-      );
+      if (this.bottleneck && this.depthReference && this.import) {
+        return (
+          "data:text/json;charset=utf-8," +
+          encodeURIComponent(
+            JSON.stringify({
+              depthReference: this.depthReference,
+              bottleneck: this.bottleneck.properties.objnam,
+              date: this.importDate
+            })
+          )
+        );
+      }
     },
     depthReferenceOptions() {
       if (
--- a/client/src/components/Maplayer.vue	Fri Mar 01 18:28:12 2019 +0100
+++ b/client/src/components/Maplayer.vue	Fri Mar 01 18:28:50 2019 +0100
@@ -337,6 +337,21 @@
     );
     layer.data.setVisible(layer.isVisible);
 
+    layer = this.getLayerByName(LAYERS.GAUGES);
+    layer.data.getSource().setLoader(
+      this.buildVectorLoader(
+        {
+          featureNS: "gemma",
+          featurePrefix: "gemma",
+          featureTypes: ["gauges_geoserver"],
+          geometryName: "geom"
+        },
+        "/internal/wfs",
+        layer.data.getSource()
+      )
+    );
+    layer.data.setVisible(layer.isVisible);
+
     layer = this.getLayerByName(LAYERS.STRETCHES);
     layer.data.getSource().setLoader(
       this.buildVectorLoader(
--- a/client/src/components/Search.vue	Fri Mar 01 18:28:12 2019 +0100
+++ b/client/src/components/Search.vue	Fri Mar 01 18:28:50 2019 +0100
@@ -58,6 +58,12 @@
             class="mr-1"
             fixed-width
           />
+          <font-awesome-icon
+            icon="ruler"
+            v-if="entry.type === 'gauge'"
+            class="mr-1"
+            fixed-width
+          />
           {{ entry.name }}
         </a>
       </div>
@@ -269,6 +275,7 @@
         if (resultEntry.type === "bottleneck") zoom = 17;
         if (resultEntry.type === "rhm") zoom = 15;
         if (resultEntry.type === "city") zoom = 13;
+        if (resultEntry.type === "gauge") zoom = 15;
         this.$store.commit("map/moveMap", {
           coordinates: resultEntry.geom.coordinates,
           zoom,
--- a/client/src/components/importoverview/ImportOverview.vue	Fri Mar 01 18:28:12 2019 +0100
+++ b/client/src/components/importoverview/ImportOverview.vue	Fri Mar 01 18:28:50 2019 +0100
@@ -7,10 +7,10 @@
     />
     <div class="d-flex flex-row w-100 justify-content-end">
       <button
-        class="btn btn-sm btn-dark align-self-start mt-3 mr-3"
+        class="btn btn-sm btn-outline-info align-self-start mt-3 mr-3"
         @click="refresh"
       >
-        <translate>Refresh</translate>
+        <font-awesome-icon icon="redo"></font-awesome-icon>
       </button>
     </div>
     <div class="d-flex flex-row w-100 border-bottom">
@@ -55,7 +55,7 @@
           icon="angle-down"
           fixed-width
         ></font-awesome-icon>
-        <Logs v-if="logsVisible"></Logs>
+        <Logs v-if="logsVisible" :reload="reload"></Logs>
         <div v-else>
           <h6>
             <small><translate>Logs</translate></small>
@@ -85,6 +85,11 @@
 
 export default {
   name: "importoverview",
+  data() {
+    return {
+      reload: false
+    };
+  },
   components: {
     Staging: () => import("./staging/Staging.vue"),
     Logs: () => import("./importlogs/Logs.vue")
@@ -100,22 +105,30 @@
       this.$store.commit("imports/setLogsVisibility", !this.logsVisible);
     },
     refresh() {
+      this.reload = true;
       this.loadImportQueue();
       this.loadLogs();
     },
     loadImportQueue() {
-      this.$store.dispatch("imports/getStaging").catch(error => {
-        const { status, data } = error.response;
-        displayError({
-          title: "Backend Error",
-          message: `${status}: ${data.message || data}`
+      this.$store
+        .dispatch("imports/getStaging")
+        .then(() => {
+          this.reload = false;
+        })
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: "Backend Error",
+            message: `${status}: ${data.message || data}`
+          });
         });
-      });
     },
     loadLogs() {
       this.$store
         .dispatch("imports/getImports")
-        .then(() => {})
+        .then(() => {
+          this.reload = false;
+        })
         .catch(error => {
           const { status, data } = error.response;
           displayError({
--- a/client/src/components/importoverview/importlogs/LogDetail.vue	Fri Mar 01 18:28:12 2019 +0100
+++ b/client/src/components/importoverview/importlogs/LogDetail.vue	Fri Mar 01 18:28:50 2019 +0100
@@ -51,72 +51,62 @@
     </div>
     <div class="detailstable d-flex flex-row">
       <div :class="collapse">
-        <table class="table table-responsive">
-          <thead>
-            <tr>
-              <th class="type pb-0">
-                <small class="condensed"><translate>Kind</translate></small>
-              </th>
-              <th class="datetime  pb-0">
-                <a href="#" @click="sortAsc = !sortAsc" class="sort-link"
-                  ><small class="condensed"><translate>Date</translate></small>
-                  <small class="condensed"
-                    ><font-awesome-icon
-                      :icon="sortIcon"
-                      class="ml-1"
-                    ></font-awesome-icon></small
-                ></a>
-              </th>
-              <th class="message pb-0">
-                <small class="condensed"><translate>Message</translate></small>
-              </th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr
-              v-for="(entry, index) in sortedEntries"
-              :key="index"
-              class="detailsrow"
+        <div class="text-left">
+          <small style="margin-right:10px" class="type condensed"
+            ><translate>Kind</translate></small
+          >
+          <a
+            href="#"
+            @click="sortAsc = !sortAsc"
+            style="margin-right:58px"
+            class="datetime sort-link"
+            ><small class="condensed"><translate>Date</translate></small>
+            <small class="message condensed"
+              ><font-awesome-icon
+                :icon="sortIcon"
+                class="ml-1"
+              ></font-awesome-icon></small
+          ></a>
+          <small class="condensed"><translate>Message</translate></small>
+        </div>
+        <div class="logentries">
+          <div
+            v-for="(entry, index) in sortedEntries"
+            :key="index"
+            class="detailsrow text-left"
+          >
+            <small
+              :class="[
+                'condensed type',
+                {
+                  'text-danger': entry.kind.toUpperCase() == 'ERROR',
+                  'text-warning': entry.kind.toUpperCase() == 'WARN'
+                }
+              ]"
+              >{{ entry.kind.toUpperCase() }}</small
             >
-              <td class="type">
-                <span
-                  :class="[
-                    'condensed',
-                    {
-                      'text-danger': entry.kind.toUpperCase() == 'ERROR',
-                      'text-warning': entry.kind.toUpperCase() == 'WARN'
-                    }
-                  ]"
-                  >{{ entry.kind.toUpperCase() }}</span
-                >
-              </td>
-              <td class="datetime">
-                <span
-                  :class="[
-                    'condensed',
-                    {
-                      'text-danger': entry.kind.toUpperCase() == 'ERROR',
-                      'text-warning': entry.kind.toUpperCase() == 'WARN'
-                    }
-                  ]"
-                  >{{ formatDateTime(entry.time) }}</span
-                >
-              </td>
-              <td class="message">
-                <span
-                  :class="[
-                    'condensed',
-                    {
-                      'text-danger': entry.kind.toUpperCase() == 'ERROR',
-                      'text-warning': entry.kind.toUpperCase() == 'WARN'
-                    }
-                  ]"
-                  >{{ entry.message }}</span
-                >
-              </td>
-            </tr>
-          </tbody>
-        </table>
+            <small
+              :class="[
+                'condensed datetime',
+                {
+                  'text-danger': entry.kind.toUpperCase() == 'ERROR',
+                  'text-warning': entry.kind.toUpperCase() == 'WARN'
+                }
+              ]"
+              >{{ formatDateTime(entry.time) }}</small
+            >
+            <small
+              :class="[
+                'condensed message',
+                {
+                  'text-danger': entry.kind.toUpperCase() == 'ERROR',
+                  'text-warning': entry.kind.toUpperCase() == 'WARN'
+                }
+              ]"
+              >{{ entry.message }}</small
+            >
+          </div>
+        </div>
       </div>
     </div>
   </div>
@@ -265,6 +255,10 @@
 </script>
 
 <style lang="scss" scoped>
+.logentries {
+  overflow: auto;
+}
+
 .condensed {
   font-stretch: condensed;
 }
@@ -316,41 +310,20 @@
 }
 
 .detailsrow {
-  line-height: 0.1em;
+  line-height: 0.7rem;
 }
 
 .type {
-  width: 65px;
   white-space: nowrap;
-  padding-left: 0px;
-  border-top: 0px;
-  padding-bottom: $small-offset;
 }
 
 .datetime {
-  width: 200px;
   white-space: nowrap;
-  padding-left: 0px;
-  border-top: 0px;
-  padding-bottom: $small-offset;
+  padding-left: 10px;
+  padding-right: 10px;
 }
 
 .message {
-  min-width: 700px;
   white-space: nowrap;
-  padding-left: 0px;
-  border-top: 0px;
-  padding-bottom: $small-offset;
-}
-
-thead,
-tbody {
-  display: block;
-}
-
-tbody {
-  height: 150px;
-  overflow-y: auto;
-  overflow-x: auto;
 }
 </style>
--- a/client/src/components/importoverview/importlogs/Logs.vue	Fri Mar 01 18:28:12 2019 +0100
+++ b/client/src/components/importoverview/importlogs/Logs.vue	Fri Mar 01 18:28:50 2019 +0100
@@ -10,7 +10,7 @@
       <button @click="setFilter('pending')" :class="pendingStyle">
         <translate>Pending</translate>
       </button>
-      <button @click="setFilter('rejected')" :class="rejectedStyle">
+      <button @click="setFilter('declined')" :class="rejectedStyle">
         <translate>Rejected</translate>
       </button>
       <button @click="setFilter('accepted')" :class="acceptedStyle">
@@ -44,17 +44,20 @@
  */
 
 import { mapState } from "vuex";
+import { displayError } from "@/lib/errors.js";
 
 export default {
   name: "logsection",
   components: {
     LogDetail: () => import("./LogDetail.vue")
   },
+  props: ["reload"],
   data() {
     return {
+      loading: false,
       failed: false,
       pending: false,
-      rejected: false,
+      declined: false,
       accepted: false,
       warning: false
     };
@@ -65,7 +68,7 @@
       return {
         btn: true,
         "btn-sm": true,
-        "btn-light": !this.pending,
+        "btn-outline-info": !this.pending,
         "btn-info": this.pending
       };
     },
@@ -73,7 +76,7 @@
       return {
         btn: true,
         "btn-sm": true,
-        "btn-light": !this.failed,
+        "btn-outline-info": !this.failed,
         "btn-info": this.failed
       };
     },
@@ -81,15 +84,15 @@
       return {
         btn: true,
         "btn-sm": true,
-        "btn-light": !this.rejected,
-        "btn-info": this.rejected
+        "btn-outline-info": !this.declined,
+        "btn-info": this.declined
       };
     },
     acceptedStyle() {
       return {
         btn: true,
         "btn-sm": true,
-        "btn-light": !this.accepted,
+        "btn-outline-info": !this.accepted,
         "btn-info": this.accepted
       };
     },
@@ -97,19 +100,31 @@
       return {
         btn: true,
         "btn-sm": true,
-        "btn-light": !this.warning,
+        "btn-outline-info": !this.warning,
         "btn-info": this.warning
       };
     }
   },
+  watch: {
+    reload() {
+      if (!this.reload) return;
+      this.warning = false;
+      this.successful = false;
+      this.failed = false;
+      this.pending = false;
+      this.accepted = false;
+      this.declined = false;
+    }
+  },
   methods: {
     setFilter(name) {
+      if (this.loading) return;
       this[name] = !this[name];
       const allSet =
         this.failed &&
         this.pending &&
         this.accepted &&
-        this.rejected &&
+        this.declined &&
         this.warning;
       if (allSet) {
         this.warning = false;
@@ -117,8 +132,32 @@
         this.failed = false;
         this.pending = false;
         this.accepted = false;
-        this.rejected = false;
+        this.declined = false;
       }
+      this.loadFiltered();
+    },
+    loadFiltered() {
+      this.loading = true;
+      const filter = [
+        "failed",
+        "pending",
+        "accepted",
+        "declined",
+        "warning"
+      ].filter(x => this[x]);
+      this.$store
+        .dispatch("imports/getImports", filter)
+        .then(() => {
+          this.loading = false;
+        })
+        .catch(error => {
+          this.loading = false;
+          const { status, data } = error.response;
+          displayError({
+            title: this.$gettext("Backend Error"),
+            message: `${status}: ${data.message || data}`
+          });
+        });
     }
   }
 };
@@ -127,6 +166,6 @@
 <style lang="scss" scoped>
 .logdetails {
   overflow-y: auto;
-  height: 650px;
+  max-height: 650px;
 }
 </style>
--- a/client/src/components/systemconfiguration/PDFTemplates.vue	Fri Mar 01 18:28:12 2019 +0100
+++ b/client/src/components/systemconfiguration/PDFTemplates.vue	Fri Mar 01 18:28:50 2019 +0100
@@ -30,7 +30,10 @@
               <td class="text-right">
                 <button
                   class="btn btn-sm btn-dark"
-                  @click="deleteTemplate(template)"
+                  @click="
+                    deleteTemplate(template);
+                    showSuccessUploadMsg = false;
+                  "
                 >
                   <font-awesome-icon icon="trash" />
                 </button>
@@ -39,7 +42,13 @@
           </transition-group>
         </table>
       </transition>
-      <button class="btn btn-info mt-2" @click="$refs.uploadTemplate.click()">
+      <button
+        class="btn btn-info mt-2"
+        @click="
+          $refs.uploadTemplate.click();
+          showSuccessUploadMsg = false;
+        "
+      >
         <font-awesome-icon
           icon="spinner"
           class="fa-spin fa-fw"
@@ -48,6 +57,11 @@
         <font-awesome-icon icon="upload" class="fa-fw" v-else />
         <translate>Upload new template</translate>
       </button>
+      <div v-if="showSuccessUploadMsg" class="text-center">
+        <p class="text-muted" v-translate>
+          {{ templateToUpload.name }} uploaded successfully
+        </p>
+      </div>
     </div>
   </div>
 </template>
@@ -84,7 +98,9 @@
   data() {
     return {
       templates: [],
-      uploading: false
+      uploading: false,
+      templateToUpload: "",
+      showSuccessUploadMsg: false
     };
   },
   methods: {
@@ -119,6 +135,8 @@
           )
             .then(() => {
               this.loadTemplates();
+              this.templateToUpload = template;
+              this.showSuccessUploadMsg = true;
             })
             .catch(e => {
               const { status, data } = e.response;
@@ -129,6 +147,7 @@
             })
             .finally(() => {
               this.uploading = false;
+              this.$refs.uploadTemplate.value = null;
             });
         } else {
           displayError({
--- a/client/src/store/imports.js	Fri Mar 01 18:28:12 2019 +0100
+++ b/client/src/store/imports.js	Fri Mar 01 18:28:50 2019 +0100
@@ -160,7 +160,8 @@
     },
     getImports({ commit }, filter) {
       let queryParams = "";
-      if (filter) queryParams = "?states=" + filter.join(",");
+      if (filter && filter.length > 0)
+        queryParams = "?states=" + filter.join(",");
       return new Promise((resolve, reject) => {
         HTTP.get("/imports" + queryParams, {
           headers: { "X-Gemma-Auth": localStorage.getItem("token") }
--- a/client/src/store/map.js	Fri Mar 01 18:28:12 2019 +0100
+++ b/client/src/store/map.js	Fri Mar 01 18:28:50 2019 +0100
@@ -20,7 +20,15 @@
 import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer.js";
 import OSM from "ol/source/OSM";
 import Draw from "ol/interaction/Draw.js";
-import { Icon, Stroke, Style, Fill, Text, Circle } from "ol/style.js";
+import {
+  Icon,
+  Stroke,
+  Style,
+  Fill,
+  Text,
+  Circle,
+  RegularShape
+} from "ol/style.js";
 import VectorSource from "ol/source/Vector.js";
 import Point from "ol/geom/Point.js";
 import { bbox as bboxStrategy } from "ol/loadingstrategy";
@@ -47,6 +55,7 @@
   BOTTLENECKISOLINE: "Bottleneck isolines",
   DISTANCEMARKS: "Distance marks",
   DISTANCEMARKSAXIS: "Distance marks, Axis",
+  GAUGES: "Gauges",
   DRAWTOOL: "Draw Tool",
   CUTTOOL: "Cut Tool"
 };
@@ -369,6 +378,39 @@
         showInLegend: true
       },
       {
+        name: LAYERS.GAUGES,
+        forLegendStyle: { point: true, resolution: 8 },
+        data: new VectorLayer({
+          source: new VectorSource({
+            strategy: bboxStrategy
+          }),
+          style: function(feature) {
+            return new Style({
+                image: new RegularShape({
+                  radius: 6,
+                  fill: new Fill({ color: "rgba(255, 255, 0, 0.1)" }),
+                  stroke: new Stroke({ color: "red", width: 1 }),
+                  points: 3,
+                  rotation: 0,
+                  angle: 0
+                }),
+                text: new Text({
+                  font: '10px "Open Sans", "sans-serif"',
+                  offsetY: 10,
+                  fill: new Fill({
+                    color: "black"
+                  }),
+                  text: feature.get("objname")
+              })
+            });
+          },
+          maxResolution: 100,
+          minResolution: 0
+        }),
+        isVisible: true,
+        showInLegend: true,
+      },
+      {
         name: LAYERS.DRAWTOOL,
         data: new VectorLayer({
           source: new VectorSource({ wrapX: false }),
--- a/schema/demo-data/published_services.sql	Fri Mar 01 18:28:12 2019 +0100
+++ b/schema/demo-data/published_services.sql	Fri Mar 01 18:28:50 2019 +0100
@@ -14,6 +14,7 @@
 INSERT INTO sys_admin.published_services (name) VALUES
     ('waterway.stretches_geoserver'),
     ('waterway.fairway_dimensions'),
+    ('waterway.gauges_geoserver'),
     ('waterway.distance_marks_ashore_geoserver'),
     ('waterway.distance_marks_geoserver'),
     ('waterway.sounding_results_contour_lines_geoserver'),
--- a/schema/gemma.sql	Fri Mar 01 18:28:12 2019 +0100
+++ b/schema/gemma.sql	Fri Mar 01 18:28:50 2019 +0100
@@ -252,6 +252,28 @@
         value int NOT NULL
     )
 
+    CREATE VIEW gauges_geoserver AS
+    SELECT
+        g.location,
+        isrs_asText(g.location) AS isrs_code,
+        g.objname,
+        g.geom,
+        g.applicability_from_km,
+        g.applicability_to_km,
+        g.validity,
+        g.zero_point,
+        g.geodref,
+        g.date_info,
+        g.source_organization,
+        json_strip_nulls(json_object_agg(coalesce(r.depth_reference,'empty'),
+                                                  r.value))
+            AS reference_water_levels
+    FROM gauges g LEFT JOIN LATERAL (
+            SELECT gauge_id, depth_reference, value
+            FROM gauges_reference_water_levels
+            ) r ON r.gauge_id = g.location
+    GROUP BY g.location
+
     CREATE TABLE gauge_measurements (
         id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
         fk_gauge_id isrs NOT NULL REFERENCES gauges,
@@ -482,7 +504,7 @@
             CHECK(ST_IsValid(CAST(area AS geometry))),
         surtyp varchar REFERENCES survey_types,
         coverage varchar REFERENCES coverage_types,
-        depth_reference varchar(4) NOT NULL REFERENCES depth_references,
+        depth_reference varchar(4) NOT NULL, -- REFERENCES depth_references,
         point_cloud geography(MULTIPOINTZ, 4326) NOT NULL
             CHECK(ST_IsSimple(CAST(point_cloud AS geometry))),
         octree_checksum varchar,
@@ -630,6 +652,7 @@
 
 -- Configure primary keys for geoserver views
 INSERT INTO waterway.gt_pk_metadata VALUES
+  ('waterway', 'gauges_geoserver', 'location'),
   ('waterway', 'distance_marks_geoserver', 'location_code'),
   ('waterway', 'distance_marks_ashore_geoserver', 'id'),
   ('waterway', 'bottlenecks_geoserver', 'id'),
--- a/schema/isrs_functions.sql	Fri Mar 01 18:28:12 2019 +0100
+++ b/schema/isrs_functions.sql	Fri Mar 01 18:28:50 2019 +0100
@@ -66,11 +66,8 @@
                         SELECT refgeom
                         UNION
                         -- Fill eventual gap
-                        SELECT ST_MakeLine(
-                            ST_ClosestPoint(
-                                ST_Boundary(refgeom), ST_Boundary(geom)),
-                            ST_ClosestPoint(
-                                ST_Boundary(geom), ST_Boundary(refgeom)))
+                        SELECT ST_ShortestLine(
+                                ST_Boundary(refgeom), ST_Boundary(geom))
                         UNION
                         -- Linestring to be added
                         SELECT geom)))
--- a/schema/isrs_tests.sql	Fri Mar 01 18:28:12 2019 +0100
+++ b/schema/isrs_tests.sql	Fri Mar 01 18:28:50 2019 +0100
@@ -42,26 +42,24 @@
     ) IS NULL,
     'ISRSrange_area returns NULL, if given area does not intersect with axis');
 
-SELECT ok(
-    ST_DWithin(
-        (SELECT geom FROM waterway.distance_marks_virtual
-            WHERE location_code = ('AT', 'XXX', '00001', '00000', 0)::isrs),
-        ST_Boundary(ISRSrange_area(isrsrange(
-                ('AT', 'XXX', '00001', '00000', 0)::isrs,
-                ('AT', 'XXX', '00001', '00000', 1)::isrs),
-            (SELECT ST_Collect(CAST(area AS geometry))
-                FROM waterway.waterway_area))),
-        1)
-    AND
-    ST_DWithin(
-        (SELECT geom FROM waterway.distance_marks_virtual
-            WHERE location_code = ('AT', 'XXX', '00001', '00000', 1)::isrs),
-        ST_Boundary(ISRSrange_area(isrsrange(
-                ('AT', 'XXX', '00001', '00000', 0)::isrs,
-                ('AT', 'XXX', '00001', '00000', 1)::isrs),
-            (SELECT ST_Collect(CAST(area AS geometry))
-                FROM waterway.waterway_area))),
-        1),
+SELECT results_eq($$
+    SELECT every(ST_DWithin(
+            ST_Boundary(ISRSrange_area(
+                isrsrange(
+                    ('AT', 'XXX', '00001', '00000', 0)::isrs,
+                    ('AT', 'XXX', '00001', '00000', 1)::isrs),
+                (SELECT ST_Collect(CAST(area AS geometry))
+                    FROM waterway.waterway_area))),
+            geom,
+            1))
+        FROM waterway.distance_marks_virtual
+        WHERE location_code IN(
+            ('AT', 'XXX', '00001', '00000', 0)::isrs,
+            ('AT', 'XXX', '00001', '00000', 1)::isrs)
+    $$,
+    $$
+    SELECT true
+    $$,
     'Resulting polygon almost ST_Touches points corresponding to stretch');
 
 \set test_area 'POLYGON((-1 1, 2 1, 2 -1, -1 -1, -1 1))'
@@ -101,11 +99,22 @@
                 4326))),
     'Self-intersecting multipolygon leads to one polygon in result');
 
-SELECT ok(
-    ISRSrange_area(
-        isrsrange(
+SELECT results_eq($$
+    SELECT every(ST_DWithin(
+            ST_Boundary(ISRSrange_area(
+                isrsrange(
+                    ('AT', 'XXX', '00001', '00000', 0)::isrs,
+                    ('AT', 'XXX', '00001', '00000', 2)::isrs),
+                (SELECT ST_Collect(CAST(area AS geometry))
+                    FROM waterway.waterway_area))),
+            geom,
+            1))
+        FROM waterway.distance_marks_virtual
+        WHERE location_code IN(
             ('AT', 'XXX', '00001', '00000', 0)::isrs,
-            ('AT', 'XXX', '00001', '00000', 2)::isrs),
-        (SELECT ST_Collect(CAST(area AS geometry))
-            FROM waterway.waterway_area)) IS NOT NULL,
+            ('AT', 'XXX', '00001', '00000', 2)::isrs)
+    $$,
+    $$
+    SELECT true
+    $$,
     'Area generated from non-matching distance mark and non-contiguous axis');
--- a/schema/search_functions.sql	Fri Mar 01 18:28:12 2019 +0100
+++ b/schema/search_functions.sql	Fri Mar 01 18:28:50 2019 +0100
@@ -51,10 +51,30 @@
 END;
 $$;
 
+CREATE OR REPLACE FUNCTION search_gauges(search_string text) RETURNS jsonb
+  LANGUAGE plpgsql
+  AS $$
+DECLARE
+  _result jsonb;
+BEGIN
+  SELECT COALESCE(json_agg(r),'[]')
+    INTO _result
+    FROM (SELECT objname AS name,
+                 ST_AsGeoJSON(geom)::json AS geom,
+                 'gauge' AS type
+            FROM waterway.gauges
+            WHERE objname ILIKE '%' || search_string || '%'
+          ORDER BY name) r;
+  RETURN _result;
+END;
+$$;
+
 CREATE OR REPLACE FUNCTION search_most(search_string text) RETURNS jsonb
   LANGUAGE plpgsql
   AS $$
 BEGIN
-  RETURN search_bottlenecks(search_string) || search_cities(search_string);
+  RETURN search_bottlenecks(search_string)
+         || search_gauges(search_string)
+         || search_cities(search_string);
 END;
 $$;