view pkg/mesh/raster.go @ 5718:3d497077f888 uploadwg

Implemented direct file upload as alternative import method for WG. For testing and data corrections it is useful to be able to import waterway gauges data directly by uploading a xml file.
author Sascha Wilde <wilde@sha-bang.de>
date Thu, 18 Apr 2024 19:23:19 +0200
parents d2ccf6bb6940
children
line wrap: on
line source

// 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 mesh

import (
	"math"
	"runtime"
	"sync"
	"time"

	"github.com/fogleman/contourmap"

	"gemma.intevation.de/gemma/pkg/common"
	"gemma.intevation.de/gemma/pkg/log"
	"gemma.intevation.de/gemma/pkg/wkb"
)

// Raster represents a 2D cell raster with a given extend.
// The cells are equally sized in X and Y direction.
type Raster struct {
	BBox     Box2D
	CellSize float64
	XCells   int
	YCells   int
	Cells    []float64
}

const noData = -math.MaxFloat64

// NewRaster creates a new Raster with the given extend
// and cell size.
func NewRaster(bbox Box2D, cellSize float64) *Raster {

	width, height := bbox.Size()

	log.Infof("raster extent: %.2f / %.2f", width, height)

	xCells := int(math.Ceil(width / cellSize))
	yCells := int(math.Ceil(height / cellSize))

	log.Infof("raster size: %d / %d\n", xCells, yCells)

	size := (xCells + 2) * (yCells + 2)
	cells := make([]float64, size)
	for i := range cells {
		cells[i] = noData
	}
	return &Raster{
		BBox:     bbox,
		CellSize: cellSize,
		XCells:   xCells,
		YCells:   yCells,
		Cells:    cells,
	}
}

// Rasterize fills the raster with the evaluation data
// received via the given eval function. The cell values
// 4x oversampled,
func (r *Raster) Rasterize(eval func(float64, float64) (float64, bool)) {
	var wg sync.WaitGroup

	rows := make(chan int)

	rasterRow := func() {
		defer wg.Done()
		quat := 0.25 * r.CellSize
		for i := range rows {
			pos := (i+1)*(r.XCells+2) + 1
			row := r.Cells[pos : pos+r.XCells]
			py := r.BBox.Y1 + float64(i)*r.CellSize + r.CellSize/2
			px := r.BBox.X1 + r.CellSize/2
			y1 := py - quat
			y2 := py + quat
			for j := range row {
				var n int
				var sum float64

				if v, ok := eval(px-quat, y1); ok {
					sum = v
					n = 1
				}
				if v, ok := eval(px-quat, y2); ok {
					sum += v
					n++
				}
				if v, ok := eval(px+quat, y1); ok {
					sum += v
					n++
				}
				if v, ok := eval(px+quat, y2); ok {
					sum += v
					n++
				}

				if n > 0 && !math.IsNaN(sum) {
					row[j] = sum / float64(n)
				} else {
					row[j] = noData
				}
				px += r.CellSize
			}
		}
	}

	for n := runtime.NumCPU(); n >= 1; n-- {
		wg.Add(1)
		go rasterRow()
	}

	for i := 0; i < r.YCells; i++ {
		rows <- i
	}
	close(rows)
	wg.Wait()
}

// Diff updates the cell values of the raster with difference
// of this raster and the values from the gien eval function.
// The data will 4x over-sampled.
func (r *Raster) Diff(eval func(float64, float64) (float64, bool)) {
	var wg sync.WaitGroup

	rows := make(chan int)

	rasterRow := func() {
		defer wg.Done()
		quat := 0.25 * r.CellSize
		for i := range rows {
			pos := (i+1)*(r.XCells+2) + 1
			row := r.Cells[pos : pos+r.XCells]
			py := r.BBox.Y1 + float64(i)*r.CellSize + r.CellSize/2
			px := r.BBox.X1 + r.CellSize/2
			y1 := py - quat
			y2 := py + quat
			for j, old := range row {
				// only diff where values
				if old == noData {
					px += r.CellSize
					continue
				}
				var n int
				var sum float64

				if v, ok := eval(px-quat, y1); ok {
					sum = v
					n = 1
				}
				if v, ok := eval(px-quat, y2); ok {
					sum += v
					n++
				}
				if v, ok := eval(px+quat, y1); ok {
					sum += v
					n++
				}
				if v, ok := eval(px+quat, y2); ok {
					sum += v
					n++
				}

				if n > 0 && !math.IsNaN(sum) {
					row[j] -= sum / float64(n)
				} else {
					row[j] = noData
				}

				px += r.CellSize
			}
		}
	}

	for n := runtime.NumCPU(); n >= 1; n-- {
		wg.Add(1)
		go rasterRow()
	}

	for i := 0; i < r.YCells; i++ {
		rows <- i
	}
	close(rows)
	wg.Wait()
}

// ZExtent returns the z-range of the raster.
// The last return value is false if the raster
// only consists of no data values.
func (r *Raster) ZExtent() (float64, float64, bool) {
	min, max := math.MaxFloat64, -math.MaxFloat64
	for _, v := range r.Cells {
		if v == noData {
			continue
		}
		if v < min {
			min = v
		}
		if v > max {
			max = v
		}
	}
	return min, max, min != math.MaxFloat64
}

