changeset 2413:f39c4b432601 staging_consolidation

merge with default
author Thomas Junk <thomas.junk@intevation.de>
date Thu, 28 Feb 2019 12:29:46 +0100
parents 228387d5f2c5 (current diff) 0ed53a7a1221 (diff)
children df56bc53e86d
files client/src/components/Sidebar.vue client/src/components/ui/box/Header.vue client/src/router.js
diffstat 14 files changed, 352 insertions(+), 152 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/ImportStretches.vue	Thu Feb 28 12:04:18 2019 +0100
+++ b/client/src/components/ImportStretches.vue	Thu Feb 28 12:29:46 2019 +0100
@@ -346,8 +346,8 @@
       this.countryCode = properties.countries;
       this.source = properties["source_organization"];
       this.edit = true;
-      this.startrhm = this.sanitizeRHM(properties.lower);
-      this.endrhm = this.sanitizeRHM(properties.upper);
+      this.startrhm = properties.lower;
+      this.endrhm = properties.upper;
       this.idEditable = false;
     },
     deleteStretch(stretch) {
@@ -535,8 +535,8 @@
     },
     pointsValid() {
       if (!this.startrhm || !this.endrhm) return true;
-      const start = this.startrhm.replace(/\D+/, "") * 1;
-      const end = this.endrhm.replace(/\D+/, "") * 1;
+      const start = this.startrhm.replace(/\D+/g, "") * 1;
+      const end = this.endrhm.replace(/\D+/g, "") * 1;
       const result = start < end;
       return result;
     }
--- a/client/src/components/Sidebar.vue	Thu Feb 28 12:04:18 2019 +0100
+++ b/client/src/components/Sidebar.vue	Thu Feb 28 12:29:46 2019 +0100
@@ -225,13 +225,15 @@
     }
   },
   mounted() {
-    this.$store.dispatch("imports/getStaging").catch(error => {
-      const { status, data } = error.response;
-      displayError({
-        title: "Backend Error",
-        message: `${status}: ${data.message || data}`
+    setTimeout(() => {
+      this.$store.dispatch("imports/getStaging").catch(error => {
+        const { status, data } = error.response;
+        displayError({
+          title: "Backend Error",
+          message: `${status}: ${data.message || data}`
+        });
       });
-    });
+    }, 15000);
   }
 };
 </script>
@@ -268,9 +270,13 @@
   .indicator {
     left: auto;
     right: 10px;
-    top: 10px;
+    top: 12px;
     border-radius: 0.25rem;
   }
+  &.router-link-exact-active .indicator {
+    background: #fff;
+    color: #333;
+  }
 }
 
 .menu a svg path {
--- a/client/src/components/identify/Identify.vue	Thu Feb 28 12:04:18 2019 +0100
+++ b/client/src/components/identify/Identify.vue	Thu Feb 28 12:29:46 2019 +0100
@@ -138,9 +138,21 @@
       // create array with {key, val} objects
       let propsArray = [];
       Object.keys(feature.getProperties()).forEach(key => {
-        // avoid cyclic object value
-        if (key !== feature.getGeometryName())
-          propsArray.push({ key, val: feature.getProperties()[key] });
+        let val = feature.getProperties()[key];
+
+        // if val is a valid json string, parse it and spread its values into
+        // the array
+        try {
+          let json = JSON.parse(val);
+          Object.keys(json).forEach(key => {
+            propsArray.push({ key, val: json[key] });
+          });
+        } catch (e) {
+          // if val is not json then just put the key value pair into the array
+          // avoid cyclic object value (I didn't further investigate what's the
+          // problem here but feature.getGeometryName() needs to be skipped)
+          if (key !== feature.getGeometryName()) propsArray.push({ key, val });
+        }
       });
 
       // change labels and remove unneeded properties
--- a/client/src/components/identify/formatter.js	Thu Feb 28 12:04:18 2019 +0100
+++ b/client/src/components/identify/formatter.js	Thu Feb 28 12:29:46 2019 +0100
@@ -10,7 +10,7 @@
       }
 
       // remove certain props
