view cmd/isoareas/main.go @ 4548:befb94e3a953 iso-areas

More debug output.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Mon, 30 Sep 2019 11:22:41 +0200
parents 6247f5a42226
children 9c65cef72753
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 main

import (
	"bufio"
	"container/list"
	"fmt"
	"io"
	"log"
	"math"
	"math/bits"
	"math/rand"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"

	svg "github.com/ajstarks/svgo"

	"gemma.intevation.de/gemma/pkg/octree"
)

const classBreaks = `1:#ff00dd,1.5,1.7,1.9,2.1,2.3,2.5:#f25f20,2.7,2.9,3.1:#f7e40e,3.3,3.5,4:#8ad51a,4.5,5,5.5,6,6.5,7:#1414ff`

func loadXYZ(r io.Reader) (octree.MultiPointZ, error) {

	scanner := bufio.NewScanner(r)

	points := make(octree.MultiPointZ, 0, 2000000)

	var x, y, z float64
	var err error

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if len(line) == 0 || strings.HasPrefix(line, "#") {
			continue
		}
		parts := strings.SplitN(line, " ", 3)
		if len(parts) != 3 {
			continue
		}

		if x, err = strconv.ParseFloat(parts[0], 64); err != nil {
			return nil, err
		}
		if y, err = strconv.ParseFloat(parts[1], 64); err != nil {
			return nil, err
		}
		if z, err = strconv.ParseFloat(parts[2], 64); err != nil {
			return nil, err
		}
		points = append(points, octree.Vertex{X: x, Y: y, Z: z})
	}

	return points, nil
}

func minMax(points []octree.Vertex) (octree.Vertex, octree.Vertex) {
	if len(points) == 0 {
		return octree.Vertex{}, octree.Vertex{}
	}

	min, max := points[0], points[0]

	for _, v := range points {
		min.Minimize(v)
		max.Maximize(v)
	}

	return min, max
}

func check(err error) {
	if err != nil {
		log.Fatalf("error: %v\n", err)
	}
}

type indexedArc struct {
	arc   int32
	index int32
}

func glue(a, b octree.LineStringZ) octree.LineStringZ {

	a1, a2 := a[0], a[len(a)-1]
	b1, b2 := b[0], b[len(b)-1]

	if a1.EpsEquals(b2) {
		c := make(octree.LineStringZ, len(a)-1+len(b))
		copy(c, b)
		copy(c[len(b):], a[1:])
		return c
	}

	if a2.EpsEquals(b1) {
		c := make(octree.LineStringZ, len(a)-1+len(b))
		copy(c, a)
		copy(c[len(a):], b[1:])
		return c
	}

	if a1.EpsEquals(b1) {
		c := make(octree.LineStringZ, len(a)-1+len(b))
		copy(c, b)
		c[:len(b)].Reverse()
		copy(c[len(b):], a[1:])
		return c
	}

	if a2.EpsEquals(b2) {
		c := make(octree.LineStringZ, len(a)-1+len(b))
		copy(c, a)
		copy(c[len(a):], b[:len(b)-1])
		c[len(a):].Reverse()
		return c
	}

	return nil
}

func connectArcs(tri *octree.Triangulation, cuts []indexedArc, arcs *[]octree.LineStringZ) {

	unique := map[int32]struct{}{}
	for i := range cuts {
		unique[cuts[i].arc] = struct{}{}
	}
	before := len(unique)

	origLen := int32(len(*arcs))

	for i := range cuts {
		if cuts[i].arc >= origLen {
			// already has a connected arc assigned.
			continue
		}

		line := (*arcs)[cuts[i].arc]

		var modified []int
		visited := map[int32]struct{}{}

		var recursive func(int32)
		recursive = func(idx int32) {
			visited[idx] = struct{}{}

			ns := neighbors(tri, idx)
			for _, n := range ns {
				n /= 3
				if _, already := visited[n]; already {
					continue
				}

				arcIdx := findArc(n, cuts)
				if arcIdx == -1 {
					continue
				}
				arc := cuts[arcIdx].arc
				if arc >= origLen {
					// already a new arc.
					continue
				}
				oline := (*arcs)[arc]

				nline := glue(line, oline)
				if nline == nil {
					// not joinable
					continue
				}
				line = nline
				modified = append(modified, arcIdx)
				recursive(cuts[arcIdx].index)
			}
		}
		recursive(cuts[i].index)
		if len(modified) > 0 {
			// alloc a new line an fix the references.
			nidx := int32(len(*arcs))
			*arcs = append(*arcs, line)
			cuts[i].arc = nidx
			for _, j := range modified {
				cuts[j].arc = nidx
			}
		}
	}

	unique = map[int32]struct{}{}
	for i := range cuts {
		unique[cuts[i].arc] = struct{}{}
	}
	log.Printf("unique arcs: before: %d after %d\n",
		before, len(unique))
}