// Trace generates contour lines of this raster given
// the given class breaks.
func (r *Raster) Trace(heights ClassBreaks) []wkb.MultiPolygonGeom {
	start := time.Now()

	tracer := contourmap.FromFloat64s(r.XCells+2, r.YCells+2, r.Cells)

	areas := make([]wkb.MultiPolygonGeom, len(heights))

	reprojX := common.Linear(0.5, r.BBox.X1, 1.5, r.BBox.X1+r.CellSize)
	reprojY := common.Linear(0.5, r.BBox.Y1, 1.5, r.BBox.Y1+r.CellSize)

	var wg sync.WaitGroup

	cnts := make(chan int)

	doContours := func() {
		defer wg.Done()
		for hIdx := range cnts {
			if c := tracer.Contours(heights[hIdx]); len(c) > 0 {
				areas[hIdx] = buildMultipolygon(c, reprojX, reprojY)
			}
		}
	}

	for n := runtime.NumCPU(); n >= 1; n-- {
		wg.Add(1)
		go doContours()
	}

	for i := range heights {
		cnts <- i
	}
	close(cnts)

	wg.Wait()
	log.Infof("Tracing areas took %v\n", time.Since(start))

	return areas
}

type contour []contourmap.Point

type bboxNode struct {
	box      Box2D
	cnt      contour
	children []*bboxNode
}

func (cnt contour) contains(o contour) bool {
	return contains(cnt, o[0].X, o[0].Y) ||
		contains(cnt, o[len(o)/2].X, o[len(o)/2].Y)
}

func (cnt contour) length() int {
	return len(cnt)
}

func (cnt contour) point(i int) (float64, float64) {
	return cnt[i].X, cnt[i].Y
}

func (cnt contour) bbox() Box2D {
	minX, minY := math.MaxFloat64, math.MaxFloat64
	maxX, maxY := -math.MaxFloat64, -math.MaxFloat64

	for _, p := range cnt {
		if p.X < minX {
			minX = p.X
		}
		if p.X > maxX {
			maxX = p.X
		}
		if p.Y < minY {
			minY = p.Y
		}
		if p.Y > maxY {
			maxY = p.Y
		}
	}
	return Box2D{X1: minX, X2: maxX, Y1: minY, Y2: maxY}
}

func (bn *bboxNode) insert(cnt contour, box Box2D) {
	// check if children are inside new
	var nr *bboxNode

	for i, r := range bn.children {
		if r.box.Inside(box) && cnt.contains(r.cnt) {
			if nr == nil {
				nr = &bboxNode{box: box, cnt: cnt}
			}
			nr.children = append(nr.children, r)
			bn.children[i] = nil
		}
	}

	// we have a new child
	if nr != nil {
		// compact the list
		for i := len(bn.children) - 1; i >= 0; i-- {
			if bn.children[i] == nil {
				if i < len(bn.children)-1 {
					copy(bn.children[i:], bn.children[i+1:])
				}
				bn.children[len(bn.children)-1] = nil
				bn.children = bn.children[:len(bn.children)-1]
			}
		}
		bn.children = append(bn.children, nr)
		return
	}

	// check if new is inside an old
	for _, r := range bn.children {
		if box.Inside(r.box) && r.cnt.contains(cnt) {
			r.insert(cnt, box)
			return
		}
	}

	// its a new child node.
	nr = &bboxNode{box: box, cnt: cnt}
	bn.children = append(bn.children, nr)
}

func (bn *bboxNode) insertRoot(cnt contour) {
	bn.insert(cnt, cnt.bbox())
}

type bboxOutFunc func(contour, []contour)

func (bn *bboxNode) generate(out bboxOutFunc) {

	var grands []*bboxNode

	holes := make([]contour, len(bn.children))

	for i, ch := range bn.children {
		holes[i] = ch.cnt
		grands = append(grands, ch.children...)
	}
	out(bn.cnt, holes)

	// the grand children are new polygons.
	for _, grand := range grands {
		grand.generate(out)
	}
}

func (bn *bboxNode) generateRoot(out bboxOutFunc) {
	for _, r := range bn.children {
		r.generate(out)
	}
}

func buildMultipolygon(
	cnts []contourmap.Contour,
	reprojX, reprojY func(float64) float64,
) wkb.MultiPolygonGeom {

	var forest bboxNode

	for _, cnt := range cnts {
		forest.insertRoot(contour(cnt))
	}

	//log.Debugf("cnts: %d roots: %d\n", len(cnts), len(bf.roots))

	var mp wkb.MultiPolygonGeom

	out := func(sh contour, hls []contour) {

		polygon := make(wkb.PolygonGeom, 1+len(hls))

		// Handle shell
		shell := make(wkb.LinearRingGeom, len(sh))
		for j, pt := range sh {
			shell[j] = wkb.PointGeom{
				X: reprojX(pt.X),
				Y: reprojY(pt.Y),
			}
		}
		if shell.CCW() {
			shell.Reverse()
		}
		polygon[0] = shell

		// handle holes
		for i, hl := range hls {
			hole := make(wkb.LinearRingGeom, len(hl))
			for j, pt := range hl {
				hole[j] = wkb.PointGeom{
					X: reprojX(pt.X),
					Y: reprojY(pt.Y),
				}
			}
			if !hole.CCW() {
				hole.Reverse()
			}
			polygon[1+i] = hole
		}

		mp = append(mp, polygon)
	}

	forest.generateRoot(out)

	return mp
}