-      let propsToRemove = ["nobjnm"];
+      let propsToRemove = ["nobjnm", "reference_water_levels"];
       if (propsToRemove.indexOf(p.key) !== -1) return null;
 
       return p;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/ui/UIBoxHeader.vue	Thu Feb 28 12:29:46 2019 +0100
@@ -0,0 +1,63 @@
+<template>
+  <h6 class="box-header">
+    <span class="box-title">
+      <font-awesome-icon
+        :icon="icon"
+        class="box-icon"
+        v-if="icon"
+        fixed-width
+      />
+      {{ $gettext(title) }}
+    </span>
+    <span class="box-close" @click="closeCallback" v-if="closeCallback">
+      <font-awesome-icon icon="times" />
+    </span>
+  </h6>
+</template>
+
+<style lang="sass">
+.box-header
+  display: flex
+  justify-content: space-between
+  align-items: center
+  min-height: 35px
+  padding-left: .5rem
+  border-bottom: 1px solid #dee2e6
+  color: $color-info
+  margin-bottom: 0
+  padding: 0.25rem
+  font-weight: bold
+  .box-title
+    padding-left: 0.25rem
+    .box-icon
+      margin-right: 0.25rem
+  .box-close
+    color: #888
+    padding: 3px 7px
+    border-radius: 0.25rem
+    cursor: pointer
+    transition: background-color 0.3s, color 0.3s
+    &:hover
+      color: #666
+      background-color: #eee
+</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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+
+export default {
+  props: ["icon", "title", "closeCallback"]
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/ui/UITableBody.vue	Thu Feb 28 12:29:46 2019 +0100
@@ -0,0 +1,51 @@
+<template>
+  <transition-group
+    name="fade"
+    tag="div"
+    class="table-body text-left small"
+    :style="'overflow-y: auto; max-height: ' + maxHeight"
+    v-if="data.length"
+  >
+    <div
+      v-for="(item, index) in data"
+      :key="index"
+      :class="['border-top row mx-0', { active: active === item }]"
+    >
+      <slot :item="item" :index="index"></slot>
+    </div>
+  </transition-group>
+  <div v-else class="small text-center py-3 border-top">
+    <translate>No results.</translate>
+  </div>
+</template>
+
+<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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+
+export default {
+  props: {
+    data: {
+      type: Array
+    },
+    maxHeight: {
+      type: String,
+      default: "18rem"
+    },
+    active: {
+      type: [Object, Array]
+    }
+  }
+};
+</script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/ui/UITableHeader.vue	Thu Feb 28 12:29:46 2019 +0100
@@ -0,0 +1,94 @@
+<template>
+  <div
+    :class="['table-header row no-gutters bg-light', { sortable: sortable }]"
+  >
+    <a
+      v-for="column in columns"
+      @click.prevent="sortBy(column.id)"
+      :key="column.id"
+      :class="[
+        'd-flex py-2 align-items-center justify-content-center small col ' +
+          column.class,
+        { active: sortColumn === column.id }
+      ]"
+    >
+      <span
+        :style="'opacity: ' + (sortColumn === column.id ? '1' : '0.3')"
+        v-if="sortable"
+      >
+        <font-awesome-icon
+          :icon="sortIcon(column.id)"
+          class="ml-1"
+          fixed-width
+        />
+      </span>
+      {{ $gettext(column.title) }}
+    </a>
+    <div v-if="extraColumnForButtons" class="col"></div>
+  </div>
+</template>
+
+<style lang="sass">
+.table-header
+  > a
+    border-right: solid 1px #e7e8e9
+    background-color: #f8f9fa
+    a
+      outline: none
+      &:hover
+        text-decoration: none
+        background-color: #f8f9fa
+  &.sortable
+    a
+      cursor: pointer
+      &:hover,
+      &.active
+        background: #e7e8e9
+</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):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+export default {
+  props: {
+    columns: { type: Array },
+    sortable: { type: Boolean, default: true },
+    extraColumnForButtons: { type: Boolean, default: true }
+  },
+  data() {
+    return {
+      sortColumn: null,
+      sortDirection: "ASC"
+    };
+  },
+  methods: {
+    sortIcon(id) {
+      if (this.sortColumn === id) {
+        return "sort-" + (this.sortDirection === "ASC" ? "down" : "up");
+      }
+      return "sort";
+    },
+    sortBy(id) {
+      if (this.sortable) {
+        this.sortColumn = id;
+        this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC";
+        this.$emit("sortingChanged", {
+          sortColumn: this.sortColumn,
+          sortDirection: this.sortDirection
+        });
+      }
+    }
+  }
+};
+</script>
--- a/client/src/components/ui/box/Header.vue	Thu Feb 28 12:04:18 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-<template>
-  <h6 class="box-header">
-    <span class="box-title">
-      <font-awesome-icon
-        :icon="icon"
-        class="box-icon"
-        v-if="icon"
-        fixed-width
-      />
-      {{ $gettext(title) }}
-    </span>
-    <span class="box-close" @click="closeCallback" v-if="closeCallback">
-      <font-awesome-icon icon="times" />
-    </span>
-  </h6>
-</template>
-
-<style lang="sass">
-.box-header
-  display: flex
-  justify-content: space-between
-  align-items: center
-  min-height: 35px
-  padding-left: .5rem
-  border-bottom: 1px solid #dee2e6
-  color: $color-info
-  margin-bottom: 0
-  padding: 0.25rem
-  font-weight: bold
-  .box-title
-    padding-left: 0.25rem
-    .box-icon
-      margin-right: 0.25rem
-  .box-close
-    color: #888
-    padding: 3px 7px
-    border-radius: 0.25rem
-    cursor: pointer
-    transition: background-color 0.3s, color 0.3s
-    &:hover
-      color: #666
-      background-color: #eee
-</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):
- * Markus Kottländer <markus.kottlaender@intevation.de>
- */
-
-export default {
-  props: ["icon", "title", "closeCallback"]
-};
-</script>
--- a/client/src/main.js	Thu Feb 28 12:04:18 2019 +0100
+++ b/client/src/main.js	Thu Feb 28 12:29:46 2019 +0100
@@ -29,7 +29,9 @@
 import store from "./store";
 import translations from "./locale/translations.json";
 import { supportedLanguages, defaultLanguage } from "./locale/languages.js";
