changeset 1034:4299f9c1f191

merge
author Markus Kottlaender <markus@intevation.de>
date Wed, 24 Oct 2018 15:38:07 +0200
parents fd7059f7cbdc (current diff) 4c0c4dd393de (diff)
children b0364b8226e0
files client/src/morphtool/store.js
diffstat 16 files changed, 436 insertions(+), 153 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/application/Main.vue	Wed Oct 24 15:35:50 2018 +0200
+++ b/client/src/application/Main.vue	Wed Oct 24 15:38:07 2018 +0200
@@ -1,8 +1,28 @@
 <template>
     <div class="main d-flex flex-column">
-        <Maplayer :drawMode="drawMode" :split="isSplitscreen" :lat="6155376" :long="1819178" :zoom="11"></Maplayer>
+        <Maplayer
+            :drawMode="drawMode"
+            :split="isSplitscreen"
+            :lat="6155376"
+            :long="1819178"
+            :zoom="11"
+        ></Maplayer>
         <div v-if="isSplitscreen" class="profile d-flex flex-row">
-            <FairwayProfile :additionalSurveys="additionalSurveys" :minAlt="minAlt" maxAlt="maxAlt" :selectedWaterLevel="selectedWaterLevel" :fairwayCoordinates="fairwayCoordinates" :waterLevels="waterLevels" :data="currentProfile" :height="height" :width="width" :xScale="xAxis" :yScaleLeft="yAxisLeft" :yScaleRight="yAxisRight" :margin="margins" :totalLength="totalLength"></FairwayProfile>
+            <FairwayProfile
+                :additionalSurveys="additionalSurveys"
+                :minAlt="minAlt"
+                maxAlt="maxAlt"
+                :selectedWaterLevel="selectedWaterLevel"
+                :fairwayCoordinates="fairwayCoordinates"
+                :waterLevels="waterLevels"
+                :height="height"
+                :width="width"
+                :xScale="xAxis"
+                :yScaleLeft="yAxisLeft"
+                :yScaleRight="yAxisRight"
+                :margin="margins"
+                :totalLength="totalLength"
+            ></FairwayProfile>
         </div>
     </div>
 </template>
@@ -65,7 +85,7 @@
       "selectedWaterLevel",
       "availableSurveys"
     ]),
