# HG changeset patch # User Sascha L. Teichmann # Date 1572960622 -3600 # Node ID f4abfd0ee8adeaa5e0c5031f05bcf583cc84d59b # Parent ec5afada70ec4bff195a966a81e317a17a02aa7d Renamed octree package to mesh as there is no octree any more. diff -r ec5afada70ec -r f4abfd0ee8ad pkg/controllers/cross.go --- a/pkg/controllers/cross.go Tue Nov 05 13:01:37 2019 +0100 +++ b/pkg/controllers/cross.go Tue Nov 05 14:30:22 2019 +0100 @@ -21,8 +21,8 @@ "net/http" "time" + "gemma.intevation.de/gemma/pkg/mesh" "gemma.intevation.de/gemma/pkg/models" - "gemma.intevation.de/gemma/pkg/octree" mw "gemma.intevation.de/gemma/pkg/middleware" ) @@ -52,7 +52,7 @@ func projectBack( ctx context.Context, - line octree.MultiLineStringZ, + line mesh.MultiLineStringZ, epsg uint32, conn *sql.Conn, ) (models.GeoJSONMultiLineCoordinatesZ, error) { @@ -74,7 +74,7 @@ ctx := req.Context() conn := mw.JSONConn(req) - tree, err := octree.FromCache( + tree, err := mesh.FromCache( ctx, conn, csi.Properties.Bottleneck, csi.Properties.Date.Time) @@ -117,16 +117,16 @@ start = time.Now() - var segments octree.MultiLineStringZ + var segments mesh.MultiLineStringZ for i := 0; i < len(coords)-1; i++ { c1 := &coords[i] c2 := &coords[i+1] - verticalLine := octree.NewVerticalLine(c1.Lat, c1.Lon, c2.Lat, c2.Lon) + verticalLine := mesh.NewVerticalLine(c1.Lat, c1.Lon, c2.Lat, c2.Lon) - var line octree.MultiLineStringZ - tree.Vertical(c1.Lat, c1.Lon, c2.Lat, c2.Lon, func(t *octree.Triangle) { + var line mesh.MultiLineStringZ + tree.Vertical(c1.Lat, c1.Lon, c2.Lat, c2.Lon, func(t *mesh.Triangle) { if ls := verticalLine.Intersection(t); len(ls) > 0 { line = append(line, ls) } diff -r ec5afada70ec -r f4abfd0ee8ad pkg/controllers/diff.go --- a/pkg/controllers/diff.go Tue Nov 05 13:01:37 2019 +0100 +++ b/pkg/controllers/diff.go Tue Nov 05 14:30:22 2019 +0100 @@ -25,8 +25,8 @@ "gemma.intevation.de/gemma/pkg/auth" "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/mesh" "gemma.intevation.de/gemma/pkg/models" - "gemma.intevation.de/gemma/pkg/octree" mw "gemma.intevation.de/gemma/pkg/middleware" ) @@ -162,7 +162,7 @@ start := time.Now() begin := start - minuendTree, err := octree.FromCache( + minuendTree, err := mesh.FromCache( ctx, conn, dci.Bottleneck, dci.Minuend.Time) @@ -179,7 +179,7 @@ epsg := minuendTree.EPSG() - var box octree.Box2D + var box mesh.Box2D switch err := conn.QueryRowContext( ctx, @@ -203,7 +203,7 @@ box.X1, box.Y1, box.X2, box.Y2) start = time.Now() - raster := octree.NewRaster(box, isoCellSize) + raster := mesh.NewRaster(box, isoCellSize) raster.Rasterize(minuendTree.Value) log.Printf("info: rasterizing minuend took %v\n", time.Since(start)) @@ -211,7 +211,7 @@ start = time.Now() - subtrahendTree, err := octree.FromCache( + subtrahendTree, err := mesh.FromCache( ctx, conn, dci.Bottleneck, dci.Subtrahend.Time) @@ -253,15 +253,15 @@ var heights []float64 - heights, err = octree.LoadClassBreaks( + heights, err = mesh.LoadClassBreaks( ctx, tx, "morphology_classbreaks_compare") if err != nil { log.Printf("warn: Loading class breaks failed: %v\n", err) err = nil - heights = octree.SampleDiffHeights(zMin, zMax, contourStep) + heights = mesh.SampleDiffHeights(zMin, zMax, contourStep) } else { - heights = octree.ExtrapolateClassBreaks(heights, zMin, zMax) + heights = mesh.ExtrapolateClassBreaks(heights, zMin, zMax) } heights = common.DedupFloat64s(heights) diff -r ec5afada70ec -r f4abfd0ee8ad pkg/imports/isr.go --- a/pkg/imports/isr.go Tue Nov 05 13:01:37 2019 +0100 +++ b/pkg/imports/isr.go Tue Nov 05 14:30:22 2019 +0100 @@ -19,7 +19,7 @@ "time" "gemma.intevation.de/gemma/pkg/common" - "gemma.intevation.de/gemma/pkg/octree" + "gemma.intevation.de/gemma/pkg/mesh" ) type IsoRefresh struct { @@ -123,7 +123,7 @@ time.Since(start)) }() - heights, err := octree.ParseClassBreaks(isr.ClassBreaks) + heights, err := mesh.ParseClassBreaks(isr.ClassBreaks) if err != nil { return nil, err } @@ -171,11 +171,11 @@ // For all sounding results in bottleneck. for _, sr := range bn.srs { - tree, err := octree.FetchMeshDirectly(ctx, tx, sr) + tree, err := mesh.FetchMeshDirectly(ctx, tx, sr) if err != nil { return err } - hs := octree.ExtrapolateClassBreaks(heights, tree.Min().Z, tree.Max().Z) + hs := mesh.ExtrapolateClassBreaks(heights, tree.Min().Z, tree.Max().Z) hs = common.DedupFloat64s(hs) // Delete the old iso areas. @@ -184,14 +184,14 @@ } // Calculate and store the iso areas. - box := octree.Box2D{ + box := mesh.Box2D{ X1: tree.Min().X, Y1: tree.Min().Y, X2: tree.Max().X, Y2: tree.Max().Y, } - raster := octree.NewRaster(box, isoCellSize) + raster := mesh.NewRaster(box, isoCellSize) raster.Rasterize(tree.Value) areas := raster.Trace(hs) diff -r ec5afada70ec -r f4abfd0ee8ad pkg/imports/sr.go --- a/pkg/imports/sr.go Tue Nov 05 13:01:37 2019 +0100 +++ b/pkg/imports/sr.go Tue Nov 05 14:30:22 2019 +0100 @@ -35,8 +35,8 @@ shp "github.com/jonas-p/go-shp" "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/mesh" "gemma.intevation.de/gemma/pkg/models" - "gemma.intevation.de/gemma/pkg/octree" "gemma.intevation.de/gemma/pkg/wkb" ) @@ -314,8 +314,8 @@ ldc /= 100 xform = chainTransforms( xform, - func(v octree.Vertex) octree.Vertex { - return octree.Vertex{X: v.X, Y: v.Y, Z: v.Z + ldc} + func(v mesh.Vertex) mesh.Vertex { + return mesh.Vertex{X: v.X, Y: v.Y, Z: v.Z + ldc} }) m.DepthReference = depthReference } @@ -324,7 +324,7 @@ return nil, common.ToError(err) } - var xyz octree.MultiPointZ + var xyz mesh.MultiPointZ if z != nil { // Scanning ZIP file for *.xyz file. var xyzf *zip.File @@ -392,7 +392,7 @@ feedback Feedback, importID int64, m *models.SoundingResultMeta, - xyz octree.MultiPointZ, + xyz mesh.MultiPointZ, boundary polygonSlice, ) (interface{}, error) { @@ -429,7 +429,7 @@ feedback.Info("Triangulate XYZ data.") start = time.Now() - tri, err := octree.Triangulate(xyz) + tri, err := mesh.Triangulate(xyz) if err != nil { return nil, err } @@ -437,12 +437,12 @@ feedback.Info("Number triangles: %d.", len(tri.Triangles)/3) var ( - clippingPolygon octree.Polygon - clippingPolygonBuffered octree.Polygon + clippingPolygon mesh.Polygon + clippingPolygonBuffered mesh.Polygon removed map[int32]struct{} polygonArea float64 clippingPolygonWKB []byte - tin *octree.Tin + tin *mesh.Tin ) if boundary == nil { @@ -450,7 +450,7 @@ tooLongEdge := tri.EstimateTooLong() feedback.Info("Eliminate triangles with edges longer than %.2f meters.", tooLongEdge) - var polygon octree.LineStringZ + var polygon mesh.LineStringZ start = time.Now() polygon, removed = tri.ConcaveHull(tooLongEdge) @@ -503,7 +503,7 @@ tin := tri.Tin() tin.EPSG = epsg - var str octree.STRTree + var str mesh.STRTree str.Build(tin) removed = str.Clip(&clippingPolygon) @@ -529,7 +529,7 @@ start = time.Now() tin := tri.Tin() - var virtual octree.STRTree + var virtual mesh.STRTree virtual.BuildWithout(tin, removed) feedback.Info("Building took %v", time.Since(start)) @@ -541,13 +541,13 @@ start = time.Now() - generated := make(octree.LineStringZ, 0, numPoints+clippingPolygon.NumVertices(0)) + generated := make(mesh.LineStringZ, 0, numPoints+clippingPolygon.NumVertices(0)) - octree.GenerateRandomVertices( + mesh.GenerateRandomVertices( numPoints, tin.Min, tin.Max, virtual.Value, - func(vertices []octree.Vertex) { + func(vertices []mesh.Vertex) { generated = append(generated, vertices...) }) @@ -562,15 +562,15 @@ } dupes[key] = struct{}{} if z, ok := virtual.Value(x, y); ok { - generated = append(generated, octree.Vertex{X: x, Y: y, Z: z}) + generated = append(generated, mesh.Vertex{X: x, Y: y, Z: z}) } }) feedback.Info("Triangulate new point cloud.") - xyz = octree.MultiPointZ(generated) + xyz = mesh.MultiPointZ(generated) start = time.Now() - tri, err = octree.Triangulate(xyz) + tri, err = mesh.Triangulate(xyz) if err != nil { return nil, err } @@ -585,7 +585,7 @@ tin = tri.Tin() tin.EPSG = epsg - var str octree.STRTree + var str mesh.STRTree str.Build(tin) feedback.Info("Building clipping index took %v", time.Since(start)) @@ -597,7 +597,7 @@ feedback.Info("Number of triangles to clip %d.", len(removed)) start = time.Now() - final := octree.STRTree{Entries: 16} + final := mesh.STRTree{Entries: 16} final.BuildWithout(tin, removed) feedback.Info("Building final mesh took %v.", time.Since(start)) @@ -742,20 +742,20 @@ return &m, nil } -type vertexTransform func(octree.Vertex) octree.Vertex +type vertexTransform func(mesh.Vertex) mesh.Vertex -func identityTransform(v octree.Vertex) octree.Vertex { return v } +func identityTransform(v mesh.Vertex) mesh.Vertex { return v } -func negateZTransform(v octree.Vertex) octree.Vertex { - return octree.Vertex{X: v.X, Y: v.Y, Z: -v.Z} +func negateZTransform(v mesh.Vertex) mesh.Vertex { + return mesh.Vertex{X: v.X, Y: v.Y, Z: -v.Z} } func chainTransforms(a, b vertexTransform) vertexTransform { - return func(v octree.Vertex) octree.Vertex { return b(a(v)) } + return func(v mesh.Vertex) mesh.Vertex { return b(a(v)) } } -func loadXYZReader(r io.Reader, feedback Feedback, xform vertexTransform) (octree.MultiPointZ, error) { - mpz := make(octree.MultiPointZ, 0, 250000) +func loadXYZReader(r io.Reader, feedback Feedback, xform vertexTransform) (mesh.MultiPointZ, error) { + mpz := make(mesh.MultiPointZ, 0, 250000) s := bufio.NewScanner(r) warnLimiter := common.WarningLimiter{Log: feedback.Warn, MaxWarnings: 100} @@ -764,7 +764,7 @@ for line := 1; s.Scan(); line++ { text := s.Text() - var p octree.Vertex + var p mesh.Vertex // fmt.Sscanf(text, "%f,%f,%f") is 4 times slower. idx := strings.IndexByte(text, ',') if idx == -1 { @@ -801,7 +801,7 @@ return mpz, nil } -func loadXYZ(f *zip.File, feedback Feedback, xform vertexTransform) (octree.MultiPointZ, error) { +func loadXYZ(f *zip.File, feedback Feedback, xform vertexTransform) (mesh.MultiPointZ, error) { r, err := f.Open() if err != nil { return nil, err @@ -810,7 +810,7 @@ return loadXYZReader(r, feedback, xform) } -func loadXYZFile(f string, feedback Feedback, xform vertexTransform) (octree.MultiPointZ, error) { +func loadXYZFile(f string, feedback Feedback, xform vertexTransform) (mesh.MultiPointZ, error) { r, err := os.Open(f) if err != nil { return nil, err @@ -859,11 +859,11 @@ ctx context.Context, tx *sql.Tx, feedback Feedback, - tree *octree.STRTree, + tree *mesh.STRTree, id int64, ) error { - heights, err := octree.LoadClassBreaks( + heights, err := mesh.LoadClassBreaks( ctx, tx, "morphology_classbreaks", ) @@ -879,7 +879,7 @@ heights = append(heights, h) } } else { - heights = octree.ExtrapolateClassBreaks(heights, minZ, maxZ) + heights = mesh.ExtrapolateClassBreaks(heights, minZ, maxZ) } /* @@ -898,7 +898,7 @@ ctx context.Context, tx *sql.Tx, feedback Feedback, - tree *octree.STRTree, + tree *mesh.STRTree, heights []float64, id int64, ) error { @@ -909,14 +909,14 @@ time.Since(total)) }() - box := octree.Box2D{ + box := mesh.Box2D{ X1: tree.Min().X, Y1: tree.Min().Y, X2: tree.Max().X, Y2: tree.Max().Y, } - raster := octree.NewRaster(box, isoCellSize) + raster := mesh.NewRaster(box, isoCellSize) raster.Rasterize(tree.Value) areas := raster.Trace(heights) diff -r ec5afada70ec -r f4abfd0ee8ad pkg/mesh/areas.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/areas.go Tue Nov 05 14:30:22 2019 +0100 @@ -0,0 +1,88 @@ +// 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 mesh + +import ( + "runtime" + "sync" + + "gemma.intevation.de/gemma/pkg/common" +) + +func GenerateRandomVertices( + n int, + min, max Vertex, + eval func(float64, float64) (float64, bool), + callback func([]Vertex), +) { + var wg sync.WaitGroup + + jobs := make(chan int) + out := make(chan []Vertex) + done := make(chan struct{}) + + cpus := runtime.NumCPU() + + free := make(chan []Vertex, cpus) + + for i := 0; i < cpus; i++ { + wg.Add(1) + go func() { + defer wg.Done() + xRange := common.Random(min.X, max.X) + yRange := common.Random(min.Y, max.Y) + + for size := range jobs { + var vertices []Vertex + select { + case vertices = <-free: + default: + vertices = make([]Vertex, 0, 1000) + } + for len(vertices) < size { + x, y := xRange(), yRange() + if z, ok := eval(x, y); ok { + vertices = append(vertices, Vertex{X: x, Y: y, Z: z}) + } + } + out <- vertices + } + }() + } + + go func() { + defer close(done) + for vertices := range out { + callback(vertices) + select { + case free <- vertices[:0]: + default: + } + } + }() + + for remain := n; remain > 0; { + if remain > 1000 { + jobs <- 1000 + remain -= 1000 + } else { + jobs <- remain + remain = 0 + } + } + close(jobs) + wg.Wait() + close(out) + <-done +} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/mesh/cache.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/cache.go Tue Nov 05 14:30:22 2019 +0100 @@ -0,0 +1,199 @@ +// 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 mesh + +import ( + "context" + "database/sql" + "sync" + "time" +) + +type ( + cacheKey struct { + date time.Time + bottleneck string + } + + cacheEntry struct { + checksum string + tree *STRTree + access time.Time + } + + // Cache holds Octrees for a defined amount of time in memory + // before they are released. + Cache struct { + sync.Mutex + entries map[cacheKey]*cacheEntry + } +) + +const ( + cleanupCacheSleep = 6 * time.Minute + maxCacheAge = 5 * time.Minute + maxCacheEntries = 4 +) + +const ( + directMeshSQL = ` +SELECT mesh_index FROM waterway.sounding_results +WHERE id = $1 +` + fetchMeshSQL = ` +SELECT mesh_checksum, mesh_index +FROM waterway.sounding_results +WHERE bottleneck_id = $1 AND date_info = $2::date + AND mesh_checksum IS NOT NULL AND mesh_index IS NOT NULL +` + checkMeshSQL = ` +SELECT CASE + WHEN mesh_checksum = $3 THEN NULL + ELSE mesh_index + END +FROM waterway.sounding_results +WHERE bottleneck_id = $1 AND date_info = $2::date + AND mesh_checksum IS NOT NULL AND mesh_index IS NOT NULL +` +) + +var cache = Cache{ + entries: map[cacheKey]*cacheEntry{}, +} + +func init() { + go cache.background() +} + +func (c *Cache) background() { + for { + time.Sleep(cleanupCacheSleep) + c.cleanup() + } +} + +func (c *Cache) cleanup() { + c.Lock() + defer c.Unlock() + good := time.Now().Add(-maxCacheAge) + for k, v := range c.entries { + if v.access.Before(good) { + delete(c.entries, k) + } + } +} + +// FromCache fetches an Octree from the global Octree cache. +func FromCache( + ctx context.Context, + conn *sql.Conn, + bottleneck string, date time.Time, +) (*STRTree, error) { + return cache.get(ctx, conn, bottleneck, date) +} + +// FetchOctreeDirectly loads an octree directly from the database. +func FetchMeshDirectly( + ctx context.Context, + tx *sql.Tx, + id int64, +) (*STRTree, error) { + var data []byte + err := tx.QueryRowContext(ctx, directMeshSQL, id).Scan(&data) + if err != nil { + return nil, err + } + tree := new(STRTree) + if err := tree.FromBytes(data); err != nil { + return nil, err + } + return tree, nil +} + +func (c *Cache) get( + ctx context.Context, + conn *sql.Conn, + bottleneck string, date time.Time, +) (*STRTree, error) { + c.Lock() + defer c.Unlock() + + key := cacheKey{date, bottleneck} + entry := c.entries[key] + + var data []byte + var checksum string + + if entry == nil { + // fetch from database + err := conn.QueryRowContext( + ctx, fetchMeshSQL, bottleneck, date).Scan(&checksum, &data) + switch { + case err == sql.ErrNoRows: + return nil, nil + case err != nil: + return nil, err + } + } else { + // check if we are not outdated. + err := conn.QueryRowContext( + ctx, checkMeshSQL, bottleneck, date, entry.checksum).Scan(&data) + switch { + case err == sql.ErrNoRows: + return nil, nil + case err != nil: + return nil, err + } + if data == nil { // we are still current + entry.access = time.Now() + return entry.tree, nil + } + } + + tree := new(STRTree) + + if err := tree.FromBytes(data); err != nil { + return nil, err + } + + now := time.Now() + + if entry != nil { + entry.tree = tree + entry.access = now + return tree, nil + } + + for len(c.entries) >= maxCacheEntries { + // Evict the entry that is accessed the longest time ago. + var oldestKey cacheKey + oldest := now + + for k, v := range c.entries { + if v.access.Before(oldest) { + oldest = v.access + oldestKey = k + } + } + delete(c.entries, oldestKey) + } + + c.entries[key] = &cacheEntry{ + checksum: checksum, + tree: tree, + access: now, + } + + return tree, nil +} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/mesh/classbreaks.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/classbreaks.go Tue Nov 05 14:30:22 2019 +0100 @@ -0,0 +1,150 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2019 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package mesh + +import ( + "context" + "database/sql" + "errors" + "math" + "sort" + "strconv" + "strings" +) + +const ( + selectClassBreaksSQL = ` +SELECT config_val FROM sys_admin.system_config +WHERE config_key = $1` +) + +func SampleDiffHeights(min, max, step float64) []float64 { + var heights []float64 + switch { + case min >= 0: // All values positive. + for v := 0.0; v <= max; v += step { + if v >= min { + heights = append(heights, v) + } + } + case max <= 0: // All values negative. + for v := 0.0; v >= min; v -= step { + if v <= max { + heights = append(heights, v) + } + } + default: // Positive and negative. + for v := step; v <= max; v += step { + 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 >= min; v -= step { + heights = append(heights, v) + } + } + return heights +} + +func ParseClassBreaks(config string) ([]float64, error) { + + parts := strings.Split(config, ",") + classes := make([]float64, 0, len(parts)) + for _, part := range parts { + if idx := strings.IndexRune(part, ':'); idx >= 0 { + part = part[:idx] + } + if part = strings.TrimSpace(part); part == "" { + continue + } + v, err := strconv.ParseFloat(part, 64) + if err != nil { + return nil, err + } + classes = append(classes, v) + } + + sort.Float64s(classes) + return classes, nil +} + +func LoadClassBreaks(ctx context.Context, tx *sql.Tx, key string) ([]float64, error) { + + var config sql.NullString + + err := tx.QueryRowContext(ctx, selectClassBreaksSQL, key).Scan(&config) + + switch { + case err == sql.ErrNoRows: + return nil, nil + case err != nil: + return nil, err + case !config.Valid: + return nil, errors.New("Invalid config string") + } + + return ParseClassBreaks(config.String) +} + +func round(v float64) float64 { + return math.Round(v*10000) / 10000 +} + +func ExtrapolateClassBreaks(cbs []float64, min, max float64) []float64 { + if min > max { + min, max = max, min + } + + n := make([]float64, len(cbs)) + copy(n, cbs) + sort.Float64s(n) + + for len(n) > 0 && n[0] < min { + n = n[1:] + } + + if len(n) == 0 { + return n + } + + for len(n) > 0 && n[len(n)-1] > max { + n = n[:len(n)-1] + } + + if len(n) == 0 { + return n + } + + for min < n[0] { + diff := n[1] - n[0] + if diff == 0 { + break + } + m := make([]float64, len(n)+1) + m[0] = round(n[0] - diff) + copy(m[1:], n) + n = m + } + + for max > n[len(n)-1] { + diff := n[len(n)-1] - n[len(n)-2] + if diff == 0 { + break + } + n = append(n, round(n[len(n)-1]+diff)) + } + + return n +} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/mesh/hull.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/hull.go Tue Nov 05 14:30:22 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 mesh + +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 ec5afada70ec -r f4abfd0ee8ad pkg/mesh/loader.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/loader.go Tue Nov 05 14:30:22 2019 +0100 @@ -0,0 +1,166 @@ +// 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 mesh + +import ( + "bufio" + "bytes" + "compress/gzip" + "encoding/binary" + "log" +) + +func (s *STRTree) deserializeIndex(r *bufio.Reader) error { + var numIndex int32 + if err := binary.Read(r, binary.LittleEndian, &numIndex); err != nil { + return err + } + index := make([]int32, numIndex) + s.index = index + + var last int32 + for i := range index { + v, err := binary.ReadVarint(r) + if err != nil { + return err + } + value := int32(v) + last + index[i] = value + last = value + } + + return nil +} + +func (s *STRTree) deserializeBBoxes(r *bufio.Reader) error { + + var numBBoxes int32 + if err := binary.Read(r, binary.LittleEndian, &numBBoxes); err != nil { + return err + } + + bboxes := make([]Box2D, numBBoxes) + s.bboxes = bboxes + + var err error + + read := func(v *float64) { + if err == nil { + err = binary.Read(r, binary.LittleEndian, v) + } + } + + for i := range bboxes { + read(&bboxes[i].X1) + read(&bboxes[i].Y1) + read(&bboxes[i].X2) + read(&bboxes[i].Y2) + } + + return err +} + +func (s *STRTree) deserialize(r *bufio.Reader) error { + s.tin = new(Tin) + + if err := s.tin.Deserialize(r); err != nil { + return err + } + var numEntries uint8 + if err := binary.Read(r, binary.LittleEndian, &numEntries); err != nil { + return err + } + s.Entries = int(numEntries) + + if err := s.deserializeIndex(r); err != nil { + return err + } + + return s.deserializeBBoxes(r) +} + +func (s *STRTree) FromBytes(data []byte) error { + r, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return err + } + return s.deserialize(bufio.NewReader(r)) +} + +func (t *Tin) Deserialize(r *bufio.Reader) error { + + if err := binary.Read(r, binary.LittleEndian, &t.EPSG); err != nil { + return err + } + + log.Printf("info: EPSG: %d\n", t.EPSG) + + if err := t.Min.Read(r); err != nil { + return err + } + + if err := t.Max.Read(r); err != nil { + return err + } + + log.Printf("info: BBOX: [[%f, %f, %f], [%f, %f, %f]]\n", + t.Min.X, t.Min.Y, t.Min.Z, + t.Max.X, t.Max.Y, t.Max.Z) + + var numVertices uint32 + if err := binary.Read(r, binary.LittleEndian, &numVertices); err != nil { + return err + } + + log.Printf("info: vertices: %d\n", numVertices) + + vertices := make([]Vertex, numVertices) + t.Vertices = vertices + + for i := range vertices { + if err := vertices[i].Read(r); err != nil { + return err + } + } + + var numTriangles uint32 + if err := binary.Read(r, binary.LittleEndian, &numTriangles); err != nil { + return err + } + + log.Printf("info: triangles: %d\n", numTriangles) + + indices := make([]int32, 3*numTriangles) + triangles := make([][]int32, numTriangles) + t.Triangles = triangles + + var last int32 + + for i := range triangles { + tri := indices[:3] + indices = indices[3:] + triangles[i] = tri + for j := range tri { + v, err := binary.ReadVarint(r) + if err != nil { + return err + } + value := int32(v) + last + tri[j] = value + last = value + } + } + + return nil +} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/mesh/node.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/node.go Tue Nov 05 14:30:22 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 mesh + +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 ec5afada70ec -r f4abfd0ee8ad pkg/mesh/plane2d_test.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/plane2d_test.go Tue Nov 05 14:30:22 2019 +0100 @@ -0,0 +1,47 @@ +// 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 mesh + +import ( + "math" + "testing" +) + +func TestIntersection(t *testing.T) { + + table := []struct { + a [4]float64 + b [4]float64 + intersects bool + x, y float64 + }{ + {[4]float64{-1, -1, 1, 1}, [4]float64{-1, 1, 1, -1}, true, 0, 0}, + {[4]float64{0, 0, 1, 1}, [4]float64{0, 1, 1, 0}, true, 0.5, 0.5}, + {[4]float64{0, 0, 1, 0}, [4]float64{0, 1, 1, 1}, false, 0, 0}, + } + + for _, e := range table { + p1 := NewPlane2D(e.a[0], e.a[1], e.a[2], e.a[3]) + p2 := NewPlane2D(e.b[0], e.b[1], e.b[2], e.b[3]) + x, y, intersects := p1.Intersection(p2) + if intersects != e.intersects { + t.Fatalf("Have %t want %t\n", intersects, e.intersects) + } + if e.intersects { + if math.Abs(e.x-x) > epsPlane || math.Abs(e.y-y) > epsPlane { + t.Fatalf("Have (%f, %f)t want (%f, %f)\n", x, y, e.x, e.y) + } + } + } +} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/mesh/polygon.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/polygon.go Tue Nov 05 14:30:22 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 mesh + +import ( + "bytes" + "encoding/binary" + "fmt" + "log" + "math" + + "github.com/tidwall/rtree" + + "gemma.intevation.de/gemma/pkg/wkb" +) + +type ( + ring []float64 + + Polygon struct { + 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. + + // Check holes first: inside a hole means outside. + if len(p.rings) > 1 { + for _, hole := range p.rings[1:] { + if contains(hole, box.X1, box.Y1) { + return IntersectionOutSide + } + } + } + + // Check shell + if contains(p.rings[0], box.X1, box.Y1) { + 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. + pX, pY := 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 contains(hole, pX, pY) { + return IntersectionOutSide + } + } + } + + // Check shell + if contains(p.rings[0], pX, pY) { + return IntersectionInside + } + return IntersectionOutSide +} + +func (rng ring) length() int { + return len(rng) / 2 +} + +func (rng ring) point(i int) (float64, float64) { + i *= 2 + return rng[i], rng[i+1] +} + +type segments interface { + length() int + point(int) (float64, float64) +} + +func contains(s segments, pX, pY float64) bool { + + n := s.length() + if n < 3 { + return false + } + + sX, sY := s.point(0) + eX, eY := s.point(n - 1) + + const eps = 0.0000001 + + if math.Abs(sX-eX) > eps || math.Abs(sY-eY) > eps { + // It's not closed! + return false + } + + var inside bool + + for i := 1; i < n; i++ { + eX, eY := s.point(i) + if intersectsWithRaycast(pX, pY, sX, sY, eX, eY) { + inside = !inside + } + sX, sY = eX, eY + } + + return inside +} + +// 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(pX, pY, sX, sY, eX, eY float64) bool { + + // Always ensure that the the first point + // has a y coordinate that is less than the second point + if sY > eY { + // Switch the points if otherwise. + sX, sY, eX, eY = eX, eY, sX, sY + } + + // Move the point's y coordinate + // outside of the bounds of the testing region + // so we can start drawing a ray + for pY == sY || pY == eY { + pY = math.Nextafter(pY, math.Inf(1)) + } + + // If we are outside of the polygon, indicate so. + if pY < sY || pY > eY { + return false + } + + if sX > eX { + if pX > sX { + return false + } + if pX < eX { + return true + } + } else { + if pX > eX { + return false + } + if pX < sX { + return true + } + } + + raySlope := (pY - sY) / (pX - sX) + diagSlope := (eY - sY) / (eX - sX) + + return raySlope >= diagSlope +} + +func (p *Polygon) NumVertices(ring int) int { + if ring < 0 || ring >= len(p.rings) { + return 0 + } + return len(p.rings[ring]) / 2 +} + +func (p *Polygon) Vertices(ring int, fn func(float64, float64)) { + if ring < 0 || ring >= len(p.rings) { + return + } + rng := p.rings[ring] + for i := 0; i < len(rng); i += 2 { + fn(rng[i+0], rng[i+1]) + } +} + +func (p *Polygon) AsWKB() []byte { + + size := 1 + 4 + 4 + for _, r := range p.rings { + size += 4 + len(r)*8 + } + + buf := bytes.NewBuffer(make([]byte, 0, size)) + + binary.Write(buf, binary.LittleEndian, wkb.NDR) + binary.Write(buf, binary.LittleEndian, wkb.Polygon) + binary.Write(buf, binary.LittleEndian, uint32(len(p.rings))) + + for _, r := range p.rings { + binary.Write(buf, binary.LittleEndian, uint32(len(r)/2)) + for i := 0; i < len(r); i += 2 { + binary.Write(buf, binary.LittleEndian, math.Float64bits(r[i+0])) + binary.Write(buf, binary.LittleEndian, math.Float64bits(r[i+1])) + } + } + + return buf.Bytes() +} + +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 +} + +func (p *Polygon) FromLineStringZ(ls LineStringZ) { + r := make([]float64, 2*len(ls)) + var pos int + for i := range ls { + r[pos+0] = ls[i].X + r[pos+1] = ls[i].Y + pos += 2 + } + p.rings = []ring{r} +} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/mesh/raster.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/raster.go Tue Nov 05 14:30:22 2019 +0100 @@ -0,0 +1,405 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2019 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann + +package mesh + +import ( + "log" + "math" + "runtime" + "sync" + "time" + + "github.com/fogleman/contourmap" + + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/wkb" +) + +type Raster struct { + BBox Box2D + CellSize float64 + XCells int + YCells int + Cells []float64 +} + +const noData = -math.MaxFloat64 + +func NewRaster(bbox Box2D, cellSize float64) *Raster { + + width, height := bbox.Size() + + log.Printf("info raster extent: %.2f / %.2f", width, height) + + xCells := int(math.Ceil(width / cellSize)) + yCells := int(math.Ceil(height / cellSize)) + + log.Printf("info raster size: %d / %d\n", xCells, yCells) + + size := (xCells + 2) * (yCells + 2) + cells := make([]float64, size) + for i := range cells { + cells[i] = noData + } + return &Raster{ + BBox: bbox, + CellSize: cellSize, + XCells: xCells, + YCells: yCells, + Cells: cells, + } +} + +func (r *Raster) Rasterize(eval func(float64, float64) (float64, bool)) { + var wg sync.WaitGroup + + rows := make(chan int) + + rasterRow := func() { + defer wg.Done() + quat := 0.25 * r.CellSize + for i := range rows { + pos := (i+1)*(r.XCells+2) + 1 + row := r.Cells[pos : pos+r.XCells] + py := r.BBox.Y1 + float64(i)*r.CellSize + r.CellSize/2 + px := r.BBox.X1 + r.CellSize/2 + y1 := py - quat + y2 := py + quat + for j := range row { + var n int + var sum float64 + + if v, ok := eval(px-quat, y1); ok { + sum = v + n = 1 + } + if v, ok := eval(px-quat, y2); ok { + sum += v + n++ + } + if v, ok := eval(px+quat, y1); ok { + sum += v + n++ + } + if v, ok := eval(px+quat, y2); ok { + sum += v + n++ + } + + if n > 0 { + row[j] = sum / float64(n) + } + px += r.CellSize + } + } + } + + for n := runtime.NumCPU(); n >= 1; n-- { + wg.Add(1) + go rasterRow() + } + + for i := 0; i < r.YCells; i++ { + rows <- i + } + close(rows) +} + +func (r *Raster) Diff(eval func(float64, float64) (float64, bool)) { + var wg sync.WaitGroup + + rows := make(chan int) + + rasterRow := func() { + defer wg.Done() + quat := 0.25 * r.CellSize + for i := range rows { + pos := (i+1)*(r.XCells+2) + 1 + row := r.Cells[pos : pos+r.XCells] + py := r.BBox.Y1 + float64(i)*r.CellSize + r.CellSize/2 + px := r.BBox.X1 + r.CellSize/2 + y1 := py - quat + y2 := py + quat + for j, old := range row { + // only diff where values + if old == noData { + px += r.CellSize + continue + } + var n int + var sum float64 + + if v, ok := eval(px-quat, y1); ok { + sum = v + n = 1 + } + if v, ok := eval(px-quat, y2); ok { + sum += v + n++ + } + if v, ok := eval(px+quat, y1); ok { + sum += v + n++ + } + if v, ok := eval(px+quat, y2); ok { + sum += v + n++ + } + + if n > 0 { + row[j] -= sum / float64(n) + } else { + row[j] = noData + } + + px += r.CellSize + } + } + } + + for n := runtime.NumCPU(); n >= 1; n-- { + wg.Add(1) + go rasterRow() + } + + for i := 0; i < r.YCells; i++ { + rows <- i + } + close(rows) +} + +func (r *Raster) ZExtent() (float64, float64, bool) { + min, max := math.MaxFloat64, -math.MaxFloat64 + for _, v := range r.Cells { + if v == noData { + continue + } + if v < min { + min = v + } + if v > max { + max = v + } + } + return min, max, min != math.MaxFloat64 +} + +func (r *Raster) Trace(heights []float64) []wkb.MultiPolygonGeom { + start := time.Now() + + tracer := contourmap.FromFloat64s(r.XCells+2, r.YCells+2, r.Cells) + + areas := make([]wkb.MultiPolygonGeom, len(heights)) + + reprojX := common.Linear(0.5, r.BBox.X1, 1.5, r.BBox.X1+r.CellSize) + reprojY := common.Linear(0.5, r.BBox.Y1, 1.5, r.BBox.Y1+r.CellSize) + + var wg sync.WaitGroup + + cnts := make(chan int) + + doContours := func() { + defer wg.Done() + for hIdx := range cnts { + if c := tracer.Contours(heights[hIdx]); len(c) > 0 { + areas[hIdx] = buildMultipolygon(c, reprojX, reprojY) + } + } + } + + for n := runtime.NumCPU(); n >= 1; n-- { + wg.Add(1) + go doContours() + } + + for i := range heights { + cnts <- i + } + close(cnts) + + wg.Wait() + log.Printf("info: Tracing areas took %v\n", time.Since(start)) + + return areas +} + +type contour []contourmap.Point + +type bboxNode struct { + box Box2D + cnt contour + children []*bboxNode +} + +func (cnt contour) contains(o contour) bool { + return contains(cnt, o[0].X, o[0].Y) || + contains(cnt, o[len(o)/2].X, o[len(o)/2].Y) +} + +func (cnt contour) length() int { + return len(cnt) +} + +func (cnt contour) point(i int) (float64, float64) { + return cnt[i].X, cnt[i].Y +} + +func (cnt contour) bbox() Box2D { + minX, minY := math.MaxFloat64, math.MaxFloat64 + maxX, maxY := -math.MaxFloat64, -math.MaxFloat64 + + for _, p := range cnt { + if p.X < minX { + minX = p.X + } + if p.X > maxX { + maxX = p.X + } + if p.Y < minY { + minY = p.Y + } + if p.Y > maxY { + maxY = p.Y + } + } + return Box2D{X1: minX, X2: maxX, Y1: minY, Y2: maxY} +} + +func (bn *bboxNode) insert(cnt contour, box Box2D) { + // check if children are inside new + var nr *bboxNode + + for i, r := range bn.children { + if r.box.Inside(box) && cnt.contains(r.cnt) { + if nr == nil { + nr = &bboxNode{box: box, cnt: cnt} + } + nr.children = append(nr.children, r) + bn.children[i] = nil + } + } + + // we have a new child + if nr != nil { + // compact the list + for i := len(bn.children) - 1; i >= 0; i-- { + if bn.children[i] == nil { + if i < len(bn.children)-1 { + copy(bn.children[i:], bn.children[i+1:]) + } + bn.children[len(bn.children)-1] = nil + bn.children = bn.children[:len(bn.children)-1] + } + } + bn.children = append(bn.children, nr) + return + } + + // check if new is inside an old + for _, r := range bn.children { + if box.Inside(r.box) && r.cnt.contains(cnt) { + r.insert(cnt, box) + return + } + } + + // its a new child node. + nr = &bboxNode{box: box, cnt: cnt} + bn.children = append(bn.children, nr) +} + +func (bn *bboxNode) insertRoot(cnt contour) { + bn.insert(cnt, cnt.bbox()) +} + +type bboxOutFunc func(contour, []contour) + +func (bn *bboxNode) generate(out bboxOutFunc) { + + var grands []*bboxNode + + holes := make([]contour, len(bn.children)) + + for i, ch := range bn.children { + holes[i] = ch.cnt + grands = append(grands, ch.children...) + } + out(bn.cnt, holes) + + // the grand children are new polygons. + for _, grand := range grands { + grand.generate(out) + } +} + +func (bn *bboxNode) generateRoot(out bboxOutFunc) { + for _, r := range bn.children { + r.generate(out) + } +} + +func buildMultipolygon( + cnts []contourmap.Contour, + reprojX, reprojY func(float64) float64, +) wkb.MultiPolygonGeom { + + var forest bboxNode + + for _, cnt := range cnts { + forest.insertRoot(contour(cnt)) + } + + //log.Printf("cnts: %d roots: %d\n", len(cnts), len(bf.roots)) + + var mp wkb.MultiPolygonGeom + + out := func(sh contour, hls []contour) { + + polygon := make(wkb.PolygonGeom, 1+len(hls)) + + // Handle shell + shell := make(wkb.LinearRingGeom, len(sh)) + for j, pt := range sh { + shell[j] = wkb.PointGeom{ + X: reprojX(pt.X), + Y: reprojY(pt.Y), + } + } + if shell.CCW() { + shell.Reverse() + } + polygon[0] = shell + + // handle holes + for i, hl := range hls { + hole := make(wkb.LinearRingGeom, len(hl)) + for j, pt := range hl { + hole[j] = wkb.PointGeom{ + X: reprojX(pt.X), + Y: reprojY(pt.Y), + } + } + if !hole.CCW() { + hole.Reverse() + } + polygon[1+i] = hole + } + + mp = append(mp, polygon) + } + + forest.generateRoot(out) + + return mp +} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/mesh/strtree.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/strtree.go Tue Nov 05 14:30:22 2019 +0100 @@ -0,0 +1,486 @@ +// 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 mesh + +import ( + "bytes" + "compress/gzip" + "encoding/binary" + "io" + "log" + "math" + "sort" +) + +const STRTreeDefaultEntries = 8 + +type STRTree struct { + Entries int + tin *Tin + index []int32 + bboxes []Box2D +} + +// Vertical does a vertical cross cut from (x1, y1) to (x2, y2). +func (s *STRTree) Vertical(x1, y1, x2, y2 float64, fn func(*Triangle)) { + box := Box2D{ + X1: math.Min(x1, x2), + Y1: math.Min(y1, y2), + X2: math.Max(x1, x2), + Y2: math.Max(y1, y2), + } + + // out of bounding box + if box.X2 < s.tin.Min.X || s.tin.Max.X < box.X1 || + box.Y2 < s.tin.Min.Y || s.tin.Max.Y < box.Y1 { + return + } + + line := NewPlane2D(x1, y1, x2, y2) + + vertices := s.tin.Vertices + + stack := []int32{s.index[0]} + + for len(stack) > 0 { + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if top > 0 { // node + if b := s.bbox(top); b.Intersects(box) && b.IntersectsPlane(line) { + n := s.index[top+1] + stack = append(stack, s.index[top+2:top+2+n]...) + } + } else { // leaf + top = -top - 1 + if b := s.bbox(top); !b.Intersects(box) || !b.IntersectsPlane(line) { + continue + } + n := s.index[top+1] + for _, idx := range s.index[top+2 : top+2+n] { + ti := s.tin.Triangles[idx] + t := Triangle{ + vertices[ti[0]], + vertices[ti[1]], + vertices[ti[2]], + } + v0 := line.Eval(t[0].X, t[0].Y) + v1 := line.Eval(t[1].X, t[1].Y) + v2 := line.Eval(t[2].X, t[2].Y) + + if onPlane(v0) || onPlane(v1) || onPlane(v2) || + sides(sides(sides(0, v0), v1), v2) == 3 { + fn(&t) + } + } + } + } +} + +func (s *STRTree) Min() Vertex { return s.tin.Min } +func (s *STRTree) Max() Vertex { return s.tin.Max } +func (s *STRTree) EPSG() uint32 { return s.tin.EPSG } + +func (s *STRTree) Value(x, y float64) (float64, bool) { + + if len(s.index) == 0 { + return 0, false + } + + stack := []int32{s.index[0]} + + vertices := s.tin.Vertices + + for len(stack) > 0 { + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if top > 0 { // node + if s.bbox(top).Contains(x, y) { + n := s.index[top+1] + stack = append(stack, s.index[top+2:top+2+n]...) + } + } else { // leaf + top = -top - 1 + if !s.bbox(top).Contains(x, y) { + continue + } + n := s.index[top+1] + for _, idx := range s.index[top+2 : top+2+n] { + ti := s.tin.Triangles[idx] + t := Triangle{ + vertices[ti[0]], + vertices[ti[1]], + vertices[ti[2]], + } + if t.Contains(x, y) { + return t.Plane3D().Z(x, y), true + } + } + } + } + return 0, false +} + +func (s *STRTree) Build(t *Tin) { + + if s.Entries == 0 { + s.Entries = STRTreeDefaultEntries + } + + 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) BuildWithout(t *Tin, remove map[int32]struct{}) { + + if s.Entries == 0 { + s.Entries = STRTreeDefaultEntries + } + + s.tin = t + + all := make([]int32, 0, len(t.Triangles)-len(remove)) + + for i := 0; i < len(t.Triangles); i++ { + idx := int32(i) + if _, found := remove[idx]; !found { + all = append(all, idx) + } + } + + 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]} + + vertices := s.tin.Vertices + + 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 + n := s.index[top+1] + stack = append(stack, s.index[top+2:top+2+n]...) + } + } else { // leaf + top = -top - 1 + switch p.IntersectionBox2D(s.bbox(top)) { + case IntersectionInside: + // all triangles are inside polygon + case IntersectionOutSide: + // all triangles are outside polygon + n := s.index[top+1] + for _, idx := range s.index[top+2 : top+2+n] { + removed[idx] = struct{}{} + } + default: + n := s.index[top+1] + for _, idx := range s.index[top+2 : top+2+n] { + ti := s.tin.Triangles[idx] + t := Triangle{ + vertices[ti[0]], + vertices[ti[1]], + vertices[ti[2]], + } + if p.IntersectionWithTriangle(&t) != IntersectionInside { + removed[idx] = struct{}{} + } + } + } + } + } + + return removed +} + +func (s *STRTree) serializeIndex(w io.Writer) error { + + if err := binary.Write(w, binary.LittleEndian, int32(len(s.index))); err != nil { + return err + } + + var buf [binary.MaxVarintLen32]byte + + var last int32 + var written int + + for _, x := range s.index { + delta := x - last + n := binary.PutVarint(buf[:], int64(delta)) + for p := buf[:n]; len(p) > 0; p = p[n:] { + var err error + if n, err = w.Write(p); err != nil { + return err + } + written += n + } + last = x + } + + log.Printf("info: compressed index in bytes: %d %.2f (%d %.2f)\n", + written, + float64(written)/(1024*1024), + 4*len(s.index), + float64(4*len(s.index))/(1024*1024), + ) + + return nil +} + +func (s *STRTree) serializeBBoxes(w io.Writer) (rr error) { + + if err := binary.Write(w, binary.LittleEndian, int32(len(s.bboxes))); err != nil { + return err + } + + var err error + + write := func(v float64) { + if err == nil { + err = binary.Write(w, binary.LittleEndian, math.Float64bits(v)) + } + } + for _, box := range s.bboxes { + write(box.X1) + write(box.Y1) + write(box.X2) + write(box.Y2) + } + + return err +} + +func (s *STRTree) Bytes() ([]byte, error) { + + var buf bytes.Buffer + w, err := gzip.NewWriterLevel(&buf, gzip.BestSpeed) + if err != nil { + return nil, err + } + + if err := s.tin.serialize(w); err != nil { + return nil, err + } + + if err := binary.Write(w, binary.LittleEndian, uint8(s.Entries)); err != nil { + return nil, err + } + + if err := s.serializeIndex(w); err != nil { + return nil, err + } + + if err := s.serializeBBoxes(w); err != nil { + return nil, err + } + + if err := w.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +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 + n := s.index[top+1] + stack = append(stack, s.index[top+2:top+2+n]...) + } else { // leaf + top = -top - 1 + n := s.index[top+1] + for _, idx := range s.index[top+2 : top+2+n] { + tris[idx] = 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(s.Entries))) + S := int(math.Ceil(math.Sqrt(float64(P)))) + + slices := s.strSplit(items, S) + + leaves := s.strJoin( + slices, S, + func(i, j int32) bool { + ti := s.tin.Triangles[i] + tj := s.tin.Triangles[j] + return s.tin.Vertices[ti[0]].Y < s.tin.Vertices[tj[0]].Y + }, + s.allocLeaf, + ) + + return s.buildNodes(leaves) +} + +func (s *STRTree) buildNodes(items []int32) int32 { + + if len(items) <= s.Entries { + 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(s.Entries))) + S := int(math.Ceil(math.Sqrt(float64(P)))) + + slices := s.strSplit(items, S) + + nodes := s.strJoin( + slices, S, + func(i, j int32) bool { return s.bbox(i).Y1 < s.bbox(j).Y1 }, + s.allocNode, + ) + + return s.buildNodes(nodes) +} + +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) strSplit(items []int32, S int) [][]int32 { + sm := S * s.Entries + slices := make([][]int32, S) + for i := range slices { + var n int + if l := len(items); l < sm { + n = l + } else { + n = sm + } + slices[i] = items[:n] + items = items[n:] + } + return slices +} + +func (s *STRTree) strJoin( + slices [][]int32, S int, + less func(int32, int32) bool, + alloc func([]int32) int32, +) []int32 { + nodes := make([]int32, 0, S*S) + + for _, slice := range slices { + sort.Slice(slice, func(i, j int) bool { + return less(slice[i], slice[j]) + }) + + for len(slice) > 0 { + var n int + if l := len(slice); l >= s.Entries { + n = s.Entries + } else { + n = l + } + nodes = append(nodes, alloc(slice[:n])) + slice = slice[n:] + } + } + return 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 ec5afada70ec -r f4abfd0ee8ad pkg/mesh/tin.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/tin.go Tue Nov 05 14:30:22 2019 +0100 @@ -0,0 +1,271 @@ +// 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 mesh + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "log" + "math" + + "gemma.intevation.de/gemma/pkg/models" + "gemma.intevation.de/gemma/pkg/wkb" +) + +var ( + errNoByteSlice = errors.New("Not a byte slice") + errTooLessPoints = errors.New("Too less points") +) + +// Tin stores a mesh of triangles with common vertices. +type Tin struct { + // EPSG holds the projection. + EPSG uint32 + // Vertices are the shared vertices. + Vertices []Vertex + // Triangles are the triangles. + Triangles [][]int32 + + // Min is the lower left corner of the bbox. + Min Vertex + // Max is the upper right corner of the bbox. + Max Vertex +} + +func (t *Tin) Clip(polygon *Polygon) map[int32]struct{} { + var tree STRTree + tree.Build(t) + return tree.Clip(polygon) +} + +// FromWKB constructs the TIN from a WKB representation. +// Shared vertices are identified and referenced by the +// same index. +func (t *Tin) FromWKB(data []byte) error { + log.Printf("info: data length %d\n", len(data)) + + 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.TinZ: + return fmt.Errorf("unknown geometry type %x", geomType) + } + + var num uint32 + if err = binary.Read(r, order, &num); err != nil { + return err + } + + vertices := make([]Vertex, 0, 100000) + + var v Vertex + + v2i := make(map[Vertex]int32, 100000) + + var indexPool []int32 + + allocIndices := func() []int32 { + if len(indexPool) == 0 { + indexPool = make([]int32, 3*8*1024) + } + ids := indexPool[:3] + indexPool = indexPool[3:] + return ids + } + + var triangles [][]int32 + + min := Vertex{math.MaxFloat64, math.MaxFloat64, math.MaxFloat64} + max := Vertex{-math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64} + + for i := uint32(0); i < num; i++ { + + endian, err = r.ReadByte() + 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) + } + + err = binary.Read(r, order, &geomType) + switch { + case err != nil: + return err + case geomType != wkb.TriangleZ: + return fmt.Errorf("unknown geometry type %d", geomType) + } + + var rings uint32 + if err = binary.Read(r, order, &rings); err != nil { + return err + } + triangle := allocIndices() + + for ring := uint32(0); ring < rings; ring++ { + var npoints uint32 + if err = binary.Read(r, order, &npoints); err != nil { + return err + } + + if npoints < 3 { + return errTooLessPoints + } + + for p := uint32(0); p < npoints; p++ { + var x, y, z uint64 + for _, addr := range []*uint64{&x, &y, &z} { + if err = binary.Read(r, order, addr); err != nil { + return err + } + } + if p >= 3 || ring >= 1 { + // Don't store the forth point. + continue + } + // Do this conversion later to spare reflect calls + // and allocs in binary.Read. + v.X = math.Float64frombits(x) + v.Y = math.Float64frombits(y) + v.Z = math.Float64frombits(z) + idx, found := v2i[v] + if !found { + idx = int32(len(vertices)) + v2i[v] = idx + vertices = append(vertices, v) + min.Minimize(v) + max.Maximize(v) + } + triangle[p] = idx + } + } + triangles = append(triangles, triangle) + } + + log.Printf("info: bbox: [[%f, %f], [%f, %f]]\n", + min.X, min.Y, max.X, max.Y) + + *t = Tin{ + EPSG: models.WGS84, + Vertices: vertices, + Triangles: triangles, + Min: min, + Max: max, + } + + return nil +} + +// Scan implements the sql.Scanner interface. +func (t *Tin) Scan(raw interface{}) error { + if raw == nil { + return nil + } + data, ok := raw.([]byte) + if !ok { + return errNoByteSlice + } + return t.FromWKB(data) +} + +func (t *Tin) serialize(w io.Writer) error { + + if err := binary.Write(w, binary.LittleEndian, t.EPSG); err != nil { + return err + } + + if err := t.Min.Write(w); err != nil { + return err + } + if err := t.Max.Write(w); err != nil { + return err + } + + if err := binary.Write( + w, binary.LittleEndian, uint32(len(t.Vertices))); err != nil { + return err + } + + var err error + vwrite := func(v float64) { + if err == nil { + err = binary.Write(w, binary.LittleEndian, math.Float64bits(v)) + } + } + + for _, v := range t.Vertices { + vwrite(v.X) + vwrite(v.Y) + vwrite(v.Z) + } + + if err != nil { + return err + } + log.Printf("info: vertices %d (%d)\n", len(t.Vertices), len(t.Vertices)*3*8) + + if err := binary.Write( + w, binary.LittleEndian, uint32(len(t.Triangles))); err != nil { + return err + } + + var buf [binary.MaxVarintLen32]byte + var written int + var last int32 + for _, triangle := range t.Triangles { + for _, idx := range triangle { + value := idx - last + n := binary.PutVarint(buf[:], int64(value)) + for p := buf[:n]; len(p) > 0; p = p[n:] { + var err error + if n, err = w.Write(p); err != nil { + return err + } + written += n + } + last = idx + } + } + log.Printf("info: compressed tin indices in bytes: %d (%d)\n", + written, 3*4*len(t.Triangles)) + + return nil +} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/mesh/triangulation.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/triangulation.go Tue Nov 05 14:30:22 2019 +0100 @@ -0,0 +1,318 @@ +// 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 mesh + +import ( + "fmt" + "log" + "math" + + "gonum.org/v1/gonum/stat" +) + +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) EstimateTooLong() float64 { + + num := len(t.Triangles) / 3 + + lengths := make([]float64, 0, num) + + points := t.Points + +tris: + for i := 0; i < num; i++ { + idx := i * 3 + var max float64 + vs := t.Triangles[idx : idx+3] + for j, vj := range vs { + if t.Halfedges[idx+j] < 0 { + continue tris + } + k := (j + 1) % 3 + if l := points[vj].Distance2D(points[vs[k]]); l > max { + max = l + } + } + lengths = append(lengths, max) + } + + std := stat.StdDev(lengths, nil) + return 3.5 * std +} + +func (t *Triangulation) ConcaveHull(tooLong float64) (LineStringZ, map[int32]struct{}) { + + if tooLong <= 0 { + tooLong = t.EstimateTooLong() + } + + tooLong *= tooLong + + var candidates []int32 + + points := t.Points + + for i, num := 0, len(t.Triangles)/3; i < num; i++ { + idx := i * 3 + var max float64 + vs := t.Triangles[idx : idx+3] + for j, vj := range vs { + k := (j + 1) % 3 + if l := points[vj].SquaredDistance2D(points[vs[k]]); l > max { + max = l + } + } + if max > tooLong { + candidates = append(candidates, int32(i)) + } + } + + removed := map[int32]struct{}{} + + isBorder := func(n int32) bool { + n *= 3 + for i := int32(0); i < 3; i++ { + e := n + i + o := t.Halfedges[e] + if o < 0 { + return true + } + if _, found := removed[o/3]; found { + return true + } + } + return false + } + + var newCandidates []int32 + + log.Printf("info: candidates: %d\n", len(candidates)) + for len(candidates) > 0 { + + oldRemoved := len(removed) + + for _, i := range candidates { + + if isBorder(i) { + removed[i] = struct{}{} + } else { + newCandidates = append(newCandidates, i) + } + } + + if oldRemoved == len(removed) { + break + } + + candidates = newCandidates + newCandidates = newCandidates[:0] + } + + log.Printf("info: candidates left: %d\n", len(candidates)) + log.Printf("info: triangles: %d\n", len(t.Triangles)/3) + log.Printf("info: triangles to remove: %d\n", len(removed)) + + type edge struct { + a, b int32 + prev, next *edge + } + + isClosed := func(e *edge) bool { + for curr := e.next; curr != nil; curr = curr.next { + if curr == e { + return true + } + } + return false + } + + open := map[int32]*edge{} + var rings []*edge + + for i, num := int32(0), int32(len(t.Triangles)/3); i < num; i++ { + if _, found := removed[i]; found { + continue + } + n := i * 3 + for j := int32(0); j < 3; j++ { + e := n + j + f := t.Halfedges[e] + if f >= 0 { + if _, found := removed[f/3]; !found { + continue + } + } + a := t.Triangles[e] + b := t.Triangles[n+(j+1)%3] + + curr := &edge{a: a, b: b} + + if old := open[a]; old != nil { + delete(open, a) + if old.a == a { + old.prev = curr + curr.next = old + } else { + old.next = curr + curr.prev = old + } + + if isClosed(old) { + rings = append(rings, old) + } + } else { + open[a] = curr + } + + if old := open[b]; old != nil { + delete(open, b) + if old.b == b { + old.next = curr + curr.prev = old + } else { + old.prev = curr + curr.next = old + } + + if isClosed(old) { + rings = append(rings, old) + } + } else { + open[b] = curr + } + } + } + + if len(open) > 0 { + log.Printf("warn: open vertices left: %d\n", len(open)) + } + + if len(rings) == 0 { + log.Println("warn: no ring found") + return nil, removed + } + + curr := rings[0] + + polygon := LineStringZ{ + points[curr.a], + points[curr.b], + } + + for curr = curr.next; curr != rings[0]; curr = curr.next { + polygon = append(polygon, points[curr.b]) + } + + polygon = append(polygon, t.Points[rings[0].a]) + + log.Printf("length of boundary: %d\n", len(polygon)) + + return polygon, removed +} + +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 ec5afada70ec -r f4abfd0ee8ad pkg/mesh/triangulator.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/triangulator.go Tue Nov 05 14:30:22 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 mesh + +import ( + "errors" + "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 (tri *triangulator) Len() int { + return len(tri.points) +} + +func (tri *triangulator) Swap(i, j int) { + tri.ids[i], tri.ids[j] = tri.ids[j], tri.ids[i] +} + +func (tri *triangulator) Less(i, j int) bool { + d1 := tri.squaredDistances[tri.ids[i]] + d2 := tri.squaredDistances[tri.ids[j]] + if d1 != d2 { + return d1 < d2 + } + p1 := tri.points[tri.ids[i]] + p2 := tri.points[tri.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 errors.New("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 (tri *triangulator) hashKey(point Vertex) int { + d := point.Sub2D(tri.center) + return int(pseudoAngle(d.X, d.Y) * float64(len(tri.hash))) +} + +func (tri *triangulator) hashEdge(e *node) { + tri.hash[tri.hashKey(tri.points[e.i])] = e +} + +func (tri *triangulator) addTriangle(i0, i1, i2, a, b, c int32) int32 { + i := int32(tri.trianglesLen) + tri.triangles[i] = i0 + tri.triangles[i+1] = i1 + tri.triangles[i+2] = i2 + tri.link(i, a) + tri.link(i+1, b) + tri.link(i+2, c) + tri.trianglesLen += 3 + return i +} + +func (tri *triangulator) link(a, b int32) { + tri.halfedges[a] = b + if b >= 0 { + tri.halfedges[b] = a + } +} + +func (tri *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 := tri.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 := tri.triangles[ar] + pr := tri.triangles[a] + pl := tri.triangles[al] + p1 := tri.triangles[bl] + + illegal := inCircle(tri.points[p0], tri.points[pr], tri.points[pl], tri.points[p1]) + + if illegal { + tri.triangles[a] = p1 + tri.triangles[b] = p0 + + // edge swapped on the other side of the hull (rare) + // fix the halfedge reference + if tri.halfedges[bl] == -1 { + e := tri.hull + for { + if e.t == bl { + e.t = a + break + } + e = e.next + if e == tri.hull { + break + } + } + } + + tri.link(a, tri.halfedges[bl]) + tri.link(b, tri.halfedges[ar]) + tri.link(ar, bl) + + br := b0 + (b+1)%3 + + tri.legalize(a) + return tri.legalize(br) + } + + return ar +} + +func (tri *triangulator) convexHull() []Vertex { + var result []Vertex + e := tri.hull + for e != nil { + result = append(result, tri.points[e.i]) + e = e.prev + if e == tri.hull { + break + } + } + return result +} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/mesh/util.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/util.go Tue Nov 05 14:30:22 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 mesh + +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 ec5afada70ec -r f4abfd0ee8ad pkg/mesh/vertex.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/mesh/vertex.go Tue Nov 05 14:30:22 2019 +0100 @@ -0,0 +1,1229 @@ +// 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 mesh + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "log" + "math" + "sort" + + "gemma.intevation.de/gemma/pkg/wkb" +) + +type ( + Point struct { + X float64 + Y float64 + } + + // Vertex is a 3D vertex. + Vertex struct { + X float64 + Y float64 + Z float64 + } + + // Triangle is a triangle consisting of three vertices. + Triangle [3]Vertex + + // Line is a line defined by first vertex on that line + // and the second being the direction. + Line [2]Vertex + + // Box is a 3D box. + Box [2]Vertex + + // MultiPointZ is a set of vertices. + MultiPointZ []Vertex + + // LineStringZ is a line string formed of vertices. + LineStringZ []Vertex + + // MultiLineStringZ is a set of line strings. + MultiLineStringZ []LineStringZ + + // Box2D is 2D area from (X1, Y1) to (X2, Y2). + Box2D struct { + X1 float64 + Y1 float64 + X2 float64 + Y2 float64 + } + + // Plane2D is a 2D plane (a line in 2D space). + Plane2D struct { + A float64 + 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 (a Box2D) Inside(b Box2D) bool { + return a.X1 >= b.X1 && a.X2 <= b.X2 && + a.Y1 >= b.Y1 && a.Y2 <= b.Y2 +} + +func (a Box2D) Size() (float64, float64) { + return a.X2 - a.X1, a.Y2 - a.Y1 +} + +func (a Box2D) Empty() bool { + const eps = 0.0000001 + return math.Abs(a.X2-a.X1) < eps && + math.Abs(a.Y2-a.Y1) < eps +} + +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 (p Plane3D) Eval(v Vertex) float64 { + return p.A*v.X + p.B*v.Y + p.C*v.Z + p.D +} + +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) +} + +func (v Vertex) Distance(w Vertex) float64 { + v = v.Sub(w) + 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) { + if w.X < v.X { + v.X = w.X + } + if w.Y < v.Y { + v.Y = w.Y + } + if w.Z < v.Z { + v.Z = w.Z + } +} + +// Maximize adjust this vertex v to hold the maximum +// values component-wise of v and w. +func (v *Vertex) Maximize(w Vertex) { + if w.X > v.X { + v.X = w.X + } + if w.Y > v.Y { + v.Y = w.Y + } + if w.Z > v.Z { + v.Z = w.Z + } +} + +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{ + v.X - w.X, + v.Y - w.Y, + v.Z - w.Z, + } +} + +// Add returns (v + w) component-wise. +func (v Vertex) Add(w Vertex) Vertex { + return Vertex{ + v.X + w.X, + v.Y + w.Y, + v.Z + w.Z, + } +} + +// Scale returns s*v component-wise. +func (v Vertex) Scale(s float64) Vertex { + return Vertex{ + s * v.X, + s * v.Y, + s * v.Z, + } +} + +// Interpolate returns a function that return s*b[1] + b[0] +// component-wise. +func (b Box) Interpolate() func(Vertex) Vertex { + v1, v2 := b[0], b[1] + v2 = v2.Sub(v1) + return func(s Vertex) Vertex { + return Vertex{ + v2.X*s.X + v1.X, + v2.Y*s.Y + v1.Y, + v2.Z*s.Z + v1.Z, + } + } +} + +func (b Box) HasX() bool { return math.Abs(b[0].X-b[1].X) > epsPlane } +func (b Box) HasY() bool { return math.Abs(b[0].Y-b[1].Y) > epsPlane } +func (b Box) HasZ() bool { return math.Abs(b[0].Z-b[1].Z) > epsPlane } + +// Less returns if one of v component is less than the +// corresponing component in w. +func (v Vertex) Less(w Vertex) bool { + return v.X < w.X || v.Y < w.Y || v.Z < w.Z +} + +// NewLine return a line of point/direction. +func NewLine(p1, p2 Vertex) Line { + return Line{ + p2.Sub(p1), + p1, + } +} + +// Eval returns the vertex for t*l[0] + l[1]. +func (l Line) Eval(t float64) Vertex { + return l[0].Scale(t).Add(l[1]) +} + +// IntersectHorizontal returns the intersection point +// for a given z value. +func (l Line) IntersectHorizontal(h float64) Vertex { + t := (h - l[1].Z) / l[0].Z + return l.Eval(t) +} + +func side(z, h float64) int { + switch { + case z < h: + return -1 + case z > h: + return +1 + } + 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].Sub2D(t[0]) + v1 := t[1].Sub2D(t[0]) + v2 := Vertex{X: x, Y: y}.Sub2D(t[0]) + + dot00 := v0.Dot2(v0) + dot01 := v0.Dot2(v1) + 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), +// one vertex (only touching an vertex) or +// two vertices (real intersection). +func (t *Triangle) IntersectHorizontal(h float64) LineStringZ { + sides := [3]int{ + side(t[0].Z, h), + side(t[1].Z, h), + side(t[2].Z, h), + } + + var points LineStringZ + + for i := 0; i < 3; i++ { + j := (i + 1) % 3 + si := sides[i] + sj := sides[j] + + switch { + case si == 0 && sj == 0: + // both on plane + points = append(points, t[i], t[j]) + case si == 0 && sj != 0: + // first on plane + points = append(points, t[i]) + case si != 0 && sj == 0: + // second on plane + points = append(points, t[j]) + case si == sj: + // both on same side + default: + // real intersection + v := NewLine(t[i], t[j]).IntersectHorizontal(h) + points = append(points, v) + } + } + + return points +} + +func linearScale(x1, y1, x2, y2 float64) func(Vertex) float64 { + dx := x2 - x1 + dy := y2 - y1 + + switch { + case dx != 0: + return func(v Vertex) float64 { + return (v.X - x1) / dx + } + case dy != 0: + return func(v Vertex) float64 { + return (v.Y - y1) / dy + } + default: + return func(Vertex) float64 { + return 0 + } + } +} + +func (ls LineStringZ) BBox() Box2D { + + min := Vertex{math.MaxFloat64, math.MaxFloat64, math.MaxFloat64} + max := Vertex{-math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64} + + for _, v := range ls { + min.Minimize(v) + max.Maximize(v) + } + + return Box2D{ + X1: min.X, + Y1: min.Y, + X2: max.X, + Y2: max.Y, + } +} + +func (ls LineStringZ) Area() float64 { + return polygonArea(ls) +} + +func (ls LineStringZ) Reverse() { + for i, j := 0, len(ls)-1; i < j; i, j = i+1, j-1 { + ls[i], ls[j] = ls[j], ls[i] + } +} + +func (ls LineStringZ) order(position func(Vertex) float64) { + type posVertex struct { + pos float64 + v Vertex + } + positions := make([]posVertex, len(ls)) + for i, v := range ls { + positions[i] = posVertex{position(v), v} + } + sort.Slice(positions, func(i, j int) bool { + return positions[i].pos < positions[j].pos + }) + for i := range positions { + ls[i] = positions[i].v + } +} + +// EpsEquals returns true if v and w are equal component-wise +// with the values within a epsilon range. +func (v Vertex) EpsEquals(w Vertex) bool { + const eps = 1e-5 + return math.Abs(v.X-w.X) < eps && + math.Abs(v.Y-w.Y) < eps && math.Abs(v.Z-w.Z) < eps +} + +// EpsEquals returns true if v and w are equal component-wise +// in the X/Y plane with the values within a epsilon range. +func (v Vertex) EpsEquals2D(w Vertex) bool { + const eps = 1e-5 + return math.Abs(v.X-w.X) < eps && + math.Abs(v.Y-w.Y) < eps +} + +// JoinOnLine joins the the elements of a given multi line string +// under the assumption that the segments are all on the line segment +// from (x1, y1) to (x2, y2). +func (mls MultiLineStringZ) JoinOnLine(x1, y1, x2, y2 float64) MultiLineStringZ { + + position := linearScale(x1, y1, x2, y2) + + type posLineString struct { + pos float64 + line LineStringZ + } + + positions := make([]posLineString, 0, len(mls)) + + for _, ls := range mls { + if len(ls) == 0 { + continue + } + // order the atoms first + ls.order(position) + positions = append(positions, posLineString{position(ls[0]), ls}) + } + + sort.Slice(positions, func(i, j int) bool { + return positions[i].pos < positions[j].pos + }) + + out := make(MultiLineStringZ, 0, len(positions)) + + var ignored int + + for i := range positions { + curr := positions[i].line + if l := len(out); l > 0 { + last := out[l-1] + + if last[len(last)-1].EpsEquals(curr[0]) { + out[l-1] = append(last[:len(last)-1], curr...) + continue + } + if position(last[len(last)-1]) > position(curr[0]) { + ignored++ + continue + } + } + out = append(out, curr) + } + + log.Printf("info: ignored parts: %d\n", ignored) + + return out +} + +// Write writes a Vertex as three 64 bit values in little endian order +// to the given writer. +func (v *Vertex) Write(w io.Writer) error { + if err := binary.Write( + w, binary.LittleEndian, math.Float64bits(v.X)); err != nil { + return err + } + if err := binary.Write( + w, binary.LittleEndian, math.Float64bits(v.Y)); err != nil { + return err + } + return binary.Write( + w, binary.LittleEndian, math.Float64bits(v.Z)) +} + +// Read fills this vertex with three 64 bit values stored as +// little endian from the given reader. +func (v *Vertex) Read(r io.Reader) error { + var buf [8]byte + b := buf[:] + if _, err := io.ReadFull(r, b); err != nil { + return nil + } + v.X = math.Float64frombits(binary.LittleEndian.Uint64(b)) + if _, err := io.ReadFull(r, b); err != nil { + return nil + } + v.Y = math.Float64frombits(binary.LittleEndian.Uint64(b)) + if _, err := io.ReadFull(r, b); err != nil { + return nil + } + v.Z = math.Float64frombits(binary.LittleEndian.Uint64(b)) + return nil +} + +// AsWKB returns the WKB representation of the given multi line string. +func (mls MultiLineStringZ) AsWKB() []byte { + + // pre-calculate size to avoid reallocations. + size := 1 + 4 + 4 + for _, ml := range mls { + size += 1 + 4 + 4 + len(ml)*3*8 + } + + buf := bytes.NewBuffer(make([]byte, 0, size)) + + 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, 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)) + binary.Write(buf, binary.LittleEndian, math.Float64bits(p.Y)) + binary.Write(buf, binary.LittleEndian, math.Float64bits(p.Z)) + } + } + + return buf.Bytes() +} + +// AsWKB2D returns the WKB representation of the given multi line string +// leaving the z component out. +func (mls MultiLineStringZ) AsWKB2D() []byte { + + // pre-calculate size to avoid reallocations. + size := 1 + 4 + 4 + for _, ml := range mls { + size += 1 + 4 + 4 + len(ml)*2*8 + } + + buf := bytes.NewBuffer(make([]byte, 0, size)) + + 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, 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)) + binary.Write(buf, binary.LittleEndian, math.Float64bits(p.Y)) + } + } + + return buf.Bytes() +} + +func (ls LineStringZ) CCW() bool { + var sum float64 + for i, v1 := range ls { + v2 := ls[(i+1)%len(ls)] + sum += (v2.X - v1.X) * (v2.Y + v1.Y) + } + return sum > 0 +} + +// Join joins two lines leaving the first of the second out. +func (ls LineStringZ) Join(other LineStringZ) LineStringZ { + nline := make(LineStringZ, len(ls)+len(other)-1) + copy(nline, ls) + copy(nline[len(ls):], other[1:]) + return nline +} + +// Merge merges line segments of a given multi line string +// by finding common start and end vertices. +func (mls MultiLineStringZ) Merge() MultiLineStringZ { + + var out MultiLineStringZ + + min := Vertex{math.MaxFloat64, math.MaxFloat64, math.MaxFloat64} + + for _, line := range mls { + for _, v := range line { + min.Minimize(v) + } + } + + type point struct { + x int64 + y int64 + } + + const precision = 1e7 + + quant := func(v Vertex) point { + return point{ + x: int64(math.Round((v.X - min.X) * precision)), + y: int64(math.Round((v.Y - min.Y) * precision)), + } + } + + heads := make(map[point]*[]LineStringZ) + + for _, line := range mls { + if len(line) < 2 { + out = append(out, line) + continue + } + head := quant(line[0]) + tail := quant(line[len(line)-1]) + if head == tail { // its already a ring + out = append(out, line) + continue + } + + if hs := heads[tail]; hs != nil { + l := len(*hs) + last := (*hs)[l-1] + if l == 1 { + delete(heads, tail) + } else { + (*hs)[l-1] = nil + *hs = (*hs)[:l-1] + } + line = line.Join(last) + + if head == quant(line[len(line)-1]) { // its a ring + out = append(out, line) + continue + } + } + + if hs := heads[head]; hs != nil { + *hs = append(*hs, line) + } else { + heads[head] = &[]LineStringZ{line} + } + } + +again: + for head, lines := range heads { + for i, line := range *lines { + tail := quant(line[len(line)-1]) + for hs := heads[tail]; hs != nil && len(*hs) > 0; hs = heads[tail] { + l := len(*hs) + last := (*hs)[l-1] + (*hs)[l-1] = nil + *hs = (*hs)[:l-1] + line = line.Join(last) + + if tail = quant(line[len(line)-1]); head == tail { // its a ring + out = append(out, line) + // remove from current lines + copy((*lines)[i:], (*lines)[i+1:]) + (*lines)[len(*lines)-1] = nil + *lines = (*lines)[:len(*lines)-1] + goto again + } + // overwrite in current lines + (*lines)[i] = line + } + } + } + + // rings := len(out) + + // The rest are open line strings. + for _, lines := range heads { + for _, line := range *lines { + out = append(out, line) + } + } + + // log.Printf("segments before/after merge: %d/%d (%d rings)\n", + // len(mls), len(out), rings) + + 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 { + return a.X1 + } + return a.X2 +} + +// Yi returns the i-th y component. +func (a Box2D) Yi(i int) float64 { + if i == 0 { + return a.Y1 + } + 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), + } +} + +func (a Box2D) Area() float64 { + return (a.X2 - a.X1) * (a.Y2 - a.Y1) +} + +// NewPlane2D creates a new Plane2D from two given points. +func NewPlane2D(x1, y1, x2, y2 float64) Plane2D { + b := x2 - x1 + a := -(y2 - y1) + + l := math.Sqrt(a*a + b*b) + a /= l + b /= l + + // a*x1 + b*y1 + c = 0 + c := -(a*x1 + b*y1) + return Plane2D{a, b, c} +} + +// Eval determines the distance of a given point +// from the plane. The sign of the result indicates +// the sideness. +func (p Plane2D) Eval(x, y float64) float64 { + return p.A*x + p.B*y + p.C +} + +const epsPlane = 1e-5 + +func sides(s int, x float64) int { + if math.Signbit(x) { + return s | 2 + } + return s | 1 +} + +// IntersectsPlane checks if a Box2D intersects with +// a given Plane2D. +func (a Box2D) IntersectsPlane(p Plane2D) bool { + var s int + for i := 0; i < 2; i++ { + x := a.Xi(i) + for j := 0; j < 2; j++ { + y := a.Yi(j) + v := p.Eval(x, y) + if math.Abs(v) < epsPlane { + //log.Printf("on line") + return true + } + if s = sides(s, v); s == 3 { + //log.Printf("... on both sides (djt)") + return true + } + } + } + //log.Printf("side: %d\n", s) + return false +} + +// Cross calculates the cross product of two vertices. +func (v Vertex) Cross(w Vertex) Vertex { + return Vertex{ + v.Y*w.Z - v.Z*w.Y, + v.Z*w.X - v.X*w.Z, + v.X*w.Y - v.Y*w.X, + } +} + +// Intersection calcultes the 2D intersection point of +// two Plane2Ds. If they do not intersect the returned +// bool flags is set to false. +func (p Plane2D) Intersection(o Plane2D) (float64, float64, bool) { + + u1 := Vertex{p.A, p.B, p.C} + u2 := Vertex{o.A, o.B, o.C} + + plane := u1.Cross(u2) + + if plane.Z == 0 { + return 0, 0, false + } + + return plane.X / plane.Z, plane.Y / plane.Z, true +} + +// VerticalLine is a 2D line segment. +type VerticalLine struct { + x1 float64 + y1 float64 + x2 float64 + y2 float64 + + line Plane2D +} + +// NewVerticalLine creates a new 2D line segment. +func NewVerticalLine(x1, y1, x2, y2 float64) *VerticalLine { + return &VerticalLine{ + x1: x1, + y1: y1, + x2: x2, + y2: y2, + line: NewPlane2D(x1, y1, x2, y2), + } +} + +func onPlane(x float64) bool { return math.Abs(x) < epsPlane } + +func relative(v1, v2 Vertex) func(x, y float64) float64 { + ls := linearScale(v1.X, v1.Y, v2.X, v2.Y) + return func(x, y float64) float64 { + return ls(Vertex{x, y, 0}) + } +} + +func interpolate(a, b float64) func(float64) float64 { + return func(x float64) float64 { + return (b-a)*x + a + } +} + +func linear(v1, v2 Vertex) func(t float64) Vertex { + return func(t float64) Vertex { + return Vertex{ + (v2.X-v1.X)*t + v1.X, + (v2.Y-v1.Y)*t + v1.Y, + (v2.Z-v1.Z)*t + v1.Z, + } + } +} + +func inRange(a float64) bool { return 0 <= a && a <= 1 } + +// Intersection intersects a line segment with a triangle. +func (vl *VerticalLine) Intersection(t *Triangle) LineStringZ { + + var out LineStringZ + + //defer func() { log.Printf("length out: %d\n", len(out)) }() + +edges: + for i := 0; i < 3 && len(out) < 2; i++ { + j := (i + 1) % 3 + edge := NewPlane2D(t[i].X, t[i].Y, t[j].X, t[j].Y) + + s1 := edge.Eval(vl.x1, vl.y1) + s2 := edge.Eval(vl.x2, vl.y2) + + o1 := onPlane(s1) + o2 := onPlane(s2) + + // log.Printf("s1, s2: %t %t (%f %f)\n", o1, o2, s1, s2) + + switch { + case o1 && o2: + pos := relative(t[i], t[j]) + t1 := pos(vl.x1, vl.y1) + t2 := pos(vl.x2, vl.y2) + + r1 := inRange(t1) + r2 := inRange(t2) + + switch { + case r1 && r2: + lin := linear(t[i], t[j]) + out = append(out, lin(t1), lin(t2)) + return out + + case !r1 && !r2: // the hole edge + out = append(out, t[i], t[j]) + return out + case !r1: + if t1 < 0 { + // below first -> clip by first + lin := linear(t[i], t[j]) + out = append(out, t[i], lin(t2)) + } else { + // above second -> clip by second + lin := linear(t[i], t[j]) + out = append(out, lin(t2), t[j]) + } + return out + case !r2: + if t2 < 0 { + // below first -> clip by first + lin := linear(t[i], t[j]) + out = append(out, t[i], lin(t1)) + } else { + // above second -> clip by second + lin := linear(t[i], t[j]) + out = append(out, lin(t1), t[j]) + } + return out + } + + case o1: + t1 := relative(t[i], t[j])(vl.x1, vl.y1) + if !inRange(t1) { + continue edges + } + out = append(out, linear(t[i], t[j])(t1)) + + case o2: + t2 := relative(t[i], t[j])(vl.x2, vl.y2) + if !inRange(t2) { + continue edges + } + out = append(out, linear(t[i], t[j])(t2)) + + default: + x, y, intersects := vl.line.Intersection(edge) + if !intersects { + continue edges + } + + // log.Println("Intersection -----------------------------") + t1 := relative(t[i], t[j])(x, y) + // log.Printf("relative pos: %f\n", t1) + if !inRange(t1) { + continue edges + } + + // log.Println("valid ***************") + + z := interpolate(t[j].Z, t[i].Z)(t1) + n := Vertex{x, y, z} + + if math.Signbit(s1) != math.Signbit(s2) { + // log.Println("\ton different sides") + // different sides -> vertex on edge. + out = append(out, n) + } else { // same side -> inside. Find peer + if len(out) > 0 { // we already have our peer + out = append(out, n) + return out + } + + for k := 0; k < 3; k++ { + if k == i { + continue + } + l := (k + 1) % 3 + other := NewPlane2D(t[k].X, t[k].Y, t[l].X, t[l].Y) + xo, yo, intersects := vl.line.Intersection(other) + if !intersects { + continue + } + t2 := relative(t[k], t[l])(xo, yo) + if !inRange(t2) { + continue + } + zo := interpolate(t[k].Z, t[l].Z)(t2) + + m := Vertex{xo, yo, zo} + + var xn, yn, xf, yf float64 + + // Which is nearer to current edge? + if math.Abs(s1) < math.Abs(s2) { + xn, yn = vl.x1, vl.y1 + xf, yf = vl.x2, vl.y2 + } else { + xn, yn = vl.x2, vl.y2 + xf, yf = vl.x1, vl.y1 + } + + if onPlane(other.Eval(xn, yn)) { + // triangle intersect in only point + // treat as no intersection + break edges + } + + pos := relative(n, m) + + tzn := pos(xn, yn) + tzf := pos(xf, yf) + + if !inRange(tzn) { + // if near is out of range far is, too. + return out + } + + lin := interpolate(n.Z, m.Z) + + if inRange(tzf) { + m.X = xf + m.Y = yf + m.Z = lin(tzf) + } // else m is clipping + + n.X = xn + n.Y = yn + n.Z = lin(tzn) + + out = append(out, n, m) + return out + } + } + } + } + + // supress single point touches. + if len(out) == 1 { + out = out[:0] + } + + return out +} + +// AsWKB returns a WKB representation of the given point cloud. +func (mpz MultiPointZ) AsWKB() []byte { + size := 1 + 4 + 4 + len(mpz)*(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(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 { + 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)) + } + + return buf.Bytes() +} + +func (mpz *MultiPointZ) 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.MultiPointZ: + return fmt.Errorf("unknown geometry type %x", geomType) + } + + var numPoints uint32 + if err := binary.Read(r, order, &numPoints); err != nil { + return err + } + + points := make(MultiPointZ, numPoints) + + for i := range points { + endian, err = r.ReadByte() + + 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) + } + + err = binary.Read(r, order, &geomType) + + switch { + case err != nil: + return err + case geomType != wkb.PointZ: + return fmt.Errorf("unknown geometry type %x", geomType) + } + + var x, y, z uint64 + if err = binary.Read(r, order, &x); err != nil { + return err + } + if err = binary.Read(r, order, &y); err != nil { + return err + } + if err = binary.Read(r, order, &z); err != nil { + return err + } + points[i] = Vertex{ + X: math.Float64frombits(x), + Y: math.Float64frombits(y), + Z: math.Float64frombits(z), + } + } + + *mpz = points + + return nil +} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/octree/areas.go --- a/pkg/octree/areas.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,88 +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 - -import ( - "runtime" - "sync" - - "gemma.intevation.de/gemma/pkg/common" -) - -func GenerateRandomVertices( - n int, - min, max Vertex, - eval func(float64, float64) (float64, bool), - callback func([]Vertex), -) { - var wg sync.WaitGroup - - jobs := make(chan int) - out := make(chan []Vertex) - done := make(chan struct{}) - - cpus := runtime.NumCPU() - - free := make(chan []Vertex, cpus) - - for i := 0; i < cpus; i++ { - wg.Add(1) - go func() { - defer wg.Done() - xRange := common.Random(min.X, max.X) - yRange := common.Random(min.Y, max.Y) - - for size := range jobs { - var vertices []Vertex - select { - case vertices = <-free: - default: - vertices = make([]Vertex, 0, 1000) - } - for len(vertices) < size { - x, y := xRange(), yRange() - if z, ok := eval(x, y); ok { - vertices = append(vertices, Vertex{X: x, Y: y, Z: z}) - } - } - out <- vertices - } - }() - } - - go func() { - defer close(done) - for vertices := range out { - callback(vertices) - select { - case free <- vertices[:0]: - default: - } - } - }() - - for remain := n; remain > 0; { - if remain > 1000 { - jobs <- 1000 - remain -= 1000 - } else { - jobs <- remain - remain = 0 - } - } - close(jobs) - wg.Wait() - close(out) - <-done -} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/octree/cache.go --- a/pkg/octree/cache.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,199 +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 - -import ( - "context" - "database/sql" - "sync" - "time" -) - -type ( - cacheKey struct { - date time.Time - bottleneck string - } - - cacheEntry struct { - checksum string - tree *STRTree - access time.Time - } - - // Cache holds Octrees for a defined amount of time in memory - // before they are released. - Cache struct { - sync.Mutex - entries map[cacheKey]*cacheEntry - } -) - -const ( - cleanupCacheSleep = 6 * time.Minute - maxCacheAge = 5 * time.Minute - maxCacheEntries = 4 -) - -const ( - directMeshSQL = ` -SELECT mesh_index FROM waterway.sounding_results -WHERE id = $1 -` - fetchMeshSQL = ` -SELECT mesh_checksum, mesh_index -FROM waterway.sounding_results -WHERE bottleneck_id = $1 AND date_info = $2::date - AND mesh_checksum IS NOT NULL AND mesh_index IS NOT NULL -` - checkMeshSQL = ` -SELECT CASE - WHEN mesh_checksum = $3 THEN NULL - ELSE mesh_index - END -FROM waterway.sounding_results -WHERE bottleneck_id = $1 AND date_info = $2::date - AND mesh_checksum IS NOT NULL AND mesh_index IS NOT NULL -` -) - -var cache = Cache{ - entries: map[cacheKey]*cacheEntry{}, -} - -func init() { - go cache.background() -} - -func (c *Cache) background() { - for { - time.Sleep(cleanupCacheSleep) - c.cleanup() - } -} - -func (c *Cache) cleanup() { - c.Lock() - defer c.Unlock() - good := time.Now().Add(-maxCacheAge) - for k, v := range c.entries { - if v.access.Before(good) { - delete(c.entries, k) - } - } -} - -// FromCache fetches an Octree from the global Octree cache. -func FromCache( - ctx context.Context, - conn *sql.Conn, - bottleneck string, date time.Time, -) (*STRTree, error) { - return cache.get(ctx, conn, bottleneck, date) -} - -// FetchOctreeDirectly loads an octree directly from the database. -func FetchMeshDirectly( - ctx context.Context, - tx *sql.Tx, - id int64, -) (*STRTree, error) { - var data []byte - err := tx.QueryRowContext(ctx, directMeshSQL, id).Scan(&data) - if err != nil { - return nil, err - } - tree := new(STRTree) - if err := tree.FromBytes(data); err != nil { - return nil, err - } - return tree, nil -} - -func (c *Cache) get( - ctx context.Context, - conn *sql.Conn, - bottleneck string, date time.Time, -) (*STRTree, error) { - c.Lock() - defer c.Unlock() - - key := cacheKey{date, bottleneck} - entry := c.entries[key] - - var data []byte - var checksum string - - if entry == nil { - // fetch from database - err := conn.QueryRowContext( - ctx, fetchMeshSQL, bottleneck, date).Scan(&checksum, &data) - switch { - case err == sql.ErrNoRows: - return nil, nil - case err != nil: - return nil, err - } - } else { - // check if we are not outdated. - err := conn.QueryRowContext( - ctx, checkMeshSQL, bottleneck, date, entry.checksum).Scan(&data) - switch { - case err == sql.ErrNoRows: - return nil, nil - case err != nil: - return nil, err - } - if data == nil { // we are still current - entry.access = time.Now() - return entry.tree, nil - } - } - - tree := new(STRTree) - - if err := tree.FromBytes(data); err != nil { - return nil, err - } - - now := time.Now() - - if entry != nil { - entry.tree = tree - entry.access = now - return tree, nil - } - - for len(c.entries) >= maxCacheEntries { - // Evict the entry that is accessed the longest time ago. - var oldestKey cacheKey - oldest := now - - for k, v := range c.entries { - if v.access.Before(oldest) { - oldest = v.access - oldestKey = k - } - } - delete(c.entries, oldestKey) - } - - c.entries[key] = &cacheEntry{ - checksum: checksum, - tree: tree, - access: now, - } - - return tree, nil -} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/octree/classbreaks.go --- a/pkg/octree/classbreaks.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,150 +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) 2019 by via donau -// – Österreichische Wasserstraßen-Gesellschaft mbH -// Software engineering by Intevation GmbH -// -// Author(s): -// * Sascha L. Teichmann - -package octree - -import ( - "context" - "database/sql" - "errors" - "math" - "sort" - "strconv" - "strings" -) - -const ( - selectClassBreaksSQL = ` -SELECT config_val FROM sys_admin.system_config -WHERE config_key = $1` -) - -func SampleDiffHeights(min, max, step float64) []float64 { - var heights []float64 - switch { - case min >= 0: // All values positive. - for v := 0.0; v <= max; v += step { - if v >= min { - heights = append(heights, v) - } - } - case max <= 0: // All values negative. - for v := 0.0; v >= min; v -= step { - if v <= max { - heights = append(heights, v) - } - } - default: // Positive and negative. - for v := step; v <= max; v += step { - 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 >= min; v -= step { - heights = append(heights, v) - } - } - return heights -} - -func ParseClassBreaks(config string) ([]float64, error) { - - parts := strings.Split(config, ",") - classes := make([]float64, 0, len(parts)) - for _, part := range parts { - if idx := strings.IndexRune(part, ':'); idx >= 0 { - part = part[:idx] - } - if part = strings.TrimSpace(part); part == "" { - continue - } - v, err := strconv.ParseFloat(part, 64) - if err != nil { - return nil, err - } - classes = append(classes, v) - } - - sort.Float64s(classes) - return classes, nil -} - -func LoadClassBreaks(ctx context.Context, tx *sql.Tx, key string) ([]float64, error) { - - var config sql.NullString - - err := tx.QueryRowContext(ctx, selectClassBreaksSQL, key).Scan(&config) - - switch { - case err == sql.ErrNoRows: - return nil, nil - case err != nil: - return nil, err - case !config.Valid: - return nil, errors.New("Invalid config string") - } - - return ParseClassBreaks(config.String) -} - -func round(v float64) float64 { - return math.Round(v*10000) / 10000 -} - -func ExtrapolateClassBreaks(cbs []float64, min, max float64) []float64 { - if min > max { - min, max = max, min - } - - n := make([]float64, len(cbs)) - copy(n, cbs) - sort.Float64s(n) - - for len(n) > 0 && n[0] < min { - n = n[1:] - } - - if len(n) == 0 { - return n - } - - for len(n) > 0 && n[len(n)-1] > max { - n = n[:len(n)-1] - } - - if len(n) == 0 { - return n - } - - for min < n[0] { - diff := n[1] - n[0] - if diff == 0 { - break - } - m := make([]float64, len(n)+1) - m[0] = round(n[0] - diff) - copy(m[1:], n) - n = m - } - - for max > n[len(n)-1] { - diff := n[len(n)-1] - n[len(n)-2] - if diff == 0 { - break - } - n = append(n, round(n[len(n)-1]+diff)) - } - - return n -} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/octree/hull.go --- a/pkg/octree/hull.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,81 +0,0 @@ -// 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 ec5afada70ec -r f4abfd0ee8ad pkg/octree/loader.go --- a/pkg/octree/loader.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +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 - -import ( - "bufio" - "bytes" - "compress/gzip" - "encoding/binary" - "log" -) - -func (s *STRTree) deserializeIndex(r *bufio.Reader) error { - var numIndex int32 - if err := binary.Read(r, binary.LittleEndian, &numIndex); err != nil { - return err - } - index := make([]int32, numIndex) - s.index = index - - var last int32 - for i := range index { - v, err := binary.ReadVarint(r) - if err != nil { - return err - } - value := int32(v) + last - index[i] = value - last = value - } - - return nil -} - -func (s *STRTree) deserializeBBoxes(r *bufio.Reader) error { - - var numBBoxes int32 - if err := binary.Read(r, binary.LittleEndian, &numBBoxes); err != nil { - return err - } - - bboxes := make([]Box2D, numBBoxes) - s.bboxes = bboxes - - var err error - - read := func(v *float64) { - if err == nil { - err = binary.Read(r, binary.LittleEndian, v) - } - } - - for i := range bboxes { - read(&bboxes[i].X1) - read(&bboxes[i].Y1) - read(&bboxes[i].X2) - read(&bboxes[i].Y2) - } - - return err -} - -func (s *STRTree) deserialize(r *bufio.Reader) error { - s.tin = new(Tin) - - if err := s.tin.Deserialize(r); err != nil { - return err - } - var numEntries uint8 - if err := binary.Read(r, binary.LittleEndian, &numEntries); err != nil { - return err - } - s.Entries = int(numEntries) - - if err := s.deserializeIndex(r); err != nil { - return err - } - - return s.deserializeBBoxes(r) -} - -func (s *STRTree) FromBytes(data []byte) error { - r, err := gzip.NewReader(bytes.NewReader(data)) - if err != nil { - return err - } - return s.deserialize(bufio.NewReader(r)) -} - -func (t *Tin) Deserialize(r *bufio.Reader) error { - - if err := binary.Read(r, binary.LittleEndian, &t.EPSG); err != nil { - return err - } - - log.Printf("info: EPSG: %d\n", t.EPSG) - - if err := t.Min.Read(r); err != nil { - return err - } - - if err := t.Max.Read(r); err != nil { - return err - } - - log.Printf("info: BBOX: [[%f, %f, %f], [%f, %f, %f]]\n", - t.Min.X, t.Min.Y, t.Min.Z, - t.Max.X, t.Max.Y, t.Max.Z) - - var numVertices uint32 - if err := binary.Read(r, binary.LittleEndian, &numVertices); err != nil { - return err - } - - log.Printf("info: vertices: %d\n", numVertices) - - vertices := make([]Vertex, numVertices) - t.Vertices = vertices - - for i := range vertices { - if err := vertices[i].Read(r); err != nil { - return err - } - } - - var numTriangles uint32 - if err := binary.Read(r, binary.LittleEndian, &numTriangles); err != nil { - return err - } - - log.Printf("info: triangles: %d\n", numTriangles) - - indices := make([]int32, 3*numTriangles) - triangles := make([][]int32, numTriangles) - t.Triangles = triangles - - var last int32 - - for i := range triangles { - tri := indices[:3] - indices = indices[3:] - triangles[i] = tri - for j := range tri { - v, err := binary.ReadVarint(r) - if err != nil { - return err - } - value := int32(v) + last - tri[j] = value - last = value - } - } - - return nil -} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/octree/node.go --- a/pkg/octree/node.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,49 +0,0 @@ -// 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 ec5afada70ec -r f4abfd0ee8ad pkg/octree/plane2d_test.go --- a/pkg/octree/plane2d_test.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +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 - -import ( - "math" - "testing" -) - -func TestIntersection(t *testing.T) { - - table := []struct { - a [4]float64 - b [4]float64 - intersects bool - x, y float64 - }{ - {[4]float64{-1, -1, 1, 1}, [4]float64{-1, 1, 1, -1}, true, 0, 0}, - {[4]float64{0, 0, 1, 1}, [4]float64{0, 1, 1, 0}, true, 0.5, 0.5}, - {[4]float64{0, 0, 1, 0}, [4]float64{0, 1, 1, 1}, false, 0, 0}, - } - - for _, e := range table { - p1 := NewPlane2D(e.a[0], e.a[1], e.a[2], e.a[3]) - p2 := NewPlane2D(e.b[0], e.b[1], e.b[2], e.b[3]) - x, y, intersects := p1.Intersection(p2) - if intersects != e.intersects { - t.Fatalf("Have %t want %t\n", intersects, e.intersects) - } - if e.intersects { - if math.Abs(e.x-x) > epsPlane || math.Abs(e.y-y) > epsPlane { - t.Fatalf("Have (%f, %f)t want (%f, %f)\n", x, y, e.x, e.y) - } - } - } -} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/octree/polygon.go --- a/pkg/octree/polygon.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,505 +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 - -import ( - "bytes" - "encoding/binary" - "fmt" - "log" - "math" - - "github.com/tidwall/rtree" - - "gemma.intevation.de/gemma/pkg/wkb" -) - -type ( - ring []float64 - - Polygon struct { - 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. - - // Check holes first: inside a hole means outside. - if len(p.rings) > 1 { - for _, hole := range p.rings[1:] { - if contains(hole, box.X1, box.Y1) { - return IntersectionOutSide - } - } - } - - // Check shell - if contains(p.rings[0], box.X1, box.Y1) { - 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. - pX, pY := 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 contains(hole, pX, pY) { - return IntersectionOutSide - } - } - } - - // Check shell - if contains(p.rings[0], pX, pY) { - return IntersectionInside - } - return IntersectionOutSide -} - -func (rng ring) length() int { - return len(rng) / 2 -} - -func (rng ring) point(i int) (float64, float64) { - i *= 2 - return rng[i], rng[i+1] -} - -type segments interface { - length() int - point(int) (float64, float64) -} - -func contains(s segments, pX, pY float64) bool { - - n := s.length() - if n < 3 { - return false - } - - sX, sY := s.point(0) - eX, eY := s.point(n - 1) - - const eps = 0.0000001 - - if math.Abs(sX-eX) > eps || math.Abs(sY-eY) > eps { - // It's not closed! - return false - } - - var inside bool - - for i := 1; i < n; i++ { - eX, eY := s.point(i) - if intersectsWithRaycast(pX, pY, sX, sY, eX, eY) { - inside = !inside - } - sX, sY = eX, eY - } - - return inside -} - -// 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(pX, pY, sX, sY, eX, eY float64) bool { - - // Always ensure that the the first point - // has a y coordinate that is less than the second point - if sY > eY { - // Switch the points if otherwise. - sX, sY, eX, eY = eX, eY, sX, sY - } - - // Move the point's y coordinate - // outside of the bounds of the testing region - // so we can start drawing a ray - for pY == sY || pY == eY { - pY = math.Nextafter(pY, math.Inf(1)) - } - - // If we are outside of the polygon, indicate so. - if pY < sY || pY > eY { - return false - } - - if sX > eX { - if pX > sX { - return false - } - if pX < eX { - return true - } - } else { - if pX > eX { - return false - } - if pX < sX { - return true - } - } - - raySlope := (pY - sY) / (pX - sX) - diagSlope := (eY - sY) / (eX - sX) - - return raySlope >= diagSlope -} - -func (p *Polygon) NumVertices(ring int) int { - if ring < 0 || ring >= len(p.rings) { - return 0 - } - return len(p.rings[ring]) / 2 -} - -func (p *Polygon) Vertices(ring int, fn func(float64, float64)) { - if ring < 0 || ring >= len(p.rings) { - return - } - rng := p.rings[ring] - for i := 0; i < len(rng); i += 2 { - fn(rng[i+0], rng[i+1]) - } -} - -func (p *Polygon) AsWKB() []byte { - - size := 1 + 4 + 4 - for _, r := range p.rings { - size += 4 + len(r)*8 - } - - buf := bytes.NewBuffer(make([]byte, 0, size)) - - binary.Write(buf, binary.LittleEndian, wkb.NDR) - binary.Write(buf, binary.LittleEndian, wkb.Polygon) - binary.Write(buf, binary.LittleEndian, uint32(len(p.rings))) - - for _, r := range p.rings { - binary.Write(buf, binary.LittleEndian, uint32(len(r)/2)) - for i := 0; i < len(r); i += 2 { - binary.Write(buf, binary.LittleEndian, math.Float64bits(r[i+0])) - binary.Write(buf, binary.LittleEndian, math.Float64bits(r[i+1])) - } - } - - return buf.Bytes() -} - -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 -} - -func (p *Polygon) FromLineStringZ(ls LineStringZ) { - r := make([]float64, 2*len(ls)) - var pos int - for i := range ls { - r[pos+0] = ls[i].X - r[pos+1] = ls[i].Y - pos += 2 - } - p.rings = []ring{r} -} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/octree/raster.go --- a/pkg/octree/raster.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,404 +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) 2019 by via donau -// – Österreichische Wasserstraßen-Gesellschaft mbH -// Software engineering by Intevation GmbH -// -// Author(s): -// * Sascha L. Teichmann - -package octree - -import ( - "log" - "math" - "runtime" - "sync" - "time" - - "gemma.intevation.de/gemma/pkg/common" - "gemma.intevation.de/gemma/pkg/wkb" - "github.com/fogleman/contourmap" -) - -type Raster struct { - BBox Box2D - CellSize float64 - XCells int - YCells int - Cells []float64 -} - -const noData = -math.MaxFloat64 - -func NewRaster(bbox Box2D, cellSize float64) *Raster { - - width, height := bbox.Size() - - log.Printf("info raster extent: %.2f / %.2f", width, height) - - xCells := int(math.Ceil(width / cellSize)) - yCells := int(math.Ceil(height / cellSize)) - - log.Printf("info raster size: %d / %d\n", xCells, yCells) - - size := (xCells + 2) * (yCells + 2) - cells := make([]float64, size) - for i := range cells { - cells[i] = noData - } - return &Raster{ - BBox: bbox, - CellSize: cellSize, - XCells: xCells, - YCells: yCells, - Cells: cells, - } -} - -func (r *Raster) Rasterize(eval func(float64, float64) (float64, bool)) { - var wg sync.WaitGroup - - rows := make(chan int) - - rasterRow := func() { - defer wg.Done() - quat := 0.25 * r.CellSize - for i := range rows { - pos := (i+1)*(r.XCells+2) + 1 - row := r.Cells[pos : pos+r.XCells] - py := r.BBox.Y1 + float64(i)*r.CellSize + r.CellSize/2 - px := r.BBox.X1 + r.CellSize/2 - y1 := py - quat - y2 := py + quat - for j := range row { - var n int - var sum float64 - - if v, ok := eval(px-quat, y1); ok { - sum = v - n = 1 - } - if v, ok := eval(px-quat, y2); ok { - sum += v - n++ - } - if v, ok := eval(px+quat, y1); ok { - sum += v - n++ - } - if v, ok := eval(px+quat, y2); ok { - sum += v - n++ - } - - if n > 0 { - row[j] = sum / float64(n) - } - px += r.CellSize - } - } - } - - for n := runtime.NumCPU(); n >= 1; n-- { - wg.Add(1) - go rasterRow() - } - - for i := 0; i < r.YCells; i++ { - rows <- i - } - close(rows) -} - -func (r *Raster) Diff(eval func(float64, float64) (float64, bool)) { - var wg sync.WaitGroup - - rows := make(chan int) - - rasterRow := func() { - defer wg.Done() - quat := 0.25 * r.CellSize - for i := range rows { - pos := (i+1)*(r.XCells+2) + 1 - row := r.Cells[pos : pos+r.XCells] - py := r.BBox.Y1 + float64(i)*r.CellSize + r.CellSize/2 - px := r.BBox.X1 + r.CellSize/2 - y1 := py - quat - y2 := py + quat - for j, old := range row { - // only diff where values - if old == noData { - px += r.CellSize - continue - } - var n int - var sum float64 - - if v, ok := eval(px-quat, y1); ok { - sum = v - n = 1 - } - if v, ok := eval(px-quat, y2); ok { - sum += v - n++ - } - if v, ok := eval(px+quat, y1); ok { - sum += v - n++ - } - if v, ok := eval(px+quat, y2); ok { - sum += v - n++ - } - - if n > 0 { - row[j] -= sum / float64(n) - } else { - row[j] = noData - } - - px += r.CellSize - } - } - } - - for n := runtime.NumCPU(); n >= 1; n-- { - wg.Add(1) - go rasterRow() - } - - for i := 0; i < r.YCells; i++ { - rows <- i - } - close(rows) -} - -func (r *Raster) ZExtent() (float64, float64, bool) { - min, max := math.MaxFloat64, -math.MaxFloat64 - for _, v := range r.Cells { - if v == noData { - continue - } - if v < min { - min = v - } - if v > max { - max = v - } - } - return min, max, min != math.MaxFloat64 -} - -func (r *Raster) Trace(heights []float64) []wkb.MultiPolygonGeom { - start := time.Now() - - tracer := contourmap.FromFloat64s(r.XCells+2, r.YCells+2, r.Cells) - - areas := make([]wkb.MultiPolygonGeom, len(heights)) - - reprojX := common.Linear(0.5, r.BBox.X1, 1.5, r.BBox.X1+r.CellSize) - reprojY := common.Linear(0.5, r.BBox.Y1, 1.5, r.BBox.Y1+r.CellSize) - - var wg sync.WaitGroup - - cnts := make(chan int) - - doContours := func() { - defer wg.Done() - for hIdx := range cnts { - if c := tracer.Contours(heights[hIdx]); len(c) > 0 { - areas[hIdx] = buildMultipolygon(c, reprojX, reprojY) - } - } - } - - for n := runtime.NumCPU(); n >= 1; n-- { - wg.Add(1) - go doContours() - } - - for i := range heights { - cnts <- i - } - close(cnts) - - wg.Wait() - log.Printf("info: Tracing areas took %v\n", time.Since(start)) - - return areas -} - -type contour []contourmap.Point - -type bboxNode struct { - box Box2D - cnt contour - children []*bboxNode -} - -func (cnt contour) contains(o contour) bool { - return contains(cnt, o[0].X, o[0].Y) || - contains(cnt, o[len(o)/2].X, o[len(o)/2].Y) -} - -func (cnt contour) length() int { - return len(cnt) -} - -func (cnt contour) point(i int) (float64, float64) { - return cnt[i].X, cnt[i].Y -} - -func (cnt contour) bbox() Box2D { - minX, minY := math.MaxFloat64, math.MaxFloat64 - maxX, maxY := -math.MaxFloat64, -math.MaxFloat64 - - for _, p := range cnt { - if p.X < minX { - minX = p.X - } - if p.X > maxX { - maxX = p.X - } - if p.Y < minY { - minY = p.Y - } - if p.Y > maxY { - maxY = p.Y - } - } - return Box2D{X1: minX, X2: maxX, Y1: minY, Y2: maxY} -} - -func (bn *bboxNode) insert(cnt contour, box Box2D) { - // check if children are inside new - var nr *bboxNode - - for i, r := range bn.children { - if r.box.Inside(box) && cnt.contains(r.cnt) { - if nr == nil { - nr = &bboxNode{box: box, cnt: cnt} - } - nr.children = append(nr.children, r) - bn.children[i] = nil - } - } - - // we have a new child - if nr != nil { - // compact the list - for i := len(bn.children) - 1; i >= 0; i-- { - if bn.children[i] == nil { - if i < len(bn.children)-1 { - copy(bn.children[i:], bn.children[i+1:]) - } - bn.children[len(bn.children)-1] = nil - bn.children = bn.children[:len(bn.children)-1] - } - } - bn.children = append(bn.children, nr) - return - } - - // check if new is inside an old - for _, r := range bn.children { - if box.Inside(r.box) && r.cnt.contains(cnt) { - r.insert(cnt, box) - return - } - } - - // its a new child node. - nr = &bboxNode{box: box, cnt: cnt} - bn.children = append(bn.children, nr) -} - -func (bn *bboxNode) insertRoot(cnt contour) { - bn.insert(cnt, cnt.bbox()) -} - -type bboxOutFunc func(contour, []contour) - -func (bn *bboxNode) generate(out bboxOutFunc) { - - var grands []*bboxNode - - holes := make([]contour, len(bn.children)) - - for i, ch := range bn.children { - holes[i] = ch.cnt - grands = append(grands, ch.children...) - } - out(bn.cnt, holes) - - // the grand children are new polygons. - for _, grand := range grands { - grand.generate(out) - } -} - -func (bn *bboxNode) generateRoot(out bboxOutFunc) { - for _, r := range bn.children { - r.generate(out) - } -} - -func buildMultipolygon( - cnts []contourmap.Contour, - reprojX, reprojY func(float64) float64, -) wkb.MultiPolygonGeom { - - var forest bboxNode - - for _, cnt := range cnts { - forest.insertRoot(contour(cnt)) - } - - //log.Printf("cnts: %d roots: %d\n", len(cnts), len(bf.roots)) - - var mp wkb.MultiPolygonGeom - - out := func(sh contour, hls []contour) { - - polygon := make(wkb.PolygonGeom, 1+len(hls)) - - // Handle shell - shell := make(wkb.LinearRingGeom, len(sh)) - for j, pt := range sh { - shell[j] = wkb.PointGeom{ - X: reprojX(pt.X), - Y: reprojY(pt.Y), - } - } - if shell.CCW() { - shell.Reverse() - } - polygon[0] = shell - - // handle holes - for i, hl := range hls { - hole := make(wkb.LinearRingGeom, len(hl)) - for j, pt := range hl { - hole[j] = wkb.PointGeom{ - X: reprojX(pt.X), - Y: reprojY(pt.Y), - } - } - if !hole.CCW() { - hole.Reverse() - } - polygon[1+i] = hole - } - - mp = append(mp, polygon) - } - - forest.generateRoot(out) - - return mp -} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/octree/strtree.go --- a/pkg/octree/strtree.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,486 +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 - -import ( - "bytes" - "compress/gzip" - "encoding/binary" - "io" - "log" - "math" - "sort" -) - -const STRTreeDefaultEntries = 8 - -type STRTree struct { - Entries int - tin *Tin - index []int32 - bboxes []Box2D -} - -// Vertical does a vertical cross cut from (x1, y1) to (x2, y2). -func (s *STRTree) Vertical(x1, y1, x2, y2 float64, fn func(*Triangle)) { - box := Box2D{ - X1: math.Min(x1, x2), - Y1: math.Min(y1, y2), - X2: math.Max(x1, x2), - Y2: math.Max(y1, y2), - } - - // out of bounding box - if box.X2 < s.tin.Min.X || s.tin.Max.X < box.X1 || - box.Y2 < s.tin.Min.Y || s.tin.Max.Y < box.Y1 { - return - } - - line := NewPlane2D(x1, y1, x2, y2) - - vertices := s.tin.Vertices - - stack := []int32{s.index[0]} - - for len(stack) > 0 { - top := stack[len(stack)-1] - stack = stack[:len(stack)-1] - - if top > 0 { // node - if b := s.bbox(top); b.Intersects(box) && b.IntersectsPlane(line) { - n := s.index[top+1] - stack = append(stack, s.index[top+2:top+2+n]...) - } - } else { // leaf - top = -top - 1 - if b := s.bbox(top); !b.Intersects(box) || !b.IntersectsPlane(line) { - continue - } - n := s.index[top+1] - for _, idx := range s.index[top+2 : top+2+n] { - ti := s.tin.Triangles[idx] - t := Triangle{ - vertices[ti[0]], - vertices[ti[1]], - vertices[ti[2]], - } - v0 := line.Eval(t[0].X, t[0].Y) - v1 := line.Eval(t[1].X, t[1].Y) - v2 := line.Eval(t[2].X, t[2].Y) - - if onPlane(v0) || onPlane(v1) || onPlane(v2) || - sides(sides(sides(0, v0), v1), v2) == 3 { - fn(&t) - } - } - } - } -} - -func (s *STRTree) Min() Vertex { return s.tin.Min } -func (s *STRTree) Max() Vertex { return s.tin.Max } -func (s *STRTree) EPSG() uint32 { return s.tin.EPSG } - -func (s *STRTree) Value(x, y float64) (float64, bool) { - - if len(s.index) == 0 { - return 0, false - } - - stack := []int32{s.index[0]} - - vertices := s.tin.Vertices - - for len(stack) > 0 { - top := stack[len(stack)-1] - stack = stack[:len(stack)-1] - - if top > 0 { // node - if s.bbox(top).Contains(x, y) { - n := s.index[top+1] - stack = append(stack, s.index[top+2:top+2+n]...) - } - } else { // leaf - top = -top - 1 - if !s.bbox(top).Contains(x, y) { - continue - } - n := s.index[top+1] - for _, idx := range s.index[top+2 : top+2+n] { - ti := s.tin.Triangles[idx] - t := Triangle{ - vertices[ti[0]], - vertices[ti[1]], - vertices[ti[2]], - } - if t.Contains(x, y) { - return t.Plane3D().Z(x, y), true - } - } - } - } - return 0, false -} - -func (s *STRTree) Build(t *Tin) { - - if s.Entries == 0 { - s.Entries = STRTreeDefaultEntries - } - - 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) BuildWithout(t *Tin, remove map[int32]struct{}) { - - if s.Entries == 0 { - s.Entries = STRTreeDefaultEntries - } - - s.tin = t - - all := make([]int32, 0, len(t.Triangles)-len(remove)) - - for i := 0; i < len(t.Triangles); i++ { - idx := int32(i) - if _, found := remove[idx]; !found { - all = append(all, idx) - } - } - - 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]} - - vertices := s.tin.Vertices - - 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 - n := s.index[top+1] - stack = append(stack, s.index[top+2:top+2+n]...) - } - } else { // leaf - top = -top - 1 - switch p.IntersectionBox2D(s.bbox(top)) { - case IntersectionInside: - // all triangles are inside polygon - case IntersectionOutSide: - // all triangles are outside polygon - n := s.index[top+1] - for _, idx := range s.index[top+2 : top+2+n] { - removed[idx] = struct{}{} - } - default: - n := s.index[top+1] - for _, idx := range s.index[top+2 : top+2+n] { - ti := s.tin.Triangles[idx] - t := Triangle{ - vertices[ti[0]], - vertices[ti[1]], - vertices[ti[2]], - } - if p.IntersectionWithTriangle(&t) != IntersectionInside { - removed[idx] = struct{}{} - } - } - } - } - } - - return removed -} - -func (s *STRTree) serializeIndex(w io.Writer) error { - - if err := binary.Write(w, binary.LittleEndian, int32(len(s.index))); err != nil { - return err - } - - var buf [binary.MaxVarintLen32]byte - - var last int32 - var written int - - for _, x := range s.index { - delta := x - last - n := binary.PutVarint(buf[:], int64(delta)) - for p := buf[:n]; len(p) > 0; p = p[n:] { - var err error - if n, err = w.Write(p); err != nil { - return err - } - written += n - } - last = x - } - - log.Printf("info: compressed index in bytes: %d %.2f (%d %.2f)\n", - written, - float64(written)/(1024*1024), - 4*len(s.index), - float64(4*len(s.index))/(1024*1024), - ) - - return nil -} - -func (s *STRTree) serializeBBoxes(w io.Writer) (rr error) { - - if err := binary.Write(w, binary.LittleEndian, int32(len(s.bboxes))); err != nil { - return err - } - - var err error - - write := func(v float64) { - if err == nil { - err = binary.Write(w, binary.LittleEndian, math.Float64bits(v)) - } - } - for _, box := range s.bboxes { - write(box.X1) - write(box.Y1) - write(box.X2) - write(box.Y2) - } - - return err -} - -func (s *STRTree) Bytes() ([]byte, error) { - - var buf bytes.Buffer - w, err := gzip.NewWriterLevel(&buf, gzip.BestSpeed) - if err != nil { - return nil, err - } - - if err := s.tin.serialize(w); err != nil { - return nil, err - } - - if err := binary.Write(w, binary.LittleEndian, uint8(s.Entries)); err != nil { - return nil, err - } - - if err := s.serializeIndex(w); err != nil { - return nil, err - } - - if err := s.serializeBBoxes(w); err != nil { - return nil, err - } - - if err := w.Close(); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -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 - n := s.index[top+1] - stack = append(stack, s.index[top+2:top+2+n]...) - } else { // leaf - top = -top - 1 - n := s.index[top+1] - for _, idx := range s.index[top+2 : top+2+n] { - tris[idx] = 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(s.Entries))) - S := int(math.Ceil(math.Sqrt(float64(P)))) - - slices := s.strSplit(items, S) - - leaves := s.strJoin( - slices, S, - func(i, j int32) bool { - ti := s.tin.Triangles[i] - tj := s.tin.Triangles[j] - return s.tin.Vertices[ti[0]].Y < s.tin.Vertices[tj[0]].Y - }, - s.allocLeaf, - ) - - return s.buildNodes(leaves) -} - -func (s *STRTree) buildNodes(items []int32) int32 { - - if len(items) <= s.Entries { - 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(s.Entries))) - S := int(math.Ceil(math.Sqrt(float64(P)))) - - slices := s.strSplit(items, S) - - nodes := s.strJoin( - slices, S, - func(i, j int32) bool { return s.bbox(i).Y1 < s.bbox(j).Y1 }, - s.allocNode, - ) - - return s.buildNodes(nodes) -} - -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) strSplit(items []int32, S int) [][]int32 { - sm := S * s.Entries - slices := make([][]int32, S) - for i := range slices { - var n int - if l := len(items); l < sm { - n = l - } else { - n = sm - } - slices[i] = items[:n] - items = items[n:] - } - return slices -} - -func (s *STRTree) strJoin( - slices [][]int32, S int, - less func(int32, int32) bool, - alloc func([]int32) int32, -) []int32 { - nodes := make([]int32, 0, S*S) - - for _, slice := range slices { - sort.Slice(slice, func(i, j int) bool { - return less(slice[i], slice[j]) - }) - - for len(slice) > 0 { - var n int - if l := len(slice); l >= s.Entries { - n = s.Entries - } else { - n = l - } - nodes = append(nodes, alloc(slice[:n])) - slice = slice[n:] - } - } - return 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 ec5afada70ec -r f4abfd0ee8ad pkg/octree/tin.go --- a/pkg/octree/tin.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,271 +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 - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - "log" - "math" - - "gemma.intevation.de/gemma/pkg/models" - "gemma.intevation.de/gemma/pkg/wkb" -) - -var ( - errNoByteSlice = errors.New("Not a byte slice") - errTooLessPoints = errors.New("Too less points") -) - -// Tin stores a mesh of triangles with common vertices. -type Tin struct { - // EPSG holds the projection. - EPSG uint32 - // Vertices are the shared vertices. - Vertices []Vertex - // Triangles are the triangles. - Triangles [][]int32 - - // Min is the lower left corner of the bbox. - Min Vertex - // Max is the upper right corner of the bbox. - Max Vertex -} - -func (t *Tin) Clip(polygon *Polygon) map[int32]struct{} { - var tree STRTree - tree.Build(t) - return tree.Clip(polygon) -} - -// FromWKB constructs the TIN from a WKB representation. -// Shared vertices are identified and referenced by the -// same index. -func (t *Tin) FromWKB(data []byte) error { - log.Printf("info: data length %d\n", len(data)) - - 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.TinZ: - return fmt.Errorf("unknown geometry type %x", geomType) - } - - var num uint32 - if err = binary.Read(r, order, &num); err != nil { - return err - } - - vertices := make([]Vertex, 0, 100000) - - var v Vertex - - v2i := make(map[Vertex]int32, 100000) - - var indexPool []int32 - - allocIndices := func() []int32 { - if len(indexPool) == 0 { - indexPool = make([]int32, 3*8*1024) - } - ids := indexPool[:3] - indexPool = indexPool[3:] - return ids - } - - var triangles [][]int32 - - min := Vertex{math.MaxFloat64, math.MaxFloat64, math.MaxFloat64} - max := Vertex{-math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64} - - for i := uint32(0); i < num; i++ { - - endian, err = r.ReadByte() - 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) - } - - err = binary.Read(r, order, &geomType) - switch { - case err != nil: - return err - case geomType != wkb.TriangleZ: - return fmt.Errorf("unknown geometry type %d", geomType) - } - - var rings uint32 - if err = binary.Read(r, order, &rings); err != nil { - return err - } - triangle := allocIndices() - - for ring := uint32(0); ring < rings; ring++ { - var npoints uint32 - if err = binary.Read(r, order, &npoints); err != nil { - return err - } - - if npoints < 3 { - return errTooLessPoints - } - - for p := uint32(0); p < npoints; p++ { - var x, y, z uint64 - for _, addr := range []*uint64{&x, &y, &z} { - if err = binary.Read(r, order, addr); err != nil { - return err - } - } - if p >= 3 || ring >= 1 { - // Don't store the forth point. - continue - } - // Do this conversion later to spare reflect calls - // and allocs in binary.Read. - v.X = math.Float64frombits(x) - v.Y = math.Float64frombits(y) - v.Z = math.Float64frombits(z) - idx, found := v2i[v] - if !found { - idx = int32(len(vertices)) - v2i[v] = idx - vertices = append(vertices, v) - min.Minimize(v) - max.Maximize(v) - } - triangle[p] = idx - } - } - triangles = append(triangles, triangle) - } - - log.Printf("info: bbox: [[%f, %f], [%f, %f]]\n", - min.X, min.Y, max.X, max.Y) - - *t = Tin{ - EPSG: models.WGS84, - Vertices: vertices, - Triangles: triangles, - Min: min, - Max: max, - } - - return nil -} - -// Scan implements the sql.Scanner interface. -func (t *Tin) Scan(raw interface{}) error { - if raw == nil { - return nil - } - data, ok := raw.([]byte) - if !ok { - return errNoByteSlice - } - return t.FromWKB(data) -} - -func (t *Tin) serialize(w io.Writer) error { - - if err := binary.Write(w, binary.LittleEndian, t.EPSG); err != nil { - return err - } - - if err := t.Min.Write(w); err != nil { - return err - } - if err := t.Max.Write(w); err != nil { - return err - } - - if err := binary.Write( - w, binary.LittleEndian, uint32(len(t.Vertices))); err != nil { - return err - } - - var err error - vwrite := func(v float64) { - if err == nil { - err = binary.Write(w, binary.LittleEndian, math.Float64bits(v)) - } - } - - for _, v := range t.Vertices { - vwrite(v.X) - vwrite(v.Y) - vwrite(v.Z) - } - - if err != nil { - return err - } - log.Printf("info: vertices %d (%d)\n", len(t.Vertices), len(t.Vertices)*3*8) - - if err := binary.Write( - w, binary.LittleEndian, uint32(len(t.Triangles))); err != nil { - return err - } - - var buf [binary.MaxVarintLen32]byte - var written int - var last int32 - for _, triangle := range t.Triangles { - for _, idx := range triangle { - value := idx - last - n := binary.PutVarint(buf[:], int64(value)) - for p := buf[:n]; len(p) > 0; p = p[n:] { - var err error - if n, err = w.Write(p); err != nil { - return err - } - written += n - } - last = idx - } - } - log.Printf("info: compressed tin indices in bytes: %d (%d)\n", - written, 3*4*len(t.Triangles)) - - return nil -} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/octree/triangulation.go --- a/pkg/octree/triangulation.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,318 +0,0 @@ -// 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" - "log" - "math" - - "gonum.org/v1/gonum/stat" -) - -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) EstimateTooLong() float64 { - - num := len(t.Triangles) / 3 - - lengths := make([]float64, 0, num) - - points := t.Points - -tris: - for i := 0; i < num; i++ { - idx := i * 3 - var max float64 - vs := t.Triangles[idx : idx+3] - for j, vj := range vs { - if t.Halfedges[idx+j] < 0 { - continue tris - } - k := (j + 1) % 3 - if l := points[vj].Distance2D(points[vs[k]]); l > max { - max = l - } - } - lengths = append(lengths, max) - } - - std := stat.StdDev(lengths, nil) - return 3.5 * std -} - -func (t *Triangulation) ConcaveHull(tooLong float64) (LineStringZ, map[int32]struct{}) { - - if tooLong <= 0 { - tooLong = t.EstimateTooLong() - } - - tooLong *= tooLong - - var candidates []int32 - - points := t.Points - - for i, num := 0, len(t.Triangles)/3; i < num; i++ { - idx := i * 3 - var max float64 - vs := t.Triangles[idx : idx+3] - for j, vj := range vs { - k := (j + 1) % 3 - if l := points[vj].SquaredDistance2D(points[vs[k]]); l > max { - max = l - } - } - if max > tooLong { - candidates = append(candidates, int32(i)) - } - } - - removed := map[int32]struct{}{} - - isBorder := func(n int32) bool { - n *= 3 - for i := int32(0); i < 3; i++ { - e := n + i - o := t.Halfedges[e] - if o < 0 { - return true - } - if _, found := removed[o/3]; found { - return true - } - } - return false - } - - var newCandidates []int32 - - log.Printf("info: candidates: %d\n", len(candidates)) - for len(candidates) > 0 { - - oldRemoved := len(removed) - - for _, i := range candidates { - - if isBorder(i) { - removed[i] = struct{}{} - } else { - newCandidates = append(newCandidates, i) - } - } - - if oldRemoved == len(removed) { - break - } - - candidates = newCandidates - newCandidates = newCandidates[:0] - } - - log.Printf("info: candidates left: %d\n", len(candidates)) - log.Printf("info: triangles: %d\n", len(t.Triangles)/3) - log.Printf("info: triangles to remove: %d\n", len(removed)) - - type edge struct { - a, b int32 - prev, next *edge - } - - isClosed := func(e *edge) bool { - for curr := e.next; curr != nil; curr = curr.next { - if curr == e { - return true - } - } - return false - } - - open := map[int32]*edge{} - var rings []*edge - - for i, num := int32(0), int32(len(t.Triangles)/3); i < num; i++ { - if _, found := removed[i]; found { - continue - } - n := i * 3 - for j := int32(0); j < 3; j++ { - e := n + j - f := t.Halfedges[e] - if f >= 0 { - if _, found := removed[f/3]; !found { - continue - } - } - a := t.Triangles[e] - b := t.Triangles[n+(j+1)%3] - - curr := &edge{a: a, b: b} - - if old := open[a]; old != nil { - delete(open, a) - if old.a == a { - old.prev = curr - curr.next = old - } else { - old.next = curr - curr.prev = old - } - - if isClosed(old) { - rings = append(rings, old) - } - } else { - open[a] = curr - } - - if old := open[b]; old != nil { - delete(open, b) - if old.b == b { - old.next = curr - curr.prev = old - } else { - old.prev = curr - curr.next = old - } - - if isClosed(old) { - rings = append(rings, old) - } - } else { - open[b] = curr - } - } - } - - if len(open) > 0 { - log.Printf("warn: open vertices left: %d\n", len(open)) - } - - if len(rings) == 0 { - log.Println("warn: no ring found") - return nil, removed - } - - curr := rings[0] - - polygon := LineStringZ{ - points[curr.a], - points[curr.b], - } - - for curr = curr.next; curr != rings[0]; curr = curr.next { - polygon = append(polygon, points[curr.b]) - } - - polygon = append(polygon, t.Points[rings[0].a]) - - log.Printf("length of boundary: %d\n", len(polygon)) - - return polygon, removed -} - -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 ec5afada70ec -r f4abfd0ee8ad pkg/octree/triangulator.go --- a/pkg/octree/triangulator.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,375 +0,0 @@ -// 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 ( - "errors" - "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 (tri *triangulator) Len() int { - return len(tri.points) -} - -func (tri *triangulator) Swap(i, j int) { - tri.ids[i], tri.ids[j] = tri.ids[j], tri.ids[i] -} - -func (tri *triangulator) Less(i, j int) bool { - d1 := tri.squaredDistances[tri.ids[i]] - d2 := tri.squaredDistances[tri.ids[j]] - if d1 != d2 { - return d1 < d2 - } - p1 := tri.points[tri.ids[i]] - p2 := tri.points[tri.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 errors.New("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 (tri *triangulator) hashKey(point Vertex) int { - d := point.Sub2D(tri.center) - return int(pseudoAngle(d.X, d.Y) * float64(len(tri.hash))) -} - -func (tri *triangulator) hashEdge(e *node) { - tri.hash[tri.hashKey(tri.points[e.i])] = e -} - -func (tri *triangulator) addTriangle(i0, i1, i2, a, b, c int32) int32 { - i := int32(tri.trianglesLen) - tri.triangles[i] = i0 - tri.triangles[i+1] = i1 - tri.triangles[i+2] = i2 - tri.link(i, a) - tri.link(i+1, b) - tri.link(i+2, c) - tri.trianglesLen += 3 - return i -} - -func (tri *triangulator) link(a, b int32) { - tri.halfedges[a] = b - if b >= 0 { - tri.halfedges[b] = a - } -} - -func (tri *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 := tri.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 := tri.triangles[ar] - pr := tri.triangles[a] - pl := tri.triangles[al] - p1 := tri.triangles[bl] - - illegal := inCircle(tri.points[p0], tri.points[pr], tri.points[pl], tri.points[p1]) - - if illegal { - tri.triangles[a] = p1 - tri.triangles[b] = p0 - - // edge swapped on the other side of the hull (rare) - // fix the halfedge reference - if tri.halfedges[bl] == -1 { - e := tri.hull - for { - if e.t == bl { - e.t = a - break - } - e = e.next - if e == tri.hull { - break - } - } - } - - tri.link(a, tri.halfedges[bl]) - tri.link(b, tri.halfedges[ar]) - tri.link(ar, bl) - - br := b0 + (b+1)%3 - - tri.legalize(a) - return tri.legalize(br) - } - - return ar -} - -func (tri *triangulator) convexHull() []Vertex { - var result []Vertex - e := tri.hull - for e != nil { - result = append(result, tri.points[e.i]) - e = e.prev - if e == tri.hull { - break - } - } - return result -} diff -r ec5afada70ec -r f4abfd0ee8ad pkg/octree/util.go --- a/pkg/octree/util.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,37 +0,0 @@ -// 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 ec5afada70ec -r f4abfd0ee8ad pkg/octree/vertex.go --- a/pkg/octree/vertex.go Tue Nov 05 13:01:37 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1229 +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 - -import ( - "bytes" - "encoding/binary" - "fmt" - "io" - "log" - "math" - "sort" - - "gemma.intevation.de/gemma/pkg/wkb" -) - -type ( - Point struct { - X float64 - Y float64 - } - - // Vertex is a 3D vertex. - Vertex struct { - X float64 - Y float64 - Z float64 - } - - // Triangle is a triangle consisting of three vertices. - Triangle [3]Vertex - - // Line is a line defined by first vertex on that line - // and the second being the direction. - Line [2]Vertex - - // Box is a 3D box. - Box [2]Vertex - - // MultiPointZ is a set of vertices. - MultiPointZ []Vertex - - // LineStringZ is a line string formed of vertices. - LineStringZ []Vertex - - // MultiLineStringZ is a set of line strings. - MultiLineStringZ []LineStringZ - - // Box2D is 2D area from (X1, Y1) to (X2, Y2). - Box2D struct { - X1 float64 - Y1 float64 - X2 float64 - Y2 float64 - } - - // Plane2D is a 2D plane (a line in 2D space). - Plane2D struct { - A float64 - 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 (a Box2D) Inside(b Box2D) bool { - return a.X1 >= b.X1 && a.X2 <= b.X2 && - a.Y1 >= b.Y1 && a.Y2 <= b.Y2 -} - -func (a Box2D) Size() (float64, float64) { - return a.X2 - a.X1, a.Y2 - a.Y1 -} - -func (a Box2D) Empty() bool { - const eps = 0.0000001 - return math.Abs(a.X2-a.X1) < eps && - math.Abs(a.Y2-a.Y1) < eps -} - -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 (p Plane3D) Eval(v Vertex) float64 { - return p.A*v.X + p.B*v.Y + p.C*v.Z + p.D -} - -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) -} - -func (v Vertex) Distance(w Vertex) float64 { - v = v.Sub(w) - 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) { - if w.X < v.X { - v.X = w.X - } - if w.Y < v.Y { - v.Y = w.Y - } - if w.Z < v.Z { - v.Z = w.Z - } -} - -// Maximize adjust this vertex v to hold the maximum -// values component-wise of v and w. -func (v *Vertex) Maximize(w Vertex) { - if w.X > v.X { - v.X = w.X - } - if w.Y > v.Y { - v.Y = w.Y - } - if w.Z > v.Z { - v.Z = w.Z - } -} - -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{ - v.X - w.X, - v.Y - w.Y, - v.Z - w.Z, - } -} - -// Add returns (v + w) component-wise. -func (v Vertex) Add(w Vertex) Vertex { - return Vertex{ - v.X + w.X, - v.Y + w.Y, - v.Z + w.Z, - } -} - -// Scale returns s*v component-wise. -func (v Vertex) Scale(s float64) Vertex { - return Vertex{ - s * v.X, - s * v.Y, - s * v.Z, - } -} - -// Interpolate returns a function that return s*b[1] + b[0] -// component-wise. -func (b Box) Interpolate() func(Vertex) Vertex { - v1, v2 := b[0], b[1] - v2 = v2.Sub(v1) - return func(s Vertex) Vertex { - return Vertex{ - v2.X*s.X + v1.X, - v2.Y*s.Y + v1.Y, - v2.Z*s.Z + v1.Z, - } - } -} - -func (b Box) HasX() bool { return math.Abs(b[0].X-b[1].X) > epsPlane } -func (b Box) HasY() bool { return math.Abs(b[0].Y-b[1].Y) > epsPlane } -func (b Box) HasZ() bool { return math.Abs(b[0].Z-b[1].Z) > epsPlane } - -// Less returns if one of v component is less than the -// corresponing component in w. -func (v Vertex) Less(w Vertex) bool { - return v.X < w.X || v.Y < w.Y || v.Z < w.Z -} - -// NewLine return a line of point/direction. -func NewLine(p1, p2 Vertex) Line { - return Line{ - p2.Sub(p1), - p1, - } -} - -// Eval returns the vertex for t*l[0] + l[1]. -func (l Line) Eval(t float64) Vertex { - return l[0].Scale(t).Add(l[1]) -} - -// IntersectHorizontal returns the intersection point -// for a given z value. -func (l Line) IntersectHorizontal(h float64) Vertex { - t := (h - l[1].Z) / l[0].Z - return l.Eval(t) -} - -func side(z, h float64) int { - switch { - case z < h: - return -1 - case z > h: - return +1 - } - 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].Sub2D(t[0]) - v1 := t[1].Sub2D(t[0]) - v2 := Vertex{X: x, Y: y}.Sub2D(t[0]) - - dot00 := v0.Dot2(v0) - dot01 := v0.Dot2(v1) - 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), -// one vertex (only touching an vertex) or -// two vertices (real intersection). -func (t *Triangle) IntersectHorizontal(h float64) LineStringZ { - sides := [3]int{ - side(t[0].Z, h), - side(t[1].Z, h), - side(t[2].Z, h), - } - - var points LineStringZ - - for i := 0; i < 3; i++ { - j := (i + 1) % 3 - si := sides[i] - sj := sides[j] - - switch { - case si == 0 && sj == 0: - // both on plane - points = append(points, t[i], t[j]) - case si == 0 && sj != 0: - // first on plane - points = append(points, t[i]) - case si != 0 && sj == 0: - // second on plane - points = append(points, t[j]) - case si == sj: - // both on same side - default: - // real intersection - v := NewLine(t[i], t[j]).IntersectHorizontal(h) - points = append(points, v) - } - } - - return points -} - -func linearScale(x1, y1, x2, y2 float64) func(Vertex) float64 { - dx := x2 - x1 - dy := y2 - y1 - - switch { - case dx != 0: - return func(v Vertex) float64 { - return (v.X - x1) / dx - } - case dy != 0: - return func(v Vertex) float64 { - return (v.Y - y1) / dy - } - default: - return func(Vertex) float64 { - return 0 - } - } -} - -func (ls LineStringZ) BBox() Box2D { - - min := Vertex{math.MaxFloat64, math.MaxFloat64, math.MaxFloat64} - max := Vertex{-math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64} - - for _, v := range ls { - min.Minimize(v) - max.Maximize(v) - } - - return Box2D{ - X1: min.X, - Y1: min.Y, - X2: max.X, - Y2: max.Y, - } -} - -func (ls LineStringZ) Area() float64 { - return polygonArea(ls) -} - -func (ls LineStringZ) Reverse() { - for i, j := 0, len(ls)-1; i < j; i, j = i+1, j-1 { - ls[i], ls[j] = ls[j], ls[i] - } -} - -func (ls LineStringZ) order(position func(Vertex) float64) { - type posVertex struct { - pos float64 - v Vertex - } - positions := make([]posVertex, len(ls)) - for i, v := range ls { - positions[i] = posVertex{position(v), v} - } - sort.Slice(positions, func(i, j int) bool { - return positions[i].pos < positions[j].pos - }) - for i := range positions { - ls[i] = positions[i].v - } -} - -// EpsEquals returns true if v and w are equal component-wise -// with the values within a epsilon range. -func (v Vertex) EpsEquals(w Vertex) bool { - const eps = 1e-5 - return math.Abs(v.X-w.X) < eps && - math.Abs(v.Y-w.Y) < eps && math.Abs(v.Z-w.Z) < eps -} - -// EpsEquals returns true if v and w are equal component-wise -// in the X/Y plane with the values within a epsilon range. -func (v Vertex) EpsEquals2D(w Vertex) bool { - const eps = 1e-5 - return math.Abs(v.X-w.X) < eps && - math.Abs(v.Y-w.Y) < eps -} - -// JoinOnLine joins the the elements of a given multi line string -// under the assumption that the segments are all on the line segment -// from (x1, y1) to (x2, y2). -func (mls MultiLineStringZ) JoinOnLine(x1, y1, x2, y2 float64) MultiLineStringZ { - - position := linearScale(x1, y1, x2, y2) - - type posLineString struct { - pos float64 - line LineStringZ - } - - positions := make([]posLineString, 0, len(mls)) - - for _, ls := range mls { - if len(ls) == 0 { - continue - } - // order the atoms first - ls.order(position) - positions = append(positions, posLineString{position(ls[0]), ls}) - } - - sort.Slice(positions, func(i, j int) bool { - return positions[i].pos < positions[j].pos - }) - - out := make(MultiLineStringZ, 0, len(positions)) - - var ignored int - - for i := range positions { - curr := positions[i].line - if l := len(out); l > 0 { - last := out[l-1] - - if last[len(last)-1].EpsEquals(curr[0]) { - out[l-1] = append(last[:len(last)-1], curr...) - continue - } - if position(last[len(last)-1]) > position(curr[0]) { - ignored++ - continue - } - } - out = append(out, curr) - } - - log.Printf("info: ignored parts: %d\n", ignored) - - return out -} - -// Write writes a Vertex as three 64 bit values in little endian order -// to the given writer. -func (v *Vertex) Write(w io.Writer) error { - if err := binary.Write( - w, binary.LittleEndian, math.Float64bits(v.X)); err != nil { - return err - } - if err := binary.Write( - w, binary.LittleEndian, math.Float64bits(v.Y)); err != nil { - return err - } - return binary.Write( - w, binary.LittleEndian, math.Float64bits(v.Z)) -} - -// Read fills this vertex with three 64 bit values stored as -// little endian from the given reader. -func (v *Vertex) Read(r io.Reader) error { - var buf [8]byte - b := buf[:] - if _, err := io.ReadFull(r, b); err != nil { - return nil - } - v.X = math.Float64frombits(binary.LittleEndian.Uint64(b)) - if _, err := io.ReadFull(r, b); err != nil { - return nil - } - v.Y = math.Float64frombits(binary.LittleEndian.Uint64(b)) - if _, err := io.ReadFull(r, b); err != nil { - return nil - } - v.Z = math.Float64frombits(binary.LittleEndian.Uint64(b)) - return nil -} - -// AsWKB returns the WKB representation of the given multi line string. -func (mls MultiLineStringZ) AsWKB() []byte { - - // pre-calculate size to avoid reallocations. - size := 1 + 4 + 4 - for _, ml := range mls { - size += 1 + 4 + 4 + len(ml)*3*8 - } - - buf := bytes.NewBuffer(make([]byte, 0, size)) - - 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, 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)) - binary.Write(buf, binary.LittleEndian, math.Float64bits(p.Y)) - binary.Write(buf, binary.LittleEndian, math.Float64bits(p.Z)) - } - } - - return buf.Bytes() -} - -// AsWKB2D returns the WKB representation of the given multi line string -// leaving the z component out. -func (mls MultiLineStringZ) AsWKB2D() []byte { - - // pre-calculate size to avoid reallocations. - size := 1 + 4 + 4 - for _, ml := range mls { - size += 1 + 4 + 4 + len(ml)*2*8 - } - - buf := bytes.NewBuffer(make([]byte, 0, size)) - - 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, 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)) - binary.Write(buf, binary.LittleEndian, math.Float64bits(p.Y)) - } - } - - return buf.Bytes() -} - -func (ls LineStringZ) CCW() bool { - var sum float64 - for i, v1 := range ls { - v2 := ls[(i+1)%len(ls)] - sum += (v2.X - v1.X) * (v2.Y + v1.Y) - } - return sum > 0 -} - -// Join joins two lines leaving the first of the second out. -func (ls LineStringZ) Join(other LineStringZ) LineStringZ { - nline := make(LineStringZ, len(ls)+len(other)-1) - copy(nline, ls) - copy(nline[len(ls):], other[1:]) - return nline -} - -// Merge merges line segments of a given multi line string -// by finding common start and end vertices. -func (mls MultiLineStringZ) Merge() MultiLineStringZ { - - var out MultiLineStringZ - - min := Vertex{math.MaxFloat64, math.MaxFloat64, math.MaxFloat64} - - for _, line := range mls { - for _, v := range line { - min.Minimize(v) - } - } - - type point struct { - x int64 - y int64 - } - - const precision = 1e7 - - quant := func(v Vertex) point { - return point{ - x: int64(math.Round((v.X - min.X) * precision)), - y: int64(math.Round((v.Y - min.Y) * precision)), - } - } - - heads := make(map[point]*[]LineStringZ) - - for _, line := range mls { - if len(line) < 2 { - out = append(out, line) - continue - } - head := quant(line[0]) - tail := quant(line[len(line)-1]) - if head == tail { // its already a ring - out = append(out, line) - continue - } - - if hs := heads[tail]; hs != nil { - l := len(*hs) - last := (*hs)[l-1] - if l == 1 { - delete(heads, tail) - } else { - (*hs)[l-1] = nil - *hs = (*hs)[:l-1] - } - line = line.Join(last) - - if head == quant(line[len(line)-1]) { // its a ring - out = append(out, line) - continue - } - } - - if hs := heads[head]; hs != nil { - *hs = append(*hs, line) - } else { - heads[head] = &[]LineStringZ{line} - } - } - -again: - for head, lines := range heads { - for i, line := range *lines { - tail := quant(line[len(line)-1]) - for hs := heads[tail]; hs != nil && len(*hs) > 0; hs = heads[tail] { - l := len(*hs) - last := (*hs)[l-1] - (*hs)[l-1] = nil - *hs = (*hs)[:l-1] - line = line.Join(last) - - if tail = quant(line[len(line)-1]); head == tail { // its a ring - out = append(out, line) - // remove from current lines - copy((*lines)[i:], (*lines)[i+1:]) - (*lines)[len(*lines)-1] = nil - *lines = (*lines)[:len(*lines)-1] - goto again - } - // overwrite in current lines - (*lines)[i] = line - } - } - } - - // rings := len(out) - - // The rest are open line strings. - for _, lines := range heads { - for _, line := range *lines { - out = append(out, line) - } - } - - // log.Printf("segments before/after merge: %d/%d (%d rings)\n", - // len(mls), len(out), rings) - - 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 { - return a.X1 - } - return a.X2 -} - -// Yi returns the i-th y component. -func (a Box2D) Yi(i int) float64 { - if i == 0 { - return a.Y1 - } - 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), - } -} - -func (a Box2D) Area() float64 { - return (a.X2 - a.X1) * (a.Y2 - a.Y1) -} - -// NewPlane2D creates a new Plane2D from two given points. -func NewPlane2D(x1, y1, x2, y2 float64) Plane2D { - b := x2 - x1 - a := -(y2 - y1) - - l := math.Sqrt(a*a + b*b) - a /= l - b /= l - - // a*x1 + b*y1 + c = 0 - c := -(a*x1 + b*y1) - return Plane2D{a, b, c} -} - -// Eval determines the distance of a given point -// from the plane. The sign of the result indicates -// the sideness. -func (p Plane2D) Eval(x, y float64) float64 { - return p.A*x + p.B*y + p.C -} - -const epsPlane = 1e-5 - -func sides(s int, x float64) int { - if math.Signbit(x) { - return s | 2 - } - return s | 1 -} - -// IntersectsPlane checks if a Box2D intersects with -// a given Plane2D. -func (a Box2D) IntersectsPlane(p Plane2D) bool { - var s int - for i := 0; i < 2; i++ { - x := a.Xi(i) - for j := 0; j < 2; j++ { - y := a.Yi(j) - v := p.Eval(x, y) - if math.Abs(v) < epsPlane { - //log.Printf("on line") - return true - } - if s = sides(s, v); s == 3 { - //log.Printf("... on both sides (djt)") - return true - } - } - } - //log.Printf("side: %d\n", s) - return false -} - -// Cross calculates the cross product of two vertices. -func (v Vertex) Cross(w Vertex) Vertex { - return Vertex{ - v.Y*w.Z - v.Z*w.Y, - v.Z*w.X - v.X*w.Z, - v.X*w.Y - v.Y*w.X, - } -} - -// Intersection calcultes the 2D intersection point of -// two Plane2Ds. If they do not intersect the returned -// bool flags is set to false. -func (p Plane2D) Intersection(o Plane2D) (float64, float64, bool) { - - u1 := Vertex{p.A, p.B, p.C} - u2 := Vertex{o.A, o.B, o.C} - - plane := u1.Cross(u2) - - if plane.Z == 0 { - return 0, 0, false - } - - return plane.X / plane.Z, plane.Y / plane.Z, true -} - -// VerticalLine is a 2D line segment. -type VerticalLine struct { - x1 float64 - y1 float64 - x2 float64 - y2 float64 - - line Plane2D -} - -// NewVerticalLine creates a new 2D line segment. -func NewVerticalLine(x1, y1, x2, y2 float64) *VerticalLine { - return &VerticalLine{ - x1: x1, - y1: y1, - x2: x2, - y2: y2, - line: NewPlane2D(x1, y1, x2, y2), - } -} - -func onPlane(x float64) bool { return math.Abs(x) < epsPlane } - -func relative(v1, v2 Vertex) func(x, y float64) float64 { - ls := linearScale(v1.X, v1.Y, v2.X, v2.Y) - return func(x, y float64) float64 { - return ls(Vertex{x, y, 0}) - } -} - -func interpolate(a, b float64) func(float64) float64 { - return func(x float64) float64 { - return (b-a)*x + a - } -} - -func linear(v1, v2 Vertex) func(t float64) Vertex { - return func(t float64) Vertex { - return Vertex{ - (v2.X-v1.X)*t + v1.X, - (v2.Y-v1.Y)*t + v1.Y, - (v2.Z-v1.Z)*t + v1.Z, - } - } -} - -func inRange(a float64) bool { return 0 <= a && a <= 1 } - -// Intersection intersects a line segment with a triangle. -func (vl *VerticalLine) Intersection(t *Triangle) LineStringZ { - - var out LineStringZ - - //defer func() { log.Printf("length out: %d\n", len(out)) }() - -edges: - for i := 0; i < 3 && len(out) < 2; i++ { - j := (i + 1) % 3 - edge := NewPlane2D(t[i].X, t[i].Y, t[j].X, t[j].Y) - - s1 := edge.Eval(vl.x1, vl.y1) - s2 := edge.Eval(vl.x2, vl.y2) - - o1 := onPlane(s1) - o2 := onPlane(s2) - - // log.Printf("s1, s2: %t %t (%f %f)\n", o1, o2, s1, s2) - - switch { - case o1 && o2: - pos := relative(t[i], t[j]) - t1 := pos(vl.x1, vl.y1) - t2 := pos(vl.x2, vl.y2) - - r1 := inRange(t1) - r2 := inRange(t2) - - switch { - case r1 && r2: - lin := linear(t[i], t[j]) - out = append(out, lin(t1), lin(t2)) - return out - - case !r1 && !r2: // the hole edge - out = append(out, t[i], t[j]) - return out - case !r1: - if t1 < 0 { - // below first -> clip by first - lin := linear(t[i], t[j]) - out = append(out, t[i], lin(t2)) - } else { - // above second -> clip by second - lin := linear(t[i], t[j]) - out = append(out, lin(t2), t[j]) - } - return out - case !r2: - if t2 < 0 { - // below first -> clip by first - lin := linear(t[i], t[j]) - out = append(out, t[i], lin(t1)) - } else { - // above second -> clip by second - lin := linear(t[i], t[j]) - out = append(out, lin(t1), t[j]) - } - return out - } - - case o1: - t1 := relative(t[i], t[j])(vl.x1, vl.y1) - if !inRange(t1) { - continue edges - } - out = append(out, linear(t[i], t[j])(t1)) - - case o2: - t2 := relative(t[i], t[j])(vl.x2, vl.y2) - if !inRange(t2) { - continue edges - } - out = append(out, linear(t[i], t[j])(t2)) - - default: - x, y, intersects := vl.line.Intersection(edge) - if !intersects { - continue edges - } - - // log.Println("Intersection -----------------------------") - t1 := relative(t[i], t[j])(x, y) - // log.Printf("relative pos: %f\n", t1) - if !inRange(t1) { - continue edges - } - - // log.Println("valid ***************") - - z := interpolate(t[j].Z, t[i].Z)(t1) - n := Vertex{x, y, z} - - if math.Signbit(s1) != math.Signbit(s2) { - // log.Println("\ton different sides") - // different sides -> vertex on edge. - out = append(out, n) - } else { // same side -> inside. Find peer - if len(out) > 0 { // we already have our peer - out = append(out, n) - return out - } - - for k := 0; k < 3; k++ { - if k == i { - continue - } - l := (k + 1) % 3 - other := NewPlane2D(t[k].X, t[k].Y, t[l].X, t[l].Y) - xo, yo, intersects := vl.line.Intersection(other) - if !intersects { - continue - } - t2 := relative(t[k], t[l])(xo, yo) - if !inRange(t2) { - continue - } - zo := interpolate(t[k].Z, t[l].Z)(t2) - - m := Vertex{xo, yo, zo} - - var xn, yn, xf, yf float64 - - // Which is nearer to current edge? - if math.Abs(s1) < math.Abs(s2) { - xn, yn = vl.x1, vl.y1 - xf, yf = vl.x2, vl.y2 - } else { - xn, yn = vl.x2, vl.y2 - xf, yf = vl.x1, vl.y1 - } - - if onPlane(other.Eval(xn, yn)) { - // triangle intersect in only point - // treat as no intersection - break edges - } - - pos := relative(n, m) - - tzn := pos(xn, yn) - tzf := pos(xf, yf) - - if !inRange(tzn) { - // if near is out of range far is, too. - return out - } - - lin := interpolate(n.Z, m.Z) - - if inRange(tzf) { - m.X = xf - m.Y = yf - m.Z = lin(tzf) - } // else m is clipping - - n.X = xn - n.Y = yn - n.Z = lin(tzn) - - out = append(out, n, m) - return out - } - } - } - } - - // supress single point touches. - if len(out) == 1 { - out = out[:0] - } - - return out -} - -// AsWKB returns a WKB representation of the given point cloud. -func (mpz MultiPointZ) AsWKB() []byte { - size := 1 + 4 + 4 + len(mpz)*(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(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 { - 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)) - } - - return buf.Bytes() -} - -func (mpz *MultiPointZ) 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.MultiPointZ: - return fmt.Errorf("unknown geometry type %x", geomType) - } - - var numPoints uint32 - if err := binary.Read(r, order, &numPoints); err != nil { - return err - } - - points := make(MultiPointZ, numPoints) - - for i := range points { - endian, err = r.ReadByte() - - 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) - } - - err = binary.Read(r, order, &geomType) - - switch { - case err != nil: - return err - case geomType != wkb.PointZ: - return fmt.Errorf("unknown geometry type %x", geomType) - } - - var x, y, z uint64 - if err = binary.Read(r, order, &x); err != nil { - return err - } - if err = binary.Read(r, order, &y); err != nil { - return err - } - if err = binary.Read(r, order, &z); err != nil { - return err - } - points[i] = Vertex{ - X: math.Float64frombits(x), - Y: math.Float64frombits(y), - Z: math.Float64frombits(z), - } - } - - *mpz = points - - return nil -}