func intersectTriangles(tri *octree.Triangulation, heights []float64) [][]octree.LineStringZ {

	cuts := make([][]indexedArc, len(heights))

	classes := make([][]int32, len(heights)+1)

	var arcs []octree.LineStringZ

all:
	for i := int32(0); i < int32(len(tri.Triangles)); i += 3 {
		ti := tri.Triangles[i : i+3]
		v0 := tri.Points[ti[0]]
		v1 := tri.Points[ti[1]]
		v2 := tri.Points[ti[2]]
		min := math.Min(v0.Z, math.Min(v1.Z, v2.Z))

		if min > heights[len(heights)-1] {
			classes[len(classes)-1] = append(classes[len(classes)-1], i/3)
			continue
		}
		max := math.Max(v0.Z, math.Max(v1.Z, v2.Z))

		for j, h := range heights {

			var l float64
			if j > 0 {
				l = heights[j-1]
			} else {
				l = -math.MaxFloat64
			}

			if l < min && max < h {
				classes[j] = append(classes[j], i/3)
				continue all
			}
			if min > h || max < h {
				continue
			}
			t := octree.Triangle{v0, v1, v2}
			cut := t.IntersectHorizontal(h)
			if len(cut) >= 2 {
				arc := int32(len(arcs))
				arcs = append(arcs, cut)
				cuts[j] = append(cuts[j], indexedArc{arc: arc, index: i / 3})
			}
		}
	}

	for l, c := range classes {
		sorted := sort.SliceIsSorted(c, func(i, j int) bool {
			return c[i] < c[j]
		})
		log.Printf("class[%d] sorted: %t\n", l, sorted)
	}

	for l, c := range cuts {
		sorted := sort.SliceIsSorted(c, func(i, j int) bool {
			return c[i].index < c[j].index
		})
		log.Printf("cut[%d] sorted: %t\n", l, sorted)
	}

	// connect the arcs in a cut list to longer arcs.

	for _, c := range cuts {
		connectArcs(tri, c, &arcs)
	}

	result := make([][]octree.LineStringZ, len(classes))

	log.Println("inside classes:")
	for i, c := range classes {

		pb := polygonBuilder{open: list.New()}

		usedArcs := map[int32]struct{}{}
		var dupes int

		var isolated, inside, found int
	allInClass:
		for _, idx := range c {
			ns := neighbors(tri, idx)
			mask := where(ns, c)
			switch bits.OnesCount8(mask) {
			case 0:
				// Totally insides do not contribute to the geometries.
				inside++
				continue allInClass
			case 3:
				// Isolated are areas w/o connections to the rest.
				isolated++
				ti := tri.Triangles[idx*3 : idx*3+3]
				pb.addTriangle(
					tri.Points[ti[0]],
					tri.Points[ti[1]],
					tri.Points[ti[2]])
				continue allInClass
			default:
				ti := tri.Triangles[idx*3 : idx*3+3]
				for j := 0; j < 3; j++ {
					if (mask & (1 << j)) == 0 {

						var curr octree.LineStringZ

						for l := i - 1; l <= i; l++ {
							if l < 0 || l >= len(cuts) {
								continue
							}
							arcIdx := findArc(ns[j]/3, cuts[l])
							if arcIdx == -1 {
								continue
							}
							found++
							aIdx := cuts[l][arcIdx].arc
							if _, already := usedArcs[aIdx]; already {
								dupes++
								continue
							}
							usedArcs[aIdx] = struct{}{}

							curr = arcs[aIdx]
							break
						}

						if curr == nil {
							k := (j + 1) % 3
							front := tri.Points[ti[j]]
							back := tri.Points[ti[k]]
							curr = octree.LineStringZ{front, back}
						}

					segment:
						for e := pb.open.Front(); e != nil; e = e.Next() {
							line := e.Value.(octree.LineStringZ)
							if nline := glue(curr, line); nline != nil {
								curr = nline
								pb.open.Remove(e)
								goto segment
							}
						} // all open

						// check if closed
						if curr[0].EpsEquals(curr[len(curr)-1]) {
							if !curr.CCW() {
								curr.Reverse()
							}
							pb.polygons = append(pb.polygons, curr)
						} else {
							pb.open.PushFront(curr)
						}
					} // for all border parts
				}
			}
		}

		log.Printf("\t%d: inside: %d / isolated: %d open: %d closed: %d dupes: %d found: %d\n",
			i, inside, isolated, pb.open.Len(), len(pb.polygons), dupes, found)

		/*
			for e := pb.open.Front(); e != nil; e = e.Next() {
				line := e.Value.(octree.LineStringZ)
				pb.polygons = append(pb.polygons, line)
			}
		*/

		result[i] = pb.polygons
	}

	log.Println("cuts:")
	for i, c := range cuts {
		log.Printf("\t%.3f: %d\n", heights[i], len(c))
	}

	return result

	// TODO: sew segments together.

}

