changeset 4597:bd2999cac246

Merge 'iso-areas' branch back into default.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Wed, 09 Oct 2019 15:10:35 +0200
parents 998f4d7d9626 (current diff) 800119befa90 (diff)
children 96283fc7de02
files style-templates/sounding_results_contour_lines_geoserver.sld-template
diffstat 24 files changed, 675 insertions(+), 308 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/layers/Layerselect.vue	Wed Oct 09 14:40:56 2019 +0200
+++ b/client/src/components/layers/Layerselect.vue	Wed Oct 09 15:10:35 2019 +0200
@@ -78,7 +78,7 @@
     refreshLegend() {
       if (this.layer.get("id") === "BOTTLENECKISOLINE") {
         this.loadLegendImage(
-          "sounding_results_contour_lines_geoserver",
+          "sounding_results_areas_geoserver",
           "isolinesLegendImgDataURL"
         );
       }
@@ -137,7 +137,7 @@
     },
     loadLegendImage(layer, storeTarget) {
       HTTP.get(
-        `/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.0.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=${layer}&legend_options=columns:4;fontAntiAliasing:true`,
+        `/internal/wms?REQUEST=GetLegendGraphic&VERSION=1.3.0&FORMAT=image/png&WIDTH=20&HEIGHT=20&LAYER=${layer}&legend_options=columns:4;fontAntiAliasing:true&SCALE=5000`,
         {
           headers: {
             Accept: "image/png",
--- a/client/src/components/map/layers.js	Wed Oct 09 14:40:56 2019 +0200
+++ b/client/src/components/map/layers.js	Wed Oct 09 15:10:35 2019 +0200
@@ -465,7 +465,7 @@
             projection: "EPSG:3857",
             url: window.location.origin + "/api/internal/wms",
             params: {
-              LAYERS: "sounding_results_contour_lines_geoserver",
+              LAYERS: "sounding_results_areas_geoserver",
               VERSION: "1.1.1",
               TILED: true
             },
--- a/go.mod	Wed Oct 09 14:40:56 2019 +0200
+++ b/go.mod	Wed Oct 09 15:10:35 2019 +0200
@@ -5,6 +5,7 @@
 require (
 	github.com/cockroachdb/apd v1.1.0 // indirect
 	github.com/etcd-io/bbolt v1.3.3
+	github.com/fogleman/contourmap v0.0.0-20190814184649-9f61d36c4199
 	github.com/gofrs/uuid v3.2.0+incompatible // indirect
 	github.com/golang/snappy v0.0.1
 	github.com/gorilla/mux v1.7.3
--- a/go.sum	Wed Oct 09 14:40:56 2019 +0200
+++ b/go.sum	Wed Oct 09 15:10:35 2019 +0200
@@ -2,6 +2,7 @@
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ=
 github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -25,6 +26,8 @@
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM=
 github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
+github.com/fogleman/contourmap v0.0.0-20190814184649-9f61d36c4199 h1:kufr0u0RIG5ACpjFsPRbbuHa0FhMWsS3tnSFZ2hf07s=
+github.com/fogleman/contourmap v0.0.0-20190814184649-9f61d36c4199/go.mod h1:mqaaaP4j7nTF8T/hx5OCljA7BYWHmrH2uh+Q023OchE=
 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -61,8 +64,6 @@
 github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
 github.com/jackc/pgx v3.6.0+incompatible h1:bJeo4JdVbDAW8KB2m8XkFeo8CPipREoG37BwEoKGz+Q=
 github.com/jackc/pgx v3.6.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
-github.com/jonas-p/go-shp v0.1.1 h1:LY81nN67DBCz6VNFn2kS64CjmnDo9IP8rmSkTvhO9jE=
-github.com/jonas-p/go-shp v0.1.1/go.mod h1:MRIhyxDQ6VVp0oYeD7yPGr5RSTNScUFKCDsI5DR7PtI=
 github.com/jonas-p/go-shp v0.1.2-0.20190401125246-9fd306ae10a6 h1:h5O7ee4tlSPVjdC75eSLX7jXZiHftthuHio/GtrhaSM=
 github.com/jonas-p/go-shp v0.1.2-0.20190401125246-9fd306ae10a6/go.mod h1:MRIhyxDQ6VVp0oYeD7yPGr5RSTNScUFKCDsI5DR7PtI=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/common/linear.go	Wed Oct 09 15:10:35 2019 +0200
@@ -0,0 +1,36 @@
+// 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) 2019 by via donau
+//   – Österreichische Wasserstraßen-Gesellschaft mbH
+// Software engineering by Intevation GmbH
+//
+// Author(s):
+//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>
+package common
+
+// Linear constructs a function which maps x1 to y1 and x2 to y2.
+// All other values are interpolated linearly.
+func Linear(x1, y1, x2, y2 float64) func(float64) float64 {
+	// f(x1) = y1
+	// f(x2) = y2
+	// y1 = x1*a + b <=> b = y1 - x1*a
+	// y2 = x2*a + b
+
+	// y1 - y2 = a*(x1 - x2)
+	// a = (y1-y2)/(x1 - x2) for x1 != x2
+
+	if x1 == x2 {
+		return func(float64) float64 {
+			return 0.5 * (y1 + y2)
+		}
+	}
+	a := (y1 - y2) / (x1 - x2)
+	b := y1 - x1*a
+	return func(x float64) float64 {
+		return x*a + b
+	}
+}
--- a/pkg/controllers/diff.go	Wed Oct 09 14:40:56 2019 +0200
+++ b/pkg/controllers/diff.go	Wed Oct 09 15:10:35 2019 +0200
@@ -36,6 +36,12 @@
 )
 
 const (
+	// isoCellSize is the side length of a raster cell when tracing
+	// iso areas.
+	isoCellSize = 0.5
+)
+
+const (
 	diffIDSQL = `
 SELECT sd.id FROM
   caching.sounding_differences sd JOIN
@@ -57,24 +63,24 @@
 WHERE m.date_info = $2::date AND s.date_info = $3::date
 RETURNING id
 `
-	insertDiffContourSQL = `
-INSERT INTO caching.sounding_differences_contour_lines (
+	insertDiffIsoAreasQL = `
+INSERT INTO caching.sounding_differences_iso_areas (
   sounding_differences_id,
   height,
-  lines
+  areas
 )
 SELECT
-  $5,
-  $4,
+  $1,
+  $2,
   ST_Transform(
     ST_Multi(
       ST_CollectionExtract(
         ST_SimplifyPreserveTopology(
           ST_Multi(ST_Collectionextract(
-            ST_MakeValid(ST_GeomFromWKB($1, $2::integer)), 2)),
-          $3
+            ST_MakeValid(ST_GeomFromWKB($4, $3::integer)), 3)),
+          $5
         ),
-        2
+        3
       )
     ),
     4326
@@ -273,11 +279,11 @@
 
 	log.Printf("info: num heights: %d\n", len(heights))
 
-	var stmt *sql.Stmt
-	if stmt, err = tx.PrepareContext(ctx, insertDiffContourSQL); err != nil {
+	var isoStmt *sql.Stmt
+	if isoStmt, err = tx.PrepareContext(ctx, insertDiffIsoAreasQL); err != nil {
 		return
 	}
-	defer stmt.Close()
+	defer isoStmt.Close()
 
 	if err = tx.QueryRowContext(
 		ctx,
@@ -291,18 +297,21 @@
 
 	heights = common.DedupFloat64s(heights)
 
-	octree.DoContours(tree, heights, func(res *octree.ContourResult) {
-		if err == nil && len(res.Lines) > 0 {
-			_, err = stmt.ExecContext(
-				ctx,
-				res.Lines.AsWKB2D(),
-				minuendTree.EPSG,
-				contourTolerance,
-				res.Height,
-				id,
-			)
+	areas := tree.TraceAreas(heights, isoCellSize)
+
+	for i, a := range areas {
+		if len(a) == 0 {
+			continue
 		}
-	})
+		if _, err = isoStmt.ExecContext(
+			ctx,
+			id, heights[i], minuendTree.EPSG,
+			a.AsWKB(),
+			contourTolerance,
+		); err != nil {
+			return
+		}
+	}
 
 	log.Printf("info: calculating and storing iso lines took %v\n",
 		time.Since(start))
--- a/pkg/controllers/system.go	Wed Oct 09 14:40:56 2019 +0200
+++ b/pkg/controllers/system.go	Wed Oct 09 15:10:35 2019 +0200
@@ -239,7 +239,7 @@
 		func(old sql.NullString, curr string) (func(*http.Request), error) {
 			return reconfigureClassBreaks(
 				old, curr,
-				"sounding_results_contour_lines_geoserver",
+				"sounding_results_areas_geoserver",
 				func(req *http.Request) {
 					if s, ok := auth.GetSession(req); ok {
 						triggerSoundingResultsContoursRecalc(s.User, curr)
--- a/pkg/geoserver/templates.go	Wed Oct 09 14:40:56 2019 +0200
+++ b/pkg/geoserver/templates.go	Wed Oct 09 15:10:35 2019 +0200
@@ -35,7 +35,7 @@
 
 func init() {
 	RegisterStylePreprocessor(
-		"sounding_results_contour_lines_geoserver",
+		"sounding_results_areas_geoserver",
 		templateContourLinesFunc("morphology_classbreaks"))
 	RegisterStylePreprocessor(
 		"sounding_differences",
--- a/pkg/imports/isr.go	Wed Oct 09 14:40:56 2019 +0200
+++ b/pkg/imports/isr.go	Wed Oct 09 15:10:35 2019 +0200
@@ -44,10 +44,7 @@
 }
 
 func (isrJobCreator) Depends() [2][]string {
-	return [2][]string{
-		{"sounding_results", "sounding_results_contour_lines"},
-		{},
-	}
+	return srJobCreator{}.Depends()
 }
 
 const (
@@ -56,8 +53,8 @@
 FROM waterway.sounding_results
 ORDER BY bottleneck_id
 `
-	deleteContourLinesSQL = `
-DELETE FROM waterway.sounding_results_contour_lines
+	deleteIsoAreasSQL = `
+DELETE FROM waterway.sounding_results_iso_areas
 WHERE sounding_result_id = $1
 `
 )
@@ -166,10 +163,11 @@
 	}
 	defer tx.Rollback()
 
-	insertStmt, err := tx.Prepare(insertContourSQL)
+	insertAreasStmt, err := tx.Prepare(insertIsoAreasSQL)
 	if err != nil {
 		return err
 	}
+	defer insertAreasStmt.Close()
 
 	// For all sounding results in bottleneck.
 	for _, sr := range bn.srs {
@@ -180,22 +178,25 @@
 		hs := octree.ExtrapolateClassBreaks(heights, tree.Min.Z, tree.Max.Z)
 		hs = common.DedupFloat64s(hs)
 
-		// Delete the old contour lines.
-		if _, err := tx.ExecContext(ctx, deleteContourLinesSQL, sr); err != nil {
+		// Delete the old iso areas.
+		if _, err := tx.ExecContext(ctx, deleteIsoAreasSQL, sr); err != nil {
 			return err
 		}
 
-		octree.DoContours(tree, hs, func(res *octree.ContourResult) {
-			if err == nil && len(res.Lines) > 0 {
-				_, err = insertStmt.ExecContext(
-					ctx,
-					sr, res.Height, tree.EPSG,
-					res.Lines.AsWKB2D(),
-					contourTolerance)
+		// Calculate and store the iso areas.
+		areas := tree.TraceAreas(hs, isoCellSize)
+		for i, a := range areas {
+			if len(a) == 0 {
+				continue
 			}
-		})
-		if err != nil {
-			return err
+			if _, err := insertAreasStmt.ExecContext(
+				ctx,
+				sr, hs[i], tree.EPSG,
+				a.AsWKB(),
+				contourTolerance,
+			); err != nil {
+				return err
+			}
 		}
 	}
 
--- a/pkg/imports/sr.go	Wed Oct 09 14:40:56 2019 +0200
+++ b/pkg/imports/sr.go	Wed Oct 09 15:10:35 2019 +0200
@@ -37,6 +37,7 @@
 	"gemma.intevation.de/gemma/pkg/common"
 	"gemma.intevation.de/gemma/pkg/models"
 	"gemma.intevation.de/gemma/pkg/octree"
+	"gemma.intevation.de/gemma/pkg/wkb"
 )
 
 // SoundingResult is a Job to import sounding reults
@@ -70,9 +71,17 @@
 )
 
 const (
+	// pointsPerSquareMeter is the average number of points
+	// when generating a artifical height model for single beam scans.
 	pointsPerSquareMeter = 2
 )
 
+const (
+	// isoCellSize is the side length of a raster cell when tracing
+	// iso areas.
+	isoCellSize = 0.5
+)
+
 // SRJobKind is the unique name of this import job type.
 const SRJobKind JobKind = "sr"
 
@@ -88,7 +97,7 @@
 
 func (srJobCreator) Depends() [2][]string {
 	return [2][]string{
-		{"sounding_results", "sounding_results_contour_lines"},
+		{"sounding_results", "sounding_results_iso_areas"},
 		{"bottlenecks"},
 	}
 }
@@ -152,11 +161,11 @@
   ST_AsBinary(ST_Buffer(ST_MakeValid(ST_GeomFromWKB($1, $2::integer)), 0.0)),
   ST_AsBinary(ST_Buffer(ST_MakeValid(ST_GeomFromWKB($1, $2::integer)), 0.1))`
 
-	insertContourSQL = `
-INSERT INTO waterway.sounding_results_contour_lines (
+	insertIsoAreasSQL = `
+INSERT INTO waterway.sounding_results_iso_areas (
   sounding_result_id,
   height,
-  lines
+  areas
 )
 SELECT
   $1,
@@ -166,10 +175,10 @@
       ST_CollectionExtract(
         ST_SimplifyPreserveTopology(
           ST_Multi(ST_Collectionextract(
-            ST_MakeValid(ST_GeomFromWKB($4, $3::integer)), 2)),
+            ST_MakeValid(ST_GeomFromWKB($4, $3::integer)), 3)),
           $5
         ),
-        2
+        3
       )
     ),
     4326
@@ -619,15 +628,10 @@
 		return nil, err
 	}
 	feedback.Info("Storing octree index took %s.", time.Since(start))
-	feedback.Info("Generate contour lines")
-
-	start = time.Now()
-	err = generateContours(ctx, tx, feedback, builder.Tree(), id)
+	err = generateIsos(ctx, tx, feedback, builder.Tree(), id)
 	if err != nil {
 		return nil, err
 	}
-	feedback.Info("Generating contour lines took %s.",
-		time.Since(start))
 
 	// Store for potential later removal.
 	if err = track(ctx, tx, importID, "waterway.sounding_results", id); err != nil {
@@ -827,23 +831,19 @@
 	return shapeToPolygon(s)
 }
 
-func generateContours(
+func generateIsos(
 	ctx context.Context,
 	tx *sql.Tx,
 	feedback Feedback,
 	tree *octree.Tree,
 	id int64,
 ) error {
-	stmt, err := tx.PrepareContext(ctx, insertContourSQL)
-	if err != nil {
-		return err
-	}
-	defer stmt.Close()
 
 	heights, err := octree.LoadClassBreaks(
 		ctx, tx,
 		"morphology_classbreaks",
 	)
+
 	if err != nil {
 		feedback.Warn("Loading class breaks failed: %v", err)
 		feedback.Info("Using default class breaks")
@@ -871,15 +871,66 @@
 
 	heights = common.DedupFloat64s(heights)
 
-	octree.DoContours(tree, heights, func(res *octree.ContourResult) {
-		if err == nil && len(res.Lines) > 0 {
-			_, err = stmt.ExecContext(
-				ctx,
-				id, res.Height, tree.EPSG,
-				res.Lines.AsWKB2D(),
-				contourTolerance)
+	return generateIsoAreas(ctx, tx, feedback, tree, heights, id)
+}
+
+func generateIsoAreas(
+	ctx context.Context,
+	tx *sql.Tx,
+	feedback Feedback,
+	tree *octree.Tree,
+	heights []float64,
+	id int64,
+) error {
+	feedback.Info("Generate iso areas")
+	total := time.Now()
+	defer func() {
+		feedback.Info("Generating iso areas took %s.",
+			time.Since(total))
+	}()
+
+	areas := tree.TraceAreas(heights, isoCellSize)
+
+	return storeAreas(
+		ctx, tx, feedback,
+		areas, tree.EPSG, heights, id)
+}
+
+func storeAreas(
+	ctx context.Context,
+	tx *sql.Tx,
+	feedback Feedback,
+	areas []wkb.MultiPolygonGeom,
+	epsg uint32,
+	heights []float64,
+	id int64,
+) error {
+	feedback.Info("Store iso areas.")
+	total := time.Now()
+	defer func() {
+		feedback.Info("Storing iso areas took %v.",
+			time.Since(total))
+	}()
+
+	stmt, err := tx.PrepareContext(ctx, insertIsoAreasSQL)
+	if err != nil {
+		return err
+	}
+	defer stmt.Close()
+
+	for i, a := range areas {
+		if len(a) == 0 {
+			continue
 		}
-	})
+		if _, err := stmt.ExecContext(
+			ctx,
+			id, heights[i], epsg,
+			a.AsWKB(),
+			contourTolerance,
+		); err != nil {
+			return err
+		}
+	}
 
-	return err
+	return nil
 }
--- a/pkg/models/colors.go	Wed Oct 09 14:40:56 2019 +0200
+++ b/pkg/models/colors.go	Wed Oct 09 15:10:35 2019 +0200
@@ -71,6 +71,28 @@
 	return cbs
 }
 
+func (cc ColorValues) Heights() []float64 {
+	heights := make([]float64, len(cc))
+	for i := range cc {
+		heights[i] = cc[i].Value
+	}
+	return heights
+}
+
+func (cc ColorValues) Clip(v float64) color.RGBA {
+	if len(cc) == 0 {
+		return color.RGBA{}
+	}
+	if v < cc[0].Value {
+		return cc[0].Color
+	}
+	if v > cc[len(cc)-1].Value {
+		return cc[len(cc)-1].Color
+	}
+	c, _ := cc.Interpolate(v)
+	return c
+}
+
 func (cc ColorValues) Interpolate(v float64) (color.RGBA, bool) {
 	if len(cc) == 0 || v < cc[0].Value || v > cc[len(cc)-1].Value {
 		return color.RGBA{}, false
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pkg/octree/areas.go	Wed Oct 09 15:10:35 2019 +0200
@@ -0,0 +1,168 @@
+// 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 octree
+
+import (
+	"log"
+	"math"
+	"runtime"
+	"sync"
+	"time"
+
+	"github.com/fogleman/contourmap"
+
+	"gemma.intevation.de/gemma/pkg/common"
+	"gemma.intevation.de/gemma/pkg/wkb"
+)
+
+func (tree *Tree) TraceAreas(
+	heights []float64,
+	cellSize float64,
+) []wkb.MultiPolygonGeom {
+	min, max := tree.Min, tree.Max
+
+	width := max.X - min.X
+	height := max.Y - min.Y
+
+	log.Printf("info: Width/Height: %.2f / %.2f\n", width, height)
+
+	xcells := int(math.Ceil(width / cellSize))
+	ycells := int(math.Ceil(height / cellSize))
+
+	log.Printf("info: Raster size: (%d, %d)\n", xcells, ycells)
+
+	start := time.Now()
+
+	// Add border for closing
+	raster := make([]float64, (xcells+2)*(ycells+2))
+
+	// prefill for no data
+	const nodata = -math.MaxFloat64
+	for i := range raster {
+		raster[i] = nodata
+	}
+
+	// rasterize the height model
+
+	var wg sync.WaitGroup
+
+	rows := make(chan int)
+
+	rasterRow := func() {
+		defer wg.Done()
+		quat := 0.25 * cellSize
+		for i := range rows {
+			pos := (i+1)*(xcells+2) + 1
+			row := raster[pos : pos+xcells]
+			py := min.Y + float64(i)*cellSize + cellSize/2
+			px := min.X + cellSize/2
+			y1 := py - quat
+			y2 := py + quat
+			for j := range row {
+				var n int
+				var sum float64
+
+				if v, ok := tree.Value(px-quat, y1); ok {
+					sum = v
+					n = 1
+				}
+				if v, ok := tree.Value(px-quat, y2); ok {
+					sum += v
+					n++
+				}
+				if v, ok := tree.Value(px+quat, y1); ok {
+					sum += v
+					n++
+				}
+				if v, ok := tree.Value(px+quat, y2); ok {
+					sum += v
+					n++
+				}
+
+				if n > 0 {
+					row[j] = sum / float64(n)
+				}
+				px += cellSize
+			}
+		}
+	}
+
+	for n := runtime.NumCPU(); n >= 1; n-- {
+		wg.Add(1)
+		go rasterRow()
+	}
+
+	for i := 0; i < ycells; i++ {
+		rows <- i
+	}
+	close(rows)
+
+	wg.Wait()
+	log.Printf("info: Rastering took %v\n", time.Since(start))
+
+	start = time.Now()
+
+	tracer := contourmap.FromFloat64s(xcells+2, ycells+2, raster)
+
+	areas := make([]wkb.MultiPolygonGeom, len(heights))
+
+	// TODO: Check if this correct!
+	reprojX := common.Linear(0.5, min.X, 1.5, min.X+cellSize)
+	reprojY := common.Linear(0.5, min.Y, 1.5, min.Y+cellSize)
+
+	cnts := make(chan int)
+
+	doContours := func() {
+		defer wg.Done()
+		for hIdx := range cnts {
+			c := tracer.Contours(heights[hIdx])
+
+			if len(c) == 0 {
+				continue
+			}
+
+			// We need to bring it back to the
+			// none raster coordinate system.
+			a := make(wkb.MultiPolygonGeom, len(c))
+
+			for i, pl := range c {
+				shell := make(wkb.LinearRingGeom, len(pl))
+				for j, pt := range pl {
+					shell[j] = wkb.PointGeom{
+						X: reprojX(pt.X),
+						Y: reprojY(pt.Y),
+					}
+				}
+				a[i] = wkb.PolygonGeom{shell}
+			}
+
+			areas[hIdx] = a
+		}
+	}
+
+	for n := runtime.NumCPU(); n >= 1; n-- {
+		wg.Add(1)
+		go doContours()
+	}
+
+	for i := range heights {
+		cnts <- i
+	}
+	close(cnts)
+
+	wg.Wait()
+	log.Printf("info: Tracing areas took %v\n", time.Since(start))
+
+	return areas
+}
--- a/pkg/octree/vertex.go	Wed Oct 09 14:40:56 2019 +0200
+++ b/pkg/octree/vertex.go	Wed Oct 09 15:10:35 2019 +0200
@@ -350,9 +350,9 @@
 }
 
 func (t *Triangle) Contains(x, y float64) bool {
-	v0 := t[2].Sub(t[0])
-	v1 := t[1].Sub(t[0])
-	v2 := Vertex{X: x, Y: y}.Sub(t[0])
+	v0 := t[2].Sub2D(t[0])
+	v1 := t[1].Sub2D(t[0])
+	v2 := Vertex{X: x, Y: y}.Sub2D(t[0])
 
 	dot00 := v0.Dot2(v0)
 	dot01 := v0.Dot2(v1)
@@ -483,6 +483,14 @@
 		math.Abs(v.Y-w.Y) < eps && math.Abs(v.Z-w.Z) < eps
 }
 
+// EpsEquals returns true if v and w are equal component-wise
+// in the X/Y plane with the values within a epsilon range.
+func (v Vertex) EpsEquals2D(w Vertex) bool {
+	const eps = 1e-5
+	return math.Abs(v.X-w.X) < eps &&
+		math.Abs(v.Y-w.Y) < eps
+}
+
 // JoinOnLine joins the the elements of a given multi line string
 // under the assumption that the segments are all on the line segment
 // from (x1, y1) to (x2, y2).
@@ -629,6 +637,15 @@
 	return buf.Bytes()
 }
 
+func (ls LineStringZ) CCW() bool {
+	var sum float64
+	for i, v1 := range ls {
+		v2 := ls[(i+1)%len(ls)]
+		sum += (v2.X - v1.X) * (v2.Y + v1.Y)
+	}
+	return sum > 0
+}
+
 // Join joins two lines leaving the first of the second out.
 func (ls LineStringZ) Join(other LineStringZ) LineStringZ {
 	nline := make(LineStringZ, len(ls)+len(other)-1)
--- a/schema/default_sysconfig.sql	Wed Oct 09 14:40:56 2019 +0200
+++ b/schema/default_sysconfig.sql	Wed Oct 09 15:10:35 2019 +0200
@@ -30,13 +30,13 @@
     ('waterway.gauges_geoserver'),
     ('waterway.distance_marks_ashore_geoserver'),
     ('waterway.distance_marks_geoserver'),
-    ('waterway.sounding_results_contour_lines_geoserver'),
     ('waterway.bottlenecks_geoserver'),
     ('waterway.bottleneck_overview'),
     ('waterway.waterway_axis'),
     ('waterway.waterway_area'),
     ('waterway.waterway_profiles'),
-    ('waterway.sounding_differences');
+    ('waterway.sounding_differences'),
+    ('waterway.sounding_results_areas_geoserver');
 
 --
 -- Settings
--- a/schema/gemma.sql	Wed Oct 09 14:40:56 2019 +0200
+++ b/schema/gemma.sql	Wed Oct 09 15:10:35 2019 +0200
@@ -690,13 +690,13 @@
         AFTER INSERT OR UPDATE ON sounding_results
         FOR EACH ROW EXECUTE FUNCTION check_sr_in_bn_area()
 
-    CREATE TABLE sounding_results_contour_lines (
+    CREATE TABLE sounding_results_iso_areas (
         sounding_result_id int NOT NULL REFERENCES sounding_results
             ON DELETE CASCADE,
         height numeric NOT NULL,
-        lines geography(multilinestring, 4326) NOT NULL,
+        areas geography(MULTIPOLYGON, 4326) NOT NULL,
         -- TODO: generate valid simple features and add constraint:
-            -- CHECK(ST_IsSimple(CAST(lines AS geometry))),
+            -- CHECK(ST_IsSimple(CAST(areas AS geometry))),
         PRIMARY KEY (sounding_result_id, height)
     )
     --
@@ -884,11 +884,11 @@
         UNIQUE (minuend, subtrahend)
     )
 
-    CREATE TABLE sounding_differences_contour_lines (
+    CREATE TABLE sounding_differences_iso_areas (
         sounding_differences_id int NOT NULL REFERENCES sounding_differences(id)
                                     ON DELETE CASCADE,
         height numeric NOT NULL,
-        lines  geography(multilinestring, 4326) NOT NULL,
+        areas  geography(MULTIPOLYGON, 4326) NOT NULL,
         PRIMARY KEY (sounding_differences_id, height)
     )
 ;
--- a/schema/geoserver_views.sql	Wed Oct 09 14:40:56 2019 +0200
+++ b/schema/geoserver_views.sql	Wed Oct 09 15:10:35 2019 +0200
@@ -182,14 +182,6 @@
             ON isrs_fromtext(g.isrs_code) <@ s.section
     GROUP BY s.id;
 
-CREATE OR REPLACE VIEW waterway.sounding_results_contour_lines_geoserver AS
-    SELECT bottleneck_id,
-        date_info,
-        height,
-        CAST(lines AS geometry(multilinestring, 4326)) AS lines
-    FROM waterway.sounding_results_contour_lines cl
-        JOIN waterway.sounding_results sr ON sr.id = cl.sounding_result_id;
-
 CREATE OR REPLACE VIEW waterway.bottleneck_overview AS
     SELECT
         objnam AS name,
@@ -205,17 +197,26 @@
     WHERE bn.validity @> current_timestamp
     ORDER BY objnam;
 
+CREATE OR REPLACE VIEW waterway.sounding_results_areas_geoserver AS
+  SELECT
+    bottleneck_id,
+    date_info,
+    height,
+    CAST(areas AS geometry(multipolygon, 4326)) as areas
+  FROM waterway.sounding_results_iso_areas ia
+  JOIN waterway.sounding_results sr ON sr.id = ia.sounding_result_id;
+
 CREATE OR REPLACE VIEW waterway.sounding_differences AS
     SELECT
         sd.id           AS id,
         bn.objnam       AS objnam,
         srm.date_info   AS minuend,
         srs.date_info   AS subtrahend,
-        sdcl.height     AS height,
-        CAST(sdcl.lines AS geometry(multilinestring, 4326)) AS lines
+        sdia.height     AS height,
+        CAST(sdia.areas AS geometry(multipolygon, 4326)) AS areas
     FROM caching.sounding_differences sd
-        JOIN caching.sounding_differences_contour_lines sdcl
-            ON sd.id = sdcl.sounding_differences_id
+        JOIN caching.sounding_differences_iso_areas sdia
+            ON sd.id = sdia.sounding_differences_id
         JOIN waterway.sounding_results srm
             ON sd.minuend = srm.id
         JOIN waterway.sounding_results srs
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/schema/updates/1204/01.create-iso-areas.sql	Wed Oct 09 15:10:35 2019 +0200
@@ -0,0 +1,27 @@
+CREATE TABLE waterway.sounding_results_iso_areas (
+    sounding_result_id int NOT NULL REFERENCES waterway.sounding_results
+        ON DELETE CASCADE,
+    height numeric NOT NULL,
+    areas geography(MULTIPOLYGON, 4326) NOT NULL,
+    -- TODO: generate valid simple features and add constraint:
+        -- CHECK(ST_IsSimple(CAST(areas AS geometry))),
+    PRIMARY KEY (sounding_result_id, height)
+);
+
+CREATE TABLE caching.sounding_differences_iso_areas (
+    sounding_differences_id int NOT NULL REFERENCES caching.sounding_differences(id)
+                                ON DELETE CASCADE,
+    height numeric NOT NULL,
+    areas  geography(MULTIPOLYGON, 4326) NOT NULL,
+    PRIMARY KEY (sounding_differences_id, height)
+);
+
+GRANT INSERT, UPDATE, DELETE ON waterway.sounding_results_iso_areas
+    TO waterway_admin;
+
+GRANT SELECT ON waterway.sounding_results_iso_areas
+    TO waterway_user;
+
+GRANT SELECT, UPDATE, DELETE, INSERT ON caching.sounding_differences_iso_areas
+    TO waterway_user;
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/schema/updates/1204/02.delete-contours.sql	Wed Oct 09 15:10:35 2019 +0200
@@ -0,0 +1,8 @@
+DELETE FROM sys_admin.published_services WHERE name = 'waterway.sounding_differences'::regclass;
+DELETE FROM sys_admin.published_services WHERE name = 'waterway.sounding_results_contour_lines_geoserver'::regclass;
+
+DROP VIEW waterway.sounding_results_contour_lines_geoserver;
+DROP VIEW waterway.sounding_differences;
+DROP TABLE caching.sounding_differences_contour_lines;
+DROP TABLE waterway.sounding_results_contour_lines;
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/schema/updates/1204/03.geoserver-views.sql	Wed Oct 09 15:10:35 2019 +0200
@@ -0,0 +1,28 @@
+CREATE OR REPLACE VIEW waterway.sounding_results_areas_geoserver AS
+  SELECT bottleneck_id,
+         date_info,
+         height,
+         CAST(areas AS geometry(multipolygon, 4326)) as areas
+  FROM waterway.sounding_results_iso_areas ia
+  JOIN waterway.sounding_results sr ON sr.id = ia.sounding_result_id;
+
+CREATE OR REPLACE VIEW waterway.sounding_differences AS
+    SELECT
+        sd.id           AS id,
+        bn.objnam       AS objnam,
+        srm.date_info   AS minuend,
+        srs.date_info   AS subtrahend,
+        sdia.height     AS height,
+        CAST(sdia.areas AS geometry(multipolygon, 4326)) AS areas
+    FROM caching.sounding_differences sd
+        JOIN caching.sounding_differences_iso_areas sdia
+            ON sd.id = sdia.sounding_differences_id
+        JOIN waterway.sounding_results srm
+            ON sd.minuend = srm.id
+        JOIN waterway.sounding_results srs
+            ON sd.subtrahend = srs.id
+        JOIN waterway.bottlenecks bn
+            ON srm.bottleneck_id = bn.bottleneck_id
+                AND srm.date_info::timestamptz <@ bn.validity;
+
+GRANT SELECT ON ALL TABLES IN SCHEMA public, users, waterway TO waterway_user;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/schema/updates/1204/04.publish.sql	Wed Oct 09 15:10:35 2019 +0200
@@ -0,0 +1,6 @@
+INSERT INTO sys_admin.published_services (name)
+   VALUES ('waterway.sounding_results_areas_geoserver'::regclass)
+   ON CONFLICT (name) DO NOTHING;
+INSERT INTO sys_admin.published_services (name)
+   VALUES ('waterway.sounding_differences'::regclass)
+   ON CONFLICT (name) DO NOTHING;
--- a/schema/version.sql	Wed Oct 09 14:40:56 2019 +0200
+++ b/schema/version.sql	Wed Oct 09 15:10:35 2019 +0200
@@ -1,1 +1,1 @@
-INSERT INTO gemma_schema_version(version) VALUES (1203);
+INSERT INTO gemma_schema_version(version) VALUES (1204);
--- a/style-templates/sounding_differences.sld-template	Wed Oct 09 14:40:56 2019 +0200
+++ b/style-templates/sounding_differences.sld-template	Wed Oct 09 15:10:35 2019 +0200
@@ -50,74 +50,69 @@
             </ogc:And>
           </ogc:Filter>
         {{- end }}
-           <se:LineSymbolizer>
+        <se:MaxScaleDenominator>34e3</se:MaxScaleDenominator>
+          <se:PolygonSymbolizer>
+            <se:Fill>
+              <se:SvgParameter name="fill">{{ .Color }}</se:SvgParameter>
+            </se:Fill>
             <se:Stroke>
-              <se:SvgParameter name="stroke">{{ .Color }}</se:SvgParameter>
+              <se:SvgParameter name="stroke">#404040</se:SvgParameter>
               <se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
             </se:Stroke>
-          </se:LineSymbolizer>
+          </se:PolygonSymbolizer>
+        </se:Rule>
+        <se:Rule>
+        {{- if not .HasLow }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsLessThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+            </ogc:PropertyIsLessThanOrEqualTo>
+          </ogc:Filter>
+        {{- else if not .HasHigh }}
+          <se:Name>&gt; {{ printf "%g" .Low }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsGreaterThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+            </ogc:PropertyIsGreaterThanOrEqualTo>
+          </ogc:Filter>
+        {{- else }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:And>
+              <ogc:PropertyIsGreaterThan>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+              </ogc:PropertyIsGreaterThan>
+              <ogc:PropertyIsLessThanOrEqualTo>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+              </ogc:PropertyIsLessThanOrEqualTo>
+            </ogc:And>
+          </ogc:Filter>
+        {{- end }}
+        <se:MinScaleDenominator>34e3</se:MinScaleDenominator>
+          <se:PolygonSymbolizer>
+            <se:Fill>
+              <se:SvgParameter name="fill">{{ .Color }}</se:SvgParameter>
+            </se:Fill>
+          </se:PolygonSymbolizer>
         </se:Rule>
         {{ end }}
       </se:FeatureTypeStyle>
       <se:FeatureTypeStyle>
-        <se:Name>contour_lines_emph</se:Name>
-        <se:Description>
-          <se:Abstract>
-            FeatureTypeStyle for emphasized contour lines
-          </se:Abstract>
-          </se:Description>
-          <se:Rule>
-            <se:LegendGraphic>
-              <se:Graphic>
-            </se:Graphic>
-          </se:LegendGraphic>
-          <ogc:Filter>
-             <ogc:Or>
-              {{ range . -}}
-              {{ if .HasHigh -}}
-                <ogc:PropertyIsEqualTo>
-                <ogc:Function name="numberFormat">
-                  <ogc:Literal>0.000000</ogc:Literal>
-                  <ogc:PropertyName>height</ogc:PropertyName>
-                </ogc:Function>
-                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-                </ogc:PropertyIsEqualTo>
-              {{ end -}}
-              {{ end }}
-            </ogc:Or>
-          </ogc:Filter>
-          <se:MaxScaleDenominator>5e3</se:MaxScaleDenominator>
-          <se:LineSymbolizer>
-            <se:Stroke>
-              <se:SvgParameter name="stroke-width">1.5</se:SvgParameter>
-              <se:SvgParameter name="stroke">
-                <ogc:Function name="Recode">
-                  <ogc:Function name="numberFormat">
-                    <ogc:Literal>0.000000</ogc:Literal>
-                    <ogc:PropertyName>height</ogc:PropertyName>
-                  </ogc:Function>
-                  {{ range . -}}
-                  {{ if .HasHigh -}}
-                  <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-                  <ogc:Literal>{{ .Color }}</ogc:Literal>
-                  {{ end -}}
-                  {{ end }}
-                </ogc:Function>
-              </se:SvgParameter>
-            </se:Stroke>
-          </se:LineSymbolizer>
-        </se:Rule>
-      </se:FeatureTypeStyle>
-      <se:FeatureTypeStyle>
         <se:Name>contour_lines_label</se:Name>
         <se:Description>
           <se:Abstract>
-            FeatureTypeStyle for labels at contour lines
+            FeatureTypeStyle for labels at color areas
           </se:Abstract>
         </se:Description>
         <se:Rule>
           <se:MaxScaleDenominator>5e3</se:MaxScaleDenominator>
           <se:TextSymbolizer>
+            <se:VendorOption name="spaceAround">50</se:VendorOption>
             <se:Label>
               <ogc:Function name="Recode">
                 <ogc:Function name="numberFormat">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/style-templates/sounding_results_areas_geoserver.sld-template	Wed Oct 09 15:10:35 2019 +0200
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<StyledLayerDescriptor
+    xmlns="http://www.opengis.net/sld"
+    xmlns:se="http://www.opengis.net/se"
+    xmlns:ogc="http://www.opengis.net/ogc"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd"
+    version="1.1.0">
+  <NamedLayer>
+    <se:Name>sounding_results_areas</se:Name>
+    <UserStyle>
+      <se:Name>sounding_results_areas</se:Name>
+      <se:FeatureTypeStyle>
+          <se:Name>area_colours</se:Name>
+        <se:Description>
+          <se:Abstract>
+            FeatureTypeStyle defining colour classes for height attribute
+          </se:Abstract>
+        </se:Description>
+        {{ range . -}}
+        <se:Rule>
+        {{- if not .HasLow }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsLessThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+            </ogc:PropertyIsLessThanOrEqualTo>
+          </ogc:Filter>
+        {{- else if not .HasHigh }}
+          <se:Name>&gt; {{ printf "%g" .Low }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsGreaterThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+            </ogc:PropertyIsGreaterThanOrEqualTo>
+          </ogc:Filter>
+        {{- else }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:And>
+              <ogc:PropertyIsGreaterThan>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+              </ogc:PropertyIsGreaterThan>
+              <ogc:PropertyIsLessThanOrEqualTo>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+              </ogc:PropertyIsLessThanOrEqualTo>
+            </ogc:And>
+          </ogc:Filter>
+        {{- end }}
+          <se:MaxScaleDenominator>34e3</se:MaxScaleDenominator>
+          <se:PolygonSymbolizer>
+            <se:Fill>
+              <se:SvgParameter name="fill">{{ .Color }}</se:SvgParameter>
+            </se:Fill>
+            <se:Stroke>
+              <se:SvgParameter name="stroke">#404040</se:SvgParameter>
+              <se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
+            </se:Stroke>
+          </se:PolygonSymbolizer>
+        </se:Rule>
+        <se:Rule>
+        {{- if not .HasLow }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsLessThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+            </ogc:PropertyIsLessThanOrEqualTo>
+          </ogc:Filter>
+        {{- else if not .HasHigh }}
+          <se:Name>&gt; {{ printf "%g" .Low }}</se:Name>
+          <ogc:Filter>
+            <ogc:PropertyIsGreaterThanOrEqualTo>
+              <ogc:PropertyName>height</ogc:PropertyName>
+              <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+            </ogc:PropertyIsGreaterThanOrEqualTo>
+          </ogc:Filter>
+        {{- else }}
+          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
+          <ogc:Filter>
+            <ogc:And>
+              <ogc:PropertyIsGreaterThan>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
+              </ogc:PropertyIsGreaterThan>
+              <ogc:PropertyIsLessThanOrEqualTo>
+                <ogc:PropertyName>height</ogc:PropertyName>
+                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
+              </ogc:PropertyIsLessThanOrEqualTo>
+            </ogc:And>
+          </ogc:Filter>
+        {{- end }}
+          <se:MinScaleDenominator>34e3</se:MinScaleDenominator>
+          <se:PolygonSymbolizer>
+            <se:Fill>
+              <se:SvgParameter name="fill">{{ .Color }}</se:SvgParameter>
+            </se:Fill>
+          </se:PolygonSymbolizer>
+        </se:Rule>
+        <se:VendorOption name="sortBy">height</se:VendorOption>
+        {{ end }}
+      </se:FeatureTypeStyle>
+      <se:FeatureTypeStyle>
+        <se:Name>area_labels</se:Name>
+        <se:Description>
+          <se:Abstract>
+            FeatureTypeStyle for labels at colour areas
+          </se:Abstract>
+        </se:Description>
+        <se:Rule>
+          <se:MaxScaleDenominator>5e3</se:MaxScaleDenominator>
+          <se:TextSymbolizer>
+            <se:VendorOption name="spaceAround">10</se:VendorOption>
+            <se:Label>
+              <ogc:Function name="Recode">
+                <ogc:Function name="numberFormat">
+                  <ogc:Literal>0.000000</ogc:Literal>
+                  <ogc:PropertyName>height</ogc:PropertyName>
+                </ogc:Function>
+                {{ range . -}}
+                {{ if .HasHigh -}}
+                    <ogc:Literal>
+                    {{- printf "%f" .High -}}
+                    </ogc:Literal><ogc:Literal>
+                    {{- printf "%g" .High -}}
+                    </ogc:Literal>
+                {{ end -}}
+                {{ end }}
+              </ogc:Function>
+            </se:Label>
+            <se:Font>
+              <se:SvgParameter name="font-family">Avenir</se:SvgParameter>
+              <se:SvgParameter name="font-family">Helvetica</se:SvgParameter>
+              <se:SvgParameter name="font-family">Arial</se:SvgParameter>
+              <se:SvgParameter name="font-family">sans-serif</se:SvgParameter>
+            </se:Font>
+            <se:LabelPlacement>
+              <se:LinePlacement>
+                <se:PerpendicularOffset>5</se:PerpendicularOffset>
+              </se:LinePlacement>
+            </se:LabelPlacement>
+            <se:Fill>
+              <se:SvgParameter name="fill">#070707</se:SvgParameter>
+            </se:Fill>
+          </se:TextSymbolizer>
+        </se:Rule>
+      </se:FeatureTypeStyle>
+    </UserStyle>
+  </NamedLayer>
+</StyledLayerDescriptor>
--- a/style-templates/sounding_results_contour_lines_geoserver.sld-template	Wed Oct 09 14:40:56 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,157 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<StyledLayerDescriptor
-    xmlns="http://www.opengis.net/sld"
-    xmlns:se="http://www.opengis.net/se"
-    xmlns:ogc="http://www.opengis.net/ogc"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:schemaLocation="http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd"
-    version="1.1.0">
-  <NamedLayer>
-    <se:Name>sounding_results_contour_lines</se:Name>
-    <UserStyle>
-      <se:Name>sounding_results_contour_lines</se:Name>
-      <se:FeatureTypeStyle>
-          <se:Name>contour_line_colours</se:Name>
-        <se:Description>
-          <se:Abstract>
-            FeatureTypeStyle defining colour classes for height attribute
-          </se:Abstract>
-        </se:Description>
-        {{ range . -}}
-        <se:Rule>
-        {{- if not .HasLow }}
-          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
-          <ogc:Filter>
-            <ogc:PropertyIsLessThanOrEqualTo>
-              <ogc:PropertyName>height</ogc:PropertyName>
-              <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-            </ogc:PropertyIsLessThanOrEqualTo>
-          </ogc:Filter>
-        {{- else if not .HasHigh }}
-          <se:Name>&gt; {{ printf "%g" .Low }}</se:Name>
-          <ogc:Filter>
-            <ogc:PropertyIsGreaterThanOrEqualTo>
-              <ogc:PropertyName>height</ogc:PropertyName>
-              <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
-            </ogc:PropertyIsGreaterThanOrEqualTo>
-          </ogc:Filter>
-        {{- else }}
-          <se:Name>&#8804; {{ printf "%g" .High }}</se:Name>
-          <ogc:Filter>
-            <ogc:And>
-              <ogc:PropertyIsGreaterThan>
-                <ogc:PropertyName>height</ogc:PropertyName>
-                <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal>
-              </ogc:PropertyIsGreaterThan>
-              <ogc:PropertyIsLessThanOrEqualTo>
-                <ogc:PropertyName>height</ogc:PropertyName>
-                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-              </ogc:PropertyIsLessThanOrEqualTo>
-            </ogc:And>
-          </ogc:Filter>
-        {{- end }}
-           <se:LineSymbolizer>
-            <se:Stroke>
-              <se:SvgParameter name="stroke">{{ .Color }}</se:SvgParameter>
-              <se:SvgParameter name="stroke-width">0.5</se:SvgParameter>
-            </se:Stroke>
-          </se:LineSymbolizer>
-        </se:Rule>
-        {{ end }}
-      </se:FeatureTypeStyle>
-      <se:FeatureTypeStyle>
-        <se:Name>contour_lines_emph</se:Name>
-        <se:Description>
-          <se:Abstract>
-            FeatureTypeStyle for emphasized contour lines
-          </se:Abstract>
-          </se:Description>
-          <se:Rule>
-            <se:LegendGraphic>
-              <se:Graphic>
-            </se:Graphic>
-          </se:LegendGraphic>
-          <ogc:Filter>
-             <ogc:Or>
-              {{ range . -}}
-              {{ if .HasHigh -}}
-                <ogc:PropertyIsEqualTo>
-                <ogc:Function name="numberFormat">
-                  <ogc:Literal>0.000000</ogc:Literal>
-                  <ogc:PropertyName>height</ogc:PropertyName>
-                </ogc:Function>
-                <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-                </ogc:PropertyIsEqualTo>
-              {{ end -}}
-              {{ end }}
-            </ogc:Or>
-          </ogc:Filter>
-          <se:MaxScaleDenominator>5e3</se:MaxScaleDenominator>
-          <se:LineSymbolizer>
-            <se:Stroke>
-              <se:SvgParameter name="stroke-width">1.5</se:SvgParameter>
-              <se:SvgParameter name="stroke">
-                <ogc:Function name="Recode">
-                  <ogc:Function name="numberFormat">
-                    <ogc:Literal>0.000000</ogc:Literal>
-                    <ogc:PropertyName>height</ogc:PropertyName>
-                  </ogc:Function>
-                  {{ range . -}}
-                  {{ if .HasHigh -}}
-                  <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal>
-                  <ogc:Literal>{{ .Color }}</ogc:Literal>
-                  {{ end -}}
-                  {{ end }}
-                </ogc:Function>
-              </se:SvgParameter>
-            </se:Stroke>
-          </se:LineSymbolizer>
-        </se:Rule>
-      </se:FeatureTypeStyle>
-      <se:FeatureTypeStyle>
-        <se:Name>contour_lines_label</se:Name>
-        <se:Description>
-          <se:Abstract>
-            FeatureTypeStyle for labels at contour lines
-          </se:Abstract>
-        </se:Description>
-        <se:Rule>
-          <se:MaxScaleDenominator>5e3</se:MaxScaleDenominator>
-          <se:TextSymbolizer>
-            <se:Label>
-              <ogc:Function name="Recode">
-                <ogc:Function name="numberFormat">
-                  <ogc:Literal>0.000000</ogc:Literal>
-                  <ogc:PropertyName>height</ogc:PropertyName>
-                </ogc:Function>
-                {{ range . -}}
-                {{ if .HasHigh -}}
-                    <ogc:Literal>
-                    {{- printf "%f" .High -}}
-                    </ogc:Literal><ogc:Literal>
-                    {{- printf "%g" .High -}}
-                    </ogc:Literal>
-                {{ end -}}
-                {{ end }}
-              </ogc:Function>
-            </se:Label>
-            <se:Font>
-              <se:SvgParameter name="font-family">Avenir</se:SvgParameter>
-              <se:SvgParameter name="font-family">Helvetica</se:SvgParameter>
-              <se:SvgParameter name="font-family">Arial</se:SvgParameter>
-              <se:SvgParameter name="font-family">sans-serif</se:SvgParameter>
-            </se:Font>
-            <se:LabelPlacement>
-              <se:LinePlacement>
-                <se:PerpendicularOffset>5</se:PerpendicularOffset>
-              </se:LinePlacement>
-            </se:LabelPlacement>
-            <se:Fill>
-              <se:SvgParameter name="fill">#070707</se:SvgParameter>
-            </se:Fill>
-          </se:TextSymbolizer>
-        </se:Rule>
-      </se:FeatureTypeStyle>
-    </UserStyle>
-  </NamedLayer>
-</StyledLayerDescriptor>