-import Header from "@/components/ui/box/Header";
+import UIBoxHeader from "@/components/ui/UIBoxHeader";
+import UITableHeader from "@/components/ui/UITableHeader";
+import UITableBody from "@/components/ui/UITableBody";
 
 // styles
 import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
@@ -74,8 +76,11 @@
   faRuler,
   faSearch,
   faShip,
+  faSort,
   faSortAmountDown,
   faSortAmountUp,
+  faSortDown,
+  faSortUp,
   faSpinner,
   faStar,
   faTasks,
@@ -126,6 +131,9 @@
   faRuler,
   faSearch,
   faShip,
+  faSort,
+  faSortDown,
+  faSortUp,
   faSortAmountDown,
   faSortAmountUp,
   faSpinner,
@@ -154,7 +162,9 @@
 
 // register global components
 Vue.component("font-awesome-icon", FontAwesomeIcon);
-Vue.component("UIBoxHeader", Header);
+Vue.component("UIBoxHeader", UIBoxHeader);
+Vue.component("UITableHeader", UITableHeader);
+Vue.component("UITableBody", UITableBody);
 
 // global vue config
 Vue.config.productionTip = false;
--- a/client/src/router.js	Thu Feb 28 12:04:18 2019 +0100
+++ b/client/src/router.js	Thu Feb 28 12:29:46 2019 +0100
@@ -168,6 +168,7 @@
         requiresAuth: true
       },
       beforeEnter: (to, from, next) => {
+        store.commit("application/searchQuery", "");
         store.commit("application/showContextBox", false);
         store.commit("application/contextBoxContent", "");
         store.commit("application/showSearchbar", false);
@@ -182,6 +183,7 @@
         requiresAuth: true
       },
       beforeEnter: (to, from, next) => {
+        store.commit("application/searchQuery", "");
         store.commit("application/showContextBox", true);
         store.commit("application/contextBoxContent", "bottlenecks");
         store.commit("application/showSearchbar", true);
@@ -200,6 +202,7 @@
         if (!isWaterwayAdmin) {
           next("/");
         } else {
+          store.commit("application/searchQuery", "");
           store.commit("application/showContextBox", true);
           store.commit("application/contextBoxContent", "staging");
           store.commit("application/showSearchbar", true);
@@ -238,6 +241,7 @@
         if (!isSysadmin) {
           next("/");
         } else {
+          store.commit("application/searchQuery", "");
           store.commit("application/showContextBox", true);
           store.commit("application/contextBoxContent", "stretches");
           store.commit("application/showSearchbar", true);
--- a/schema/gemma.sql	Thu Feb 28 12:04:18 2019 +0100
+++ b/schema/gemma.sql	Thu Feb 28 12:29:46 2019 +0100
@@ -473,56 +473,6 @@
         PRIMARY KEY (bottleneck_id, riverbed)
     )
 
-    -- Published view for GeoServer
-    CREATE VIEW bottlenecks_geoserver AS
-    WITH fairway_availability_latest AS (
-        SELECT DISTINCT ON (bottleneck_id) bottleneck_id,date_info,critical
-        FROM fairway_availability
-        ORDER BY bottleneck_id, date_info DESC NULLS LAST),
-    gauge_measurements_waterlevel AS (
-        SELECT DISTINCT ON (fk_gauge_id) fk_gauge_id,measure_date,predicted,water_level
-        FROM gauge_measurements WHERE predicted ='false'
-        ORDER BY fk_gauge_id, measure_date DESC NULLS LAST)
-    SELECT
-        b.id,
-        b.bottleneck_id,
-        b.objnam,
-        b.nobjnm,
-        b.stretch,
-        b.area,
-        b.rb,
-        b.lb,
-        b.responsible_country,
-        b.revisiting_time,
-        b.limiting,
-        b.date_info,
-        b.source_organization,
-        g.location AS gauge_isrs_code,
-        g.objname AS gauge_objname,
-        rwl_ldc.value AS ldc,
-        rwl_mw.value AS mw,
-        rwl_hdc.value AS hdc,
-        fal.date_info AS fa_date_info,
-        fal.critical AS fa_critical,
-        gmw.water_level as gm_waterlevel
-    FROM bottlenecks b, gauges g,
-        (SELECT gauge_id, value FROM gauges_reference_water_levels
-           WHERE depth_reference = 'LDC') rwl_ldc,
-        (SELECT gauge_id, value FROM gauges_reference_water_levels
-           WHERE depth_reference = 'MW') rwl_mw,
-        (SELECT gauge_id, value FROM gauges_reference_water_levels
-           WHERE depth_reference = 'HDC') rwl_hdc
-        LEFT JOIN LATERAL (
-            SELECT bottleneck_id,date_info,critical
-            FROM  fairway_availability_latest
-            WHERE b.id=bottleneck_id) fal ON TRUE
-        LEFT JOIN LATERAL (
-            SELECT water_level
-            FROM  gauge_measurements_waterlevel
-            WHERE b.fk_g_fid=fk_gauge_id) gmw ON TRUE
-    WHERE b.fk_g_fid = g.location AND g.location = rwl_ldc.gauge_id
-      AND g.location = rwl_mw.gauge_id AND g.location = rwl_hdc.gauge_id
-
     CREATE TABLE sounding_results (
         id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
         bottleneck_id int NOT NULL REFERENCES bottlenecks(id),
@@ -630,6 +580,52 @@
       SELECT bottleneck_id, max(date_info) AS current FROM sounding_results
       GROUP BY bottleneck_id) sr ON sr.bottleneck_id = bn.id
     ORDER BY objnam
+
+    -- Published view for GeoServer
+    CREATE VIEW bottlenecks_geoserver AS
+    WITH fairway_availability_latest AS (
+        SELECT DISTINCT ON (bottleneck_id) bottleneck_id,date_info,critical
+        FROM fairway_availability
+        ORDER BY bottleneck_id, date_info DESC NULLS LAST),
+    gauge_measurements_waterlevel AS (
+        SELECT DISTINCT ON (fk_gauge_id)
+            fk_gauge_id, measure_date, predicted, water_level
+        FROM gauge_measurements WHERE predicted ='false'
+        ORDER BY fk_gauge_id, measure_date DESC NULLS LAST)
+    SELECT
+        b.id,
+        b.bottleneck_id,
+        b.objnam,
+        b.nobjnm,
+        b.stretch,
+        b.area,
+        b.rb,
+        b.lb,
+        b.responsible_country,
+        b.revisiting_time,
+        b.limiting,
+        b.date_info,
+        b.source_organization,
+        g.location AS gauge_isrs_code,
+        g.objname AS gauge_objname,
+        json_object_agg(r.depth_reference, r.value) AS reference_water_levels,
+        fal.date_info AS fa_date_info,
+        fal.critical AS fa_critical,
+        gmw.water_level as gm_waterlevel
+    FROM bottlenecks b LEFT JOIN gauges g ON b.fk_g_fid = g.location
+        LEFT JOIN LATERAL (
+            SELECT gauge_id,depth_reference,value
+            FROM gauges_reference_water_levels
+            ) r ON r.gauge_id = b.fk_g_fid
+        LEFT JOIN LATERAL (
+            SELECT bottleneck_id,date_info,critical
+            FROM  fairway_availability_latest
+            WHERE b.id=bottleneck_id) fal ON TRUE
+        LEFT JOIN LATERAL (
+            SELECT water_level
+            FROM  gauge_measurements_waterlevel
+            WHERE b.fk_g_fid=fk_gauge_id) gmw ON TRUE
+    GROUP BY b.id, g.location, fal.date_info, fal.critical, gmw.water_level;
 ;
 
 -- Configure primary keys for geoserver views
--- a/schema/isrs_functions.sql	Thu Feb 28 12:04:18 2019 +0100
+++ b/schema/isrs_functions.sql	Thu Feb 28 12:29:46 2019 +0100
@@ -82,25 +82,29 @@
                     ST_Dump(ST_Transform(area, z)) AS a_dmp
                 WHERE ST_Intersects(a_dmp.geom, axis_substring.line)
             ),
+        rotated_ends AS (
+            SELECT ST_Collect(ST_Scale(
+                    ST_Translate(e,
+                        (ST_X(p1) - ST_X(p2)) / 2,
+                        (ST_Y(p1) - ST_Y(p2)) / 2),
+                    ST_Point(d, d), p1)) AS blade
+                FROM axis_substring, area_subset,
+                    LATERAL (SELECT i, ST_PointN(line, i) AS p1
+                        FROM (VALUES (1), (-1)) AS idx (i)) AS ep,
+                    ST_Rotate(ST_PointN(line, i*2), pi()/2, p1) AS ep2 (p2),
+                    ST_Makeline(p1, p2) AS e (e),
+                    LATERAL (SELECT (ST_MaxDistance(p1, area) / ST_Length(e))
+                            * 2) AS d (d)),
         range_area AS (
-            -- Create a buffer around the clipped axis, as large as it could
-            -- potentially be intersecting with the area polygon that
-            -- intersects with the clipped axis. Get the intersection of that
-            -- buffer with the area polygon, which can potentially
-            -- be a multipolygon.
-            SELECT (ST_Dump(ST_Intersection(
-                    ST_Buffer(
-                        axis_substring.line,
-                        ST_MaxDistance(
-                            axis_substring.line,
-                            area_subset.area),
-                        'endcap=flat'),
-                    area_subset.area))).geom
-                FROM axis_substring, area_subset)
+            -- Split area by orthogonal lines at the ends of the clipped axis
+            SELECT (ST_Dump(ST_CollectionExtract(
+                    ST_Split(area, blade), 3))).geom
+                FROM area_subset, rotated_ends)
         -- From the polygons returned by the last CTE, select only those
         -- around the clipped axis