type polygonBuilder struct {
	polygons []octree.LineStringZ

	open *list.List
}

func (pb *polygonBuilder) addTriangle(v0, v1, v2 octree.Vertex) {
	polygon := octree.LineStringZ{v0, v1, v2, v0}
	pb.polygons = append(pb.polygons, polygon)
}

func neighbors(t *octree.Triangulation, idx int32) []int32 {
	idx *= 3
	return t.Halfedges[idx : idx+3]
}

func findArc(needle int32, haystack []indexedArc) int {
	lo, hi := 0, len(haystack)-1
	for lo <= hi {
		mid := (hi-lo)/2 + lo
		switch v := haystack[mid].index; {
		case v < needle:
			lo = mid + 1
		case v > needle:
			hi = mid - 1
		default:
			return mid
		}
	}
	return -1
}

func contains(needle int32, haystack []int32) bool {
	lo, hi := 0, len(haystack)-1
	for lo <= hi {
		mid := (hi-lo)/2 + lo
		switch v := haystack[mid]; {
		case v < needle:
			lo = mid + 1
		case v > needle:
			hi = mid - 1
		default:
			return true
		}
	}
	return false
}

func where(neighbors, indices []int32) byte {
	var mask byte
	for i, n := range neighbors {
		if n < 0 || !contains(n/3, indices) {
			mask |= 1 << i
		}
	}
	return mask
}

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
	}
}

func dumpPolygonsSVG(
	w io.Writer,
	min, max octree.Vertex,
	classPolygons [][]octree.LineStringZ,
) (err error) {

	ratio := (max.X - min.X) / (max.Y - min.Y)

	log.Printf("ratio: %.2f\n", ratio)

	const width = 50000
	height := int(math.Ceil(width * ratio))

	px := linear(min.X, 0, max.X, width)
	py := linear(min.Y, float64(height), max.Y, 0)

	out := bufio.NewWriter(w)
	defer func() { err = out.Flush() }()

	canvas := svg.New(out)
	canvas.Start(width, height)

	rnd := rand.New(rand.NewSource(42))

	for _, polygons := range classPolygons {

		r := byte(rnd.Intn(256))
		g := byte(rnd.Intn(256))
		b := byte(rnd.Intn(256))
		style := fmt.Sprintf("fill:#%02x%02x%02x", r, g, b)

		for _, polygon := range polygons {

			x := make([]int, len(polygon))
			y := make([]int, len(polygon))
			for i, p := range polygon {
				x[i] = int(math.Round(px(p.X)))
				y[i] = int(math.Round(py(p.Y)))
			}

			canvas.Polygon(x, y, style)
		}

	}

	canvas.End()

	return nil
}

func main() {

	heights, err := octree.ParseClassBreaks(classBreaks)
	check(err)
	log.Printf("num classes: %d\n", len(heights))

	start := time.Now()
	xyz, err := loadXYZ(os.Stdin)
	check(err)
	log.Printf("loading took %v\n", time.Since(start))

	log.Printf("num vertices: %d\n", len(xyz))

	min, max := minMax(xyz)

	log.Printf("(%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)\n",
		min.X, min.Y, min.Z,
		max.X, max.Y, max.Z)

	start = time.Now()
	tri, err := octree.Triangulate(xyz)
	check(err)
	log.Printf("triangulation took %v\n", time.Since(start))

	log.Printf("number of triangles: %d\n", len(tri.Triangles)/3)

	start = time.Now()
	polygons := intersectTriangles(tri, heights)
	log.Printf("intersecting triangles took %v.\n", time.Since(start))

	check(dumpPolygonsSVG(os.Stdout, min, max, polygons))
}