-    ...mapState("morphstore", ["selectedMorph"]),
+    ...mapState("fairwayprofile", ["selectedMorph"]),
     additionalSurveys() {
       if (!this.availableSurveys) return [];
       return this.availableSurveys.surveys.filter(x => {
--- a/client/src/application/lib/geo.js	Wed Oct 24 15:35:50 2018 +0200
+++ b/client/src/application/lib/geo.js	Wed Oct 24 15:38:07 2018 +0200
@@ -22,6 +22,12 @@
 
 import { GeoJSON } from "ol/format.js";
 import Feature from "ol/Feature";
+import distance from "@turf/distance";
+import {
+  lineString as turfLineString,
+  polygon as turfPolygon
+} from "@turf/helpers";
+import lineIntersect from "@turf/line-intersect";
 
 const EARTHRADIUS = 6378137.0;
 
@@ -163,13 +169,40 @@
   };
 };
 
-const generateFeatureRequest = (profileLine, survey) => {
+const generateFeatureRequest = (profileLine, bottleneck_id, date_info) => {
   const feature = new Feature({
     geometry: profileLine,
-    bottleneck: survey.bottleneck_id,
-    date: survey.date_info
+    bottleneck: bottleneck_id,
+    date: date_info
   });
   return new GeoJSON({ geometryName: "geometry" }).writeFeature(feature);
 };
 
-export { generateFeatureRequest, prepareProfile };
+const calculateFairwayCoordinates = (profileLine, fairwayGeometry, depth) => {
+  // both geometries have to be in EPSG:4326
+  // uses turfjs distance() function
+  let fairwayCoordinates = [];
+  var line = turfLineString(profileLine.getCoordinates());
+  var polygon = turfPolygon(fairwayGeometry.getCoordinates());
+  var intersects = lineIntersect(line, polygon);
+  var l = intersects.features.length;
+  if (l % 2 != 0) {
+    console.log("Ignoring fairway because profile only intersects once.");
+  } else {
+    for (let i = 0; i < l; i += 2) {
+      let pStartPoint = profileLine.getCoordinates()[0];
+      let fStartPoint = intersects.features[i].geometry.coordinates;
+      let fEndPoint = intersects.features[i + 1].geometry.coordinates;
+      let opts = { units: "kilometers" };
+
+      fairwayCoordinates.push([
+        distance(pStartPoint, fStartPoint, opts) * 1000,
+        distance(pStartPoint, fEndPoint, opts) * 1000,
+        depth
+      ]);
+    }
+  }
+  return fairwayCoordinates;
+};
+
+export { generateFeatureRequest, prepareProfile, calculateFairwayCoordinates };
--- a/client/src/fairway/Fairwayprofile.vue	Wed Oct 24 15:35:50 2018 +0200
+++ b/client/src/fairway/Fairwayprofile.vue	Wed Oct 24 15:38:07 2018 +0200
@@ -1,22 +1,28 @@
 <template>
     <div class="profiledisplay d-flex flex-row">
-        <div class="fairwayprofile">
-        </div>
+        <div class="fairwayprofile"></div>
         <div class="additionalsurveys d-flex flex-column">
             <small class="label">Available Additional Surveys</small>
             <select v-model="additionalSurvey" @change="selectAdditionalSurveyData">
                 <option value="">None</option>
-                <option v-for="survey in additionalSurveys" :key="survey.date_info">
-                    {{survey.date_info}}
-                </option>
+                <option
+                    v-for="survey in additionalSurveys"
+                    :key="survey.date_info"
+                >{{survey.date_info}}</option>
             </select>
             <small class="mt-2">
-                <b>Start:</b><br>
-                Lat: {{ startPoint[1] }}<br>
-                Lon: {{ startPoint[0] }}<br>
-                <b>End:</b><br>
-                Lat: {{ endPoint[1] }}<br>
-                Lon: {{ endPoint[0] }}<br>
+                <b>Start:</b>
+                <br>
+                Lat: {{ startPoint[1] }}
+                <br>
+                Lon: {{ startPoint[0] }}
+                <br>
+                <b>End:</b>
+                <br>
+                Lat: {{ endPoint[1] }}
+                <br>
+                Lon: {{ endPoint[0] }}
+                <br>
             </small>
         </div>
     </div>
@@ -79,7 +85,6 @@
 export default {
   name: "fairwayprofile",
   props: [
-    "data",
     "width",
     "height",
     "xScale",
@@ -95,7 +100,16 @@
     "additionalSurveys"
   ],
   computed: {
-    ...mapState("fairwayprofile", ["startPoint", "endPoint"]),
+    ...mapState("fairwayprofile", [
+      "startPoint",
+      "endPoint",
+      "currentProfile",
+      "selectedMorph"
+    ]),
+    currentData() {
+      const currentSurveyDate = this.selectedMorph.date_info;
+      return this.currentProfile[currentSurveyDate];
+    },
     waterColor() {
       const result = this.waterLevels.find(
         x => x.level === this.selectedWaterLevel
@@ -109,7 +123,7 @@
     };
   },
   watch: {
-    data() {
+    currentProfile() {
       this.drawDiagram();
     },
     width() {
@@ -140,7 +154,7 @@
       svg.attr("height", this.height);
       const width = this.width - this.margin.right - 1.5 * this.margin.left;
       const height = this.height - this.margin.top - 2 * this.margin.bottom;
-      const currentData = this.data;
+      const currentData = this.currentData;
       const {
         xScale,
         yScaleRight,
--- a/client/src/fairway/store.js	Wed Oct 24 15:35:50 2018 +0200
+++ b/client/src/fairway/store.js	Wed Oct 24 15:38:07 2018 +0200
@@ -12,8 +12,10 @@
  * Author(s):
  * Thomas Junk <thomas.junk@intevation.de>
  */
-
+import { HTTP } from "../application/lib/http";
 import { prepareProfile } from "../application/lib/geo";
+import LineString from "ol/geom/LineString.js";
+import { generateFeatureRequest } from "../application/lib/geo.js";
 
 const DEMOLEVEL = 149.345;
 
@@ -24,12 +26,13 @@
     totalLength: 0,
     minAlt: 0,
     maxAlt: 0,
-    currentProfile: [],
+    currentProfile: {},
     waterLevels: [{ year: "2016", level: DEMOLEVEL, color: "#005DFF" }],
     selectedWaterLevel: DEMOLEVEL,
     fairwayCoordinates: [],
     startPoint: null,
-    endPoint: null
+    endPoint: null,
+    selectedMorph: null
   },
   getters: {
     length: state => {
@@ -37,13 +40,17 @@
     }
   },
   mutations: {
+    setSelectedMorph: (state, selectedMorph) => {
+      state.selectedMorph = selectedMorph;
+    },
     setAvailableSurveys: (state, surveys) => {
       state.availableSurveys = surveys;
     },
     setSelectedWaterLevel: (state, level) => {
       state.selectedWaterLevel = level;
     },
-    profileLoaded: (state, response) => {
+    profileLoaded: (state, answer) => {
+      const { response, surveyDate } = answer;
       const { data } = response;
       const coordinates = data.geometry.coordinates;
       if (!coordinates) return;
@@ -51,7 +58,7 @@
       const endPoint = state.endPoint;
       const geoJSON = data;
       const result = prepareProfile({ geoJSON, startPoint, endPoint });
-      state.currentProfile = result.points;
+      state.currentProfile[surveyDate] = result.points;
       state.minAlt = result.minAlt;
       state.maxAlt = result.maxAlt;
       state.totalLength = result.lengthPolyLine;
@@ -66,7 +73,33 @@
       state.fairwayCoordinates = coordinates;
     },
     clearCurrentProfile: state => {
-      state.currentProfile = [];
+      state.currentProfile = {};
+    }
+  },
+  actions: {
+    loadProfile({ commit, state }, date_info) {
+      return new Promise((resolve, reject) => {
+        const profileLine = new LineString([state.startPoint, state.endPoint]);
+        const geoJSON = generateFeatureRequest(
+          profileLine,
+          state.selectedMorph.bottleneck_id,
+          date_info
+        );
+        HTTP.post("/cross", geoJSON, {
+          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+        })
+          .then(response => {
+            commit("profileLoaded", {
+              response: response,
+              surveyDate: date_info
+            });
+            resolve(response);
+          })
+          .catch(error => {
+            commit("clear_auth");
+            reject(error);
+          });
+      });
     }
   }
 };
--- a/client/src/linetool/Linetool.vue	Wed Oct 24 15:35:50 2018 +0200
+++ b/client/src/linetool/Linetool.vue	Wed Oct 24 15:38:07 2018 +0200
@@ -51,8 +51,8 @@
   },
   computed: {
     ...mapGetters("application", ["drawMode"]),
-    ...mapState("identifystore", ["identifiedFeatures", "selectedMorph"]),
-    ...mapState("morphstore", ["selectedMorph"]),
+    ...mapState("identifystore", ["identifiedFeatures"]),
+    ...mapState("fairwayprofile", ["selectedMorph"]),
     icon() {
       return {
         fa: true,
--- a/client/src/map/Maplayer.vue	Wed Oct 24 15:35:50 2018 +0200
+++ b/client/src/map/Maplayer.vue	Wed Oct 24 15:38:07 2018 +0200
@@ -50,7 +50,6 @@
 import { Vector as VectorSource } from "ol/source.js";
 import { getLength } from "ol/sphere.js";
 import { Icon, Stroke, Style, Fill } from "ol/style.js";
-import { generateFeatureRequest } from "../application/lib/geo.js";
 
 import distance from "@turf/distance";
 import {
@@ -59,6 +58,7 @@
 } from "@turf/helpers";
 import lineIntersect from "@turf/line-intersect";
 import { displayError } from "../application/lib/errors.js";
+import { calculateFairwayCoordinates } from "../application/lib/geo.js";
 
 const DEMODATA = 2.5;
 
@@ -78,7 +78,7 @@
   computed: {
     ...mapGetters("mapstore", ["layers", "getLayerByName"]),
     ...mapState("mapstore", ["openLayersMap"]),
-    ...mapState("morphstore", ["selectedMorph"]),
+    ...mapState("fairwayprofile", ["selectedMorph"]),
     mapStyle() {
       return {
         mapfull: !this.split,
@@ -176,80 +176,52 @@
         const [start, end] = inputLineString.getCoordinates();
         this.$store.commit("fairwayprofile/setStartPoint", start);
         this.$store.commit("fairwayprofile/setEndPoint", end);
-        this.requestProfile(start, end, this.selectedMorph);
+        const profileLine = new LineString([start, end]);
+        this.$store
+          .dispatch("fairwayprofile/loadProfile", this.selectedMorph.date_info)
+          .then(() => {
+            var vectorSource = this.getLayerByName(
+              "Fairway Dimensions"
+            ).data.getSource();
+            this.calculateIntersection(vectorSource, profileLine);
+          })
+          .then(() => {
+            this.$store.commit("application/openSplitScreen");
+          })
+          .catch(error => {
+            const { status, data } = error.response;
+            displayError({
+              title: "Backend Error",
+              message: `${status}: ${data.message || data}`
+            });
+          });
       }
     },
-    requestProfile(start, end, survey) {
-      // survey has to have the properties bottleneck_id and date_info
-      // prepare to send the first line seqment to the server as GeoJSON
-      const profileLine = new LineString([start, end]);
-      const geoJSON = generateFeatureRequest(profileLine, survey);
-      HTTP.post("/cross", geoJSON, {
-        headers: { "X-Gemma-Auth": localStorage.getItem("token") }
-      })
-        .then(response => {
-          this.$store.commit("fairwayprofile/profileLoaded", response);
-        })
-        .then(() => {
-          var vectorSource = this.getLayerByName(
-            "Fairway Dimensions"
-          ).data.getSource();
-          vectorSource.forEachFeatureIntersectingExtent(
-            // need to use EPSG:3857 which is the proj of vectorSource
-            profileLine
-              .clone()
-              .transform("EPSG:4326", "EPSG:3857")
-              .getExtent(),
-            feature => {
-              // transform back to prepare for usage
-              var intersectingPolygon = feature
-                .getGeometry()
-                .clone()
-                .transform("EPSG:3857", "EPSG:4326");
-              this.addToFairwayRectangle(
-                profileLine,
-                intersectingPolygon,
-                DEMODATA
-              );
-            }
-          );
-          this.$store.commit("application/openSplitScreen");
-        })
-        .catch(error => {
-          const { status, data } = error.response;
-          displayError({
-            title: "Backend Error",
-            message: `${status}: ${data.message || data}`
-          });
-        });
-    },
-    addToFairwayRectangle(profileLine, fairwayGeometry, depth) {
-      // both geometries have to be in EPSG:4326
-      // uses turfjs distance() function
-      let fairwayCoordinates = [];
-      var line = turfLineString(profileLine.getCoordinates());
-      var polygon = turfPolygon(fairwayGeometry.getCoordinates());
-      var intersects = lineIntersect(line, polygon);
-      var l = intersects.features.length;
-      if (l % 2 != 0) {
-        console.log("Ignoring fairway because profile only intersects once.");
-      } else {
-        for (let i = 0; i < l; i += 2) {
-          let pStartPoint = profileLine.getCoordinates()[0];
-          let fStartPoint = intersects.features[i].geometry.coordinates;
-          let fEndPoint = intersects.features[i + 1].geometry.coordinates;
-          let opts = { units: "kilometers" };
-
-          fairwayCoordinates.push([
-            distance(pStartPoint, fStartPoint, opts) * 1000,
-            distance(pStartPoint, fEndPoint, opts) * 1000,
-            depth
-          ]);
-        }
-      }
-      this.$store.commit(
-        "fairwayprofile/setFairwayCoordinates",
-        fairwayCoordinates
+    calculateIntersection(vectorSource, profileLine) {
+      const transformedLine = profileLine
+        .clone()
+        .transform("EPSG:4326", "EPSG:3857")
+        .getExtent();
+      const featureCallback = feature => {
+        // transform back to prepare for usage
+        var intersectingPolygon = feature
+          .getGeometry()
+          .clone()
+          .transform("EPSG:3857", "EPSG:4326");
+        const fairwayCoordinates = calculateFairwayCoordinates(
+          profileLine,
+          intersectingPolygon,
+          DEMODATA
+        );
+        this.$store.commit(
+          "fairwayprofile/setFairwayCoordinates",
+          fairwayCoordinates
+        );
+      };
+      vectorSource.forEachFeatureIntersectingExtent(
+        // need to use EPSG:3857 which is the proj of vectorSource
+        transformedLine,
+        featureCallback
       );
     },
     activateInteraction() {
--- a/client/src/morphtool/Morphtool.vue	Wed Oct 24 15:35:50 2018 +0200
+++ b/client/src/morphtool/Morphtool.vue	Wed Oct 24 15:38:07 2018 +0200
@@ -5,13 +5,21 @@
                 <div class="headline">
                     <h4>{{bottleneckName}}</h4>
                     <hr>
-                    <div @click="clearSelection" class="float-left ui-element d-flex morphtoolminus">
+                    <div
+                        @click="clearSelection"
+                        class="float-left ui-element d-flex morphtoolminus"
+                    >
                         <i class="fa fa-close morphtoolsminus"></i>
                     </div>
                 </div>
                 <ul class="list-group surveylist">
-                    <li v-for="survey of surveyList.surveys" :key="survey.data_info" class="list-group-item" @click.prevent="selectSurvey(survey)">
-                        <a href="#" @click.prevent="">{{survey.date_info}}</a>
+                    <li
+                        v-for="survey of surveyList.surveys"
+                        :key="survey.data_info"
+                        class="list-group-item"
+                        @click.prevent="selectSurvey(survey)"
+                    >
+                        <a href="#" @click.prevent>{{survey.date_info}}</a>
                     </li>
                 </ul>
             </div>
@@ -20,7 +28,10 @@
             <div class="d-flex flex-row justify-content-between">
                 <i class="fa fa-close text-danger"></i>
                 <small>Bottleneck:&nbsp;</small>
-                <h6>{{bottleneckName}} <small>( {{selectedMorph.date_info}} )</small></h6>
+                <h6>
+                    {{bottleneckName}}
+                    <small>( {{selectedMorph.date_info}} )</small>
+                </h6>
             </div>
         </div>
     </div>
@@ -111,14 +122,14 @@
   computed: {
     ...mapGetters("application", ["drawMode"]),
     ...mapState("identifystore", ["identifiedFeatures"]),
-    ...mapState("morphstore", ["selectedMorph"]),
+    ...mapState("fairwayprofile", ["selectedMorph"]),
     selectedBottleneck: function() {
       if (this.identifiedFeatures && !this.drawMode) {
         for (let feature of this.identifiedFeatures) {
           let id = feature.getId();
           // RegExp.prototype.test() works with number, str and undefined
           if (/^bottlenecks\./.test(id)) {
-            this.$store.commit("morphstore/setSelectedMorph", null);
+            this.$store.commit("fairwayprofile/setSelectedMorph", null);
             return feature;
           }
         }
@@ -163,12 +174,12 @@
         });
     },
     selectSurvey(survey) {
-      this.$store.commit("morphstore/setSelectedMorph", survey);
+      this.$store.commit("fairwayprofile/setSelectedMorph", survey);
       this.surveyList = null;
     },
     clearSelection() {
       this.$store.commit("identifystore/setIdentifiedFeatures", []);
-      this.$store.commit("morphstore/setSelectedMorph", null);
+      this.$store.commit("fairwayprofile/setSelectedMorph", null);
       this.surveyList = null;
       if (this.drawMode) {
         this.$store.commit("application/toggleDrawModeLine");
--- a/client/src/morphtool/store.js	Wed Oct 24 15:35:50 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-/*
- * 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>
- */
-const MorphStore = {
-  namespaced: true,
-  state: {
-    selectedMorph: null
-  },
-  getters: {
-    selectedMorph: state => {
-      return state.selectedMorph;
-    }
-  },
-  mutations: {
-    setSelectedMorph: (state, selectedMorph) => {
-      state.selectedMorph = selectedMorph;
-    }
-  }
-};
-
-export default MorphStore;
--- a/client/src/store.js	Wed Oct 24 15:35:50 2018 +0200
+++ b/client/src/store.js	Wed Oct 24 15:38:07 2018 +0200
@@ -21,7 +21,6 @@
 import mapstore from "./map/store";
 import FairwayProfile from "./fairway/store";
 import IdentifyStore from "./identify/store";
-import MorphStore from "./morphtool/store";
 
 Vue.use(Vuex);
 
@@ -31,7 +30,6 @@
     fairwayprofile: FairwayProfile,
     identifystore: IdentifyStore,
     mapstore: mapstore,
-    morphstore: MorphStore,
     user: user,
     usermanagement: usermanagement
   }
--- a/cmd/gemma/main.go	Wed Oct 24 15:35:50 2018 +0200
+++ b/cmd/gemma/main.go	Wed Oct 24 15:38:07 2018 +0200
@@ -62,7 +62,7 @@
 	m.PathPrefix("/").Handler(http.FileServer(http.Dir(web)))
 
 	addr := fmt.Sprintf("%s:%d", config.WebHost(), config.WebPort())
-	log.Printf("listen on %s\n", addr)
+	log.Printf("info: listen on %s\n", addr)
 
 	var h http.Handler
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/controllers/importqueue.go	Wed Oct 24 15:38:07 2018 +0200
@@ -0,0 +1,163 @@
+// 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):
+//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
+
+package controllers
+
+import (
+	"database/sql"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"gemma.intevation.de/gemma/pkg/models"
+	"github.com/gorilla/mux"
+)
+
+const (
+	selectImportsUnpagedSQL = `
+SELECT
+  id,
+  state::varchar,
+  enqueued,
+  kind,
+  username
+FROM waterway.imports
+ORDER BY id`
+
+	selectImportPagedSQL = selectImportsUnpagedSQL + `
+LIMIT $1 OFFSET $2`
+
+	selectHasImportSQL = `
+SELECT true FROM Waterway.imports WHERE id = $1`
+
+	selectImportLogsSQL = `
+SELECT
+  time,
+  kind::varchar,
+  msg
+FROM waterway.import_logs
+WHERE import_id = $1
+ORDER BY time`
+)
+
+func listImports(
+	_ interface{},
+	req *http.Request,
+	conn *sql.Conn,
+) (jr JSONResult, err error) {
+	vars := mux.Vars(req)
+
+	off, of := vars["offset"]
+	lim, lf := vars["limit"]
+
+	var rows *sql.Rows
+
+	if of && lf {
+		offset, _ := strconv.ParseInt(off, 10, 64)
+		limit, _ := strconv.ParseInt(lim, 10, 64)
+		rows, err = conn.QueryContext(
+			req.Context(), selectImportPagedSQL, limit, offset)
+	} else {
+		rows, err = conn.QueryContext(
+			req.Context(), selectImportsUnpagedSQL)
+	}
+	if err != nil {
+		return
+	}
+	defer rows.Close()
+
+	imports := make([]*models.Import, 0, 20)
+
+	for rows.Next() {
+		var it models.Import
+		if err = rows.Scan(
+			&it.ID,
+			&it.State,
+			&it.Enqueued,
+			&it.Kind,
+			&it.User,
+		); err != nil {
+			return
+		}
+		imports = append(imports, &it)
+	}
+
+	if err = rows.Err(); err != nil {
+		return
+	}
+
+	jr = JSONResult{
+		Result: struct {
+			Imports []*models.Import `json:"imports"`
+		}{
+			Imports: imports,
+		},
+	}
+	return
+}
+
+func importLogs(
+	_ interface{},
+	req *http.Request,
+	conn *sql.Conn,
+) (jr JSONResult, err error) {
+
+	ctx := req.Context()
+
+	id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64)
+
+	// Check if he have such a import job first.
+	var dummy bool
+	err = conn.QueryRowContext(ctx, selectHasImportSQL, id).Scan(&dummy)
+	switch {
+	case err == sql.ErrNoRows:
+		err = JSONError{
+			Code:    http.StatusNotFound,
+			Message: fmt.Sprintf("Cannot find import #%d.", id),
+		}
+		return
+	case err != nil:
+		return
+	}
+
+	// We have it -> generate log entries.
+	var rows *sql.Rows
+	rows, err = conn.QueryContext(ctx, selectImportLogsSQL, id)
+	if err != nil {
+		return
+	}
+	defer rows.Close()
+
+	entries := make([]*models.ImportLogEntry, 0, 10)
+
+	for rows.Next() {
+		var entry models.ImportLogEntry
+		if err = rows.Scan(&entry.Time, &entry.Kind, &entry.Message); err != nil {
+			return
+		}
+		entries = append(entries, &entry)
+	}
+
+	if err = rows.Err(); err != nil {
+		return
+	}
+
+	jr = JSONResult{
+		Result: struct {
+			Entries []*models.ImportLogEntry `json:"entries"`
+		}{
+			Entries: entries,
+		},
+	}
+	return
+}
--- a/pkg/controllers/routes.go	Wed Oct 24 15:35:50 2018 +0200
+++ b/pkg/controllers/routes.go	Wed Oct 24 15:38:07 2018 +0200
@@ -158,6 +158,25 @@
 	api.Handle("/imports/soundingresult",
 		waterwayAdmin(http.HandlerFunc(importSoundingResult))).Methods(http.MethodPost)
 
+	// Import queue
+	lsImports := waterwayAdmin(&JSONHandler{
+		Handle: listImports,
+	})
+
+	api.Handle("/imports", lsImports).
+		Methods(http.MethodGet).
+		Queries(
+			"offset", "{offset:[0-9]+}",
+			"limit", "{limit:[0-9]+}")
+
+	api.Handle("/imports", lsImports).
+		Methods(http.MethodGet)
+
+	api.Handle("/imports/{id:[0-9]+}", waterwayAdmin(&JSONHandler{
+		Handle: importLogs,
+	})).
+		Methods(http.MethodGet)
+
 	// Token handling: Login/Logout.
 	api.HandleFunc("/login", login).
 		Methods(http.MethodPost)
--- a/pkg/controllers/search.go	Wed Oct 24 15:35:50 2018 +0200
+++ b/pkg/controllers/search.go	Wed Oct 24 15:38:07 2018 +0200
@@ -26,12 +26,13 @@
 const (
 	searchHectometreSQL = `SELECT COALESCE(json_agg(r),'[]')
 FROM (SELECT (location_code).hectometre || ' rhm' AS name,
-             ST_AsGeoJSON(geom)::json AS geom
+             ST_AsGeoJSON(geom)::json AS geom, 'rhm' AS type
       FROM waterway.distance_marks_virtual
       WHERE (location_code).hectometre = $1) r`
 	searchBottleneckSQL = `SELECT COALESCE(json_agg(r),'[]')
 FROM (SELECT objnam AS name,
-             ST_AsGeoJSON(ST_Centroid(area))::json AS geom
+             ST_AsGeoJSON(ST_Centroid(area))::json AS geom,
+             'bottleneck' AS type
       FROM waterway.bottlenecks
       WHERE objnam ILIKE $1) r`
 )
--- a/pkg/geoserver/boot.go	Wed Oct 24 15:35:50 2018 +0200
+++ b/pkg/geoserver/boot.go	Wed Oct 24 15:38:07 2018 +0200
@@ -51,7 +51,7 @@
 
 	if err := json.NewEncoder(&buf).Encode(x); err != nil {
 		// Should not happen
-		log.Printf("bad JSON: %v\n", err)
+		log.Printf("warn: bad JSON: %v\n", err)
 	}
 	return bytes.NewReader(buf.Bytes())
 }
@@ -379,7 +379,7 @@
 
 func updateStyle(entry *models.IntEntry, create bool) error {
 
-	log.Printf("creating style %s\n", entry.Name)
+	log.Printf("info: creating style %s\n", entry.Name)
 
 	// Try to load the style data.
 	data, err := entry.LoadStyle()
@@ -525,7 +525,7 @@
 	for i := range entries {
 		entry := &entries[i]
 		if stls.hasStyle(entry.Name) {
-			log.Printf("already has style for %s\n", entry.Name)
+			log.Printf("warn: already has style for %s\n", entry.Name)
 			continue
 		}
 		if err := updateStyle(entry, true); err != nil {
--- a/pkg/geoserver/reconf.go	Wed Oct 24 15:35:50 2018 +0200
+++ b/pkg/geoserver/reconf.go	Wed Oct 24 15:38:07 2018 +0200
@@ -47,7 +47,7 @@
 }
 
 func reconfigure(fn func() error) error {
-	log.Println("Configure GeoServer...")
+	log.Println("info: configure GeoServer...")
 	const (
 		maxTries = 10
 		sleep    = time.Second * 5
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/models/import.go	Wed Oct 24 15:38:07 2018 +0200
@@ -0,0 +1,51 @@
+// 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):
+//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
+
+package models
+
+import (
+	"encoding/json"
+	"time"
+)
+
+type (
+	ImportTime struct{ time.Time }
+
+	Import struct {
+		ID       int64      `json:"id"`
+		State    string     `json:"state"`
+		Enqueued ImportTime `json:"enqueued"`
+		Kind     string     `json:"kind"`
+		User     string     `json:"user"`
+	}
+
+	ImportLogEntry struct {
+		Time    ImportTime `json:"time"`
+		Kind    string     `json:"kind"`
+		Message string     `json:"message"`
+	}
+)
+
+func (it ImportTime) MarshalJSON() ([]byte, error) {
+	return json.Marshal(it.Format("2006-01-02T15:04:05"))
+}
+
+func (it *ImportTime) Scan(x interface{}) error {
+	t, ok := x.(time.Time)
+	if !ok {
+		*it = ImportTime{}
+	} else {
+		*it = ImportTime{t}
+	}
+	return nil
+}