-        SELECT ST_Collect(ST_Transform(range_area.geom, ST_SRID(area)))
+        SELECT ST_Multi(ST_Union(ST_Transform(range_area.geom, ST_SRID(area))))
             FROM axis_substring, range_area
-            WHERE ST_Intersects(range_area.geom, axis_substring.line)
+            WHERE ST_Intersects(ST_Buffer(range_area.geom, -0.0001),
+                axis_substring.line)
     $$
     LANGUAGE sql;
--- a/schema/isrs_tests.sql	Thu Feb 28 12:04:18 2019 +0100
+++ b/schema/isrs_tests.sql	Thu Feb 28 12:29:46 2019 +0100
@@ -42,7 +42,6 @@
     ) IS NULL,
     'ISRSrange_area returns NULL, if given area does not intersect with axis');
 
-\set test_area 'POLYGON((-1 1, 2 1, 2 -1, -1 -1, -1 1))'
 SELECT ok(
     ST_DWithin(
         (SELECT geom FROM waterway.distance_marks_virtual
@@ -50,8 +49,8 @@
         ST_Boundary(ISRSrange_area(isrsrange(
                 ('AT', 'XXX', '00001', '00000', 0)::isrs,
                 ('AT', 'XXX', '00001', '00000', 1)::isrs),
-            ST_SetSRID(:'test_area'::geometry,
-                4326)))::geography,
+            (SELECT ST_Collect(CAST(area AS geometry))
+                FROM waterway.waterway_area))),
         1)
     AND
     ST_DWithin(
@@ -60,11 +59,12 @@
         ST_Boundary(ISRSrange_area(isrsrange(
                 ('AT', 'XXX', '00001', '00000', 0)::isrs,
                 ('AT', 'XXX', '00001', '00000', 1)::isrs),
-            ST_SetSRID(:'test_area'::geometry,
-                4326)))::geography,
+            (SELECT ST_Collect(CAST(area AS geometry))
+                FROM waterway.waterway_area))),
         1),
     'Resulting polygon almost ST_Touches points corresponding to stretch');
 
