# HG changeset patch # User Sascha L. Teichmann # Date 1551434787 -3600 # Node ID 3cf5d27a6c8b1d5c96bcaaceda7d1c7daa8483e0 # Parent 242104c338ff2c77bb58ade2778d4ffb3ca15a1f# Parent 9de710bdb535300aafcda2f6635390163a7d8796 Merged defualt into octree-diff branch. diff -r 9de710bdb535 -r 3cf5d27a6c8b cmd/octreediff/db.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd/octreediff/db.go Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,52 @@ +// 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 + +package main + +import ( + "database/sql" + "flag" + + "github.com/jackc/pgx" + "github.com/jackc/pgx/stdlib" +) + +var ( + dbhost = flag.String("dbhost", "localhost", "database host") + dbport = flag.Uint("dbport", 5432, "database port") + dbname = flag.String("dbname", "gemma", "database user") + dbuser = flag.String("dbuser", "scott", "database user") + dbpassword = flag.String("dbpw", "tiger", "database password") + dbssl = flag.String("dbssl", "prefer", "database SSL mode") +) + +func run(fn func(*sql.DB) error) error { + + // To ease SSL config ride a bit on parsing. + cc, err := pgx.ParseConnectionString("sslmode=" + *dbssl) + if err != nil { + return err + } + + // Do the rest manually to allow whitespace in user/password. + cc.Host = *dbhost + cc.Port = uint16(*dbport) + cc.User = *dbuser + cc.Password = *dbpassword + cc.Database = *dbname + + db := stdlib.OpenDB(cc) + defer db.Close() + + return fn(db) +} diff -r 9de710bdb535 -r 3cf5d27a6c8b cmd/octreediff/main.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd/octreediff/main.go Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,471 @@ +// 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 + +package main + +import ( + "bytes" + "context" + "database/sql" + "encoding/binary" + "errors" + "flag" + "fmt" + "log" + "math" + "runtime" + "sync" + "time" + + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/octree" + "gemma.intevation.de/gemma/pkg/wkb" +) + +var ( + bottleneck = flag.String("bottleneck", "", "name of the bottleneck") + first = flag.String("first", "", "date of the first sounding result") + second = flag.String("second", "", "date of the second sounding result") +) + +const contourTolerance = 0.1 + +const ( + loadOctreeSQL = ` +SELECT sr.octree_index +FROM waterway.sounding_results sr JOIN waterway.bottlenecks bn + ON sr.bottleneck_id = bn.id +WHERE bn.bottleneck_id = $1 AND sr.date_info = $2::date + AND sr.octree_index IS NOT NULL` + + triangulateSQL = ` +WITH joined AS ( + SELECT + sr.area AS area, + sr.date_info AS date_info + FROM waterway.sounding_results sr JOIN + waterway.bottlenecks bn ON sr.bottleneck_id = bn.id + WHERE bn.bottleneck_id = $1 +), +inter AS ( + SELECT + ST_Buffer( + ST_intersection( + (SELECT ST_Transform(area::geometry, $2::int) FROM joined WHERE date_info = $3::date), + (SELECT ST_Transform(area::geometry, $2::int) FROM joined WHERE date_info = $4::date) + ), + 0.001) AS area +), +triangles AS ( + SELECT t.geom AS geom, ST_MakePolygon(ST_ExteriorRing(t.geom)) AS poly FROM ( + SELECT (ST_Dump( + ST_DelaunayTriangles(ST_GeomFromWKB($5, $2::int), 0, 2))).geom + ) t +) +SELECT ST_AsBinary(ST_Collect(triangles.geom)) FROM triangles, inter +WHERE ST_Covers(inter.area, triangles.poly) +` + + //INSERT INTO redis_diff_countour_lines ( + //INSERT INTO diff_contour_lines ( + //INSERT INTO diff_contour_lines_extern ( + insertContourSQL = ` +INSERT INTO diff_contour_lines ( + height, + lines +) +SELECT + $1, + ST_Transform( + ST_Multi( + ST_CollectionExtract( + ST_SimplifyPreserveTopology( + ST_Multi(ST_Collectionextract( + ST_MakeValid(ST_GeomFromWKB($2, $3::integer)), 2)), + $4 + ), + 2 + ) + ), + 4326 + )` +) + +func check(err error) { + if err != nil { + log.Fatalf("error: %v\n", err) + } +} + +type point struct { + x float64 + y float64 +} + +type pointMap map[point]float64 + +func (pm pointMap) asWKB() []byte { + size := 1 + 4 + 4 + len(pm)*(1+4+3*8) + + buf := bytes.NewBuffer(make([]byte, 0, size)) + + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.MultiPointZ) + binary.Write(buf, binary.LittleEndian, uint32(len(pm))) + + perPoint := bytes.NewBuffer(make([]byte, 0, 1+4)) + binary.Write(perPoint, binary.LittleEndian, wkb.NDR) + binary.Write(perPoint, binary.LittleEndian, wkb.PointZ) + hdr := perPoint.Bytes() + + for p, z := range pm { + buf.Write(hdr) + binary.Write(buf, binary.LittleEndian, math.Float64bits(p.x)) + binary.Write(buf, binary.LittleEndian, math.Float64bits(p.y)) + binary.Write(buf, binary.LittleEndian, math.Float64bits(z)) + } + + return buf.Bytes() +} + +func sliceWork( + vs []octree.Vertex, + dst pointMap, + fn func([]octree.Vertex, func([]octree.Vertex) []octree.Vertex), +) { + n := runtime.NumCPU() + + wg := new(sync.WaitGroup) + + slices := make(chan []octree.Vertex) + out := make(chan []octree.Vertex) + + pool := make(chan []octree.Vertex, n) + + const pageSize = 2048 + + turn := func(p []octree.Vertex) []octree.Vertex { + if p != nil { + out <- p + } + select { + case p = <-pool: + default: + p = make([]octree.Vertex, 0, pageSize) + } + return p + } + + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for slice := range slices { + fn(slice, turn) + } + }() + } + done := make(chan struct{}) + go func() { + defer close(done) + for s := range out { + for i := range s { + v := &s[i] + key := point{x: v.X, y: v.Y} + if z, found := dst[key]; found { + dst[key] = (z + v.Z) * 0.5 + } else { + dst[key] = v.Z + } + } + select { + case pool <- s[:0:pageSize]: + default: + } + } + }() + + size := len(vs)/n + 1 + for len(vs) > 0 { + var l int + if len(vs) < size { + l = len(vs) + } else { + l = size + } + slices <- vs[:l] + vs = vs[l:] + } + close(slices) + wg.Wait() + close(out) + <-done +} + +func process(bottleneck string, firstDate, secondDate time.Time) error { + start := time.Now() + defer func() { log.Printf("processing took %v\n", time.Since(start)) }() + + ctx := context.Background() + + return run(func(db *sql.DB) error { + + conn, err := db.Conn(ctx) + if err != nil { + return err + } + defer conn.Close() + + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return err + } + + type load struct { + date time.Time + data []byte + err *error + dst **octree.Tree + } + + out := make(chan *load) + wg := new(sync.WaitGroup) + + n := runtime.NumCPU() + if n > 2 { + n = 2 + } + + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for l := range out { + if *l.err == nil { + *l.dst, *l.err = octree.Deserialize(l.data) + } + } + }() + } + + var firstErr, secondErr error + var first, second *octree.Tree + + for _, l := range []*load{ + {date: firstDate, dst: &first, err: &firstErr}, + {date: secondDate, dst: &second, err: &secondErr}, + } { + var data []byte + if err := tx.QueryRowContext( + ctx, + loadOctreeSQL, + bottleneck, + l.date, + ).Scan(&data); err != nil { + *l.err = err + } else { + l.data = data + } + out <- l + } + close(out) + + wg.Wait() + + if firstErr != nil || secondErr != nil { + if firstErr != nil && secondErr != nil { + return fmt.Errorf("%v, %v", firstErr, secondErr) + } + if firstErr != nil { + return firstErr + } + return secondErr + } + + if first.EPSG != second.EPSG { + return errors.New("EPSG codes mismatch. Needs transformation slow pass.") + } + + now := time.Now() + log.Printf("loading took %v\n", now.Sub(start)) + last := now + + firstVs, secondVs := first.Vertices(), second.Vertices() + + result := make(pointMap, len(firstVs)+len(secondVs)) + + sliceWork( + firstVs, + result, + func( + slice []octree.Vertex, + turn func([]octree.Vertex) []octree.Vertex, + ) { + p := turn(nil) + for i := range slice { + v := &slice[i] + if z, found := second.Value(v.X, v.Y); found { + p = append(p, octree.Vertex{v.X, v.Y, v.Z - z}) + if len(p) == cap(p) { + p = turn(p) + } + } + } + if len(p) > 0 { + turn(p) + } + }) + + sliceWork( + secondVs, + result, + func( + slice []octree.Vertex, + turn func([]octree.Vertex) []octree.Vertex, + ) { + p := turn(nil) + for i := range slice { + v := &slice[i] + if z, found := first.Value(v.X, v.Y); found { + p = append(p, octree.Vertex{v.X, v.Y, z - v.Z}) + if len(p) == cap(p) { + p = turn(p) + } + } + } + if len(p) > 0 { + turn(p) + } + }) + + now = time.Now() + log.Printf("setting in took %v\n", now.Sub(last)) + last = now + log.Printf("num points: %d\n", len(result)) + + data := result.asWKB() + + now = time.Now() + log.Printf("turing into WKB took %v\n", now.Sub(last)) + last = now + + log.Printf("WKB size %.3fMB\n", float64(len(data))/(1024*1024)) + + var tin octree.Tin + + if err := tx.QueryRowContext( + ctx, + triangulateSQL, + bottleneck, + first.EPSG, + firstDate, + secondDate, + data, + ).Scan(&tin); err != nil { + return err + } + + now = time.Now() + log.Printf("triangulation took %v\n", now.Sub(last)) + last = now + + builder := octree.NewBuilder(&tin) + builder.Build() + + now = time.Now() + log.Printf("building octree took %v\n", now.Sub(last)) + last = now + + tree := builder.Tree() + + log.Printf("min/max: %f %f\n", tree.Min.Z, tree.Max.Z) + + var heights []float64 + + switch { + case tree.Min.Z >= 0: // All values positive. + for v := 0.0; v <= tree.Max.Z; v += 0.1 { + if v >= tree.Min.Z { + heights = append(heights, v) + } + } + case tree.Max.Z <= 0: // All values negative. + for v := 0.0; v >= tree.Min.Z; v -= 0.1 { + if v <= tree.Max.Z { + heights = append(heights, v) + } + } + default: // Positive and negative. + for v := 0.1; v <= tree.Max.Z; v += 0.1 { + heights = append(heights, v) + } + for i, j := 0, len(heights)-1; i < j; i, j = i+1, j-1 { + heights[i], heights[j] = heights[j], heights[i] + } + for v := 0.0; v >= tree.Min.Z; v -= 0.1 { + heights = append(heights, v) + } + } + + var dataSize int + + stmt, err := tx.PrepareContext(ctx, insertContourSQL) + if err != nil { + return err + } + defer stmt.Close() + + octree.DoContours(tree, heights, func(res *octree.ContourResult) { + if err == nil && len(res.Lines) > 0 { + log.Printf("%f: lines: %d\n", res.Height, len(res.Lines)) + wkb := res.Lines.AsWKB2D() + dataSize += len(wkb) + _, err = stmt.ExecContext( + ctx, + res.Height, + wkb, + first.EPSG, + contourTolerance, + ) + } + }) + + if err != nil { + return err + } + + now = time.Now() + log.Printf("Number of iso lines: %d\n", len(heights)) + log.Printf("Total WKB size: %.2fMB\n", float64(dataSize)/(1024*1024)) + log.Printf("generating iso lines took %v\n", now.Sub(last)) + last = now + + return tx.Commit() + }) +} + +func main() { + + flag.Parse() + + firstDate, err := time.Parse(common.DateFormat, *first) + check(err) + secondDate, err := time.Parse(common.DateFormat, *second) + check(err) + + if *bottleneck == "" { + log.Fatalln("Missing bottleneck name") + } + + check(process(*bottleneck, firstDate, secondDate)) +} diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/imports/wkb.go --- a/pkg/imports/wkb.go Fri Mar 01 10:53:52 2019 +0100 +++ b/pkg/imports/wkb.go Fri Mar 01 11:06:27 2019 +0100 @@ -20,6 +20,8 @@ "math" shp "github.com/jonas-p/go-shp" + + "gemma.intevation.de/gemma/pkg/wkb" ) type ( @@ -28,22 +30,14 @@ polygonSlice [][][]float64 ) -const ( - wkbNDR byte = 1 - - wkbPoint uint32 = 1 - wkbLineString uint32 = 2 - wkbPolygon uint32 = 3 -) - func (l lineSlice) asWKB() []byte { size := 1 + 4 + 4 + len(l)*(2*8) buf := bytes.NewBuffer(make([]byte, 0, size)) - binary.Write(buf, binary.LittleEndian, wkbNDR) - binary.Write(buf, binary.LittleEndian, wkbLineString) + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.LineString) binary.Write(buf, binary.LittleEndian, uint32(len(l))) for _, c := range l { @@ -67,8 +61,8 @@ buf := bytes.NewBuffer(make([]byte, 0, size)) - binary.Write(buf, binary.LittleEndian, wkbNDR) - binary.Write(buf, binary.LittleEndian, wkbPoint) + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.Point) var lat, lon float64 if len(p) > 0 { @@ -95,8 +89,8 @@ buf := bytes.NewBuffer(make([]byte, 0, size)) - binary.Write(buf, binary.LittleEndian, wkbNDR) - binary.Write(buf, binary.LittleEndian, wkbPolygon) + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.Polygon) binary.Write(buf, binary.LittleEndian, uint32(len(p))) for _, ring := range p { diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/models/cross.go --- a/pkg/models/cross.go Fri Mar 01 10:53:52 2019 +0100 +++ b/pkg/models/cross.go Fri Mar 01 11:06:27 2019 +0100 @@ -21,6 +21,8 @@ "fmt" "math" "time" + + "gemma.intevation.de/gemma/pkg/wkb" ) type ( @@ -145,22 +147,14 @@ return nil } -const ( - wkbXDR byte = 0 - wkbNDR byte = 1 - wkbLineString uint32 = 2 - wkbLineStringZ uint32 = 1000 + 2 - wkbMultiLineStringZ uint32 = 1000 + 5 -) - func (lc GeoJSONLineCoordinates) AsWKB() []byte { size := 1 + 4 + 4 + len(lc)*(2*8) buf := bytes.NewBuffer(make([]byte, 0, size)) - binary.Write(buf, binary.LittleEndian, wkbNDR) - binary.Write(buf, binary.LittleEndian, wkbLineString) + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.LineString) binary.Write(buf, binary.LittleEndian, uint32(len(lc))) for i := range lc { @@ -197,9 +191,9 @@ switch { case err != nil: return err - case endian == wkbNDR: + case endian == wkb.NDR: order = binary.LittleEndian - case endian == wkbXDR: + case endian == wkb.XDR: order = binary.BigEndian default: return fmt.Errorf("unknown byte order %x", endian) @@ -211,7 +205,7 @@ switch { case err != nil: return err - case geomType != wkbLineStringZ: + case geomType != wkb.LineStringZ: return fmt.Errorf("unknown geometry type %x", geomType) } @@ -273,9 +267,9 @@ switch { case err != nil: return err - case endian == wkbNDR: + case endian == wkb.NDR: order = binary.LittleEndian - case endian == wkbXDR: + case endian == wkb.XDR: order = binary.BigEndian default: return fmt.Errorf("unknown byte order %x", endian) @@ -287,7 +281,7 @@ switch { case err != nil: return err - case geomType != wkbMultiLineStringZ: + case geomType != wkb.MultiLineStringZ: return fmt.Errorf("unknown geometry type %d", geomType) } @@ -304,9 +298,9 @@ switch { case err != nil: return err - case endian == wkbNDR: + case endian == wkb.NDR: order = binary.LittleEndian - case endian == wkbXDR: + case endian == wkb.XDR: order = binary.BigEndian default: return fmt.Errorf("unknown byte order %x", endian) @@ -316,7 +310,7 @@ switch { case err != nil: return err - case geomType != wkbLineStringZ: + case geomType != wkb.LineStringZ: return fmt.Errorf("unknown geometry type %d", geomType) } diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/octree/builder.go --- a/pkg/octree/builder.go Fri Mar 01 10:53:52 2019 +0100 +++ b/pkg/octree/builder.go Fri Mar 01 11:06:27 2019 +0100 @@ -18,6 +18,9 @@ "encoding/binary" "io" "log" + "runtime" + "sync" + "sync/atomic" "github.com/golang/snappy" ) @@ -28,8 +31,12 @@ nodes int leaves int index []int32 + + mu sync.Mutex } +type buildStep func(chan buildStep) + var cubes = [8][2]Vertex{ makeCube(0), makeCube(1), @@ -71,35 +78,154 @@ triangles[i] = int32(i) } + n := runtime.NumCPU() + + steps := make(chan buildStep) + + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for step := range steps { + step(steps) + } + }() + } + tb.index = append(tb.index, 0) - tb.buildRecursive(triangles, tb.t.Min, tb.t.Max, 0) + root := func(int32) { + close(steps) + } + + steps <- tb.buildConcurrent( + triangles, + tb.t.Min, tb.t.Max, + 0, + root) + + wg.Wait() + + /* + tb.buildRecursive(triangles, tb.t.Min, tb.t.Max, 0) + */ tb.index[0] = int32(len(tb.index)) log.Printf("info: num nodes: %d\n", tb.index[0]) - log.Printf("info: nodes: %d leaves: %d index %d\n", tb.nodes, tb.leaves, tb.index[0]) } +func (tb *Builder) buildConcurrent( + triangles []int32, + min, max Vertex, + depth int, + parent func(int32), +) buildStep { + + return func(steps chan buildStep) { + + // none concurrent for small parts. + if len(triangles) <= 1024 || depth > 8 { + parent(tb.buildRecursive(triangles, min, max, depth)) + return + } + + bbox := Interpolate(min, max) + + bboxes := make([][2]Vertex, len(cubes)) + + for i := range cubes { + bboxes[i] = [2]Vertex{ + bbox(cubes[i][0]), + bbox(cubes[i][1]), + } + } + + var quandrants [8][]int32 + + for _, tri := range triangles { + triangle := tb.t.Triangles[tri] + v0 := tb.t.Vertices[triangle[0]] + v1 := tb.t.Vertices[triangle[1]] + v2 := tb.t.Vertices[triangle[2]] + + l := v0 + l.Minimize(v1) + l.Minimize(v2) + + h := v0 + h.Maximize(v1) + h.Maximize(v2) + + for i := range bboxes { + if !(h.Less(bboxes[i][0]) || bboxes[i][1].Less(l)) { + quandrants[i] = append(quandrants[i], tri) + } + } + } + + used := new(int32) + for i := range quandrants { + if len(quandrants[i]) > 0 { + *used++ + } + } + + pos := tb.allocNode() + + for i := range quandrants { + if len(quandrants[i]) > 0 { + j := int32(i) + parent := func(v int32) { + tb.index[pos+j] = v + if atomic.AddInt32(used, -1) == 0 { + parent(pos) + } + } + step := tb.buildConcurrent( + quandrants[i], + bboxes[i][0], bboxes[i][1], + depth+1, + parent) + select { + case steps <- step: + default: + // all slots busy -> execute directly. + step(steps) + } + } + } + } +} + +func (tb *Builder) allocNode() int32 { + tb.mu.Lock() + pos := int32(len(tb.index)) + tb.index = append(tb.index, + 0, 0, 0, 0, + 0, 0, 0, 0) + tb.nodes++ + tb.mu.Unlock() + return pos +} + func (tb *Builder) buildRecursive( triangles []int32, min, max Vertex, depth int, ) int32 { if len(triangles) <= 16 || depth > 8 { + tb.mu.Lock() pos := len(tb.index) tb.index = append(tb.index, int32(len(triangles))) tb.index = append(tb.index, triangles...) //log.Printf("leaf entries: %d (%d)\n", len(triangles), depth) tb.leaves++ + tb.mu.Unlock() return int32(-(pos + 1)) } - pos := len(tb.index) - tb.index = append(tb.index, - 0, 0, 0, 0, - 0, 0, 0, 0) - bbox := Interpolate(min, max) bboxes := make([][2]Vertex, len(cubes)) @@ -134,17 +260,19 @@ } } + pos := tb.allocNode() + for i := range quandrants { if len(quandrants[i]) > 0 { child := tb.buildRecursive( quandrants[i], bboxes[i][0], bboxes[i][1], depth+1) - tb.index[pos+i] = child + tb.index[pos+int32(i)] = child } } - tb.nodes++ - return int32(pos) + + return pos } func (tb *Builder) serialize(w io.Writer) error { diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/octree/cache.go --- a/pkg/octree/cache.go Fri Mar 01 10:53:52 2019 +0100 +++ b/pkg/octree/cache.go Fri Mar 01 11:06:27 2019 +0100 @@ -139,7 +139,7 @@ } } - tree, err := deserialize(data) + tree, err := Deserialize(data) if err != nil { return nil, err } diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/octree/contours.go --- a/pkg/octree/contours.go Fri Mar 01 10:53:52 2019 +0100 +++ b/pkg/octree/contours.go Fri Mar 01 11:06:27 2019 +0100 @@ -15,6 +15,7 @@ package octree import ( + "log" "runtime" "sync" ) @@ -66,6 +67,12 @@ // function in order of the original heights values. func DoContours(tree *Tree, heights []float64, store func(*ContourResult)) { + if len(heights) == 0 { + return + } + + log.Printf("num heights: %d\n", len(heights)) + contours := make([]*ContourResult, len(heights)) for i, h := range heights { diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/octree/loader.go --- a/pkg/octree/loader.go Fri Mar 01 10:53:52 2019 +0100 +++ b/pkg/octree/loader.go Fri Mar 01 11:06:27 2019 +0100 @@ -111,7 +111,7 @@ return tree, nil } -func deserialize(data []byte) (*Tree, error) { +func Deserialize(data []byte) (*Tree, error) { return loadReader( bufio.NewReader( snappy.NewReader( diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/octree/tin.go --- a/pkg/octree/tin.go Fri Mar 01 10:53:52 2019 +0100 +++ b/pkg/octree/tin.go Fri Mar 01 11:06:27 2019 +0100 @@ -25,6 +25,7 @@ "math" "gemma.intevation.de/gemma/pkg/models" + "gemma.intevation.de/gemma/pkg/wkb" ) var ( @@ -62,9 +63,9 @@ switch { case err != nil: return err - case endian == wkbNDR: + case endian == wkb.NDR: order = binary.LittleEndian - case endian == wkbXDR: + case endian == wkb.XDR: order = binary.BigEndian default: return fmt.Errorf("unknown byte order %x", endian) @@ -76,7 +77,7 @@ switch { case err != nil: return err - case geomType != wkbTinZ: + case geomType != wkb.TinZ: return fmt.Errorf("unknown geometry type %x", geomType) } @@ -113,9 +114,9 @@ switch { case err != nil: return err - case endian == wkbNDR: + case endian == wkb.NDR: order = binary.LittleEndian - case endian == wkbXDR: + case endian == wkb.XDR: order = binary.BigEndian default: return fmt.Errorf("unknown byte order %x", endian) @@ -125,7 +126,7 @@ switch { case err != nil: return err - case geomType != wkbTriangleZ: + case geomType != wkb.TriangleZ: return fmt.Errorf("unknown geometry type %d", geomType) } diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/octree/tree.go --- a/pkg/octree/tree.go Fri Mar 01 10:53:52 2019 +0100 +++ b/pkg/octree/tree.go Fri Mar 01 11:06:27 2019 +0100 @@ -32,6 +32,10 @@ Max Vertex } +func (ot *Tree) Vertices() []Vertex { + return ot.vertices +} + var scale = [4][4]float64{ {0.0, 0.0, 0.5, 0.5}, {0.5, 0.0, 1.0, 0.5}, @@ -39,6 +43,76 @@ {0.5, 0.5, 1.0, 1.0}, } +func (ot *Tree) Value(x, y float64) (float64, bool) { + + // out of bounding box + if x < ot.Min.X || ot.Max.X < x || + y < ot.Min.Y || ot.Max.Y < y { + return 0, false + } + + type frame struct { + pos int32 + Box2D + } + + all := Box2D{ot.Min.X, ot.Min.Y, ot.Max.X, ot.Max.Y} + + stack := []frame{{1, all}} + + for len(stack) > 0 { + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if top.pos > 0 { // node + if index := ot.index[top.pos:]; len(index) > 7 { + for i := 0; i < 4; i++ { + a := index[i] + b := index[i+4] + if a == 0 && b == 0 { + continue + } + dx := top.X2 - top.X1 + dy := top.Y2 - top.Y1 + nbox := Box2D{ + dx*scale[i][0] + top.X1, + dy*scale[i][1] + top.Y1, + dx*scale[i][2] + top.X1, + dy*scale[i][3] + top.Y1, + } + if nbox.Contains(x, y) { + if a != 0 { + stack = append(stack, frame{a, nbox}) + } + if b != 0 { + stack = append(stack, frame{b, nbox}) + } + break + } + } + } + } else { // leaf + pos := -top.pos - 1 + n := ot.index[pos] + indices := ot.index[pos+1 : pos+1+n] + + for _, idx := range indices { + tri := ot.triangles[idx] + t := Triangle{ + ot.vertices[tri[0]], + ot.vertices[tri[1]], + ot.vertices[tri[2]], + } + if t.Contains(x, y) { + return t.Plane3D().Z(x, y), true + } + } + } + } + + return 0, false +} + // Vertical does a vertical cross cut from (x1, y1) to (x2, y2). func (ot *Tree) Vertical(x1, y1, x2, y2 float64, fn func(*Triangle)) { @@ -164,12 +238,14 @@ } else { max = mid } - if index := ot.index[pos:]; len(index) > 3 { - stack = append(stack, - frame{index[0], min, max}, - frame{index[1], min, max}, - frame{index[2], min, max}, - frame{index[3], min, max}) + if pos < int32(len(ot.index)) { + if index := ot.index[pos:]; len(index) > 3 { + stack = append(stack, + frame{index[0], min, max}, + frame{index[1], min, max}, + frame{index[2], min, max}, + frame{index[3], min, max}) + } } } else { // leaf pos = -pos - 1 diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/octree/vertex.go --- a/pkg/octree/vertex.go Fri Mar 01 10:53:52 2019 +0100 +++ b/pkg/octree/vertex.go Fri Mar 01 11:06:27 2019 +0100 @@ -20,6 +20,8 @@ "log" "math" "sort" + + "gemma.intevation.de/gemma/pkg/wkb" ) type ( @@ -60,8 +62,54 @@ B float64 C float64 } + + Plane3D struct { + A float64 + B float64 + C float64 + D float64 + } ) +func (t *Triangle) Plane3D() Plane3D { + + v0 := t[1].Sub(t[0]) + v1 := t[2].Sub(t[0]) + n := v0.Cross(v1).Normalize() + + // x*nx+ y*ny+ z*nz + d = 0 + // d = - (x*nx+ y*ny + z*nz) + d := -t[0].Dot(n) + return Plane3D{ + A: n.X, + B: n.Y, + C: n.Z, + D: d, + } +} + +func (p Plane3D) Z(x, y float64) float64 { + // p.A*x + p.B*y + p.C*z + p.D = 0 + return -(p.A*x + p.B*y + p.D) / p.C +} + +func (v Vertex) Normalize() Vertex { + s := 1 / v.Length() + return Vertex{ + X: s * v.X, + Y: s * v.Y, + Z: s * v.Z, + } +} + +func (v Vertex) Dot(w Vertex) float64 { + return v.X*w.X + v.Y*w.Y + v.Z*w.Z +} + +func (v Vertex) Length() float64 { + return math.Sqrt(v.Dot(v)) +} + // Minimize adjust this vertex v to hold the minimum // values component-wise of v and w. func (v *Vertex) Minimize(w Vertex) { @@ -166,6 +214,30 @@ return 0 } +func (v Vertex) Dot2(w Vertex) float64 { + return v.X*w.X + v.Y*w.Y +} + +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]) + + dot00 := v0.Dot2(v0) + dot01 := v0.Dot2(v1) + dot02 := v0.Dot2(v2) + dot11 := v1.Dot2(v1) + dot12 := v1.Dot2(v2) + + // Compute barycentric coordinates + invDenom := 1 / (dot00*dot11 - dot01*dot01) + u := (dot11*dot02 - dot01*dot12) * invDenom + v := (dot00*dot12 - dot01*dot02) * invDenom + + // Check if point is in triangle + return u >= 0 && v >= 0 && u+v < 1 +} + // IntersectHorizontal calculates the line string that // results when cutting a triangle a a certain height. // Can be empty (on intersection), @@ -351,13 +423,13 @@ buf := bytes.NewBuffer(make([]byte, 0, size)) - binary.Write(buf, binary.LittleEndian, wkbNDR) - binary.Write(buf, binary.LittleEndian, wkbMultiLineStringZ) + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.MultiLineStringZ) binary.Write(buf, binary.LittleEndian, uint32(len(mls))) for _, ml := range mls { - binary.Write(buf, binary.LittleEndian, wkbNDR) - binary.Write(buf, binary.LittleEndian, wkbLineStringZ) + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.LineStringZ) binary.Write(buf, binary.LittleEndian, uint32(len(ml))) for _, p := range ml { binary.Write(buf, binary.LittleEndian, math.Float64bits(p.X)) @@ -381,13 +453,13 @@ buf := bytes.NewBuffer(make([]byte, 0, size)) - binary.Write(buf, binary.LittleEndian, wkbNDR) - binary.Write(buf, binary.LittleEndian, wkbMultiLineString) + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.MultiLineString) binary.Write(buf, binary.LittleEndian, uint32(len(mls))) for _, ml := range mls { - binary.Write(buf, binary.LittleEndian, wkbNDR) - binary.Write(buf, binary.LittleEndian, wkbLineString) + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.LineString) binary.Write(buf, binary.LittleEndian, uint32(len(ml))) for _, p := range ml { binary.Write(buf, binary.LittleEndian, math.Float64bits(p.X)) @@ -518,6 +590,11 @@ a.Y2 < a.Y1 || a.Y2 < b.Y1) } +func (a Box2D) Contains(x, y float64) bool { + return a.X1 <= x && x <= a.X2 && + a.Y1 <= y && y <= a.Y2 +} + // Xi returns the i-th x component. func (a Box2D) Xi(i int) float64 { if i == 0 { @@ -843,13 +920,17 @@ buf := bytes.NewBuffer(make([]byte, 0, size)) - binary.Write(buf, binary.LittleEndian, wkbNDR) - binary.Write(buf, binary.LittleEndian, wkbMultiPointZ) + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.MultiPointZ) binary.Write(buf, binary.LittleEndian, uint32(len(mpz))) + perPoint := bytes.NewBuffer(make([]byte, 0, 1+4)) + binary.Write(perPoint, binary.LittleEndian, wkb.NDR) + binary.Write(perPoint, binary.LittleEndian, wkb.PointZ) + hdr := perPoint.Bytes() + for _, p := range mpz { - binary.Write(buf, binary.LittleEndian, wkbNDR) - binary.Write(buf, binary.LittleEndian, wkbPointZ) + buf.Write(hdr) binary.Write(buf, binary.LittleEndian, math.Float64bits(p.X)) binary.Write(buf, binary.LittleEndian, math.Float64bits(p.Y)) binary.Write(buf, binary.LittleEndian, math.Float64bits(p.Z)) diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/octree/wkb.go --- a/pkg/octree/wkb.go Fri Mar 01 10:53:52 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,28 +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): -// * Sascha L. Teichmann - -package octree - -const ( - wkbXDR byte = 0 - wkbNDR byte = 1 - - wkbLineString uint32 = 2 - wkbMultiLineString uint32 = 5 - wkbPointZ uint32 = 1000 + 1 - wkbLineStringZ uint32 = 1000 + 2 - wkbMultiPointZ uint32 = 1000 + 4 - wkbMultiLineStringZ uint32 = 1000 + 5 - wkbTinZ uint32 = 1000 + 16 - wkbTriangleZ uint32 = 1000 + 17 -) diff -r 9de710bdb535 -r 3cf5d27a6c8b pkg/wkb/wkb.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/wkb/wkb.go Fri Mar 01 11:06:27 2019 +0100 @@ -0,0 +1,30 @@ +// 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 + +package wkb + +const ( + XDR byte = 0 + NDR byte = 1 + + Point uint32 = 1 + LineString uint32 = 2 + Polygon uint32 = 3 + MultiLineString uint32 = 5 + PointZ uint32 = 1000 + 1 + LineStringZ uint32 = 1000 + 2 + MultiPointZ uint32 = 1000 + 4 + MultiLineStringZ uint32 = 1000 + 5 + TinZ uint32 = 1000 + 16 + TriangleZ uint32 = 1000 + 17 +)