# HG changeset patch # User Sascha L. Teichmann # Date 1551804223 -3600 # Node ID 2fa12229c94658de444e0587a859d55c27b89b1d # Parent ac61c250337b6f3d8b21a414482fe42a6e31106e# Parent e199655809c1a5ac96530534216686711c7436f6 Merged default into octree-diff branch. diff -r e199655809c1 -r 2fa12229c946 3rdpartylibs.sh --- a/3rdpartylibs.sh Tue Mar 05 17:37:48 2019 +0100 +++ b/3rdpartylibs.sh Tue Mar 05 17:43:43 2019 +0100 @@ -35,6 +35,9 @@ go get -u -v github.com/robfig/cron # MIT +go get -u -v github.com/tidwall/rtree +# MIT + ## list of additional licenses that get fetched and installed as dependencies # github.com/fsnotify/fsnotify/ BSD-3-Clause # github.com/hashicorp/hcl/ MPL-2.0 diff -r e199655809c1 -r 2fa12229c946 cmd/octreediff/db.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd/octreediff/db.go Tue Mar 05 17:43:43 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 e199655809c1 -r 2fa12229c946 cmd/octreediff/main.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd/octreediff/main.go Tue Mar 05 17:43:43 2019 +0100 @@ -0,0 +1,505 @@ +// 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 ( + "context" + "database/sql" + "errors" + "flag" + "fmt" + "log" + "runtime" + "sync" + "time" + + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/octree" +) + +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` + + insertContourSQL = ` +INSERT INTO diff_contour_lines ( + height, + lines +) +SELECT + $4, + ST_Transform( + ST_Multi( + ST_CollectionExtract( + ST_SimplifyPreserveTopology( + ST_Multi(ST_Collectionextract( + ST_MakeValid(ST_GeomFromWKB($1, $2::integer)), 2)), + $3 + ), + 2 + ) + ), + 4326 + ) +` + clippingPolygonSQL = ` +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 +) +SELECT ST_AsBinary( + 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) + ) + ) AS area +` + insertContourSQLClipped = ` +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 +) +INSERT INTO diff_contour_lines ( + height, + lines +) +SELECT + $5, + ST_Transform( + ST_Multi( + ST_intersection( + ST_CollectionExtract( + ST_SimplifyPreserveTopology( + ST_Multi(ST_Collectionextract( + ST_MakeValid(ST_GeomFromWKB($6, $2::integer)), 2)), + $7 + ), + 2 + ), + area + ) + ), + 4326 + ) + FROM inter` +) + +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) triangulate() (*octree.Triangulation, error) { + vertices := make([]octree.Vertex, len(pm)) + var i int + for p, z := range pm { + vertices[i] = octree.Vertex{X: p.x, Y: p.y, Z: z} + i++ + } + return octree.Triangulate(vertices) +} + +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 + } + defer tx.Rollback() + + 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)) + + var clip []byte + + if err := tx.QueryRowContext( + ctx, clippingPolygonSQL, + bottleneck, + first.EPSG, + firstDate, secondDate, + ).Scan(&clip); err != nil { + return err + } + + var clippingPolygon octree.Polygon + if err := clippingPolygon.FromWKB(clip); err != nil { + return err + } + clippingPolygon.Indexify() + + now = time.Now() + log.Printf("loading clipping polygon took %v\n", now.Sub(last)) + last = now + + tri, err := result.triangulate() + if err != nil { + return err + } + + now = time.Now() + log.Printf("triangulation took %v\n", now.Sub(last)) + last = now + + var str octree.STRTree + + tin := tri.Tin() + + str.Build(tin) + + now = time.Now() + log.Printf("building STR tree took %v\n", now.Sub(last)) + last = now + + removed := str.Clip(&clippingPolygon) + now = time.Now() + log.Printf("clipping STR tree took %v\n", now.Sub(last)) + last = now + + log.Printf("Number of triangles to clip: %d\n", len(removed)) + + builder := octree.NewBuilder(tin) + builder.Build(removed) + + 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) + } + } + + log.Printf("num heights: %d\n", len(heights)) + + var dataSize int + + stmt, err := tx.PrepareContext(ctx, insertContourSQL) + if err != nil { + return err + } + defer stmt.Close() + + log.Println("do contours") + octree.DoContours(tree, heights, func(res *octree.ContourResult) { + if err == nil && len(res.Lines) > 0 { + log.Printf("%.2f: lines: %d\n", res.Height, len(res.Lines)) + wkb := res.Lines.AsWKB2D() + dataSize += len(wkb) + _, err = stmt.ExecContext( + ctx, + wkb, + first.EPSG, + contourTolerance, + res.Height, + ) + } + }) + + 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 e199655809c1 -r 2fa12229c946 pkg/imports/sr.go --- a/pkg/imports/sr.go Tue Mar 05 17:37:48 2019 +0100 +++ b/pkg/imports/sr.go Tue Mar 05 17:43:43 2019 +0100 @@ -265,7 +265,7 @@ feedback.Info("Building octree...") builder := octree.NewBuilder(tin) start = time.Now() - builder.Build() + builder.Build(nil) octreeIndex, err := builder.Bytes() tin = nil // not needed from now on feedback.Info("building octree took %s", time.Since(start)) diff -r e199655809c1 -r 2fa12229c946 pkg/imports/wkb.go --- a/pkg/imports/wkb.go Tue Mar 05 17:37:48 2019 +0100 +++ b/pkg/imports/wkb.go Tue Mar 05 17:43:43 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 e199655809c1 -r 2fa12229c946 pkg/models/cross.go --- a/pkg/models/cross.go Tue Mar 05 17:37:48 2019 +0100 +++ b/pkg/models/cross.go Tue Mar 05 17:43:43 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 e199655809c1 -r 2fa12229c946 pkg/octree/builder.go --- a/pkg/octree/builder.go Tue Mar 05 17:37:48 2019 +0100 +++ b/pkg/octree/builder.go Tue Mar 05 17:43:43 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), @@ -64,42 +71,174 @@ } // Build builds the Octree. -func (tb *Builder) Build() { +func (tb *Builder) Build(removed map[int32]struct{}) { + + var triangles []int32 - triangles := make([]int32, len(tb.t.Triangles)) - for i := range triangles { - triangles[i] = int32(i) + if len(removed) > 0 { + triangles = make([]int32, len(tb.t.Triangles)-len(removed)) + idx := 0 + for i := range tb.t.Triangles { + if _, found := removed[int32(i)]; !found { + triangles[idx] = int32(i) + idx++ + } + } + } else { + triangles = make([]int32, len(tb.t.Triangles)) + for i := range triangles { + 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 +273,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 e199655809c1 -r 2fa12229c946 pkg/octree/cache.go --- a/pkg/octree/cache.go Tue Mar 05 17:37:48 2019 +0100 +++ b/pkg/octree/cache.go Tue Mar 05 17:43:43 2019 +0100 @@ -139,7 +139,7 @@ } } - tree, err := deserialize(data) + tree, err := Deserialize(data) if err != nil { return nil, err } diff -r e199655809c1 -r 2fa12229c946 pkg/octree/contours.go --- a/pkg/octree/contours.go Tue Mar 05 17:37:48 2019 +0100 +++ b/pkg/octree/contours.go Tue Mar 05 17:43:43 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 e199655809c1 -r 2fa12229c946 pkg/octree/hull.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/octree/hull.go Tue Mar 05 17:43:43 2019 +0100 @@ -0,0 +1,81 @@ +// Copyright (C) 2018 Michael Fogleman +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package octree + +import "sort" + +func cross2D(p, a, b Vertex) float64 { + return (a.X-p.X)*(b.Y-p.Y) - (a.Y-p.Y)*(b.X-p.X) +} + +// ConvexHull returns the convex hull of the provided points. +func ConvexHull(points []Vertex) []Vertex { + // copy points + pointsCopy := make([]Vertex, len(points)) + copy(pointsCopy, points) + points = pointsCopy + + // sort points + sort.Slice(points, func(i, j int) bool { + a := points[i] + b := points[j] + if a.X != b.X { + return a.X < b.X + } + return a.Y < b.Y + }) + + // filter nearly-duplicate points + distinctPoints := points[:0] + for i, p := range points { + if i > 0 && p.SquaredDistance2D(points[i-1]) < eps { + continue + } + distinctPoints = append(distinctPoints, p) + } + points = distinctPoints + + // find upper and lower portions + var U, L []Vertex + for _, p := range points { + for len(U) > 1 && cross2D(U[len(U)-2], U[len(U)-1], p) > 0 { + U = U[:len(U)-1] + } + for len(L) > 1 && cross2D(L[len(L)-2], L[len(L)-1], p) < 0 { + L = L[:len(L)-1] + } + U = append(U, p) + L = append(L, p) + } + + // reverse upper portion + for i, j := 0, len(U)-1; i < j; i, j = i+1, j-1 { + U[i], U[j] = U[j], U[i] + } + + // construct complete hull + if len(U) > 0 { + U = U[:len(U)-1] + } + if len(L) > 0 { + L = L[:len(L)-1] + } + return append(L, U...) +} diff -r e199655809c1 -r 2fa12229c946 pkg/octree/loader.go --- a/pkg/octree/loader.go Tue Mar 05 17:37:48 2019 +0100 +++ b/pkg/octree/loader.go Tue Mar 05 17:43:43 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 e199655809c1 -r 2fa12229c946 pkg/octree/node.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/octree/node.go Tue Mar 05 17:43:43 2019 +0100 @@ -0,0 +1,49 @@ +// Copyright (C) 2018 Michael Fogleman +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package octree + +type node struct { + i int32 + t int32 + prev *node + next *node +} + +func newNode(nodes []node, i int32, prev *node) *node { + n := &nodes[i] + n.i = i + if prev == nil { + n.prev = n + n.next = n + } else { + n.next = prev.next + n.prev = prev + prev.next.prev = n + prev.next = n + } + return n +} + +func (n *node) remove() *node { + n.prev.next = n.next + n.next.prev = n.prev + n.i = -1 + return n.prev +} diff -r e199655809c1 -r 2fa12229c946 pkg/octree/polygon.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/octree/polygon.go Tue Mar 05 17:43:43 2019 +0100 @@ -0,0 +1,433 @@ +// 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 + +import ( + "bytes" + "encoding/binary" + "fmt" + "log" + "math" + + "github.com/tidwall/rtree" + + "gemma.intevation.de/gemma/pkg/wkb" +) + +type ( + ring []float64 + + Polygon struct { + // TODO: Implement me! + rings []ring + + indices []*rtree.RTree + } + + IntersectionType byte + + lineSegment []float64 +) + +const ( + IntersectionInside IntersectionType = iota + IntersectionOutSide + IntersectionOverlaps +) + +func (ls lineSegment) Rect(interface{}) ([]float64, []float64) { + + var min, max [2]float64 + + if ls[0] < ls[2] { + min[0] = ls[0] + max[0] = ls[2] + } else { + min[0] = ls[2] + max[0] = ls[0] + } + + if ls[1] < ls[3] { + min[1] = ls[1] + max[1] = ls[3] + } else { + min[1] = ls[3] + max[1] = ls[1] + } + + return min[:], max[:] +} + +func (p *Polygon) Indexify() { + indices := make([]*rtree.RTree, len(p.rings)) + + for i := range indices { + index := rtree.New(nil) + indices[i] = index + + rng := p.rings[i] + + for i := 0; i < len(rng); i += 2 { + var ls lineSegment + if i+4 <= len(rng) { + ls = lineSegment(rng[i : i+4]) + } else { + ls = []float64{rng[i], rng[i+1], rng[0], rng[1]} + } + index.Insert(ls) + } + } + + p.indices = indices +} + +func (ls lineSegment) intersects(a Box2D) bool { + p1x := ls[0] + p1y := ls[1] + p2x := ls[2] + p2y := ls[3] + + left := a.X1 + right := a.X2 + top := a.Y1 + bottom := a.Y2 + + // The direction of the ray + dx := p2x - p1x + dy := p2y - p1y + + min, max := 0.0, 1.0 + + var t0, t1 float64 + + // Left and right sides. + // - If the line is parallel to the y axis. + if dx == 0 { + if p1x < left || p1x > right { + return false + } + } else { + // - Make sure t0 holds the smaller value by checking the direction of the line. + if dx > 0 { + t0 = (left - p1x) / dx + t1 = (right - p1x) / dx + } else { + t1 = (left - p1x) / dx + t0 = (right - p1x) / dx + } + + if t0 > min { + min = t0 + } + if t1 < max { + max = t1 + } + if min > max || max < 0 { + return false + } + } + + // The top and bottom side. + // - If the line is parallel to the x axis. + if dy == 0 { + if p1y < top || p1y > bottom { + return false + } + } else { + // - Make sure t0 holds the smaller value by checking the direction of the line. + if dy > 0 { + t0 = (top - p1y) / dy + t1 = (bottom - p1y) / dy + } else { + t1 = (top - p1y) / dy + t0 = (bottom - p1y) / dy + } + + if t0 > min { + min = t0 + } + if t1 < max { + max = t1 + } + if min > max || max < 0 { + return false + } + } + + // The point of intersection + // ix = p1x + dx*min + // iy = p1y + dy*min + return true +} + +func (ls lineSegment) intersectsLineSegment(o lineSegment) bool { + + p0 := ls[:2] + p1 := ls[2:4] + p2 := o[:2] + p3 := o[2:4] + + s10x := p1[0] - p0[0] + s10y := p1[1] - p0[1] + s32x := p3[0] - p2[0] + s32y := p3[1] - p2[1] + + den := s10x*s32y - s32x*s10y + + if den == 0 { + return false + } + + denPos := den > 0 + + s02x := p0[0] - p2[0] + s02y := p0[1] - p2[1] + + sNum := s10x*s02y - s10y*s02x + if sNum < 0 == denPos { + return false + } + + tNum := s32x*s02y - s32y*s02x + if tNum < 0 == denPos { + return false + } + + if sNum > den == denPos || tNum > den == denPos { + return false + } + + // t := tNum / den + // intersection at( p0[0] + (t * s10x), p0[1] + (t * s10y) ) + return true +} + +func (p *Polygon) IntersectionBox2D(box Box2D) IntersectionType { + + if len(p.rings) == 0 { + return IntersectionOutSide + } + + for _, index := range p.indices { + var intersects bool + index.Search(box, func(item rtree.Item) bool { + if item.(lineSegment).intersects(box) { + intersects = true + return false + } + return true + }) + if intersects { + return IntersectionOverlaps + } + } + + // No intersection -> check inside or outside + // if an abritrary point is inside or not. + point := []float64{box.X1, box.Y1} + + // Check holes first: inside a hole means outside. + if len(p.rings) > 1 { + for _, hole := range p.rings[1:] { + if hole.contains(point) { + return IntersectionOutSide + } + } + } + + // Check shell + if p.rings[0].contains(point) { + return IntersectionInside + } + return IntersectionOutSide +} + +func (p *Polygon) IntersectionWithTriangle(t *Triangle) IntersectionType { + box := t.BBox() + for _, index := range p.indices { + var intersects bool + index.Search(box, func(item rtree.Item) bool { + ls := item.(lineSegment) + other := make(lineSegment, 4) + for i := range t { + other[0] = t[i].X + other[1] = t[i].Y + other[2] = t[(i+1)%len(t)].X + other[3] = t[(i+1)%len(t)].Y + if ls.intersectsLineSegment(other) { + intersects = true + return false + } + } + return true + }) + if intersects { + return IntersectionOverlaps + } + } + // No intersection -> check inside or outside + // if an abritrary point is inside or not. + point := []float64{t[0].X, t[0].Y} + + // Check holes first: inside a hole means outside. + if len(p.rings) > 1 { + for _, hole := range p.rings[1:] { + if hole.contains(point) { + return IntersectionOutSide + } + } + } + + // Check shell + if p.rings[0].contains(point) { + return IntersectionInside + } + return IntersectionOutSide +} + +func (rng ring) isClosed() bool { return (len(rng) / 2) >= 3 } + +func (rng ring) contains(point []float64) bool { + if !rng.isClosed() { + return false + } + + end := len(rng)/2 - 1 + + contains := intersectsWithRaycast(point, rng[:2], rng[end*2:end*2+2]) + + for i := 2; i < len(rng); i += 2 { + if intersectsWithRaycast(point, rng[i-2:i], rng[i:i+2]) { + contains = !contains + } + } + + return contains +} + +// Using the raycast algorithm, this returns whether or not the passed in point +// Intersects with the edge drawn by the passed in start and end points. +// Original implementation: http://rosettacode.org/wiki/Ray-casting_algorithm#Go +func intersectsWithRaycast(point, start, end []float64) bool { + + // Always ensure that the the first point + // has a y coordinate that is less than the second point + if start[1] > end[1] { + // Switch the points if otherwise. + start, end = end, start + } + + // Move the point's y coordinate + // outside of the bounds of the testing region + // so we can start drawing a ray + for point[1] == start[1] || point[1] == end[1] { + y := math.Nextafter(point[1], math.Inf(1)) + point = []float64{point[0], y} + } + + // If we are outside of the polygon, indicate so. + if point[1] < start[1] || point[1] > end[1] { + return false + } + + if start[0] > end[0] { + if point[0] > start[0] { + return false + } + if point[0] < end[0] { + return true + } + } else { + if point[0] > end[0] { + return false + } + if point[0] < start[0] { + return true + } + } + + raySlope := (point[1] - start[1]) / (point[0] - start[0]) + diagSlope := (end[1] - start[1]) / (end[0] - start[0]) + + return raySlope >= diagSlope +} + +func (p *Polygon) FromWKB(data []byte) error { + + r := bytes.NewReader(data) + + endian, err := r.ReadByte() + + var order binary.ByteOrder + + switch { + case err != nil: + return err + case endian == wkb.NDR: + order = binary.LittleEndian + case endian == wkb.XDR: + order = binary.BigEndian + default: + return fmt.Errorf("unknown byte order %x", endian) + } + + var geomType uint32 + err = binary.Read(r, order, &geomType) + + switch { + case err != nil: + return err + case geomType != wkb.Polygon: + return fmt.Errorf("unknown geometry type %x", geomType) + } + + var numRings uint32 + if err = binary.Read(r, order, &numRings); err != nil { + return err + } + + rngs := make([]ring, numRings) + + log.Printf("info: Number of rings: %d\n", len(rngs)) + + for rng := uint32(0); rng < numRings; rng++ { + var numVertices uint32 + if err = binary.Read(r, order, &numVertices); err != nil { + return err + } + + log.Printf("info: Number of vertices in ring %d: %d\n", rng, numVertices) + + numVertices *= 2 + vertices := make([]float64, numVertices) + + for v := uint32(0); v < numVertices; v += 2 { + var lat, lon uint64 + if err = binary.Read(r, order, &lat); err != nil { + return err + } + if err = binary.Read(r, order, &lon); err != nil { + return err + } + vertices[v] = math.Float64frombits(lat) + vertices[v+1] = math.Float64frombits(lon) + } + + rngs[rng] = vertices + } + + p.rings = rngs + + return nil +} diff -r e199655809c1 -r 2fa12229c946 pkg/octree/strtree.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/octree/strtree.go Tue Mar 05 17:43:43 2019 +0100 @@ -0,0 +1,247 @@ +// 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 + +import ( + "math" + "sort" +) + +const numEntries = 8 + +type STRTree struct { + tin *Tin + index []int32 + bboxes []Box2D +} + +func (s *STRTree) Build(t *Tin) { + + s.tin = t + + all := make([]int32, len(t.Triangles)) + + for i := range all { + all[i] = int32(i) + } + + s.index = append(s.index, 0) + + root := s.build(all) + + s.index[0] = root +} + +func (s *STRTree) Clip(p *Polygon) map[int32]struct{} { + + removed := make(map[int32]struct{}) + + stack := []int32{s.index[0]} + + for len(stack) > 0 { + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if top > 0 { // node + switch p.IntersectionBox2D(s.bbox(top)) { + case IntersectionInside: + // all triangles are inside polygon + case IntersectionOutSide: + // all triangles are outside polygon + s.allTriangles(top, removed) + default: + // mixed bag + for i, n := int32(0), s.index[top+1]; i < n; i++ { + stack = append(stack, s.index[top+2+i]) + } + } + } else { // leaf + top = -top - 1 + for i, n := int32(0), s.index[top+1]; i < n; i++ { + removed[s.index[top+2+i]] = struct{}{} + } + } + } + + return removed +} + +func (s *STRTree) allTriangles(pos int32, tris map[int32]struct{}) { + stack := []int32{pos} + for len(stack) > 0 { + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if top > 0 { // node + for i, n := int32(0), s.index[top+1]; i < n; i++ { + stack = append(stack, s.index[top+2+i]) + } + } else { // leaf + top = -top - 1 + for i, n := int32(0), s.index[top+1]; i < n; i++ { + tris[s.index[top+2+i]] = struct{}{} + } + } + } +} + +func (s *STRTree) build(items []int32) int32 { + sort.Slice(items, func(i, j int) bool { + ti := s.tin.Triangles[items[i]] + tj := s.tin.Triangles[items[j]] + return s.tin.Vertices[ti[0]].X < s.tin.Vertices[tj[0]].X + }) + + P := int(math.Ceil(float64(len(items)) / float64(numEntries))) + S := int(math.Ceil(math.Sqrt(float64(P)))) + + sm := S * numEntries + + slices := make([][]int32, S) + rest := items + for i := range slices { + var n int + if l := len(rest); l < sm { + n = l + } else { + n = sm + } + slices[i] = rest[:n] + rest = rest[n:] + } + + leaves := make([]int32, 0, S*S) + + for _, slice := range slices { + sort.Slice(slice, func(i, j int) bool { + ti := s.tin.Triangles[slice[i]] + tj := s.tin.Triangles[slice[j]] + return s.tin.Vertices[ti[0]].Y < s.tin.Vertices[tj[0]].Y + }) + + for len(slice) > 0 { + n := numEntries + if l := len(slice); l < numEntries { + n = l + } + leaves = append(leaves, s.allocLeaf(slice[:n])) + slice = slice[n:] + } + } + + return s.buildNodes(leaves) +} + +func (s *STRTree) bbox(idx int32) Box2D { + if idx < 0 { // Don't care if leaf or node. + idx = -idx - 1 + } + return s.bboxes[s.index[idx]] +} + +func (s *STRTree) buildNodes(items []int32) int32 { + + if len(items) <= numEntries { + return s.allocNode(items) + } + + sort.Slice(items, func(i, j int) bool { + return s.bbox(items[i]).X1 < s.bbox(items[j]).X1 + }) + + P := int(math.Ceil(float64(len(items)) / float64(numEntries))) + S := int(math.Ceil(math.Sqrt(float64(P)))) + + sm := S * numEntries + + slices := make([][]int32, S) + + rest := items + for i := range slices { + var n int + if l := len(rest); l < sm { + n = l + } else { + n = sm + } + slices[i] = rest[:n] + rest = rest[n:] + } + + nodes := make([]int32, 0, S*S) + + for _, slice := range slices { + sort.Slice(slice, func(i, j int) bool { + return s.bbox(slice[i]).Y1 < s.bbox(slice[j]).Y1 + }) + + for len(slice) > 0 { + n := numEntries + if l := len(slice); l < numEntries { + n = l + } + nodes = append(nodes, s.allocNode(slice[:n])) + slice = slice[n:] + } + } + + return s.buildNodes(nodes) +} + +func (s *STRTree) allocNode(items []int32) int32 { + pos := len(s.index) + s.index = append(s.index, 0, int32(len(items))) + s.index = append(s.index, items...) + if len(items) > 0 { + box := s.bbox(items[0]) + for i := 1; i < len(items); i++ { + box = box.Union(s.bbox(items[i])) + } + s.index[pos] = int32(s.allocBBox(box)) + } + return int32(pos) +} + +func (s *STRTree) allocBBox(box Box2D) int { + pos := len(s.bboxes) + s.bboxes = append(s.bboxes, box) + return pos +} + +func (s *STRTree) allocLeaf(items []int32) int32 { + pos := len(s.index) + s.index = append(s.index, 0, int32(len(items))) + s.index = append(s.index, items...) + if len(items) > 0 { + vertices := s.tin.Vertices + ti := s.tin.Triangles[items[0]] + t := Triangle{ + vertices[ti[0]], + vertices[ti[1]], + vertices[ti[2]], + } + box := t.BBox() + for i := 1; i < len(items); i++ { + it := items[i] + ti := s.tin.Triangles[it] + t := Triangle{ + vertices[ti[0]], + vertices[ti[1]], + vertices[ti[2]], + } + box = box.Union(t.BBox()) + } + s.index[pos] = int32(s.allocBBox(box)) + } + return int32(-(pos + 1)) +} diff -r e199655809c1 -r 2fa12229c946 pkg/octree/tin.go --- a/pkg/octree/tin.go Tue Mar 05 17:37:48 2019 +0100 +++ b/pkg/octree/tin.go Tue Mar 05 17:43:43 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 e199655809c1 -r 2fa12229c946 pkg/octree/tree.go --- a/pkg/octree/tree.go Tue Mar 05 17:37:48 2019 +0100 +++ b/pkg/octree/tree.go Tue Mar 05 17:43:43 2019 +0100 @@ -32,6 +32,15 @@ Max Vertex } +type boxFrame struct { + pos int32 + Box2D +} + +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 +48,71 @@ {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 + } + + all := Box2D{ot.Min.X, ot.Min.Y, ot.Max.X, ot.Max.Y} + + stack := []boxFrame{{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, boxFrame{a, nbox}) + } + if b != 0 { + stack = append(stack, boxFrame{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)) { @@ -57,18 +131,13 @@ line := NewPlane2D(x1, y1, x2, y2) - type frame struct { - pos int32 - Box2D - } - dupes := map[int32]struct{}{} all := Box2D{ot.Min.X, ot.Min.Y, ot.Max.X, ot.Max.Y} //log.Printf("area: %f\n", (box.x2-box.x1)*(box.y2-box.y1)) //log.Printf("all: %f\n", (all.x2-all.x1)*(all.y2-all.y1)) - stack := []frame{{1, all}} + stack := []boxFrame{{1, all}} for len(stack) > 0 { top := stack[len(stack)-1] @@ -92,10 +161,10 @@ } if nbox.Intersects(box) && nbox.IntersectsPlane(line) { if a != 0 { - stack = append(stack, frame{a, nbox}) + stack = append(stack, boxFrame{a, nbox}) } if b != 0 { - stack = append(stack, frame{b, nbox}) + stack = append(stack, boxFrame{b, nbox}) } } } @@ -164,12 +233,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 e199655809c1 -r 2fa12229c946 pkg/octree/triangulation.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/octree/triangulation.go Tue Mar 05 17:43:43 2019 +0100 @@ -0,0 +1,116 @@ +// Copyright (C) 2018 Michael Fogleman +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package octree + +import ( + "fmt" + "math" +) + +type Triangulation struct { + Points []Vertex + ConvexHull []Vertex + Triangles []int32 + Halfedges []int32 +} + +// Triangulate returns a Delaunay triangulation of the provided points. +func Triangulate(points []Vertex) (*Triangulation, error) { + t := newTriangulator(points) + err := t.triangulate() + return &Triangulation{points, t.convexHull(), t.triangles, t.halfedges}, err +} + +func (t *Triangulation) TriangleSlices() [][]int32 { + sl := make([][]int32, len(t.Triangles)/3) + var j int + for i := range sl { + sl[i] = t.Triangles[j : j+3] + j += 3 + } + return sl +} + +func (t *Triangulation) Tin() *Tin { + + min := Vertex{math.MaxFloat64, math.MaxFloat64, math.MaxFloat64} + max := Vertex{-math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64} + + vertices := t.Points + + for _, v := range vertices { + min.Minimize(v) + max.Maximize(v) + } + + return &Tin{ + Vertices: vertices, + Triangles: t.TriangleSlices(), + Min: min, + Max: max, + } +} + +func (t *Triangulation) area() float64 { + var result float64 + points := t.Points + ts := t.Triangles + for i := 0; i < len(ts); i += 3 { + p0 := points[ts[i+0]] + p1 := points[ts[i+1]] + p2 := points[ts[i+2]] + result += area(p0, p1, p2) + } + return result / 2 +} + +// Validate performs several sanity checks on the Triangulation to check for +// potential errors. Returns nil if no issues were found. You normally +// shouldn't need to call this function but it can be useful for debugging. +func (t *Triangulation) Validate() error { + // verify halfedges + for i1, i2 := range t.Halfedges { + if i1 != -1 && t.Halfedges[i1] != i2 { + return fmt.Errorf("invalid halfedge connection") + } + if i2 != -1 && t.Halfedges[i2] != int32(i1) { + return fmt.Errorf("invalid halfedge connection") + } + } + + // verify convex hull area vs sum of triangle areas + hull1 := t.ConvexHull + hull2 := ConvexHull(t.Points) + area1 := polygonArea(hull1) + area2 := polygonArea(hull2) + area3 := t.area() + if math.Abs(area1-area2) > 1e-9 || math.Abs(area1-area3) > 1e-9 { + return fmt.Errorf("hull areas disagree: %f, %f, %f", area1, area2, area3) + } + + // verify convex hull perimeter + perimeter1 := polygonPerimeter(hull1) + perimeter2 := polygonPerimeter(hull2) + if math.Abs(perimeter1-perimeter2) > 1e-9 { + return fmt.Errorf("hull perimeters disagree: %f, %f", perimeter1, perimeter2) + } + + return nil +} diff -r e199655809c1 -r 2fa12229c946 pkg/octree/triangulator.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/octree/triangulator.go Tue Mar 05 17:43:43 2019 +0100 @@ -0,0 +1,375 @@ +// Copyright (C) 2018 Michael Fogleman +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package octree + +import ( + "fmt" + "math" + "sort" +) + +type triangulator struct { + points []Vertex + squaredDistances []float64 + ids []int32 + center Vertex + triangles []int32 + halfedges []int32 + trianglesLen int + hull *node + hash []*node +} + +func newTriangulator(points []Vertex) *triangulator { + return &triangulator{points: points} +} + +// sorting a triangulator sorts the `ids` such that the referenced points +// are in order by their distance to `center` +func (a *triangulator) Len() int { + return len(a.points) +} + +func (a *triangulator) Swap(i, j int) { + a.ids[i], a.ids[j] = a.ids[j], a.ids[i] +} + +func (a *triangulator) Less(i, j int) bool { + d1 := a.squaredDistances[a.ids[i]] + d2 := a.squaredDistances[a.ids[j]] + if d1 != d2 { + return d1 < d2 + } + p1 := a.points[a.ids[i]] + p2 := a.points[a.ids[j]] + if p1.X != p2.X { + return p1.X < p2.X + } + return p1.Y < p2.Y +} + +func (tri *triangulator) triangulate() error { + points := tri.points + + n := len(points) + if n == 0 { + return nil + } + + tri.ids = make([]int32, n) + + // compute bounds + x0 := points[0].X + y0 := points[0].Y + x1 := points[0].X + y1 := points[0].Y + for i, p := range points { + if p.X < x0 { + x0 = p.X + } + if p.X > x1 { + x1 = p.X + } + if p.Y < y0 { + y0 = p.Y + } + if p.Y > y1 { + y1 = p.Y + } + tri.ids[i] = int32(i) + } + + var i0, i1, i2 int32 + + // pick a seed point close to midpoint + m := Vertex{X: (x0 + x1) / 2, Y: (y0 + y1) / 2} + minDist := infinity + for i, p := range points { + d := p.SquaredDistance2D(m) + if d < minDist { + i0 = int32(i) + minDist = d + } + } + + // find point closest to seed point + minDist = infinity + for i, p := range points { + if int32(i) == i0 { + continue + } + d := p.SquaredDistance2D(points[i0]) + if d > 0 && d < minDist { + i1 = int32(i) + minDist = d + } + } + + // find the third point which forms the smallest circumcircle + minRadius := infinity + for i, p := range points { + if int32(i) == i0 || int32(i) == i1 { + continue + } + r := circumradius(points[i0], points[i1], p) + if r < minRadius { + i2 = int32(i) + minRadius = r + } + } + if minRadius == infinity { + return fmt.Errorf("No Delaunay triangulation exists for this input.") + } + + // swap the order of the seed points for counter-clockwise orientation + if area(points[i0], points[i1], points[i2]) < 0 { + i1, i2 = i2, i1 + } + + tri.center = circumcenter(points[i0], points[i1], points[i2]) + + // sort the points by distance from the seed triangle circumcenter + tri.squaredDistances = make([]float64, n) + for i, p := range points { + tri.squaredDistances[i] = p.SquaredDistance2D(tri.center) + } + sort.Sort(tri) + + // initialize a hash table for storing edges of the advancing convex hull + hashSize := int(math.Ceil(math.Sqrt(float64(n)))) + tri.hash = make([]*node, hashSize) + + // initialize a circular doubly-linked list that will hold an advancing convex hull + nodes := make([]node, n) + + e := newNode(nodes, i0, nil) + e.t = 0 + tri.hashEdge(e) + + e = newNode(nodes, i1, e) + e.t = 1 + tri.hashEdge(e) + + e = newNode(nodes, i2, e) + e.t = 2 + tri.hashEdge(e) + + tri.hull = e + + maxTriangles := 2*n - 5 + tri.triangles = make([]int32, maxTriangles*3) + tri.halfedges = make([]int32, maxTriangles*3) + + tri.addTriangle(i0, i1, i2, -1, -1, -1) + + pp := Vertex{X: infinity, Y: infinity} + for k := 0; k < n; k++ { + i := tri.ids[k] + p := points[i] + + // skip nearly-duplicate points + if p.SquaredDistance2D(pp) < eps { + continue + } + pp = p + + // skip seed triangle points + if i == int32(i0) || i == int32(i1) || i == int32(i2) { + continue + } + + // find a visible edge on the convex hull using edge hash + var start *node + key := tri.hashKey(p) + for j := 0; j < len(tri.hash); j++ { + start = tri.hash[key] + if start != nil && start.i >= 0 { + break + } + key++ + if key >= len(tri.hash) { + key = 0 + } + } + start = start.prev + + e := start + for area(p, points[e.i], points[e.next.i]) >= 0 { + e = e.next + if e == start { + e = nil + break + } + } + if e == nil { + // likely a near-duplicate point; skip it + continue + } + walkBack := e == start + + // add the first triangle from the point + t := tri.addTriangle(e.i, i, e.next.i, -1, -1, e.t) + e.t = t // keep track of boundary triangles on the hull + e = newNode(nodes, i, e) + + // recursively flip triangles from the point until they satisfy the Delaunay condition + e.t = tri.legalize(t + 2) + + // walk forward through the hull, adding more triangles and flipping recursively + q := e.next + for area(p, points[q.i], points[q.next.i]) < 0 { + t = tri.addTriangle(q.i, i, q.next.i, q.prev.t, -1, q.t) + q.prev.t = tri.legalize(t + 2) + tri.hull = q.remove() + q = q.next + } + + if walkBack { + // walk backward from the other side, adding more triangles and flipping + q := e.prev + for area(p, points[q.prev.i], points[q.i]) < 0 { + t = tri.addTriangle(q.prev.i, i, q.i, -1, q.t, q.prev.t) + tri.legalize(t + 2) + q.prev.t = t + tri.hull = q.remove() + q = q.prev + } + } + + // save the two new edges in the hash table + tri.hashEdge(e) + tri.hashEdge(e.prev) + } + + tri.triangles = tri.triangles[:tri.trianglesLen] + tri.halfedges = tri.halfedges[:tri.trianglesLen] + + return nil +} + +func (t *triangulator) hashKey(point Vertex) int { + d := point.Sub2D(t.center) + return int(pseudoAngle(d.X, d.Y) * float64(len(t.hash))) +} + +func (t *triangulator) hashEdge(e *node) { + t.hash[t.hashKey(t.points[e.i])] = e +} + +func (t *triangulator) addTriangle(i0, i1, i2, a, b, c int32) int32 { + i := int32(t.trianglesLen) + t.triangles[i] = i0 + t.triangles[i+1] = i1 + t.triangles[i+2] = i2 + t.link(i, a) + t.link(i+1, b) + t.link(i+2, c) + t.trianglesLen += 3 + return i +} + +func (t *triangulator) link(a, b int32) { + t.halfedges[a] = b + if b >= 0 { + t.halfedges[b] = a + } +} + +func (t *triangulator) legalize(a int32) int32 { + // if the pair of triangles doesn't satisfy the Delaunay condition + // (p1 is inside the circumcircle of [p0, pl, pr]), flip them, + // then do the same check/flip recursively for the new pair of triangles + // + // pl pl + // /||\ / \ + // al/ || \bl al/ \a + // / || \ / \ + // / a||b \ flip /___ar___\ + // p0\ || /p1 => p0\---bl---/p1 + // \ || / \ / + // ar\ || /br b\ /br + // \||/ \ / + // pr pr + + b := t.halfedges[a] + + a0 := a - a%3 + b0 := b - b%3 + + al := a0 + (a+1)%3 + ar := a0 + (a+2)%3 + bl := b0 + (b+2)%3 + + if b < 0 { + return ar + } + + p0 := t.triangles[ar] + pr := t.triangles[a] + pl := t.triangles[al] + p1 := t.triangles[bl] + + illegal := inCircle(t.points[p0], t.points[pr], t.points[pl], t.points[p1]) + + if illegal { + t.triangles[a] = p1 + t.triangles[b] = p0 + + // edge swapped on the other side of the hull (rare) + // fix the halfedge reference + if t.halfedges[bl] == -1 { + e := t.hull + for { + if e.t == bl { + e.t = a + break + } + e = e.next + if e == t.hull { + break + } + } + } + + t.link(a, t.halfedges[bl]) + t.link(b, t.halfedges[ar]) + t.link(ar, bl) + + br := b0 + (b+1)%3 + + t.legalize(a) + return t.legalize(br) + } + + return ar +} + +func (t *triangulator) convexHull() []Vertex { + var result []Vertex + e := t.hull + for e != nil { + result = append(result, t.points[e.i]) + e = e.prev + if e == t.hull { + break + } + } + return result +} diff -r e199655809c1 -r 2fa12229c946 pkg/octree/util.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/octree/util.go Tue Mar 05 17:43:43 2019 +0100 @@ -0,0 +1,37 @@ +// Copyright (C) 2018 Michael Fogleman +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package octree + +import "math" + +var ( + eps = math.Nextafter(1, 2) - 1 + infinity = math.Inf(1) +) + +func pseudoAngle(dx, dy float64) float64 { + p := dx / (math.Abs(dx) + math.Abs(dy)) + if dy > 0 { + p = (3 - p) / 4 + } else { + p = (1 + p) / 4 + } + return math.Max(0, math.Min(1-eps, p)) +} diff -r e199655809c1 -r 2fa12229c946 pkg/octree/vertex.go --- a/pkg/octree/vertex.go Tue Mar 05 17:37:48 2019 +0100 +++ b/pkg/octree/vertex.go Tue Mar 05 17:43:43 2019 +0100 @@ -20,6 +20,8 @@ "log" "math" "sort" + + "gemma.intevation.de/gemma/pkg/wkb" ) type ( @@ -60,8 +62,148 @@ 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 (t *Triangle) BBox() Box2D { + minX := math.Min(math.Min(t[0].X, t[1].X), t[2].X) + maxX := math.Max(math.Max(t[0].X, t[1].X), t[2].X) + minY := math.Min(math.Min(t[0].Y, t[1].Y), t[2].Y) + maxY := math.Max(math.Max(t[0].Y, t[1].Y), t[2].Y) + return Box2D{ + X1: minX, Y1: minY, + X2: maxX, Y2: maxY, + } +} + +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)) +} + +func area(a, b, c Vertex) float64 { + return (b.Y-a.Y)*(c.X-b.X) - (b.X-a.X)*(c.Y-b.Y) +} + +func inCircle(a, b, c, p Vertex) bool { + dx := a.X - p.X + dy := a.Y - p.Y + ex := b.X - p.X + ey := b.Y - p.Y + fx := c.X - p.X + fy := c.Y - p.Y + + ap := dx*dx + dy*dy + bp := ex*ex + ey*ey + cp := fx*fx + fy*fy + + return dx*(ey*cp-bp*fy)-dy*(ex*cp-bp*fx)+ap*(ex*fy-ey*fx) < 0 +} + +func circumradius(a, b, c Vertex) float64 { + dx := b.X - a.X + dy := b.Y - a.Y + ex := c.X - a.X + ey := c.Y - a.Y + + bl := dx*dx + dy*dy + cl := ex*ex + ey*ey + d := dx*ey - dy*ex + + x := (ey*bl - dy*cl) * 0.5 / d + y := (dx*cl - ex*bl) * 0.5 / d + + r := x*x + y*y + + if bl == 0 || cl == 0 || d == 0 || r == 0 { + return infinity + } + + return r +} + +func circumcenter(a, b, c Vertex) Vertex { + dx := b.X - a.X + dy := b.Y - a.Y + ex := c.X - a.X + ey := c.Y - a.Y + + bl := dx*dx + dy*dy + cl := ex*ex + ey*ey + d := dx*ey - dy*ex + + x := a.X + (ey*bl-dy*cl)*0.5/d + y := a.Y + (dx*cl-ex*bl)*0.5/d + + return Vertex{X: x, Y: y} +} + +func polygonArea(points []Vertex) float64 { + var result float64 + for i, p := range points { + q := points[(i+1)%len(points)] + result += (p.X - q.X) * (p.Y + q.Y) + } + return result / 2 +} + +func polygonPerimeter(points []Vertex) float64 { + if len(points) == 0 { + return 0 + } + var result float64 + q := points[len(points)-1] + for _, p := range points { + result += p.Distance2D(q) + q = p + } + return result +} + +func (v Vertex) Distance2D(w Vertex) float64 { + return math.Hypot(v.X-w.X, v.Y-w.Y) +} + // Minimize adjust this vertex v to hold the minimum // values component-wise of v and w. func (v *Vertex) Minimize(w Vertex) { @@ -90,6 +232,20 @@ } } +func (v Vertex) SquaredDistance2D(w Vertex) float64 { + dx := v.X - w.X + dy := v.Y - w.Y + return dx*dx + dy*dy +} + +// Sub2D returns (v - w) component-wise. +func (v Vertex) Sub2D(w Vertex) Vertex { + return Vertex{ + X: v.X - w.X, + Y: v.Y - w.Y, + } +} + // Sub returns (v - w) component-wise. func (v Vertex) Sub(w Vertex) Vertex { return Vertex{ @@ -166,6 +322,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 +531,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 +561,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)) @@ -512,12 +692,21 @@ return out } +func (a Box2D) Rect(interface{}) ([]float64, []float64) { + return []float64{a.X1, a.Y1}, []float64{a.X2, a.Y2} +} + // Intersects checks if two Box2Ds intersect. func (a Box2D) Intersects(b Box2D) bool { return !(a.X2 < a.X1 || a.X2 < b.X1 || 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 { @@ -534,6 +723,15 @@ return a.Y2 } +func (a Box2D) Union(b Box2D) Box2D { + return Box2D{ + X1: math.Min(a.X1, b.X1), + Y1: math.Min(a.Y1, b.Y1), + X2: math.Max(a.X2, b.X2), + Y2: math.Max(a.Y2, b.Y2), + } +} + // NewPlane2D creates a new Plane2D from two given points. func NewPlane2D(x1, y1, x2, y2 float64) Plane2D { b := x2 - x1 @@ -843,13 +1041,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 e199655809c1 -r 2fa12229c946 pkg/octree/wkb.go --- a/pkg/octree/wkb.go Tue Mar 05 17:37:48 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 e199655809c1 -r 2fa12229c946 pkg/wkb/wkb.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/wkb/wkb.go Tue Mar 05 17:43:43 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 +)