+\set test_area 'POLYGON((-1 1, 2 1, 2 -1, -1 -1, -1 1))'
 SELECT ok(
     2 = ST_NumGeometries(
         ISRSrange_area(
--- a/schema/tap_tests_data.sql	Thu Feb 28 12:04:18 2019 +0100
+++ b/schema/tap_tests_data.sql	Thu Feb 28 12:29:46 2019 +0100
@@ -80,7 +80,12 @@
 
 INSERT INTO waterway.waterway_axis (wtwaxs, objnam) VALUES (
     ST_SetSRID(ST_CurveToLine(
-        'CIRCULARSTRING(0 0, 0.5 0.5, 1 0, 1.5 -0.2, 2 0)'::geometry),
+        'CIRCULARSTRING(0 0, 0.5 0.5, 0.6 0.4)'),
+        4326),
+    'testriver'
+), (
+    ST_SetSRID(ST_CurveToLine(
+        'CIRCULARSTRING(0.6 0.4, 1 0, 2 0)'),
         4326),
     'testriver'
 ), (
@@ -88,6 +93,24 @@
     'testriver'
 );
 
+-- Simulate waterway area as non-intersecting buffers around axis
+WITH RECURSIVE
+buffer AS (
+    SELECT id, ST_Buffer(wtwaxs, 10000, 'endcap=flat')::geometry AS buf
+        FROM waterway.waterway_axis),
+cleaned AS (
+    (SELECT ARRAY[id] AS ids, buf AS cbuf, buf AS others
+        FROM buffer ORDER BY id FETCH FIRST ROW ONLY)
+    UNION
+    (SELECT ids || id,
+            ST_Difference(buf, others),
+            ST_Union(buf, others)
+        FROM cleaned, buffer
+        WHERE id <> ALL(ids)
+        ORDER BY id ASC, ids DESC
+        FETCH FIRST ROW ONLY))
+INSERT INTO waterway.waterway_area (area) SELECT cbuf FROM cleaned;
+
 INSERT INTO users.templates (template_name, country, template_data)
     VALUES ('AT', 'AT', '\x'), ('RO', 'RO', '\x');