From f275e366d7e055c8a658fbc4ba22ef059d7b60c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Tue, 8 May 2018 08:54:46 -0600 Subject: [PATCH 01/12] Initial commit of incr. Delaunay triangulation --- planar/triangulate/LICENSE | 30 + planar/triangulate/README.md | 27 + planar/triangulate/delaunay_test.go | 169 ++++ .../delaunaytriangulationbuilder.go | 214 ++++ .../delaunaytriangulationbuilder_test.go | 51 + .../incrementaldelaunaytriangulator.go | 122 +++ .../quadedge/lastfoundquadedgelocator.go | 63 ++ planar/triangulate/quadedge/quadedge.go | 404 ++++++++ .../triangulate/quadedge/quadedgelocator.go | 26 + .../quadedge/quadedgesubdivision.go | 944 ++++++++++++++++++ .../quadedge/quadedgesubdivision_test.go | 238 +++++ .../triangulate/quadedge/trianglepredicate.go | 307 ++++++ .../quadedge/trianglepredicate_test.go | 44 + planar/triangulate/quadedge/vertex.go | 352 +++++++ planar/triangulate/quadedge/vertex_test.go | 102 ++ 15 files changed, 3093 insertions(+) create mode 100644 planar/triangulate/LICENSE create mode 100644 planar/triangulate/README.md create mode 100644 planar/triangulate/delaunay_test.go create mode 100644 planar/triangulate/delaunaytriangulationbuilder.go create mode 100644 planar/triangulate/delaunaytriangulationbuilder_test.go create mode 100644 planar/triangulate/incrementaldelaunaytriangulator.go create mode 100644 planar/triangulate/quadedge/lastfoundquadedgelocator.go create mode 100644 planar/triangulate/quadedge/quadedge.go create mode 100644 planar/triangulate/quadedge/quadedgelocator.go create mode 100644 planar/triangulate/quadedge/quadedgesubdivision.go create mode 100644 planar/triangulate/quadedge/quadedgesubdivision_test.go create mode 100644 planar/triangulate/quadedge/trianglepredicate.go create mode 100644 planar/triangulate/quadedge/trianglepredicate_test.go create mode 100644 planar/triangulate/quadedge/vertex.go create mode 100644 planar/triangulate/quadedge/vertex_test.go diff --git a/planar/triangulate/LICENSE b/planar/triangulate/LICENSE new file mode 100644 index 00000000..1071fed9 --- /dev/null +++ b/planar/triangulate/LICENSE @@ -0,0 +1,30 @@ +Eclipse Distribution License - v 1.0 + +Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + Neither the name of the Eclipse Foundation, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/planar/triangulate/README.md b/planar/triangulate/README.md new file mode 100644 index 00000000..1035b2b1 --- /dev/null +++ b/planar/triangulate/README.md @@ -0,0 +1,27 @@ + +This is a lazy port of the JTS triangulation routines to Go. The goals when +porting are: + +* Provide a go-ish interface to the functionality available in JTS's + triangulate package. +* Stay true to JTS's implementation, but make the necessary changes to stay + go-ish and fit nicely within the geom package. +* Only implement what is needed as it is needed. +* When possible, keep modifications to the functionality segregated into + different directories. This will help minimize the cost of porting more of + JTS's functionality or porting JTS's future changes. + +To make porting easier, the original Java code will be kept in the source +files until the specific function has been ported at which time the Java +version of the function/method should be removed. + +The original code was taken from: + +https://github.com/locationtech/jts/tree/jts-1.15.0 +tag jts-1.15.0 (b7d7a00fef7106fe6609d6f53be1fe8046f3274c) + +To be consistent with JTS, this code is licensed under EDL v1.0 which is very +similar to BSD 3. The specific license information can be found in LICENSE.md. + +Send issues to: https://github.com/go-spatial/geom/ + diff --git a/planar/triangulate/delaunay_test.go b/planar/triangulate/delaunay_test.go new file mode 100644 index 00000000..1393c19a --- /dev/null +++ b/planar/triangulate/delaunay_test.go @@ -0,0 +1,169 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package triangulate + +import ( + "encoding/hex" + "strconv" + "testing" + + "github.com/go-spatial/geom/encoding/wkb" + "github.com/go-spatial/geom/encoding/wkt" +) + +/* +TestDelaunayTriangulation test cases were taken from JTS and converted to +GeoJSON. +*/ +func TestDelaunayTriangulation(t *testing.T) { + type tcase struct { + // provided for readability + inputWKT string + // this can be removed if/when geom has a WKT decoder. + inputWKB string + expectedEdges string + expectedTris string + } + + fn := func(t *testing.T, tc tcase) { + bytes, err := hex.DecodeString(tc.inputWKB) + if err != nil { + t.Fatalf("error decoding hex string: %v", err) + return + } + sites, err := wkb.DecodeBytes(bytes) + if err != nil { + t.Fatalf("error decoding WKB: %v", err) + return + } + + builder := new(DelaunayTriangulationBuilder) + builder.tolerance = 1e-6 + builder.SetSites(sites) + + edges := builder.getEdges() + edgesWKT, err := wkt.Encode(edges) + if err != nil { + t.Errorf("error, expected nil got %v", err) + return + } + if edgesWKT != tc.expectedEdges { + t.Errorf("error, expected %v got %v", tc.expectedEdges, edgesWKT) + return + } + + tris, err := builder.GetTriangles() + if err != nil { + t.Errorf("error, expected nil got %v", err) + return + } + trisWKT, err := wkt.Encode(tris) + if err != nil { + t.Errorf("error, expected nil got %v", err) + return + } + if trisWKT != tc.expectedTris { + t.Errorf("error, expected %v got %v", tc.expectedTris, trisWKT) + return + } + } + testcases := []tcase{ + { + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20)`, + inputWKB: `010500000003000000010200000002000000000000000000244000000000000034400000000000003440000000000000344001020000000200000000000000000024400000000000002440000000000000244000000000000034400102000000020000000000000000002440000000000000244000000000000034400000000000003440`, + expectedEdges: `MULTILINESTRING ((10 20,20 20),(10 10,10 20),(10 10,20 20))`, + // the ordering and values are the same as JTS, but reformatted as a + // MULTIPOLYGON + expectedTris: `MULTIPOLYGON (((10 20,10 10,20 20,10 20)))`, + }, + { + inputWKT: "MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)", + inputWKB: "010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440", + // This is not the same ordering as JTS, but the segments are the same. + // This ordering appears to be correct. JTS uses these as primary edges: + // (10 10,0 20),(10 10,0 10) + // Where according to the rules in getPrimary, it appears that these + // should be the primary edges: + // (0 20,10 10),(0 10,10 10) + // Since this appears to be more correct, I will not try to make them + // consistent. + expectedEdges: "MULTILINESTRING ((10 20,20 20),(0 20,10 20),(0 10,0 20),(0 0,0 10),(0 0,10 0),(10 0,20 0),(20 0,20 10),(20 10,20 20),(10 20,20 10),(10 10,20 10),(10 10,10 20),(0 20,10 10),(0 10,10 10),(10 0,10 10),(0 10,10 0),(10 10,20 0))", + // the ordering and values are the same as JTS, but reformatted as a + // MULTIPOLYGON + expectedTris: "MULTIPOLYGON (((0 20,0 10,10 10,0 20)),((0 20,10 10,10 20,0 20)),((10 20,10 10,20 10,10 20)),((10 20,20 10,20 20,10 20)),((10 0,20 0,10 10,10 0)),((10 0,10 10,0 10,10 0)),((10 0,0 10,0 0,10 0)),((10 10,20 0,20 10,10 10)))", + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +/* + public void testRandom() + throws ParseException + { + String wkt = "MULTIPOINT ((50 40), (140 70), (80 100), (130 140), (30 150), (70 180), (190 110), (120 20))"; + String expected = "MULTILINESTRING ((70 180, 190 110), (30 150, 70 180), (30 150, 50 40), (50 40, 120 20), (190 110, 120 20), (120 20, 140 70), (190 110, 140 70), (130 140, 140 70), (130 140, 190 110), (70 180, 130 140), (80 100, 130 140), (70 180, 80 100), (30 150, 80 100), (50 40, 80 100), (80 100, 120 20), (80 100, 140 70))"; + runDelaunayEdges(wkt, expected); + String expectedTri = "GEOMETRYCOLLECTION (POLYGON ((30 150, 50 40, 80 100, 30 150)), POLYGON ((30 150, 80 100, 70 180, 30 150)), POLYGON ((70 180, 80 100, 130 140, 70 180)), POLYGON ((70 180, 130 140, 190 110, 70 180)), POLYGON ((190 110, 130 140, 140 70, 190 110)), POLYGON ((190 110, 140 70, 120 20, 190 110)), POLYGON ((120 20, 140 70, 80 100, 120 20)), POLYGON ((120 20, 80 100, 50 40, 120 20)), POLYGON ((80 100, 140 70, 130 140, 80 100)))"; + runDelaunay(wkt, true, expectedTri); + } + + public void testCircle() + throws ParseException + { + String wkt = "POLYGON ((42 30, 41.96 29.61, 41.85 29.23, 41.66 28.89, 41.41 28.59, 41.11 28.34, 40.77 28.15, 40.39 28.04, 40 28, 39.61 28.04, 39.23 28.15, 38.89 28.34, 38.59 28.59, 38.34 28.89, 38.15 29.23, 38.04 29.61, 38 30, 38.04 30.39, 38.15 30.77, 38.34 31.11, 38.59 31.41, 38.89 31.66, 39.23 31.85, 39.61 31.96, 40 32, 40.39 31.96, 40.77 31.85, 41.11 31.66, 41.41 31.41, 41.66 31.11, 41.85 30.77, 41.96 30.39, 42 30))"; + String expected = "MULTILINESTRING ((41.66 31.11, 41.85 30.77), (41.41 31.41, 41.66 31.11), (41.11 31.66, 41.41 31.41), (40.77 31.85, 41.11 31.66), (40.39 31.96, 40.77 31.85), (40 32, 40.39 31.96), (39.61 31.96, 40 32), (39.23 31.85, 39.61 31.96), (38.89 31.66, 39.23 31.85), (38.59 31.41, 38.89 31.66), (38.34 31.11, 38.59 31.41), (38.15 30.77, 38.34 31.11), (38.04 30.39, 38.15 30.77), (38 30, 38.04 30.39), (38 30, 38.04 29.61), (38.04 29.61, 38.15 29.23), (38.15 29.23, 38.34 28.89), (38.34 28.89, 38.59 28.59), (38.59 28.59, 38.89 28.34), (38.89 28.34, 39.23 28.15), (39.23 28.15, 39.61 28.04), (39.61 28.04, 40 28), (40 28, 40.39 28.04), (40.39 28.04, 40.77 28.15), (40.77 28.15, 41.11 28.34), (41.11 28.34, 41.41 28.59), (41.41 28.59, 41.66 28.89), (41.66 28.89, 41.85 29.23), (41.85 29.23, 41.96 29.61), (41.96 29.61, 42 30), (41.96 30.39, 42 30), (41.85 30.77, 41.96 30.39), (41.66 31.11, 41.96 30.39), (41.41 31.41, 41.96 30.39), (41.41 28.59, 41.96 30.39), (41.41 28.59, 41.41 31.41), (38.59 28.59, 41.41 28.59), (38.59 28.59, 41.41 31.41), (38.59 28.59, 38.59 31.41), (38.59 31.41, 41.41 31.41), (38.59 31.41, 39.61 31.96), (39.61 31.96, 41.41 31.41), (39.61 31.96, 40.39 31.96), (40.39 31.96, 41.41 31.41), (40.39 31.96, 41.11 31.66), (38.04 30.39, 38.59 28.59), (38.04 30.39, 38.59 31.41), (38.04 30.39, 38.34 31.11), (38.04 29.61, 38.59 28.59), (38.04 29.61, 38.04 30.39), (39.61 28.04, 41.41 28.59), (38.59 28.59, 39.61 28.04), (38.89 28.34, 39.61 28.04), (40.39 28.04, 41.41 28.59), (39.61 28.04, 40.39 28.04), (41.96 29.61, 41.96 30.39), (41.41 28.59, 41.96 29.61), (41.66 28.89, 41.96 29.61), (40.39 28.04, 41.11 28.34), (38.04 29.61, 38.34 28.89), (38.89 31.66, 39.61 31.96))"; + runDelaunayEdges(wkt, expected); + } + + public void testPolygonWithChevronHoles() + throws ParseException + { + String wkt = "POLYGON ((0 0, 0 200, 180 200, 180 0, 0 0), (20 180, 160 180, 160 20, 152.625 146.75, 20 180), (30 160, 150 30, 70 90, 30 160))"; + String expected = "MULTILINESTRING ((0 200, 180 200), (0 0, 0 200), (0 0, 180 0), (180 200, 180 0), (152.625 146.75, 180 0), (152.625 146.75, 180 200), (152.625 146.75, 160 180), (160 180, 180 200), (0 200, 160 180), (20 180, 160 180), (0 200, 20 180), (20 180, 30 160), (30 160, 0 200), (0 0, 30 160), (30 160, 70 90), (0 0, 70 90), (70 90, 150 30), (150 30, 0 0), (150 30, 160 20), (0 0, 160 20), (160 20, 180 0), (152.625 146.75, 160 20), (150 30, 152.625 146.75), (70 90, 152.625 146.75), (30 160, 152.625 146.75), (30 160, 160 180))"; + runDelaunayEdges(wkt, expected); + } + + static final double COMPARISON_TOLERANCE = 1.0e-7; + + void runDelaunayEdges(String sitesWKT, String expectedWKT) + throws ParseException + { + runDelaunay(sitesWKT, false, expectedWKT); + } + + void runDelaunay(String sitesWKT, boolean computeTriangles, String expectedWKT) + throws ParseException + { + Geometry sites = reader.read(sitesWKT); + DelaunayTriangulationBuilder builder = new DelaunayTriangulationBuilder(); + builder.setSites(sites); + + Geometry result = null; + if (computeTriangles) { + result = builder.getTriangles(geomFact); + } + else { + result = builder.getEdges(geomFact); + } + //System.out.println(result); + + Geometry expected = reader.read(expectedWKT); + result.normalize(); + expected.normalize(); + assertTrue(expected.equalsExact(result, COMPARISON_TOLERANCE)); + } +}*/ diff --git a/planar/triangulate/delaunaytriangulationbuilder.go b/planar/triangulate/delaunaytriangulationbuilder.go new file mode 100644 index 00000000..01b3e6f4 --- /dev/null +++ b/planar/triangulate/delaunaytriangulationbuilder.go @@ -0,0 +1,214 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package triangulate + +import ( + "sort" + + "github.com/go-spatial/geom" + "github.com/go-spatial/geom/cmp" + "github.com/go-spatial/geom/planar/triangulate/quadedge" +) + +/* +A utility class which creates Delaunay Triangulations +from collections of points and extract the resulting +triangulation edges or triangles as geometries. + +Author Martin Davis +Ported to Go by Jason R. Surratt +*/ +type DelaunayTriangulationBuilder struct { + siteCoords []quadedge.Vertex + tolerance float64 + subdiv *quadedge.QuadEdgeSubdivision +} + +type PointByXY []quadedge.Vertex + +func (xy PointByXY) Less(i, j int) bool { return cmp.XYLessPoint(xy[i], xy[j]) } +func (xy PointByXY) Swap(i, j int) { xy[i], xy[j] = xy[j], xy[i] } +func (xy PointByXY) Len() int { return len(xy) } + +/* +extractUniqueCoordinates extracts the unique points from the given Geometry. + +geom - the geometry to extract from +Returns a List of the unique Coordinates +*/ +func (dtb *DelaunayTriangulationBuilder) extractUniqueCoordinates(g geom.Geometry) ([]quadedge.Vertex, error) { + if g == nil { + return []quadedge.Vertex{}, nil + } + + coords, err := geom.GetCoordinates(g) + if err != nil { + return nil, err + } + + vertices := make([]quadedge.Vertex, len(coords)) + for i := range coords { + vertices[i] = quadedge.Vertex{coords[i][0], coords[i][1]} + } + + return dtb.unique(vertices), nil +} + +func (dtb *DelaunayTriangulationBuilder) unique(points []quadedge.Vertex) []quadedge.Vertex { + sort.Sort(PointByXY(points)) + + // we can use a slice trick to avoid copying the array again. Maybe better + // than two index variables... + uniqued := points[:0] + for i := 0; i < len(points); i++ { + if i == 0 || cmp.PointEqual(points[i], points[i-1]) == false { + uniqued = append(uniqued, points[i]) + } + } + + return uniqued +} + +/** + * Converts all {@link Coordinate}s in a collection to {@link Vertex}es. + * @param coords the coordinates to convert + * @return a List of Vertex objects +public static List toVertices(Collection coords) +{ + List verts = new ArrayList(); + for (Iterator i = coords.iterator(); i.hasNext(); ) { + Coordinate coord = (Coordinate) i.next(); + verts.add(new Vertex(coord)); + } + return verts; +} +*/ + +/** + * Computes the {@link Envelope} of a collection of {@link Coordinate}s. + * + * @param coords a List of Coordinates + * @return the envelope of the set of coordinates +public static Envelope envelope(Collection coords) +{ + Envelope env = new Envelope(); + for (Iterator i = coords.iterator(); i.hasNext(); ) { + Coordinate coord = (Coordinate) i.next(); + env.expandToInclude(coord); + } + return env; +} +*/ + +/* +Sets the sites (vertices) which will be triangulated. +All vertices of the given geometry will be used as sites. + +geom - the geometry from which the sites will be extracted. +*/ +func (dtb *DelaunayTriangulationBuilder) SetSites(g geom.Geometry) error { + // remove any duplicate points (they will cause the triangulation to fail) + c, err := dtb.extractUniqueCoordinates(g) + dtb.siteCoords = c + return err +} + +/** + * Sets the sites (vertices) which will be triangulated + * from a collection of {@link Coordinate}s. + * + * @param coords a collection of Coordinates. +public void setSites(Collection coords) +{ + // remove any duplicate points (they will cause the triangulation to fail) + siteCoords = unique(CoordinateArrays.toCoordinateArray(coords)); +} +*/ + +/** + * Sets the snapping tolerance which will be used + * to improved the robustness of the triangulation computation. + * A tolerance of 0.0 specifies that no snapping will take place. + * + * @param tolerance the tolerance distance to use +public void setTolerance(double tolerance) +{ + this.tolerance = tolerance; +} +*/ + +/* +create will create the triangulation. + +return true on success, false on failure. +*/ +func (dtb *DelaunayTriangulationBuilder) create() bool { + if dtb.subdiv != nil { + return true + } + if len(dtb.siteCoords) == 0 { + return false + } + + var siteEnv *geom.Extent + for _, v := range dtb.siteCoords { + if siteEnv == nil { + siteEnv = geom.NewExtent(v) + } + siteEnv.AddGeometry(v) + } + + dtb.subdiv = quadedge.NewQuadEdgeSubdivision(*siteEnv, dtb.tolerance) + triangulator := new(IncrementalDelaunayTriangulator) + triangulator.subdiv = dtb.subdiv + triangulator.InsertSites(dtb.siteCoords) + + return true +} + +/** + * Gets the {@link QuadEdgeSubdivision} which models the computed triangulation. + * + * @return the subdivision containing the triangulation +public QuadEdgeSubdivision getSubdivision() +{ + create(); + return subdiv; +} +*/ + +/* +GetEdges gets the edges of the computed triangulation as a MultiLineString. + +returns the edges of the triangulation +*/ +func (dtb *DelaunayTriangulationBuilder) getEdges() geom.MultiLineString { + if !dtb.create() { + return geom.MultiLineString{} + } + return dtb.subdiv.GetEdgesAsMultiLineString() +} + +/* +GetTriangles Gets the faces of the computed triangulation as a +GeometryCollection Polygons. + +Unlike JTS, this method returns a MultiPolygon. I found not all viewers like +displaying collections. -JRS +*/ +func (dtb *DelaunayTriangulationBuilder) GetTriangles() (geom.MultiPolygon, error) { + if !dtb.create() { + return geom.MultiPolygon{}, nil + } + return dtb.subdiv.GetTriangles() +} diff --git a/planar/triangulate/delaunaytriangulationbuilder_test.go b/planar/triangulate/delaunaytriangulationbuilder_test.go new file mode 100644 index 00000000..8a70465e --- /dev/null +++ b/planar/triangulate/delaunaytriangulationbuilder_test.go @@ -0,0 +1,51 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package triangulate + +import ( + "reflect" + "strconv" + "testing" + + "github.com/go-spatial/geom/planar/triangulate/quadedge" +) + +func TestUnique(t *testing.T) { + type tcase struct { + points []quadedge.Vertex + expected []quadedge.Vertex + } + + fn := func(t *testing.T, tc tcase) { + var uut DelaunayTriangulationBuilder + result := uut.unique(tc.points) + if reflect.DeepEqual(result, tc.expected) == false { + t.Errorf("error, expected %v got %v", tc.expected, result) + } + } + testcases := []tcase{ + { + points: []quadedge.Vertex{quadedge.Vertex{0, 1}, quadedge.Vertex{0, 1}}, + expected: []quadedge.Vertex{quadedge.Vertex{0, 1}}, + }, + { + points: []quadedge.Vertex{quadedge.Vertex{0, 1}, quadedge.Vertex{0, 1}, quadedge.Vertex{1, 0}}, + expected: []quadedge.Vertex{quadedge.Vertex{0, 1}, quadedge.Vertex{1, 0}}, + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} diff --git a/planar/triangulate/incrementaldelaunaytriangulator.go b/planar/triangulate/incrementaldelaunaytriangulator.go new file mode 100644 index 00000000..0ed54e79 --- /dev/null +++ b/planar/triangulate/incrementaldelaunaytriangulator.go @@ -0,0 +1,122 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package triangulate + +import ( + "github.com/go-spatial/geom/planar/triangulate/quadedge" +) + +/* +Computes a Delaunay Triangulation of a set of {@link Vertex}es, using an +incremental insertion algorithm. + +Author Martin Davis +Ported to Go by Jason R. Surratt +*/ +type IncrementalDelaunayTriangulator struct { + subdiv *quadedge.QuadEdgeSubdivision +} + +/* +InsertSites inserts all sites in a collection. The inserted vertices MUST be +unique up to the provided tolerance value. (i.e. no two vertices should be +closer than the provided tolerance value). They do not have to be rounded +to the tolerance grid, however. + +vertices - a Collection of Vertex +Returns ErrLocateFailure if the location algorithm fails to converge in a +reasonable number of iterations. If this occurs the triangulator is left in +an unknown state with 0 or more of the vertices inserted. +*/ +func (idt *IncrementalDelaunayTriangulator) InsertSites(vertices []quadedge.Vertex) error { + for _, v := range vertices { + _, err := idt.InsertSite(v) + if err != nil { + return err + } + } + + return nil +} + +/* +Inserts a new point into a subdivision representing a Delaunay +triangulation, and fixes the affected edges so that the result is still a +Delaunay triangulation. + +Returns a tuple with a quadedge containing the inserted vertex and an error +code. If there is an error then the vertex will not be inserted and the +triangulator will still be in a consistent state. +*/ +func (idt *IncrementalDelaunayTriangulator) InsertSite(v quadedge.Vertex) (*quadedge.QuadEdge, error) { + + // log.Printf("Inserting: %v", v); + // log.Printf("Initial: %v", idt.subdiv.DebugDumpEdges()) + /* + This code is based on Guibas and Stolfi (1985), with minor modifications + and a bug fix from Dani Lischinski (Graphic Gems 1993). (The modification + I believe is the test for the inserted site falling exactly on an + existing edge. Without this test zero-width triangles have been observed + to be created) + */ + e, err := idt.subdiv.Locate(v) + // log.Printf("e: %v -> %v", e.Orig(), e.Dest()) + if err != nil { + return nil, err + } + + if idt.subdiv.IsVertexOfEdge(e, v) { + // log.Printf("On Vertex"); + // point is already in subdivision. + return e, nil + } + if idt.subdiv.IsOnEdge(e, v) { + // log.Printf("On Edge"); + // the point lies exactly on an edge, so delete the edge + // (it will be replaced by a pair of edges which have the point as a vertex) + e = e.OPrev() + idt.subdiv.Delete(e.ONext()) + } + + /* + Connect the new point to the vertices of the containing triangle + (or quadrilateral, if the new point fell on an existing edge.) + */ + base := quadedge.MakeEdge(e.Orig(), v) + // log.Printf("Made Edge: %v -> %v", base.Orig(), base.Dest()); + quadedge.Splice(base, e) + startEdge := base + done := false + for !done { + base = idt.subdiv.Connect(e, base.Sym()) + e = base.OPrev() + if e.LNext() == startEdge { + done = true + } + } + + // Examine suspect edges to ensure that the Delaunay condition + // is satisfied. + for { + t := e.OPrev() + if t.Dest().RightOf(*e) && v.IsInCircle(e.Orig(), t.Dest(), e.Dest()) { + quadedge.Swap(e) + e = e.OPrev() + } else if e.ONext() == startEdge { + // log.Printf("New State: %v", idt.subdiv.DebugDumpEdges()) + return base, nil // no more suspect edges. + } else { + e = e.ONext().LPrev() + } + } +} diff --git a/planar/triangulate/quadedge/lastfoundquadedgelocator.go b/planar/triangulate/quadedge/lastfoundquadedgelocator.go new file mode 100644 index 00000000..76327cdc --- /dev/null +++ b/planar/triangulate/quadedge/lastfoundquadedgelocator.go @@ -0,0 +1,63 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package quadedge + +/* +LastFoundQuadEdgeLocator Locates QuadEdges in a QuadEdgeSubdivision, +optimizing the search by starting in the locality of the last edge found. + +Implements the QuadEdgeLocator interface. + +Author Martin Davis +Ported to Go by Jason R. Surratt +*/ +type LastFoundQuadEdgeLocator struct { + subdiv *QuadEdgeSubdivision + lastEdge *QuadEdge +} + +func NewLastFoundQuadEdgeLocator(subdiv *QuadEdgeSubdivision) *LastFoundQuadEdgeLocator { + var lf LastFoundQuadEdgeLocator + + lf.subdiv = subdiv + lf.init() + return &lf +} + +func (lf *LastFoundQuadEdgeLocator) init() { + lf.lastEdge = lf.findEdge() +} + +func (lf *LastFoundQuadEdgeLocator) findEdge() *QuadEdge { + edges := lf.subdiv.GetEdges() + // assume there is an edge - otherwise will get an exception + return edges[0] +} + +/* +Locate an edge e, such that either v is on e, or e is an edge of a triangle +containing v. The search starts from the last located edge and proceeds on the +general direction of v. +*/ +func (lf *LastFoundQuadEdgeLocator) Locate(v Vertex) (*QuadEdge, error) { + if !lf.lastEdge.IsLive() { + lf.init() + } + + e, err := lf.subdiv.LocateFromEdge(v, lf.lastEdge) + if err != nil { + return nil, err + } + lf.lastEdge = e + return lf.lastEdge, nil +} diff --git a/planar/triangulate/quadedge/quadedge.go b/planar/triangulate/quadedge/quadedge.go new file mode 100644 index 00000000..cdaf103c --- /dev/null +++ b/planar/triangulate/quadedge/quadedge.go @@ -0,0 +1,404 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package quadedge + +import ( + "github.com/go-spatial/geom/cmp" +) + +/* +QuadEdge represents the edge data structure which implements the quadedge +algebra. The quadedge algebra was described in a well-known paper by Guibas +and Stolfi, "Primitives for the manipulation of general subdivisions and the +computation of Voronoi diagrams", ACM Transactions on Graphics, 4(2), 1985, +75-123. + +Each edge object is part of a quartet of 4 edges, linked via their rot +references. Any edge in the group may be accessed using a series of rot() +operations. Quadedges in a subdivision are linked together via their next +references. The linkage between the quadedge quartets determines the topology +of the subdivision. + +The edge class does not contain separate information for vertices or faces; a +vertex is implicitly defined as a ring of edges (created using the next field). + +Author David Skea +Author Martin Davis +Ported to Go by Jason R. Surratt +*/ +type QuadEdge struct { + rot *QuadEdge + vertex Vertex + next *QuadEdge + data interface{} +} + +/* +MakeEdge creates a new QuadEdge quartet from {@link Vertex} o to {@link Vertex} d. + +o - the origin Vertex +d - the destination Vertex +returns the new QuadEdge quartet +*/ +func MakeEdge(o Vertex, d Vertex) *QuadEdge { + q0 := new(QuadEdge) + q1 := new(QuadEdge) + q2 := new(QuadEdge) + q3 := new(QuadEdge) + + q0.rot = q1 + q1.rot = q2 + q2.rot = q3 + q3.rot = q0 + + q0.SetNext(q0) + q1.SetNext(q3) + q2.SetNext(q2) + q3.SetNext(q1) + + base := q0 + base.setOrig(o) + base.setDest(d) + return base +} + +/* +Connect creates a new QuadEdge connecting the destination of a to the origin of +b, in such a way that all three have the same left face after the +connection is complete. Additionally, the data pointers of the new edge +are set. + +Returns the connected edge. +*/ +func Connect(a *QuadEdge, b *QuadEdge) *QuadEdge { + e := MakeEdge(a.Dest(), b.Orig()) + Splice(e, a.LNext()) + Splice(e.Sym(), b) + return e +} + +/* +Splices two edges together or apart. +Splice affects the two edge rings around the origins of a and b, and, independently, the two +edge rings around the left faces of a and b. +In each case, (i) if the two rings are distinct, +Splice will combine them into one, or (ii) if the two are the same ring, Splice will break it +into two separate pieces. Thus, Splice can be used both to attach the two edges together, and +to break them apart. + +a - an edge to splice +b - an edge to splice +*/ +func Splice(a *QuadEdge, b *QuadEdge) { + alpha := a.ONext().Rot() + beta := b.ONext().Rot() + + t1 := b.ONext() + t2 := a.ONext() + t3 := beta.ONext() + t4 := alpha.ONext() + + a.SetNext(t1) + b.SetNext(t2) + alpha.SetNext(t3) + beta.SetNext(t4) +} + +/* +Swap Turns an edge counterclockwise inside its enclosing quadrilateral. + +e - the quadedge to turn +*/ +func Swap(e *QuadEdge) { + a := e.OPrev() + b := e.Sym().OPrev() + Splice(e, a) + Splice(e.Sym(), b) + Splice(e, a.LNext()) + Splice(e.Sym(), b.LNext()) + e.setOrig(a.Dest()) + e.setDest(b.Dest()) +} + +/* +Quadedges must be made using {@link makeEdge}, +to ensure proper construction. +private QuadEdge() +{ + +} +*/ + +/* +getPrimary gets the primary edge of this quadedge and its sym. The primary +edge is the one for which the origin and destination coordinates are ordered +according to the standard Point ordering. + +Returns the primary quadedge +*/ +func (qe *QuadEdge) GetPrimary() *QuadEdge { + v1 := qe.Orig() + v2 := qe.Dest() + if cmp.PointLess(v1, v2) || cmp.PointEqual(v1, v2) { + return qe + } + return qe.Sym() +} + +/* +Sets the external data value for this edge. + +@param data an object containing external data +public void setData(Object data) { + this.data = data; +} +*/ + +/* +Gets the external data value for this edge. + +@return the data object +public Object getData() { + return data; +} +*/ + +/* +Marks this quadedge as being deleted. +This does not free the memory used by +this quadedge quartet, but indicates +that this edge no longer participates +in a subdivision. +*/ +func (qe *QuadEdge) Delete() { + qe.rot = nil +} + +/* +IsLive tests whether this edge has been deleted. + +Returns true if this edge has not been deleted. +*/ +func (qe *QuadEdge) IsLive() bool { + return qe.rot != nil +} + +// SetNext sets the connected edge +func (qe *QuadEdge) SetNext(next *QuadEdge) { + qe.next = next +} + +/************************************************************************** +QuadEdge Algebra + *************************************************************************** +*/ + +/* +Gets the dual of this edge, directed from its right to its left. + +@return the rotated edge +*/ +func (qe *QuadEdge) Rot() *QuadEdge { + return qe.rot +} + +/* +Gets the dual of this edge, directed from its left to its right. + +@return the inverse rotated edge. +*/ +func (qe *QuadEdge) InvRot() *QuadEdge { + return qe.rot.Sym() +} + +/* +Gets the edge from the destination to the origin of this edge. + +@return the sym of the edge +*/ +func (qe *QuadEdge) Sym() *QuadEdge { + return qe.rot.rot +} + +/* +Gets the next CCW edge around the origin of this edge. + +@return the next linked edge. +*/ +func (qe *QuadEdge) ONext() *QuadEdge { + return qe.next +} + +/* +Gets the next CW edge around (from) the origin of this edge. + +@return the previous edge. +*/ +func (qe *QuadEdge) OPrev() *QuadEdge { + return qe.rot.next.rot +} + +/* +Gets the next CCW edge around (into) the destination of this edge. + +@return the next destination edge. +*/ +func (qe *QuadEdge) DNext() *QuadEdge { + return qe.Sym().ONext().Sym() +} + +/* +Gets the next CW edge around (into) the destination of this edge. + +@return the previous destination edge. +*/ +func (qe *QuadEdge) DPrev() *QuadEdge { + return qe.InvRot().ONext().InvRot() +} + +/* +Gets the CCW edge around the left face following this edge. + +@return the next left face edge. +*/ +func (qe *QuadEdge) LNext() *QuadEdge { + return qe.InvRot().ONext().Rot() +} + +/* +Gets the CCW edge around the left face before this edge. + +@return the previous left face edge. +*/ +func (qe *QuadEdge) LPrev() *QuadEdge { + return qe.next.Sym() +} + +/* +Gets the edge around the right face ccw following this edge. + +@return the next right face edge. +*/ +func (qe *QuadEdge) RNext() *QuadEdge { + return qe.rot.next.InvRot() +} + +/* +Gets the edge around the right face ccw before this edge. + +@return the previous right face edge. +*/ +func (qe *QuadEdge) RPrev() *QuadEdge { + return qe.Sym().ONext() +} + +/********************************************************************************************** +Data Access + **********************************************************************************************/ + +/* +SetOrig sets the vertex for this edge's origin + +o - the origin vertex +*/ +func (qe *QuadEdge) setOrig(o Vertex) { + qe.vertex = o +} + +/* +SetDest sets the vertex for this edge's destination + +d - the destination vertex +*/ +func (qe *QuadEdge) setDest(d Vertex) { + qe.Sym().setOrig(d) +} + +/* +Gets the vertex for the edge's origin + +returns the origin vertex +*/ +func (qe *QuadEdge) Orig() Vertex { + return qe.vertex +} + +/* +dest Gets the vertex for the edge's destination + +returns the destination vertex +*/ +func (qe *QuadEdge) Dest() Vertex { + return qe.Sym().Orig() +} + +/* +Gets the length of the geometry of this quadedge. + +@return the length of the quadedge +public double getLength() { + return orig().getCoordinate().distance(dest().getCoordinate()); +} +*/ + +/* +Tests if this quadedge and another have the same line segment geometry, +regardless of orientation. + +@param qe a quadedge +@return true if the quadedges are based on the same line segment regardless of orientation +public boolean equalsNonOriented(QuadEdge qe) { + if (equalsOriented(qe)) + return true; + if (equalsOriented(qe.sym())) + return true; + return false; +} +*/ + +/* +Tests if this quadedge and another have the same line segment geometry +with the same orientation. + +@param qe a quadedge +@return true if the quadedges are based on the same line segment +public boolean equalsOriented(QuadEdge qe) { + if (orig().getCoordinate().equals2D(qe.orig().getCoordinate()) + && dest().getCoordinate().equals2D(qe.dest().getCoordinate())) + return true; + return false; +} +*/ + +/* +Creates a {@link LineSegment} representing the +geometry of this edge. + +@return a LineSegment +public LineSegment toLineSegment() +{ + return new LineSegment(vertex.getCoordinate(), dest().getCoordinate()); +} +*/ + +/* +Converts this edge to a WKT two-point LINESTRING indicating +the geometry of this edge. + +@return a String representing this edge's geometry +public String toString() { + Coordinate p0 = vertex.getCoordinate(); + Coordinate p1 = dest().getCoordinate(); + return WKTWriter.toLineString(p0, p1); +} +*/ diff --git a/planar/triangulate/quadedge/quadedgelocator.go b/planar/triangulate/quadedge/quadedgelocator.go new file mode 100644 index 00000000..48b2b332 --- /dev/null +++ b/planar/triangulate/quadedge/quadedgelocator.go @@ -0,0 +1,26 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package quadedge + +/* +An interface for classes which locate an edge in a {@link QuadEdgeSubdivision} +which either contains a given {@link Vertex} V or is an edge of a triangle +which contains V. Implementors may utilized different strategies for +optimizing locating containing edges/triangles. + +Author Martin Davis +Ported to Go by Jason R. Surratt +*/ +type QuadEdgeLocator interface { + Locate(v Vertex) (*QuadEdge, error) +} diff --git a/planar/triangulate/quadedge/quadedgesubdivision.go b/planar/triangulate/quadedge/quadedgesubdivision.go new file mode 100644 index 00000000..30782f0c --- /dev/null +++ b/planar/triangulate/quadedge/quadedgesubdivision.go @@ -0,0 +1,944 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package quadedge + +import ( + "errors" + "fmt" + + "github.com/go-spatial/geom" + "github.com/go-spatial/geom/planar" +) + +var ErrLocateFailure = errors.New("failure locating edge") + +/* +A class that contains the QuadEdges representing a planar subdivision that +models a triangulation. The subdivision is constructed using the quadedge +algebra defined in the class QuadEdge. All metric calculations are done in the +Vertex class. In addition to a triangulation, subdivisions support extraction +of Voronoi diagrams. This is easily accomplished, since the Voronoi diagram is +the dual of the Delaunay triangulation. + +Subdivisions can be provided with a tolerance value. Inserted vertices which +are closer than this value to vertices already in the subdivision will be +ignored. Using a suitable tolerance value can prevent robustness failures +from happening during Delaunay triangulation. + +Subdivisions maintain a frame triangle around the client-created +edges. The frame is used to provide a bounded "container" for all edges +within a TIN. Normally the frame edges, frame connecting edges, and frame +triangles are not included in client processing. + +Author David Skea +Author Martin Davis +Ported to Go by Jason R. Surratt +*/ +type QuadEdgeSubdivision struct { + + // used for edge extraction to ensure edge uniqueness + visitedKey int + quadEdges []*QuadEdge + startingEdge *QuadEdge + tolerance float64 + edgeCoincidenceTolerance float64 + frameVertex [3]Vertex + frameEnv geom.Extent + locator QuadEdgeLocator +} + +var EDGE_COINCIDENCE_TOL_FACTOR float64 = 1000 + +// /** +// * Gets the edges for the triangle to the left of the given {@link QuadEdge}. +// * +// * @param startQE +// * @param triEdge +// * +// * @throws IllegalArgumentException +// * if the edges do not form a triangle +// */ +// public static void getTriangleEdges(QuadEdge startQE, QuadEdge[] triEdge) { +// triEdge[0] = startQE; +// triEdge[1] = triEdge[0].lNext(); +// triEdge[2] = triEdge[1].lNext(); +// if (triEdge[2].lNext() != triEdge[0]) +// throw new IllegalArgumentException("Edges do not form a triangle"); +// } + +/* +Creates a new instance of a quad-edge subdivision based on a frame triangle +that encloses a supplied bounding box. A new super-bounding box that +contains the triangle is computed and stored. + +env - the bounding box to surround +tolerance - the tolerance value for determining if two sites are equal +*/ +func NewQuadEdgeSubdivision(env geom.Extent, tolerance float64) *QuadEdgeSubdivision { + var qes QuadEdgeSubdivision + qes.tolerance = tolerance + qes.edgeCoincidenceTolerance = tolerance / EDGE_COINCIDENCE_TOL_FACTOR + + qes.createFrame(env) + qes.startingEdge = qes.initSubdiv() + qes.locator = NewLastFoundQuadEdgeLocator(&qes) + return &qes +} + +func (qes *QuadEdgeSubdivision) createFrame(env geom.Extent) { + deltaX := env.XSpan() + deltaY := env.YSpan() + offset := 0.0 + if deltaX > deltaY { + offset = deltaX * 10.0 + } else { + offset = deltaY * 10.0 + } + + qes.frameVertex[0] = Vertex{(env.MaxX() + env.MinX()) / 2.0, env.MaxY() + offset} + qes.frameVertex[1] = Vertex{env.MinX() - offset, env.MinY() - offset} + qes.frameVertex[2] = Vertex{env.MaxX() + offset, env.MinY() - offset} + + qes.frameEnv = *geom.NewExtent(qes.frameVertex[0], qes.frameVertex[1], qes.frameVertex[2]) +} + +func (qes *QuadEdgeSubdivision) initSubdiv() *QuadEdge { + // build initial subdivision from frame + ea := qes.MakeEdge(qes.frameVertex[0], qes.frameVertex[1]) + eb := qes.MakeEdge(qes.frameVertex[1], qes.frameVertex[2]) + Splice(ea.Sym(), eb) + ec := qes.MakeEdge(qes.frameVertex[2], qes.frameVertex[0]) + Splice(eb.Sym(), ec) + Splice(ec.Sym(), ea) + return ea +} + +/* +Gets the vertex-equality tolerance value +used in this subdivision + +return the tolerance value +*/ +func (qes *QuadEdgeSubdivision) GetTolerance() float64 { + return qes.tolerance +} + +/* +Gets the envelope of the Subdivision (including the frame). + +@return the envelope +*/ +func (qes *QuadEdgeSubdivision) GetEnvelope() geom.Extent { + // returns a deep copy to avoid modification by caller + return qes.frameEnv.Extent() +} + +/* +GetEdges gets the collection of base {@link QuadEdge}s (one for every pair of +vertices which is connected). + +return a collection of QuadEdges +*/ +func (qes *QuadEdgeSubdivision) GetEdges() []*QuadEdge { + return qes.quadEdges +} + +// /** +// * Sets the {@link QuadEdgeLocator} to use for locating containing triangles +// * in this subdivision. +// * +// * @param locator +// * a QuadEdgeLocator +// */ +// public void setLocator(QuadEdgeLocator locator) { +// this.locator = locator; +// } + +/* +MakeEdge creates a new quadedge, recording it in the edges list. + +return a new quadedge +*/ +func (qes *QuadEdgeSubdivision) MakeEdge(o Vertex, d Vertex) *QuadEdge { + q := MakeEdge(o, d) + qes.quadEdges = append(qes.quadEdges, q) + return q +} + +/* +Connect creates a new QuadEdge connecting the destination of a to the origin +of b, in such a way that all three have the same left face after the connection +is complete. The quadedge is recorded in the edges list. + +@param a +@param b +@return a quadedge +*/ +func (qes *QuadEdgeSubdivision) Connect(a *QuadEdge, b *QuadEdge) *QuadEdge { + q := Connect(a, b) + qes.quadEdges = append(qes.quadEdges, q) + return q +} + +/* +Deletes a quadedge from the subdivision. Linked quadedges are updated to +reflect the deletion. + +e - the quadedge to delete +*/ +func (qes *QuadEdgeSubdivision) Delete(e *QuadEdge) { + Splice(e, e.OPrev()) + Splice(e.Sym(), e.Sym().OPrev()) + + eSym := e.Sym() + eRot := e.Rot() + eRotSym := e.Rot().Sym() + + // this is inefficient on an array, but this method should be called + // infrequently + newArray := make([]*QuadEdge, 0, len(qes.quadEdges)) + for _, ele := range qes.quadEdges { + if ele != e && ele != eSym && ele != eRot && ele != eRotSym { + newArray = append(newArray, ele) + } + } + + e.Delete() + eSym.Delete() + eRot.Delete() + eRotSym.Delete() +} + +/* +Locates an edge of a triangle which contains a location specified by a Vertex +v. The edge returned has the property that either v is on e, or e is an edge +of a triangle containing v. The search starts from startEdge amd proceeds on +the general direction of v. + +This locate algorithm relies on the subdivision being Delaunay. For +non-Delaunay subdivisions, this may loop for ever. + +v - the location to search for +startEdge - an edge of the subdivision to start searching at +return a QuadEdge which contains v, or is on the edge of a triangle containing +v + +If the location algorithm fails to converge in a reasonable number of +iterations a ErrLocateFailure will be returned. +*/ +func (qes *QuadEdgeSubdivision) LocateFromEdge(v Vertex, startEdge *QuadEdge) (*QuadEdge, error) { + iter := 0 + maxIter := len(qes.quadEdges) + + e := startEdge + + for { + iter++ + + /* + So far it has always been the case that failure to locate indicates an + invalid subdivision. So just fail completely. (An alternative would be + to perform an exhaustive search for the containing triangle, but this + would mask errors in the subdivision topology) + + This can also happen if two vertices are located very close together, + since the orientation predicates may experience precision failures. + */ + if iter > maxIter { + return nil, ErrLocateFailure + // String msg = "Locate failed to converge (at edge: " + e + "). + // Possible causes include invalid Subdivision topology or very close + // sites"; + // System.err.println(msg); + // dumpTriangles(); + } + + if v.Equals(e.Orig()) || v.Equals(e.Dest()) { + break + } else if v.RightOf(*e) { + e = e.Sym() + } else if !v.RightOf(*e.ONext()) { + e = e.ONext() + } else if !v.RightOf(*e.DPrev()) { + e = e.DPrev() + } else { + // on edge or in triangle containing edge + break + } + } + // System.out.println("Locate count: " + iter); + return e, nil +} + +/* +Finds a quadedge of a triangle containing a location +specified by a {@link Vertex}, if one exists. + +v - the vertex to locate +Return a quadedge on the edge of a triangle which touches or contains the +location or nil if no such triangle exists +*/ +func (qes *QuadEdgeSubdivision) Locate(v Vertex) (*QuadEdge, error) { + return qes.locator.Locate(v) +} + +// /** +// * Locates the edge between the given vertices, if it exists in the +// * subdivision. +// * +// * @param p0 a coordinate +// * @param p1 another coordinate +// * @return the edge joining the coordinates, if present +// * or null if no such edge exists +// */ +// public QuadEdge locate(Coordinate p0, Coordinate p1) { +// // find an edge containing one of the points +// QuadEdge e = locator.locate(new Vertex(p0)); +// if (e == null) +// return null; + +// // normalize so that p0 is origin of base edge +// QuadEdge base = e; +// if (e.dest().getCoordinate().equals2D(p0)) +// base = e.sym(); +// // check all edges around origin of base edge +// QuadEdge locEdge = base; +// do { +// if (locEdge.dest().getCoordinate().equals2D(p1)) +// return locEdge; +// locEdge = locEdge.oNext(); +// } while (locEdge != base); +// return null; +// } + +/** + * Inserts a new site into the Subdivision, connecting it to the vertices of + * the containing triangle (or quadrilateral, if the split point falls on an + * existing edge). + *

+ * This method does NOT maintain the Delaunay condition. If desired, this must + * be checked and enforced by the caller. + *

+ * This method does NOT check if the inserted vertex falls on an edge. This + * must be checked by the caller, since this situation may cause erroneous + * triangulation + * + * @param v + * the vertex to insert + * @return a new quad edge terminating in v + */ +// public QuadEdge insertSite(Vertex v) { +// QuadEdge e = locate(v); + +// if ((v.equals(e.orig(), tolerance)) || (v.equals(e.dest(), tolerance))) { +// return e; // point already in subdivision. +// } + +// // Connect the new point to the vertices of the containing +// // triangle (or quadrilateral, if the new point fell on an +// // existing edge.) +// QuadEdge base = makeEdge(e.orig(), v); +// QuadEdge.splice(base, e); +// QuadEdge startEdge = base; +// do { +// base = connect(e, base.sym()); +// e = base.oPrev(); +// } while (e.lNext() != startEdge); + +// return startEdge; +// } + +/* +isFrameEdge tests whether a QuadEdge is an edge incident on a frame triangle +vertex. + +e - the edge to test +return true if the edge is connected to the frame triangle +*/ +func (qes *QuadEdgeSubdivision) isFrameEdge(e *QuadEdge) bool { + if qes.isFrameVertex(e.Orig()) || qes.isFrameVertex(e.Dest()) { + return true + } + return false +} + +// /** +// * Tests whether a QuadEdge is an edge on the border of the frame facets and +// * the internal facets. E.g. an edge which does not itself touch a frame +// * vertex, but which touches an edge which does. +// * +// * @param e +// * the edge to test +// * @return true if the edge is on the border of the frame +// */ +// public boolean isFrameBorderEdge(QuadEdge e) { +// // MD debugging +// QuadEdge[] leftTri = new QuadEdge[3]; +// getTriangleEdges(e, leftTri); +// // System.out.println(new QuadEdgeTriangle(leftTri).toString()); +// QuadEdge[] rightTri = new QuadEdge[3]; +// getTriangleEdges(e.sym(), rightTri); +// // System.out.println(new QuadEdgeTriangle(rightTri).toString()); + +// // check other vertex of triangle to left of edge +// Vertex vLeftTriOther = e.lNext().dest(); +// if (isFrameVertex(vLeftTriOther)) +// return true; +// // check other vertex of triangle to right of edge +// Vertex vRightTriOther = e.sym().lNext().dest(); +// if (isFrameVertex(vRightTriOther)) +// return true; + +// return false; +// } + +/* +isFrameVertex tests whether a vertex is a vertex of the outer triangle. + +v - the vertex to test +returns true if the vertex is an outer triangle vertex +*/ +func (qes *QuadEdgeSubdivision) isFrameVertex(v Vertex) bool { + if v.Equals(qes.frameVertex[0]) { + return true + } + if v.Equals(qes.frameVertex[1]) { + return true + } + if v.Equals(qes.frameVertex[2]) { + return true + } + return false +} + +// private LineSegment seg = new LineSegment(); + +/* +IsOnEdge Tests whether a point lies on a QuadEdge, up to a tolerance +determined by the subdivision tolerance. + +Returns true if the vertex lies on the edge +*/ +func (qes *QuadEdgeSubdivision) IsOnEdge(e *QuadEdge, p geom.Pointer) bool { + dist := planar.DistanceToLineSegment(p, e.Orig(), e.Dest()) + + // heuristic (hack?) + return dist < qes.edgeCoincidenceTolerance +} + +/* +IsVertexOfEdge tests whether a {@link Vertex} is the start or end vertex of a +QuadEdge, up to the subdivision tolerance distance. + +Returns true if the vertex is a endpoint of the edge +*/ +func (qes *QuadEdgeSubdivision) IsVertexOfEdge(e *QuadEdge, v Vertex) bool { + if (v.EqualsTolerance(e.Orig(), qes.tolerance)) || (v.EqualsTolerance(e.Dest(), qes.tolerance)) { + return true + } + return false +} + +// /** +// * Gets the unique {@link Vertex}es in the subdivision, +// * including the frame vertices if desired. +// * +// * @param includeFrame +// * true if the frame vertices should be included +// * @return a collection of the subdivision vertices +// * +// * @see #getVertexUniqueEdges +// */ +// public Collection getVertices(boolean includeFrame) +// { +// Set vertices = new HashSet(); +// for (Iterator i = quadEdges.iterator(); i.hasNext();) { +// QuadEdge qe = (QuadEdge) i.next(); +// Vertex v = qe.orig(); +// //System.out.println(v); +// if (includeFrame || ! isFrameVertex(v)) +// vertices.add(v); + +// /** +// * Inspect the sym edge as well, since it is +// * possible that a vertex is only at the +// * dest of all tracked quadedges. +// */ +// Vertex vd = qe.dest(); +// //System.out.println(vd); +// if (includeFrame || ! isFrameVertex(vd)) +// vertices.add(vd); +// } +// return vertices; +// } + +// /** +// * Gets a collection of {@link QuadEdge}s whose origin +// * vertices are a unique set which includes +// * all vertices in the subdivision. +// * The frame vertices can be included if required. +// *

+// * This is useful for algorithms which require traversing the +// * subdivision starting at all vertices. +// * Returning a quadedge for each vertex +// * is more efficient than +// * the alternative of finding the actual vertices +// * using {@link #getVertices} and then locating +// * quadedges attached to them. +// * +// * @param includeFrame true if the frame vertices should be included +// * @return a collection of QuadEdge with the vertices of the subdivision as their origins +// */ +// public List getVertexUniqueEdges(boolean includeFrame) +// { +// List edges = new ArrayList(); +// Set visitedVertices = new HashSet(); +// for (Iterator i = quadEdges.iterator(); i.hasNext();) { +// QuadEdge qe = (QuadEdge) i.next(); +// Vertex v = qe.orig(); +// //System.out.println(v); +// if (! visitedVertices.contains(v)) { +// visitedVertices.add(v); +// if (includeFrame || ! isFrameVertex(v)) { +// edges.add(qe); +// } +// } + +// /** +// * Inspect the sym edge as well, since it is +// * possible that a vertex is only at the +// * dest of all tracked quadedges. +// */ +// QuadEdge qd = qe.sym(); +// Vertex vd = qd.orig(); +// //System.out.println(vd); +// if (! visitedVertices.contains(vd)) { +// visitedVertices.add(vd); +// if (includeFrame || ! isFrameVertex(vd)) { +// edges.add(qd); +// } +// } +// } +// return edges; +// } + +type edgeStack []*QuadEdge +type edgeSet map[*QuadEdge]bool + +func (es *edgeStack) push(edge *QuadEdge) { + *es = append(*es, edge) +} + +func (es *edgeStack) pop() *QuadEdge { + if len(*es) == 0 { + return nil + } + result := (*es)[len(*es)-1] + *es = (*es)[:len(*es)-1] + return result +} + +/* +contains returns true if edge is in the map. + +This just isn't natural for me yet... +if _, ok := es[edge]; ok { +*/ +func (es *edgeSet) contains(edge *QuadEdge) bool { + _, ok := (*es)[edge] + return ok +} + +/** + * Gets all primary quadedges in the subdivision. +* A primary edge is a {@link QuadEdge} + * which occupies the 0'th position in its array of associated quadedges. + * These provide the unique geometric edges of the triangulation. + * + * @param includeFrame true if the frame edges are to be included + * @return a List of QuadEdges +*/ +func (qes *QuadEdgeSubdivision) GetPrimaryEdges(includeFrame bool) []*QuadEdge { + qes.visitedKey++ + + var edges []*QuadEdge + var stack edgeStack + stack.push(qes.startingEdge) + + visitedEdges := make(edgeSet) + + for len(stack) > 0 { + edge := stack.pop() + + if !visitedEdges.contains(edge) { + priQE := edge.GetPrimary() + + if includeFrame || !qes.isFrameEdge(priQE) { + edges = append(edges, priQE) + } + + stack.push(edge.ONext()) + stack.push(edge.Sym().ONext()) + + visitedEdges[edge] = true + visitedEdges[edge.Sym()] = true + } + } + return edges +} + +// /** +// * A TriangleVisitor which computes and sets the +// * circumcentre as the origin of the dual +// * edges originating in each triangle. +// * +// * @author mbdavis +// * +// */ +// private static class TriangleCircumcentreVisitor implements TriangleVisitor +// { +// public TriangleCircumcentreVisitor() { +// } + +// public void visit(QuadEdge[] triEdges) +// { +// Coordinate a = triEdges[0].orig().getCoordinate(); +// Coordinate b = triEdges[1].orig().getCoordinate(); +// Coordinate c = triEdges[2].orig().getCoordinate(); + +// // TODO: choose the most accurate circumcentre based on the edges +// Coordinate cc = Triangle.circumcentre(a, b, c); +// Vertex ccVertex = new Vertex(cc); +// // save the circumcentre as the origin for the dual edges originating in this triangle +// for (int i = 0; i < 3; i++) { +// triEdges[i].rot().setOrig(ccVertex); +// } +// } +// } + +// /***************************************************************************** +// * Visitors +// ****************************************************************************/ + +func (qes *QuadEdgeSubdivision) visitTriangles(triVisitor func(triEdges []*QuadEdge), includeFrame bool) { + qes.visitedKey++ + + // visited flag is used to record visited edges of triangles + // setVisitedAll(false); + var stack *edgeStack = new(edgeStack) + stack.push(qes.startingEdge) + + visitedEdges := make(edgeSet) + + for len(*stack) > 0 { + edge := stack.pop() + if !visitedEdges.contains(edge) { + triEdges := qes.fetchTriangleToVisit(edge, stack, includeFrame, visitedEdges) + if triEdges != nil { + triVisitor(triEdges) + } + } + } +} + +/* +Stores the edges for a visited triangle. Also pushes sym (neighbour) edges +on stack to visit later. + +@param edge +@param edgeStack +@param includeFrame +@return the visited triangle edges +or null if the triangle should not be visited (for instance, if it is + outer) +*/ +func (qes *QuadEdgeSubdivision) fetchTriangleToVisit(edge *QuadEdge, stack *edgeStack, includeFrame bool, visitedEdges edgeSet) []*QuadEdge { + triEdges := make([]*QuadEdge, 0, 3) + curr := edge + var isFrame bool + var done bool + for !done { + triEdges = append(triEdges, curr) + + if qes.isFrameEdge(curr) { + isFrame = true + } + + // push sym edges to visit next + sym := curr.Sym() + if !visitedEdges.contains(sym) { + stack.push(sym) + } + + // mark this edge as visited + visitedEdges[curr] = true + + curr = curr.LNext() + + if curr == edge { + done = true + } + } + + if isFrame && !includeFrame { + return nil + } + return triEdges +} + +// /** +// * Gets a list of the triangles +// * in the subdivision, specified as +// * an array of the primary quadedges around the triangle. +// * +// * @param includeFrame +// * true if the frame triangles should be included +// * @return a List of QuadEdge[3] arrays +// */ +// public List getTriangleEdges(boolean includeFrame) { +// TriangleEdgesListVisitor visitor = new TriangleEdgesListVisitor(); +// visitTriangles(visitor, includeFrame); +// return visitor.getTriangleEdges(); +// } + +// private static class TriangleEdgesListVisitor implements TriangleVisitor { +// private List triList = new ArrayList(); + +// public void visit(QuadEdge[] triEdges) { +// triList.add(triEdges); +// } + +// public List getTriangleEdges() { +// return triList; +// } +// } + +// /** +// * Gets a list of the triangles in the subdivision, +// * specified as an array of the triangle {@link Vertex}es. +// * +// * @param includeFrame +// * true if the frame triangles should be included +// * @return a List of Vertex[3] arrays +// */ +// public List getTriangleVertices(boolean includeFrame) { +// TriangleVertexListVisitor visitor = new TriangleVertexListVisitor(); +// visitTriangles(visitor, includeFrame); +// return visitor.getTriangleVertices(); +// } + +// private static class TriangleVertexListVisitor implements TriangleVisitor { +// private List triList = new ArrayList(); + +// public void visit(QuadEdge[] triEdges) { +// triList.add(new Vertex[] { triEdges[0].orig(), triEdges[1].orig(), +// triEdges[2].orig() }); +// } + +// public List getTriangleVertices() { +// return triList; +// } +// } + +/** +Gets the coordinates for each triangle in the subdivision as an array. + +@param includeFrame + true if the frame triangles should be included +@return a list of Coordinate[4] representing each triangle +*/ +func (qes *QuadEdgeSubdivision) GetTriangleCoordinates(includeFrame bool) ([]geom.Polygon, error) { + var visitor TriangleCoordinatesVisitor + qes.visitTriangles(visitor.visit, includeFrame) + if visitor.err != nil { + return nil, visitor.err + } + return visitor.getTriangles(), nil +} + +type TriangleCoordinatesVisitor struct { + triCoords []geom.Polygon + err error +} + +func (tcv *TriangleCoordinatesVisitor) visit(triEdges []*QuadEdge) { + if tcv.err != nil { + return + } + + var triangle geom.Polygon + triangle = append(triangle, [][2]float64{}) + for i := 0; i < 3; i++ { + v := triEdges[i].Orig() + triangle[0] = append(triangle[0], [2]float64(v)) + } + if len(triangle[0]) > 0 { + // close the ring + triangle[0] = append(triangle[0], triangle[0][0]) + if len(triangle[0]) != 4 { + //checkTriangleSize(pts); + tcv.err = fmt.Errorf("invalid triangle: %v", triangle) + + return + } + + tcv.triCoords = append(tcv.triCoords, triangle) + } +} + +func (tcv *TriangleCoordinatesVisitor) getTriangles() []geom.Polygon { + return tcv.triCoords +} + +// private void checkTriangleSize(Coordinate[] pts) +// { +// String loc = ""; +// if (pts.length >= 2) +// loc = WKTWriter.toLineString(pts[0], pts[1]); +// else { +// if (pts.length >= 1) +// loc = WKTWriter.toPoint(pts[0]); +// } +// // Assert.isTrue(pts.length == 4, "Too few points for visited triangle at " + loc); +// //com.vividsolutions.jts.util.Debug.println("too few points for triangle at " + loc); +// } + +/* +GetEdgesAsMultiLineString gets the geometry for the edges in the subdivision +as a MultiLineString containing 2-point lines. + +returns a MultiLineString +*/ +func (qes *QuadEdgeSubdivision) GetEdgesAsMultiLineString() geom.MultiLineString { + quadEdges := qes.GetPrimaryEdges(false) + var ms geom.MultiLineString + for _, qe := range quadEdges { + var ls [][2]float64 + ls = append(ls, qe.Orig().XY(), qe.Dest().XY()) + ms = append(ms, ls) + } + return ms +} + +/* +GetTriangles gets the geometry for the triangles in a triangulated subdivision +as a MultiPolygon of triangular Polygons. + +Unlike JTS, this method returns a MultiPolygon. I found not all viewers like +displaying collections. -JRS + +Returns a MultiPolygon of triangular Polygons +*/ +func (qes *QuadEdgeSubdivision) GetTriangles() (geom.MultiPolygon, error) { + tris, err := qes.GetTriangleCoordinates(false) + if err != nil { + return nil, err + } + var gc geom.MultiPolygon + for i := 0; i < len(tris); i++ { + gc = append(gc, tris[i]) + } + return gc, nil +} + +// /** +// * Gets the cells in the Voronoi diagram for this triangulation. +// * The cells are returned as a {@link GeometryCollection} of {@link Polygon}s +// *

+// * The userData of each polygon is set to be the {@link Coordinate} +// * of the cell site. This allows easily associating external +// * data associated with the sites to the cells. +// * +// * @param geomFact a geometry factory +// * @return a GeometryCollection of Polygons +// */ +// public Geometry getVoronoiDiagram(GeometryFactory geomFact) +// { +// List vorCells = getVoronoiCellPolygons(geomFact); +// return geomFact.createGeometryCollection(GeometryFactory.toGeometryArray(vorCells)); +// } + +// /** +// * Gets a List of {@link Polygon}s for the Voronoi cells +// * of this triangulation. +// *

+// * The userData of each polygon is set to be the {@link Coordinate} +// * of the cell site. This allows easily associating external +// * data associated with the sites to the cells. +// * +// * @param geomFact a geometry factory +// * @return a List of Polygons +// */ +// public List getVoronoiCellPolygons(GeometryFactory geomFact) +// { +// /* +// * Compute circumcentres of triangles as vertices for dual edges. +// * Precomputing the circumcentres is more efficient, +// * and more importantly ensures that the computed centres +// * are consistent across the Voronoi cells. +// */ +// visitTriangles(new TriangleCircumcentreVisitor(), true); + +// List cells = new ArrayList(); +// Collection edges = getVertexUniqueEdges(false); +// for (Iterator i = edges.iterator(); i.hasNext(); ) { +// QuadEdge qe = (QuadEdge) i.next(); +// cells.add(getVoronoiCellPolygon(qe, geomFact)); +// } +// return cells; +// } + +// /** +// * Gets the Voronoi cell around a site specified +// * by the origin of a QuadEdge. +// *

+// * The userData of the polygon is set to be the {@link Coordinate} +// * of the site. This allows attaching external +// * data associated with the site to this cell polygon. +// * +// * @param qe a quadedge originating at the cell site +// * @param geomFact a factory for building the polygon +// * @return a polygon indicating the cell extent +// */ +// public Polygon getVoronoiCellPolygon(QuadEdge qe, GeometryFactory geomFact) +// { +// List cellPts = new ArrayList(); +// QuadEdge startQE = qe; +// do { +// // Coordinate cc = circumcentre(qe); +// // use previously computed circumcentre +// Coordinate cc = qe.rot().orig().getCoordinate(); +// cellPts.add(cc); + +// // move to next triangle CW around vertex +// qe = qe.oPrev(); +// } while (qe != startQE); + +// CoordinateList coordList = new CoordinateList(); +// coordList.addAll(cellPts, false); +// coordList.closeRing(); + +// if (coordList.size() < 4) { +// System.out.println(coordList); +// coordList.add(coordList.get(coordList.size()-1), true); +// } + +// Coordinate[] pts = coordList.toCoordinateArray(); +// Polygon cellPoly = geomFact.createPolygon(geomFact.createLinearRing(pts)); + +// Vertex v = startQE.orig(); +// cellPoly.setUserData(v.getCoordinate()); +// return cellPoly; +// } + +// } diff --git a/planar/triangulate/quadedge/quadedgesubdivision_test.go b/planar/triangulate/quadedge/quadedgesubdivision_test.go new file mode 100644 index 00000000..497ed6c9 --- /dev/null +++ b/planar/triangulate/quadedge/quadedgesubdivision_test.go @@ -0,0 +1,238 @@ +package quadedge + +import ( + "fmt" + "strconv" + "testing" + + "github.com/go-spatial/geom" + "github.com/go-spatial/geom/encoding/wkt" +) + +func TestNewQuadEdgeSubdivision(t *testing.T) { + type tcase struct { + env geom.Extent + tolerance float64 + frameVertex string + expectedEnvelope geom.Extent + } + + fn := func(t *testing.T, tc tcase) { + uut := NewQuadEdgeSubdivision(tc.env, tc.tolerance) + + if fmt.Sprint(uut.frameVertex) != tc.frameVertex { + t.Errorf("error, expected %v got %v", tc.frameVertex, uut.frameVertex) + } + if uut.GetTolerance() != tc.tolerance { + t.Errorf("error, expected %v got %v", tc.tolerance, uut.tolerance) + } + if uut.GetEnvelope() != tc.expectedEnvelope { + t.Errorf("error, expected %v got %v", tc.env, uut.GetEnvelope()) + } + } + testcases := []tcase{ + { + env: geom.Extent{0, 0, 20, 10}, + tolerance: 0.01, + frameVertex: "[[10 210] [-200 -200] [220 -200]]", + expectedEnvelope: geom.Extent{-200, -200, 220, 210}, + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +func TestQuadEdgeSubdivisionDelete(t *testing.T) { + type tcase struct { + // insert this site + a, b, c Vertex + // expecting this neighbor + expected string + } + + fn := func(t *testing.T, tc tcase) { + uut := NewQuadEdgeSubdivision(geom.Extent{0, 0, 20, 10}, 0.01) + + // this should implicitly connect c to a + e1 := uut.MakeEdge(tc.a, tc.b) + uut.Connect(uut.startingEdge, e1) + e2 := uut.MakeEdge(tc.b, tc.c) + uut.Connect(e2, e1) + + uut.Delete(e2) + + edges := uut.GetEdgesAsMultiLineString() + edgesWKT, err := wkt.Encode(edges) + if err != nil { + t.Fatalf("expected nil got %v", err) + } + + if edgesWKT != tc.expected { + t.Errorf("expected %v got %v", tc.expected, edgesWKT) + } + } + testcases := []tcase{ + {Vertex{0, 0}, Vertex{5, 5}, Vertex{0, 5}, "MULTILINESTRING ((0 0,5 5),(0 0,0 5))"}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +func TestQuadEdgeSubdivisionGetEdges(t *testing.T) { + type tcase struct { + // insert this site + a, b, c Vertex + // expecting this neighbor + expected string + } + + fn := func(t *testing.T, tc tcase) { + uut := NewQuadEdgeSubdivision(geom.Extent{0, 0, 20, 10}, 0.01) + + // this should implicitly connect c to a + e1 := uut.MakeEdge(tc.a, tc.b) + uut.Connect(uut.startingEdge, e1) + e2 := uut.MakeEdge(tc.b, tc.c) + uut.Connect(e2, e1) + + edges := uut.GetEdgesAsMultiLineString() + edgesWKT, err := wkt.Encode(edges) + if err != nil { + t.Fatalf("expected nil got %v", err) + } + + if edgesWKT != tc.expected { + t.Errorf("expected %v got %v", tc.expected, edgesWKT) + } + + // This process does not form a proper triangle. + tris, err := uut.GetTriangles() + if err != nil { + t.Fatalf("expected nil got %v", err) + } + trisWKT, err := wkt.Encode(tris) + if err != nil { + t.Fatalf("expected nil got %v", err) + } + + if trisWKT != "MULTIPOLYGON EMPTY" { + t.Errorf("expected %v got %v", tc.expected, trisWKT) + } + } + testcases := []tcase{ + {Vertex{0, 0}, Vertex{5, 5}, Vertex{0, 5}, "MULTILINESTRING ((0 0,5 5),(0 0,0 5),(0 5,5 5))"}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +func TestQuadEdgeSubdivisionIsOnEdge(t *testing.T) { + type tcase struct { + // edge to test + e1, e2 Vertex + // point to test + p Vertex + expected bool + } + + fn := func(t *testing.T, tc tcase) { + uut := NewQuadEdgeSubdivision(geom.Extent{0, 0, 20, 10}, 0.01) + + // this should implicitly connect c to a + e1 := uut.MakeEdge(tc.e1, tc.e2) + + onEdge := uut.IsOnEdge(e1, tc.p) + + if onEdge != tc.expected { + t.Fatalf("expected %v got %v", tc.expected, onEdge) + } + } + testcases := []tcase{ + {Vertex{0, 0}, Vertex{5, 5}, Vertex{3, 3}, true}, + // a small deviation should still be considered on the edge + {Vertex{0, 0}, Vertex{5, 5}, Vertex{3 + 1e-5, 3}, true}, + // a slightly larger deviation is not on the edge. + {Vertex{0, 0}, Vertex{5, 5}, Vertex{3 + 1e-4, 3}, false}, + {Vertex{2, 3}, Vertex{2, 5}, Vertex{2, 3}, true}, + {Vertex{2, 3}, Vertex{2, 5}, Vertex{2, 2}, false}, + {Vertex{2, 3}, Vertex{6, 3}, Vertex{6, 3}, true}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +func TestQuadEdgeSubdivisionIsVertexOfEdge(t *testing.T) { + type tcase struct { + // edge to test + e1, e2 Vertex + // point to test + p Vertex + expected bool + } + + fn := func(t *testing.T, tc tcase) { + uut := NewQuadEdgeSubdivision(geom.Extent{0, 0, 20, 10}, 0.01) + + // this should implicitly connect c to a + e1 := uut.MakeEdge(tc.e1, tc.e2) + + onEdge := uut.IsVertexOfEdge(e1, tc.p) + + if onEdge != tc.expected { + t.Fatalf("expected %v got %v", tc.expected, onEdge) + } + } + testcases := []tcase{ + {Vertex{0, 0}, Vertex{5, 5}, Vertex{3, 3}, false}, + {Vertex{2, 3}, Vertex{2, 5}, Vertex{2, 3}, true}, + {Vertex{2, 3}, Vertex{2, 5}, Vertex{2, 2}, false}, + {Vertex{2, 3}, Vertex{6, 3}, Vertex{6, 3}, true}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +func TestQuadEdgeSubdivisionLocate(t *testing.T) { + type tcase struct { + // search from this vertex + from Vertex + // expecting this neighbor + expected string + } + + fn := func(t *testing.T, tc tcase) { + uut := NewQuadEdgeSubdivision(geom.Extent{0, 0, 20, 10}, 0.01) + + r, err := uut.Locate(tc.from) + if err != nil { + t.Fatalf("expected nil got %v", err) + } + fmt.Sprintf("%v %v", r.Orig(), r.Dest()) + // if r != tc.expected { + // t.Errorf("expected %v got %v", tc.expected, r) + // } + } + testcases := []tcase{ + {Vertex{0, 0}, "[10 210] [-200 -200]"}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} diff --git a/planar/triangulate/quadedge/trianglepredicate.go b/planar/triangulate/quadedge/trianglepredicate.go new file mode 100644 index 00000000..50b62457 --- /dev/null +++ b/planar/triangulate/quadedge/trianglepredicate.go @@ -0,0 +1,307 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package quadedge + +import ( + "github.com/go-spatial/geom" +) + +/* +Algorithms for computing values and predicates associated with triangles. +For some algorithms extended-precision implementations are provided, which are +more robust (i.e. they produce correct answers in more cases). Also, some more +robust formulations of some algorithms are provided, which utilize +normalization to the origin. + +Author Martin Davis +Ported to Go by Jason R. Surratt + +The empty struct gives us a "static" TrianglePredicate namespace. +*/ +type trianglePredicate struct{} + +var TrianglePredicate trianglePredicate + +/** + * Tests if a point is inside the circle defined by + * the triangle with vertices a, b, c (oriented counter-clockwise). + * This test uses simple + * double-precision arithmetic, and thus may not be robust. + * + * @param a a vertex of the triangle + * @param b a vertex of the triangle + * @param c a vertex of the triangle + * @param p the point to test + * @return true if this point is inside the circle defined by the points a, b, c + public static boolean isInCircleNonRobust( + Coordinate a, Coordinate b, Coordinate c, + Coordinate p) { + boolean isInCircle = + (a.x * a.x + a.y * a.y) * triArea(b, c, p) + - (b.x * b.x + b.y * b.y) * triArea(a, c, p) + + (c.x * c.x + c.y * c.y) * triArea(a, b, p) + - (p.x * p.x + p.y * p.y) * triArea(a, b, c) + > 0; + return isInCircle; + } +*/ + +/* +Tests if a point is inside the circle defined by +the triangle with vertices a, b, c (oriented counter-clockwise). +This test uses simple +double-precision arithmetic, and thus is not 100% robust. +However, by using normalization to the origin +it provides improved robustness and increased performance. + +Based on code by J.R.Shewchuk. + +a - a vertex of the triangle +b - a vertex of the triangle +c - a vertex of the triangle +p - the point to test +Returns true if this point is inside the circle defined by the points a, b, c +*/ +func (_ trianglePredicate) IsInCircleNormalized(a geom.Pointer, b geom.Pointer, c geom.Pointer, p geom.Pointer) bool { + adx := a.XY()[0] - p.XY()[0] + ady := a.XY()[1] - p.XY()[1] + bdx := b.XY()[0] - p.XY()[0] + bdy := b.XY()[1] - p.XY()[1] + cdx := c.XY()[0] - p.XY()[0] + cdy := c.XY()[1] - p.XY()[1] + + abdet := adx*bdy - bdx*ady + bcdet := bdx*cdy - cdx*bdy + cadet := cdx*ady - adx*cdy + alift := adx*adx + ady*ady + blift := bdx*bdx + bdy*bdy + clift := cdx*cdx + cdy*cdy + + disc := alift*bcdet + blift*cadet + clift*abdet + return disc > 0 +} + +/** + * Computes twice the area of the oriented triangle (a, b, c), i.e., the area is positive if the + * triangle is oriented counterclockwise. + * + * @param a a vertex of the triangle + * @param b a vertex of the triangle + * @param c a vertex of the triangle + private static double triArea(Coordinate a, Coordinate b, Coordinate c) { + return (b.x - a.x) * (c.y - a.y) + - (b.y - a.y) * (c.x - a.x); + } +*/ + +/* +Tests if a point is inside the circle defined by +the triangle with vertices a, b, c (oriented counter-clockwise). +This method uses more robust computation. + +a - a vertex of the triangle +b - a vertex of the triangle +c - a vertex of the triangle +p - the point to test +Returns true if this point is inside the circle defined by the points a, b, c +*/ +func (tp trianglePredicate) IsInCircleRobust(a geom.Pointer, b geom.Pointer, c geom.Pointer, p geom.Pointer) bool { + //checkRobustInCircle(a, b, c, p); + // return isInCircleNonRobust(a, b, c, p); + return tp.IsInCircleNormalized(a, b, c, p) +} + +/** + * Tests if a point is inside the circle defined by + * the triangle with vertices a, b, c (oriented counter-clockwise). + * The computation uses {@link DD} arithmetic for robustness. + * + * @param a a vertex of the triangle + * @param b a vertex of the triangle + * @param c a vertex of the triangle + * @param p the point to test + * @return true if this point is inside the circle defined by the points a, b, c + public static boolean isInCircleDDSlow( + Coordinate a, Coordinate b, Coordinate c, + Coordinate p) { + DD px = DD.valueOf(p.x); + DD py = DD.valueOf(p.y); + DD ax = DD.valueOf(a.x); + DD ay = DD.valueOf(a.y); + DD bx = DD.valueOf(b.x); + DD by = DD.valueOf(b.y); + DD cx = DD.valueOf(c.x); + DD cy = DD.valueOf(c.y); + + DD aTerm = (ax.multiply(ax).add(ay.multiply(ay))) + .multiply(triAreaDDSlow(bx, by, cx, cy, px, py)); + DD bTerm = (bx.multiply(bx).add(by.multiply(by))) + .multiply(triAreaDDSlow(ax, ay, cx, cy, px, py)); + DD cTerm = (cx.multiply(cx).add(cy.multiply(cy))) + .multiply(triAreaDDSlow(ax, ay, bx, by, px, py)); + DD pTerm = (px.multiply(px).add(py.multiply(py))) + .multiply(triAreaDDSlow(ax, ay, bx, by, cx, cy)); + + DD sum = aTerm.subtract(bTerm).add(cTerm).subtract(pTerm); + boolean isInCircle = sum.doubleValue() > 0; + + return isInCircle; + } +*/ + +/** + * Computes twice the area of the oriented triangle (a, b, c), i.e., the area + * is positive if the triangle is oriented counterclockwise. + * The computation uses {@link DD} arithmetic for robustness. + * + * @param ax the x ordinate of a vertex of the triangle + * @param ay the y ordinate of a vertex of the triangle + * @param bx the x ordinate of a vertex of the triangle + * @param by the y ordinate of a vertex of the triangle + * @param cx the x ordinate of a vertex of the triangle + * @param cy the y ordinate of a vertex of the triangle + public static DD triAreaDDSlow(DD ax, DD ay, + DD bx, DD by, DD cx, DD cy) { + return (bx.subtract(ax).multiply(cy.subtract(ay)).subtract(by.subtract(ay) + .multiply(cx.subtract(ax)))); + } + + public static boolean isInCircleDDFast( + Coordinate a, Coordinate b, Coordinate c, + Coordinate p) { + DD aTerm = (DD.sqr(a.x).selfAdd(DD.sqr(a.y))) + .selfMultiply(triAreaDDFast(b, c, p)); + DD bTerm = (DD.sqr(b.x).selfAdd(DD.sqr(b.y))) + .selfMultiply(triAreaDDFast(a, c, p)); + DD cTerm = (DD.sqr(c.x).selfAdd(DD.sqr(c.y))) + .selfMultiply(triAreaDDFast(a, b, p)); + DD pTerm = (DD.sqr(p.x).selfAdd(DD.sqr(p.y))) + .selfMultiply(triAreaDDFast(a, b, c)); + + DD sum = aTerm.selfSubtract(bTerm).selfAdd(cTerm).selfSubtract(pTerm); + boolean isInCircle = sum.doubleValue() > 0; + + return isInCircle; + } + + public static DD triAreaDDFast( + Coordinate a, Coordinate b, Coordinate c) { + + DD t1 = DD.valueOf(b.x).selfSubtract(a.x) + .selfMultiply( + DD.valueOf(c.y).selfSubtract(a.y)); + + DD t2 = DD.valueOf(b.y).selfSubtract(a.y) + .selfMultiply( + DD.valueOf(c.x).selfSubtract(a.x)); + + return t1.selfSubtract(t2); + } + + public static boolean isInCircleDDNormalized( + Coordinate a, Coordinate b, Coordinate c, + Coordinate p) { + DD adx = DD.valueOf(a.x).selfSubtract(p.x); + DD ady = DD.valueOf(a.y).selfSubtract(p.y); + DD bdx = DD.valueOf(b.x).selfSubtract(p.x); + DD bdy = DD.valueOf(b.y).selfSubtract(p.y); + DD cdx = DD.valueOf(c.x).selfSubtract(p.x); + DD cdy = DD.valueOf(c.y).selfSubtract(p.y); + + DD abdet = adx.multiply(bdy).selfSubtract(bdx.multiply(ady)); + DD bcdet = bdx.multiply(cdy).selfSubtract(cdx.multiply(bdy)); + DD cadet = cdx.multiply(ady).selfSubtract(adx.multiply(cdy)); + DD alift = adx.multiply(adx).selfAdd(ady.multiply(ady)); + DD blift = bdx.multiply(bdx).selfAdd(bdy.multiply(bdy)); + DD clift = cdx.multiply(cdx).selfAdd(cdy.multiply(cdy)); + + DD sum = alift.selfMultiply(bcdet) + .selfAdd(blift.selfMultiply(cadet)) + .selfAdd(clift.selfMultiply(abdet)); + + boolean isInCircle = sum.doubleValue() > 0; + + return isInCircle; + } +*/ + +/** + * Computes the inCircle test using distance from the circumcentre. + * Uses standard double-precision arithmetic. + *

+ * In general this doesn't + * appear to be any more robust than the standard calculation. However, there + * is at least one case where the test point is far enough from the + * circumcircle that this test gives the correct answer. + *

+   * LINESTRING
+   * (1507029.9878 518325.7547, 1507022.1120341457 518332.8225183258,
+   * 1507029.9833 518325.7458, 1507029.9896965567 518325.744909031)
+   * 
+ * + * @param a a vertex of the triangle + * @param b a vertex of the triangle + * @param c a vertex of the triangle + * @param p the point to test + * @return true if this point is inside the circle defined by the points a, b, c + public static boolean isInCircleCC(Coordinate a, Coordinate b, Coordinate c, + Coordinate p) { + Coordinate cc = Triangle.circumcentre(a, b, c); + double ccRadius = a.distance(cc); + double pRadiusDiff = p.distance(cc) - ccRadius; + return pRadiusDiff <= 0; + } +*/ + +/** + * Checks if the computed value for isInCircle is correct, using + * double-double precision arithmetic. + * + * @param a a vertex of the triangle + * @param b a vertex of the triangle + * @param c a vertex of the triangle + * @param p the point to test +private static void checkRobustInCircle(Coordinate a, Coordinate b, Coordinate c, + Coordinate p) +{ + boolean nonRobustInCircle = isInCircleNonRobust(a, b, c, p); + boolean isInCircleDD = TrianglePredicate.isInCircleDDSlow(a, b, c, p); + boolean isInCircleCC = TrianglePredicate.isInCircleCC(a, b, c, p); + + Coordinate circumCentre = Triangle.circumcentre(a, b, c); + System.out.println("p radius diff a = " + + Math.abs(p.distance(circumCentre) - a.distance(circumCentre)) + / a.distance(circumCentre)); + + if (nonRobustInCircle != isInCircleDD || nonRobustInCircle != isInCircleCC) { + System.out.println("inCircle robustness failure (double result = " + + nonRobustInCircle + + ", DD result = " + isInCircleDD + + ", CC result = " + isInCircleCC + ")"); + System.out.println(WKTWriter.toLineString(new CoordinateArraySequence( + new Coordinate[] { a, b, c, p }))); + System.out.println("Circumcentre = " + WKTWriter.toPoint(circumCentre) + + " radius = " + a.distance(circumCentre)); + System.out.println("p radius diff a = " + + Math.abs(p.distance(circumCentre)/a.distance(circumCentre) - 1)); + System.out.println("p radius diff b = " + + Math.abs(p.distance(circumCentre)/b.distance(circumCentre) - 1)); + System.out.println("p radius diff c = " + + Math.abs(p.distance(circumCentre)/c.distance(circumCentre) - 1)); + System.out.println(); + } +} + + +} +*/ diff --git a/planar/triangulate/quadedge/trianglepredicate_test.go b/planar/triangulate/quadedge/trianglepredicate_test.go new file mode 100644 index 00000000..6143cd43 --- /dev/null +++ b/planar/triangulate/quadedge/trianglepredicate_test.go @@ -0,0 +1,44 @@ +package quadedge + +import ( + "strconv" + "testing" +) + +func TestIsInCircleRobust(t *testing.T) { + type tcase struct { + circle [3]Vertex + point Vertex + expected bool + } + + fn := func(t *testing.T, tc tcase) { + r := TrianglePredicate.IsInCircleRobust(tc.circle[0], tc.circle[1], tc.circle[2], tc.point) + if r != tc.expected { + t.Errorf("error, expected %v got %v", tc.expected, r) + return + } + } + testcases := []tcase{ + { + circle: [3]Vertex{Vertex{10, 10}, Vertex{15, 120}, Vertex{10, 20}}, + point: Vertex{20, 10}, + expected: true, + }, + { + circle: [3]Vertex{Vertex{15, 120}, Vertex{10, 10}, Vertex{20, 20}}, + point: Vertex{10, 20}, + expected: true, + }, + { + circle: [3]Vertex{Vertex{10, 10}, Vertex{15, 120}, Vertex{10, 20}}, + point: Vertex{0, 10}, + expected: false, + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} diff --git a/planar/triangulate/quadedge/vertex.go b/planar/triangulate/quadedge/vertex.go new file mode 100644 index 00000000..ead0b383 --- /dev/null +++ b/planar/triangulate/quadedge/vertex.go @@ -0,0 +1,352 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package quadedge + +import ( + "encoding/json" + "math" + + "github.com/go-spatial/geom/planar" +) + +const ( + LEFT = iota + RIGHT + BEYOND + BEHIND + BETWEEN + ORIGIN + DESTINATION +) + +/* +Vertex models a site (node) in a QuadEdgeSubdivision. The sites can be points +on a line string representing a linear site. + +The vertex can be considered as a vector with a norm, length, inner product, +cross product, etc. Additionally, point relations (e.g., is a point to the +left of a line, the circle defined by this point and two others, etc.) are +also defined in this class. + +Author David Skea +Author Martin Davis +Ported to Go by Jason R. Surratt +*/ +type Vertex [2]float64 + +// XY implements the geom.Pointer interface +func (u Vertex) XY() [2]float64 { return u } +func (u Vertex) X() float64 { return u[0] } +func (u Vertex) Y() float64 { return u[1] } + +func (u Vertex) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + X float64 + Y float64 + }{ + u.X(), + u.Y(), + }) +} + +func (u Vertex) Equals(other Vertex) bool { + if u.X() == other.X() && u.Y() == other.Y() { + return true + } + return false +} + +func (u Vertex) EqualsTolerance(other Vertex, tolerance float64) bool { + if planar.PointDistance(u, other) < tolerance { + return true + } + return false +} + +func (u Vertex) Classify(p0 Vertex, p1 Vertex) int { + p2 := u + a := p1.Sub(p0) + b := p2.Sub(p0) + sa := a.CrossProduct(b) + + switch { + case sa > 0.0: + return LEFT + case sa < 0.0: + return RIGHT + case a.X()*b.X() < 0.0 || a.Y()*b.Y() < 0.0: + return BEHIND + case a.Magn() < b.Magn(): + return BEYOND + case p0.Equals(p2): + return ORIGIN + case p1.Equals(p2): + return DESTINATION + default: + return BETWEEN + } +} + +/* +CrossProduct computes the cross product k = u X v. + +@param v a vertex +@return returns the magnitude of u X v +*/ +func (u Vertex) CrossProduct(v Vertex) float64 { + return (u.X()*v.Y() - u.Y()*v.X()) +} + +/* +Computes the inner or dot product + +@param v a vertex +@return returns the dot product u.v +*/ +func (u Vertex) Dot(v Vertex) float64 { + return u.X()*v.X() + u.Y()*v.Y() +} + +/* +Times computes the scalar product c(v) + +@param v a vertex +@return returns the scaled vector +*/ +func (u Vertex) Times(c float64) Vertex { + return Vertex{u.X() * c, u.Y() * c} +} + +// Sum u + v and return the new Vertex +func (u Vertex) Sum(v Vertex) Vertex { + return Vertex{u.X() + v.X(), u.Y() + v.Y()} +} + +// Sub subtracts u - v and returns the new Vertex +func (u Vertex) Sub(v Vertex) Vertex { + return Vertex{u.X() - v.X(), u.Y() - v.Y()} +} + +// Magn returns the magnitude of the vector +func (u Vertex) Magn() float64 { + return math.Sqrt(u.X()*u.X() + u.Y()*u.Y()) +} + +/* returns k X v (cross product). this is a vector perpendicular to v */ +func (u Vertex) Cross() Vertex { + return Vertex{u.Y(), -u.X()} +} + +/********************************************************************************************* +Geometric primitives / +**********************************************************************************************/ + +/* +IsInCircle tests if the vertex is inside the circle defined by +the triangle with vertices a, b, c (oriented counter-clockwise). + +a - a vertex of the triangle +b - a vertex of the triangle +c - a vertex of the triangle +Return true if this vertex is in the circumcircle of (a,b,c) +*/ +func (u Vertex) IsInCircle(a Vertex, b Vertex, c Vertex) bool { + return TrianglePredicate.IsInCircleRobust(a, b, c, u) + // non-robust - best to not use + //return TrianglePredicate.isInCircle(a.p, b.p, c.p, this.p); +} + +/** +IsCCW Tests whether the triangle formed by this vertex and two +other vertices is in CCW orientation. + +b - a vertex +c - a vertex +return true if the triangle is oriented CCW +*/ +func (u Vertex) IsCCW(b Vertex, c Vertex) bool { + // // test code used to check for robustness of triArea + // boolean isCCW = (b.p.x - p.x)(c.p.y - p.y) + // - (b.p.y - p.y)(c.p.x - p.x) > 0; + // //boolean isCCW = triArea(this, b, c) > 0; + // boolean isCCWRobust = CGAlgorithms.orientationIndex(p, b.p, c.p) == CGAlgorithms.COUNTERCLOCKWISE; + // if (isCCWRobust != isCCW) + // System.out.println("CCW failure"); + + // is equal to the signed area of the triangle + + return (b.X()-u.X())*(c.Y()-u.Y())-(b.Y()-u.Y())*(c.X()-u.X()) > 0 + + // original rolled code + //boolean isCCW = triArea(this, b, c) > 0; + //return isCCW; + +} + +func (u Vertex) RightOf(e QuadEdge) bool { + return u.IsCCW(e.Dest(), e.Orig()) +} + +func (u Vertex) LeftOf(e QuadEdge) bool { + return u.IsCCW(e.Orig(), e.Dest()) +} + +/* +private HCoordinate bisector(Vertex a, Vertex b) { + // returns the perpendicular bisector of the line segment ab + double dx = b.getX() - a.getX(); + double dy = b.getY() - a.getY(); + HCoordinate l1 = new HCoordinate(a.getX() + dx / 2.0, a.getY() + dy / 2.0, 1.0); + HCoordinate l2 = new HCoordinate(a.getX() - dy + dx / 2.0, a.getY() + dx + dy / 2.0, 1.0); + return new HCoordinate(l1, l2); +} + +private double distance(Vertex v1, Vertex v2) { + return Math.sqrt(Math.pow(v2.getX() - v1.getX(), 2.0) + + Math.pow(v2.getY() - v1.getY(), 2.0)); +} +*/ + +/** +Computes the value of the ratio of the circumradius to shortest edge. If smaller than some +given tolerance B, the associated triangle is considered skinny. For an equal lateral +triangle this value is 0.57735. The ratio is related to the minimum triangle angle theta by: +circumRadius/shortestEdge = 1/(2sin(theta)). + +@param b second vertex of the triangle +@param c third vertex of the triangle +@return ratio of circumradius to shortest edge. + +public double circumRadiusRatio(Vertex b, Vertex c) { + Vertex x = this.circleCenter(b, c); + double radius = distance(x, b); + double edgeLength = distance(this, b); + double el = distance(b, c); + if (el < edgeLength) { + edgeLength = el; + } + el = distance(c, this); + if (el < edgeLength) { + edgeLength = el; + } + return radius / edgeLength; +} +*/ + +/** +returns a new vertex that is mid-way between this vertex and another end point. + +@param a the other end point. +@return the point mid-way between this and that. + +public Vertex midPoint(Vertex a) { + double xm = (p.x + a.getX()) / 2.0; + double ym = (p.y + a.getY()) / 2.0; + double zm = (p.z + a.getZ()) / 2.0; + return new Vertex(xm, ym, zm); +} +*/ + +/** +Computes the centre of the circumcircle of this vertex and two others. + +@param b +@param c +@return the Coordinate which is the circumcircle of the 3 points. + +public Vertex circleCenter(Vertex b, Vertex c) { + Vertex a = new Vertex(this.getX(), this.getY()); + // compute the perpendicular bisector of cord ab + HCoordinate cab = bisector(a, b); + // compute the perpendicular bisector of cord bc + HCoordinate cbc = bisector(b, c); + // compute the intersection of the bisectors (circle radii) + HCoordinate hcc = new HCoordinate(cab, cbc); + Vertex cc = null; + try { + cc = new Vertex(hcc.getX(), hcc.getY()); + } catch (NotRepresentableException nre) { + System.err.println("a: " + a + " b: " + b + " c: " + c); + System.err.println(nre); + } + return cc; +} +*/ + +/** +For this vertex enclosed in a triangle defined by three vertices v0, v1 and v2, interpolate +a z value from the surrounding vertices. + +public double interpolateZValue(Vertex v0, Vertex v1, Vertex v2) { + double x0 = v0.getX(); + double y0 = v0.getY(); + double a = v1.getX() - x0; + double b = v2.getX() - x0; + double c = v1.getY() - y0; + double d = v2.getY() - y0; + double det = ad - bc; + double dx = this.getX() - x0; + double dy = this.getY() - y0; + double t = (ddx - bdy) / det; + double u = (-cdx + ady) / det; + double z = v0.getZ() + t(v1.getZ() - v0.getZ()) + u(v2.getZ() - v0.getZ()); + return z; +} +*/ + +/** +Interpolates the Z-value (height) of a point enclosed in a triangle +whose vertices all have Z values. +The containing triangle must not be degenerate +(in other words, the three vertices must enclose a +non-zero area). + +@param p the point to interpolate the Z value of +@param v0 a vertex of a triangle containing the p +@param v1 a vertex of a triangle containing the p +@param v2 a vertex of a triangle containing the p +@return the interpolated Z-value (height) of the point + +public static double interpolateZ(Coordinate p, Coordinate v0, Coordinate v1, Coordinate v2) { + double x0 = v0.x; + double y0 = v0.y; + double a = v1.x - x0; + double b = v2.x - x0; + double c = v1.y - y0; + double d = v2.y - y0; + double det = ad - bc; + double dx = p.x - x0; + double dy = p.y - y0; + double t = (ddx - bdy) / det; + double u = (-cdx + ady) / det; + double z = v0.z + t(v1.z - v0.z) + u(v2.z - v0.z); + return z; +} +*/ + +/** +Computes the interpolated Z-value for a point p lying on the segment p0-p1 + +@param p +@param p0 +@param p1 +@return the interpolated Z value + +public static double interpolateZ(Coordinate p, Coordinate p0, Coordinate p1) { + double segLen = p0.distance(p1); + double ptLen = p.distance(p0); + double dz = p1.z - p0.z; + double pz = p0.z + dz(ptLen / segLen); + return pz; +} +*/ diff --git a/planar/triangulate/quadedge/vertex_test.go b/planar/triangulate/quadedge/vertex_test.go new file mode 100644 index 00000000..f42d4e9e --- /dev/null +++ b/planar/triangulate/quadedge/vertex_test.go @@ -0,0 +1,102 @@ +package quadedge + +import ( + "strconv" + "testing" +) + +func TestVertexEquals(t *testing.T) { + type tcase struct { + v1 Vertex + v2 Vertex + expected bool + } + + fn := func(t *testing.T, tc tcase) { + r := tc.v1.Equals(tc.v2) + if r != tc.expected { + t.Errorf("error, expected %v got %v", tc.expected, r) + return + } + } + testcases := []tcase{ + { + v1: Vertex{1, 2}, + v2: Vertex{1, 2}, + expected: true, + }, + { + v1: Vertex{1, 2}, + v2: Vertex{2, 3}, + expected: false, + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +func TestVertexEqualsTolerance(t *testing.T) { + type tcase struct { + v1 Vertex + v2 Vertex + tolerance float64 + expected bool + } + + fn := func(t *testing.T, tc tcase) { + r := tc.v1.EqualsTolerance(tc.v2, tc.tolerance) + if r != tc.expected { + t.Errorf("error, expected %v got %v", tc.expected, r) + return + } + } + testcases := []tcase{ + {Vertex{1, 2}, Vertex{1, 2}, 0.1, true}, + {Vertex{1, 2}, Vertex{1.09, 2}, 0.1, true}, + {Vertex{1, 2}, Vertex{1.1, 2}, 0.1, false}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +/* +TestVertexClassify tests for basic classification values. The test is quite simplistic +in that it only tests against a vertical vector, but should be good enough for a sniff +test. +*/ +func TestVertexClassify(t *testing.T) { + type tcase struct { + u Vertex + p0 Vertex + p1 Vertex + expected int + } + + fn := func(t *testing.T, tc tcase) { + r := tc.u.Classify(tc.p0, tc.p1) + if r != tc.expected { + t.Errorf("error, expected %v got %v", tc.expected, r) + return + } + } + testcases := []tcase{ + {Vertex{1.1, 2.5}, Vertex{1, 2}, Vertex{1, 3}, RIGHT}, + {Vertex{0.9, 2.5}, Vertex{1, 2}, Vertex{1, 3}, LEFT}, + {Vertex{1, 1}, Vertex{1, 2}, Vertex{1, 3}, BEHIND}, + {Vertex{1, 4}, Vertex{1, 2}, Vertex{1, 3}, BEYOND}, + {Vertex{1, 2}, Vertex{1, 2}, Vertex{1, 3}, ORIGIN}, + {Vertex{1, 3}, Vertex{1, 2}, Vertex{1, 3}, DESTINATION}, + {Vertex{1, 2.5}, Vertex{1, 2}, Vertex{1, 3}, BETWEEN}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} From 836a1ec63c5436372cfca848d7c748bcd69ace66 Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Tue, 8 May 2018 13:04:37 -0600 Subject: [PATCH 02/12] Add more test cases for delaunay --- planar/triangulate/delaunay_test.go | 76 +++++++---------------------- 1 file changed, 18 insertions(+), 58 deletions(-) diff --git a/planar/triangulate/delaunay_test.go b/planar/triangulate/delaunay_test.go index 1393c19a..f6abe1d7 100644 --- a/planar/triangulate/delaunay_test.go +++ b/planar/triangulate/delaunay_test.go @@ -102,6 +102,24 @@ func TestDelaunayTriangulation(t *testing.T) { // MULTIPOLYGON expectedTris: "MULTIPOLYGON (((0 20,0 10,10 10,0 20)),((0 20,10 10,10 20,0 20)),((10 20,10 10,20 10,10 20)),((10 20,20 10,20 20,10 20)),((10 0,20 0,10 10,10 0)),((10 0,10 10,0 10,10 0)),((10 0,0 10,0 0,10 0)),((10 10,20 0,20 10,10 10)))", }, + { + inputWKT: "MULTIPOINT ((50 40),(140 70),(80 100),(130 140),(30 150),(70 180),(190 110),(120 20))", + inputWKB: "01040000000800000001010000000000000000004940000000000000444001010000000000000000806140000000000080514001010000000000000000005440000000000000594001010000000000000000406040000000000080614001010000000000000000003e400000000000c0624001010000000000000000805140000000000080664001010000000000000000c067400000000000805b4001010000000000000000005e400000000000003440", + expectedEdges: "MULTILINESTRING ((70 180,190 110),(30 150,70 180),(30 150,50 40),(50 40,120 20),(120 20,190 110),(120 20,140 70),(140 70,190 110),(130 140,140 70),(130 140,190 110),(70 180,130 140),(80 100,130 140),(70 180,80 100),(30 150,80 100),(50 40,80 100),(80 100,120 20),(80 100,140 70))", + expectedTris: "MULTIPOLYGON (((30 150,50 40,80 100,30 150)),((30 150,80 100,70 180,30 150)),((70 180,80 100,130 140,70 180)),((70 180,130 140,190 110,70 180)),((190 110,130 140,140 70,190 110)),((190 110,140 70,120 20,190 110)),((120 20,140 70,80 100,120 20)),((120 20,80 100,50 40,120 20)),((80 100,140 70,130 140,80 100)))", + }, + { + inputWKT: "POLYGON ((42 30, 41.96 29.61, 41.85 29.23, 41.66 28.89, 41.41 28.59, 41.11 28.34, 40.77 28.15, 40.39 28.04, 40 28, 39.61 28.04, 39.23 28.15, 38.89 28.34, 38.59 28.59, 38.34 28.89, 38.15 29.23, 38.04 29.61, 38 30, 38.04 30.39, 38.15 30.77, 38.34 31.11, 38.59 31.41, 38.89 31.66, 39.23 31.85, 39.61 31.96, 40 32, 40.39 31.96, 40.77 31.85, 41.11 31.66, 41.41 31.41, 41.66 31.11, 41.85 30.77, 41.96 30.39, 42 30))", + inputWKB: "0103000000010000002100000000000000000045400000000000003e407b14ae47e1fa44405c8fc2f5289c3d40cdccccccccec44407b14ae47e13a3d4014ae47e17ad44440a4703d0ad7e33c4014ae47e17ab44440d7a3703d0a973c40ae47e17a148e4440d7a3703d0a573c40c3f5285c8f6244406666666666263c4052b81e85eb3144400ad7a3703d0a3c4000000000000044400000000000003c40ae47e17a14ce43400ad7a3703d0a3c403d0ad7a3709d43406666666666263c4052b81e85eb714340d7a3703d0a573c40ec51b81e854b4340d7a3703d0a973c40ec51b81e852b4340a4703d0ad7e33c4033333333331343407b14ae47e13a3d4085eb51b81e0543405c8fc2f5289c3d4000000000000043400000000000003e4085eb51b81e054340a4703d0ad7633e40333333333313434085eb51b81ec53e40ec51b81e852b43405c8fc2f5281c3f40ec51b81e854b4340295c8fc2f5683f4052b81e85eb714340295c8fc2f5a83f403d0ad7a3709d43409a99999999d93f40ae47e17a14ce4340f6285c8fc2f53f400000000000004440000000000000404052b81e85eb314440f6285c8fc2f53f40c3f5285c8f6244409a99999999d93f40ae47e17a148e4440295c8fc2f5a83f4014ae47e17ab44440295c8fc2f5683f4014ae47e17ad444405c8fc2f5281c3f40cdccccccccec444085eb51b81ec53e407b14ae47e1fa4440a4703d0ad7633e4000000000000045400000000000003e40", + expectedEdges: "MULTILINESTRING ((41.66 31.11,41.85 30.77),(41.41 31.41,41.66 31.11),(41.11 31.66,41.41 31.41),(40.77 31.85,41.11 31.66),(40.39 31.96,40.77 31.85),(40 32,40.39 31.96),(39.61 31.96,40 32),(39.23 31.85,39.61 31.96),(38.89 31.66,39.23 31.85),(38.59 31.41,38.89 31.66),(38.34 31.11,38.59 31.41),(38.15 30.77,38.34 31.11),(38.04 30.39,38.15 30.77),(38 30,38.04 30.39),(38 30,38.04 29.61),(38.04 29.61,38.15 29.23),(38.15 29.23,38.34 28.89),(38.34 28.89,38.59 28.59),(38.59 28.59,38.89 28.34),(38.89 28.34,39.23 28.15),(39.23 28.15,39.61 28.04),(39.61 28.04,40 28),(40 28,40.39 28.04),(40.39 28.04,40.77 28.15),(40.77 28.15,41.11 28.34),(41.11 28.34,41.41 28.59),(41.41 28.59,41.66 28.89),(41.66 28.89,41.85 29.23),(41.85 29.23,41.96 29.61),(41.96 29.61,42 30),(41.96 30.39,42 30),(41.85 30.77,41.96 30.39),(41.66 31.11,41.96 30.39),(41.41 31.41,41.96 30.39),(41.41 28.59,41.96 30.39),(41.41 28.59,41.41 31.41),(38.59 28.59,41.41 28.59),(38.59 28.59,41.41 31.41),(38.59 28.59,38.59 31.41),(38.59 31.41,41.41 31.41),(38.59 31.41,39.61 31.96),(39.61 31.96,41.41 31.41),(39.61 31.96,40.39 31.96),(40.39 31.96,41.41 31.41),(40.39 31.96,41.11 31.66),(38.04 30.39,38.59 28.59),(38.04 30.39,38.59 31.41),(38.04 30.39,38.34 31.11),(38.04 29.61,38.59 28.59),(38.04 29.61,38.04 30.39),(39.61 28.04,41.41 28.59),(38.59 28.59,39.61 28.04),(38.89 28.34,39.61 28.04),(40.39 28.04,41.41 28.59),(39.61 28.04,40.39 28.04),(41.96 29.61,41.96 30.39),(41.41 28.59,41.96 29.61),(41.66 28.89,41.96 29.61),(40.39 28.04,41.11 28.34),(38.04 29.61,38.34 28.89),(38.89 31.66,39.61 31.96))", + expectedTris: "MULTIPOLYGON (((38.15 30.77,38.04 30.39,38.34 31.11,38.15 30.77)),((38.34 31.11,38.04 30.39,38.59 31.41,38.34 31.11)),((38.59 31.41,38.04 30.39,38.59 28.59,38.59 31.41)),((38.59 31.41,38.59 28.59,41.41 31.41,38.59 31.41)),((38.59 31.41,41.41 31.41,39.61 31.96,38.59 31.41)),((38.59 31.41,39.61 31.96,38.89 31.66,38.59 31.41)),((38.89 31.66,39.61 31.96,39.23 31.85,38.89 31.66)),((39.61 31.96,41.41 31.41,40.39 31.96,39.61 31.96)),((39.61 31.96,40.39 31.96,40 32,39.61 31.96)),((40.39 31.96,41.41 31.41,41.11 31.66,40.39 31.96)),((40.39 31.96,41.11 31.66,40.77 31.85,40.39 31.96)),((41.41 31.41,38.59 28.59,41.41 28.59,41.41 31.41)),((41.41 31.41,41.41 28.59,41.96 30.39,41.41 31.41)),((41.41 31.41,41.96 30.39,41.66 31.11,41.41 31.41)),((41.66 31.11,41.96 30.39,41.85 30.77,41.66 31.11)),((40 28,40.39 28.04,39.61 28.04,40 28)),((39.61 28.04,40.39 28.04,41.41 28.59,39.61 28.04)),((39.61 28.04,41.41 28.59,38.59 28.59,39.61 28.04)),((39.61 28.04,38.59 28.59,38.89 28.34,39.61 28.04)),((39.61 28.04,38.89 28.34,39.23 28.15,39.61 28.04)),((41.41 28.59,40.39 28.04,41.11 28.34,41.41 28.59)),((41.11 28.34,40.39 28.04,40.77 28.15,41.11 28.34)),((41.41 28.59,41.66 28.89,41.96 29.61,41.41 28.59)),((41.41 28.59,41.96 29.61,41.96 30.39,41.41 28.59)),((41.96 30.39,41.96 29.61,42 30,41.96 30.39)),((41.96 29.61,41.66 28.89,41.85 29.23,41.96 29.61)),((38.59 28.59,38.04 30.39,38.04 29.61,38.59 28.59)),((38.59 28.59,38.04 29.61,38.34 28.89,38.59 28.59)),((38.34 28.89,38.04 29.61,38.15 29.23,38.34 28.89)),((38.04 29.61,38.04 30.39,38 30,38.04 29.61)))", + }, + { + inputWKT: "POLYGON ((0 0, 0 200, 180 200, 180 0, 0 0), (20 180, 160 180, 160 20, 152.625 146.75, 20 180), (30 160, 150 30, 70 90, 30 160))", + inputWKB: "010300000003000000050000000000000000000000000000000000000000000000000000000000000000006940000000000080664000000000000069400000000000806640000000000000000000000000000000000000000000000000050000000000000000003440000000000080664000000000000064400000000000806640000000000000644000000000000034400000000000146340000000000058624000000000000034400000000000806640040000000000000000003e4000000000000064400000000000c062400000000000003e40000000000080514000000000008056400000000000003e400000000000006440", + expectedEdges: "MULTILINESTRING ((0 200,180 200),(0 0,0 200),(0 0,180 0),(180 0,180 200),(152.625 146.75,180 0),(152.625 146.75,180 200),(152.625 146.75,160 180),(160 180,180 200),(0 200,160 180),(20 180,160 180),(0 200,20 180),(20 180,30 160),(0 200,30 160),(0 0,30 160),(30 160,70 90),(0 0,70 90),(70 90,150 30),(0 0,150 30),(150 30,160 20),(0 0,160 20),(160 20,180 0),(152.625 146.75,160 20),(150 30,152.625 146.75),(70 90,152.625 146.75),(30 160,152.625 146.75),(30 160,160 180))", + expectedTris: "MULTIPOLYGON (((0 200,0 0,30 160,0 200)),((0 200,30 160,20 180,0 200)),((0 200,20 180,160 180,0 200)),((0 200,160 180,180 200,0 200)),((180 200,160 180,152.625 146.75,180 200)),((180 200,152.625 146.75,180 0,180 200)),((0 0,180 0,160 20,0 0)),((0 0,160 20,150 30,0 0)),((0 0,150 30,70 90,0 0)),((0 0,70 90,30 160,0 0)),((30 160,70 90,152.625 146.75,30 160)),((30 160,152.625 146.75,160 180,30 160)),((30 160,160 180,20 180,30 160)),((152.625 146.75,70 90,150 30,152.625 146.75)),((152.625 146.75,150 30,160 20,152.625 146.75)),((152.625 146.75,160 20,180 0,152.625 146.75)))", + }, } for i, tc := range testcases { @@ -109,61 +127,3 @@ func TestDelaunayTriangulation(t *testing.T) { t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) } } - -/* - public void testRandom() - throws ParseException - { - String wkt = "MULTIPOINT ((50 40), (140 70), (80 100), (130 140), (30 150), (70 180), (190 110), (120 20))"; - String expected = "MULTILINESTRING ((70 180, 190 110), (30 150, 70 180), (30 150, 50 40), (50 40, 120 20), (190 110, 120 20), (120 20, 140 70), (190 110, 140 70), (130 140, 140 70), (130 140, 190 110), (70 180, 130 140), (80 100, 130 140), (70 180, 80 100), (30 150, 80 100), (50 40, 80 100), (80 100, 120 20), (80 100, 140 70))"; - runDelaunayEdges(wkt, expected); - String expectedTri = "GEOMETRYCOLLECTION (POLYGON ((30 150, 50 40, 80 100, 30 150)), POLYGON ((30 150, 80 100, 70 180, 30 150)), POLYGON ((70 180, 80 100, 130 140, 70 180)), POLYGON ((70 180, 130 140, 190 110, 70 180)), POLYGON ((190 110, 130 140, 140 70, 190 110)), POLYGON ((190 110, 140 70, 120 20, 190 110)), POLYGON ((120 20, 140 70, 80 100, 120 20)), POLYGON ((120 20, 80 100, 50 40, 120 20)), POLYGON ((80 100, 140 70, 130 140, 80 100)))"; - runDelaunay(wkt, true, expectedTri); - } - - public void testCircle() - throws ParseException - { - String wkt = "POLYGON ((42 30, 41.96 29.61, 41.85 29.23, 41.66 28.89, 41.41 28.59, 41.11 28.34, 40.77 28.15, 40.39 28.04, 40 28, 39.61 28.04, 39.23 28.15, 38.89 28.34, 38.59 28.59, 38.34 28.89, 38.15 29.23, 38.04 29.61, 38 30, 38.04 30.39, 38.15 30.77, 38.34 31.11, 38.59 31.41, 38.89 31.66, 39.23 31.85, 39.61 31.96, 40 32, 40.39 31.96, 40.77 31.85, 41.11 31.66, 41.41 31.41, 41.66 31.11, 41.85 30.77, 41.96 30.39, 42 30))"; - String expected = "MULTILINESTRING ((41.66 31.11, 41.85 30.77), (41.41 31.41, 41.66 31.11), (41.11 31.66, 41.41 31.41), (40.77 31.85, 41.11 31.66), (40.39 31.96, 40.77 31.85), (40 32, 40.39 31.96), (39.61 31.96, 40 32), (39.23 31.85, 39.61 31.96), (38.89 31.66, 39.23 31.85), (38.59 31.41, 38.89 31.66), (38.34 31.11, 38.59 31.41), (38.15 30.77, 38.34 31.11), (38.04 30.39, 38.15 30.77), (38 30, 38.04 30.39), (38 30, 38.04 29.61), (38.04 29.61, 38.15 29.23), (38.15 29.23, 38.34 28.89), (38.34 28.89, 38.59 28.59), (38.59 28.59, 38.89 28.34), (38.89 28.34, 39.23 28.15), (39.23 28.15, 39.61 28.04), (39.61 28.04, 40 28), (40 28, 40.39 28.04), (40.39 28.04, 40.77 28.15), (40.77 28.15, 41.11 28.34), (41.11 28.34, 41.41 28.59), (41.41 28.59, 41.66 28.89), (41.66 28.89, 41.85 29.23), (41.85 29.23, 41.96 29.61), (41.96 29.61, 42 30), (41.96 30.39, 42 30), (41.85 30.77, 41.96 30.39), (41.66 31.11, 41.96 30.39), (41.41 31.41, 41.96 30.39), (41.41 28.59, 41.96 30.39), (41.41 28.59, 41.41 31.41), (38.59 28.59, 41.41 28.59), (38.59 28.59, 41.41 31.41), (38.59 28.59, 38.59 31.41), (38.59 31.41, 41.41 31.41), (38.59 31.41, 39.61 31.96), (39.61 31.96, 41.41 31.41), (39.61 31.96, 40.39 31.96), (40.39 31.96, 41.41 31.41), (40.39 31.96, 41.11 31.66), (38.04 30.39, 38.59 28.59), (38.04 30.39, 38.59 31.41), (38.04 30.39, 38.34 31.11), (38.04 29.61, 38.59 28.59), (38.04 29.61, 38.04 30.39), (39.61 28.04, 41.41 28.59), (38.59 28.59, 39.61 28.04), (38.89 28.34, 39.61 28.04), (40.39 28.04, 41.41 28.59), (39.61 28.04, 40.39 28.04), (41.96 29.61, 41.96 30.39), (41.41 28.59, 41.96 29.61), (41.66 28.89, 41.96 29.61), (40.39 28.04, 41.11 28.34), (38.04 29.61, 38.34 28.89), (38.89 31.66, 39.61 31.96))"; - runDelaunayEdges(wkt, expected); - } - - public void testPolygonWithChevronHoles() - throws ParseException - { - String wkt = "POLYGON ((0 0, 0 200, 180 200, 180 0, 0 0), (20 180, 160 180, 160 20, 152.625 146.75, 20 180), (30 160, 150 30, 70 90, 30 160))"; - String expected = "MULTILINESTRING ((0 200, 180 200), (0 0, 0 200), (0 0, 180 0), (180 200, 180 0), (152.625 146.75, 180 0), (152.625 146.75, 180 200), (152.625 146.75, 160 180), (160 180, 180 200), (0 200, 160 180), (20 180, 160 180), (0 200, 20 180), (20 180, 30 160), (30 160, 0 200), (0 0, 30 160), (30 160, 70 90), (0 0, 70 90), (70 90, 150 30), (150 30, 0 0), (150 30, 160 20), (0 0, 160 20), (160 20, 180 0), (152.625 146.75, 160 20), (150 30, 152.625 146.75), (70 90, 152.625 146.75), (30 160, 152.625 146.75), (30 160, 160 180))"; - runDelaunayEdges(wkt, expected); - } - - static final double COMPARISON_TOLERANCE = 1.0e-7; - - void runDelaunayEdges(String sitesWKT, String expectedWKT) - throws ParseException - { - runDelaunay(sitesWKT, false, expectedWKT); - } - - void runDelaunay(String sitesWKT, boolean computeTriangles, String expectedWKT) - throws ParseException - { - Geometry sites = reader.read(sitesWKT); - DelaunayTriangulationBuilder builder = new DelaunayTriangulationBuilder(); - builder.setSites(sites); - - Geometry result = null; - if (computeTriangles) { - result = builder.getTriangles(geomFact); - } - else { - result = builder.getEdges(geomFact); - } - //System.out.println(result); - - Geometry expected = reader.read(expectedWKT); - result.normalize(); - expected.normalize(); - assertTrue(expected.equalsExact(result, COMPARISON_TOLERANCE)); - } -}*/ From ed1be697055a338e66bfe2732438164ae2e999e1 Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Tue, 15 May 2018 16:39:48 -0600 Subject: [PATCH 03/12] Adds minimalist constrained delaunay 1. Provides constrained delaunay triangulation & tests of small/simple polygons. No testing has been done with large/complex polygons. 2. This does not handle overlapping edges or vertices. --- geom.go | 76 +++ geom_test.go | 119 ++++ planar/algorithm/LICENSE | 30 + planar/algorithm/lineintersector.go | 401 ++++++++++++ planar/algorithm/robustlineintersector.go | 450 +++++++++++++ .../triangulate/constraineddelaunay/LICENSE | 21 + .../triangulate/constraineddelaunay/README.md | 12 + .../constraineddelaunay/triangle.go | 225 +++++++ .../constraineddelaunay/triangle_test.go | 3 + .../constraineddelaunay/triangulator.go | 591 ++++++++++++++++++ .../constraineddelaunay/triangulator_test.go | 248 ++++++++ planar/triangulate/delaunay_test.go | 15 +- .../delaunaytriangulationbuilder.go | 26 +- .../incrementaldelaunaytriangulator.go | 5 +- planar/triangulate/quadedge/debug.go | 84 +++ planar/triangulate/quadedge/quadedge.go | 21 +- .../quadedge/quadedgesubdivision.go | 109 ++-- .../quadedge/quadedgesubdivision_test.go | 1 + planar/triangulate/segment.go | 196 ++++++ 19 files changed, 2571 insertions(+), 62 deletions(-) create mode 100644 planar/algorithm/LICENSE create mode 100644 planar/algorithm/lineintersector.go create mode 100644 planar/algorithm/robustlineintersector.go create mode 100644 planar/triangulate/constraineddelaunay/LICENSE create mode 100644 planar/triangulate/constraineddelaunay/README.md create mode 100644 planar/triangulate/constraineddelaunay/triangle.go create mode 100644 planar/triangulate/constraineddelaunay/triangle_test.go create mode 100644 planar/triangulate/constraineddelaunay/triangulator.go create mode 100644 planar/triangulate/constraineddelaunay/triangulator_test.go create mode 100644 planar/triangulate/quadedge/debug.go create mode 100644 planar/triangulate/segment.go diff --git a/geom.go b/geom.go index f89fd810..2e2cccf6 100644 --- a/geom.go +++ b/geom.go @@ -133,3 +133,79 @@ func GetCoordinates(g Geometry) (pts []Point, err error) { err = getCoordinates(g, &pts) return pts, err } + +// extractLines is a helper function for ExtractLines to avoid too many +// array copies and still provide a convenient interface to the user. +func extractLines(g Geometry, lines *[]Line) error { + switch gg := g.(type) { + + default: + + return ErrUnknownGeometry + + case Pointer: + + return nil + + case MultiPointer: + + return nil + + case LineStringer: + + v := gg.Verticies() + for i := 0; i < len(v) - 1; i++ { + *lines = append(*lines, Line{v[i], v[i + 1]}) + } + return nil + + case MultiLineStringer: + + for _, ls := range gg.LineStrings() { + if err := extractLines(LineString(ls), lines); err != nil { + return err + } + } + return nil + + case Polygoner: + + for _, ls := range gg.LinearRings() { + if err := extractLines(LineString(ls), lines); err != nil { + return err + } + } + return nil + + case MultiPolygoner: + + for _, p := range gg.Polygons() { + if err := extractLines(Polygon(p), lines); err != nil { + return err + } + } + return nil + + case Collectioner: + + for _, child := range gg.Geometries() { + if err := extractLines(child, lines); err != nil { + return err + } + } + return nil + + } +} + +/* +ExtractLines extracts all linear components from a geometry (line segements). +If the geometry contains no line segements (e.g. empty geometry or +point), then an empty array will be returned. + +Duplicate lines will not be removed. +*/ +func ExtractLines(g Geometry) (lines []Line, err error) { + err = extractLines(g, &lines) + return lines, err +} diff --git a/geom_test.go b/geom_test.go index 3efb2d74..1644c8ba 100644 --- a/geom_test.go +++ b/geom_test.go @@ -124,3 +124,122 @@ func TestGetCoordinates(t *testing.T) { t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) } } + +func TestExtractLines(t *testing.T) { + + type tcase struct { + geom Geometry + expected []Line + err error + } + + fn := func(t *testing.T, tc tcase) { + r, err := ExtractLines(tc.geom) + if err != tc.err { + t.Errorf("error, expected %v got %v", tc.err, err) + } + if !(len(r) == 0 && len(tc.expected) == 0) && !reflect.DeepEqual(r, tc.expected) { + t.Errorf("error, expected %v got %v", tc.expected, r) + } + } + testcases := []tcase{ + { + geom: Point{10, 20}, + expected: []Line{}, + err: nil, + }, + { + geom: MultiPoint{ + {10, 20}, + {30, 40}, + {-10, -5}, + }, + expected: []Line{}, + err: nil, + }, + { + geom: LineString{ + {10, 20}, + {30, 40}, + {-10, -5}, + }, + expected: []Line{{{10, 20}, {30, 40}}, {{30, 40}, {-10, -5}}}, + err: nil, + }, + { + geom: MultiLineString{ + { + {10, 20}, + {30, 40}, + }, + { + {-10, -5}, + {15, 20}, + }, + }, + expected: []Line{{{10, 20}, {30, 40}}, {{-10, -5}, {15, 20}}}, + err: nil, + }, + { + geom: Polygon{ + { + {10, 20}, + {30, 40}, + {-10, -5}, + }, + { + {1, 2}, + {3, 4}, + }, + }, + expected: []Line{{{10, 20}, {30, 40}}, {{30, 40}, {-10, -5}}, {{1, 2}, {3, 4}}}, + err: nil, + }, + { + geom: MultiPolygon{ + { + { + {10, 20}, + {30, 40}, + {-10, -5}, + }, + { + {1, 2}, + {3, 4}, + }, + }, + { + { + {5, 6}, + {7, 8}, + {9, 10}, + }, + }, + }, + expected: []Line{{{10, 20}, {30, 40}}, {{30, 40}, {-10, -5}}, {{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}, {{7, 8}, {9, 10}}}, + err: nil, + }, + { + geom: Collection{ + Point{10, 20}, + MultiPoint{ + {10, 20}, + {30, 40}, + {-10, -5}, + }, + LineString{ + {1, 2}, + {3, 4}, + {5, 6}, + }, + }, + expected: []Line{{{1, 2}, {3, 4}}, {{3, 4}, {5, 6}}}, + err: nil, + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} diff --git a/planar/algorithm/LICENSE b/planar/algorithm/LICENSE new file mode 100644 index 00000000..1071fed9 --- /dev/null +++ b/planar/algorithm/LICENSE @@ -0,0 +1,30 @@ +Eclipse Distribution License - v 1.0 + +Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + Neither the name of the Eclipse Foundation, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/planar/algorithm/lineintersector.go b/planar/algorithm/lineintersector.go new file mode 100644 index 00000000..b9237a71 --- /dev/null +++ b/planar/algorithm/lineintersector.go @@ -0,0 +1,401 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package algorithm + +const ( + // Indicates that line segments do not intersect + NO_INTERSECTION = 0 + // Indicates that line segments intersect in a single point + POINT_INTERSECTION = 1 + // Indicates that line segments intersect in a line segment + COLLINEAR_INTERSECTION = 2 +) + +/* +LineIntersector is an algorithm that can both test whether +two line segments intersect and compute the intersection point(s) +if they do. + +There are three possible outcomes when determining whether two line segments intersect: + +NO_INTERSECTION - the segments do not intersect +POINT_INTERSECTION - the segments intersect in a single point +COLLINEAR_INTERSECTION - the segments are collinear and they intersect in a line segment + +For segments which intersect in a single point, the point may be either an endpoint +or in the interior of each segment. +If the point lies in the interior of both segments, +this is termed a proper intersection. +The method isProper() test for this situation. + +The intersection point(s) may be computed in a precise or non-precise manner. +Computing an intersection point precisely involves rounding it +via a supplied PrecisionModel. + +LineIntersectors do not perform an initial envelope intersection test +to determine if the segments are disjoint. +This is because this class is likely to be used in a context where +envelope overlap is already known to occur (or be likely). +*/ +type lineIntersector struct { + +} + +public abstract class LineIntersector +{ + + /** + * Computes the "edge distance" of an intersection point p along a segment. + * The edge distance is a metric of the point along the edge. + * The metric used is a robust and easy to compute metric function. + * It is not equivalent to the usual Euclidean metric. + * It relies on the fact that either the x or the y ordinates of the + * points in the edge are unique, depending on whether the edge is longer in + * the horizontal or vertical direction. + *

+ * NOTE: This function may produce incorrect distances + * for inputs where p is not precisely on p1-p2 + * (E.g. p = (139,9) p1 = (139,10), p2 = (280,1) produces distance 0.0, which is incorrect. + *

+ * My hypothesis is that the function is safe to use for points which are the + * result of rounding points which lie on the line, + * but not safe to use for truncated points. + */ + public static double computeEdgeDistance( + Coordinate p, + Coordinate p0, + Coordinate p1) + { + double dx = Math.abs(p1.x - p0.x); + double dy = Math.abs(p1.y - p0.y); + + double dist = -1.0; // sentinel value + if (p.equals(p0)) { + dist = 0.0; + } + else if (p.equals(p1)) { + if (dx > dy) + dist = dx; + else + dist = dy; + } + else { + double pdx = Math.abs(p.x - p0.x); + double pdy = Math.abs(p.y - p0.y); + if (dx > dy) + dist = pdx; + else + dist = pdy; + // + // hack to ensure that non-endpoints always have a non-zero distance + if (dist == 0.0 && ! p.equals(p0)) + { + dist = Math.max(pdx, pdy); + } + } + Assert.isTrue(! (dist == 0.0 && ! p.equals(p0)), "Bad distance calculation"); + return dist; + } + + /** + * This function is non-robust, since it may compute the square of large numbers. + * Currently not sure how to improve this. + */ + public static double nonRobustComputeEdgeDistance( + Coordinate p, + Coordinate p1, + Coordinate p2) + { + double dx = p.x - p1.x; + double dy = p.y - p1.y; + double dist = Math.sqrt(dx * dx + dy * dy); // dummy value + Assert.isTrue(! (dist == 0.0 && ! p.equals(p1)), "Invalid distance calculation"); + return dist; + } + + protected int result; + protected Coordinate[][] inputLines = new Coordinate[2][2]; + protected Coordinate[] intPt = new Coordinate[2]; + /** + * The indexes of the endpoints of the intersection lines, in order along + * the corresponding line + */ + protected int[][] intLineIndex; + protected boolean isProper; + protected Coordinate pa; + protected Coordinate pb; + /** + * If makePrecise is true, computed intersection coordinates will be made precise + * using Coordinate#makePrecise + */ + protected PrecisionModel precisionModel = null; +//public int numIntersects = 0; + + public LineIntersector() { + intPt[0] = new Coordinate(); + intPt[1] = new Coordinate(); + // alias the intersection points for ease of reference + pa = intPt[0]; + pb = intPt[1]; + result = 0; + } + + /** + * Force computed intersection to be rounded to a given precision model + * @param precisionModel + * @deprecated use setPrecisionModel instead + */ + public void setMakePrecise(PrecisionModel precisionModel) + { + this.precisionModel = precisionModel; + } + + /** + * Force computed intersection to be rounded to a given precision model. + * No getter is provided, because the precision model is not required to be specified. + * @param precisionModel + */ + public void setPrecisionModel(PrecisionModel precisionModel) + { + this.precisionModel = precisionModel; + } + + /** + * Gets an endpoint of an input segment. + * + * @param segmentIndex the index of the input segment (0 or 1) + * @param ptIndex the index of the endpoint (0 or 1) + * @return the specified endpoint + */ + public Coordinate getEndpoint(int segmentIndex, int ptIndex) + { + return inputLines[segmentIndex][ptIndex]; + } + + /** + * Compute the intersection of a point p and the line p1-p2. + * This function computes the boolean value of the hasIntersection test. + * The actual value of the intersection (if there is one) + * is equal to the value of p. + */ + public abstract void computeIntersection( + Coordinate p, + Coordinate p1, Coordinate p2); + + protected boolean isCollinear() { + return result == COLLINEAR_INTERSECTION; + } + + /** + * Computes the intersection of the lines p1-p2 and p3-p4. + * This function computes both the boolean value of the hasIntersection test + * and the (approximate) value of the intersection point itself (if there is one). + */ + public void computeIntersection( + Coordinate p1, Coordinate p2, + Coordinate p3, Coordinate p4) { + inputLines[0][0] = p1; + inputLines[0][1] = p2; + inputLines[1][0] = p3; + inputLines[1][1] = p4; + result = computeIntersect(p1, p2, p3, p4); +//numIntersects++; + } + + protected abstract int computeIntersect( + Coordinate p1, Coordinate p2, + Coordinate q1, Coordinate q2); + +/* + public String toString() { + String str = inputLines[0][0] + "-" + + inputLines[0][1] + " " + + inputLines[1][0] + "-" + + inputLines[1][1] + " : " + + getTopologySummary(); + return str; + } +*/ + + public String toString() { + return WKTWriter.toLineString(inputLines[0][0], inputLines[0][1]) + " - " + + WKTWriter.toLineString(inputLines[1][0], inputLines[1][1]) + + getTopologySummary(); + } + + private String getTopologySummary() + { + StringBuilder catBuilder = new StringBuilder(); + if (isEndPoint()) catBuilder.append(" endpoint"); + if (isProper) catBuilder.append(" proper"); + if (isCollinear()) catBuilder.append(" collinear"); + return catBuilder.toString(); + } + + protected boolean isEndPoint() { + return hasIntersection() && !isProper; + } + + /** + * Tests whether the input geometries intersect. + * + * @return true if the input geometries intersect + */ + public boolean hasIntersection() { + return result != NO_INTERSECTION; + } + + /** + * Returns the number of intersection points found. This will be either 0, 1 or 2. + * + * @return the number of intersection points found (0, 1, or 2) + */ + public int getIntersectionNum() { return result; } + + /** + * Returns the intIndex'th intersection point + * + * @param intIndex is 0 or 1 + * + * @return the intIndex'th intersection point + */ + public Coordinate getIntersection(int intIndex) { return intPt[intIndex]; } + + protected void computeIntLineIndex() { + if (intLineIndex == null) { + intLineIndex = new int[2][2]; + computeIntLineIndex(0); + computeIntLineIndex(1); + } + } + + /** + * Test whether a point is a intersection point of two line segments. + * Note that if the intersection is a line segment, this method only tests for + * equality with the endpoints of the intersection segment. + * It does not return true if + * the input point is internal to the intersection segment. + * + * @return true if the input point is one of the intersection points. + */ + public boolean isIntersection(Coordinate pt) { + for (int i = 0; i < result; i++) { + if (intPt[i].equals2D(pt)) { + return true; + } + } + return false; + } + + /** + * Tests whether either intersection point is an interior point of one of the input segments. + * + * @return true if either intersection point is in the interior of one of the input segments + */ + public boolean isInteriorIntersection() + { + if (isInteriorIntersection(0)) return true; + if (isInteriorIntersection(1)) return true; + return false; + } + + /** + * Tests whether either intersection point is an interior point of the specified input segment. + * + * @return true if either intersection point is in the interior of the input segment + */ + public boolean isInteriorIntersection(int inputLineIndex) + { + for (int i = 0; i < result; i++) { + if (! ( intPt[i].equals2D(inputLines[inputLineIndex][0]) + || intPt[i].equals2D(inputLines[inputLineIndex][1]) )) { + return true; + } + } + return false; + } + + /** + * Tests whether an intersection is proper. + *
+ * The intersection between two line segments is considered proper if + * they intersect in a single point in the interior of both segments + * (e.g. the intersection is a single point and is not equal to any of the + * endpoints). + *

+ * The intersection between a point and a line segment is considered proper + * if the point lies in the interior of the segment (e.g. is not equal to + * either of the endpoints). + * + * @return true if the intersection is proper + */ + public boolean isProper() { + return hasIntersection() && isProper; + } + + /** + * Computes the intIndex'th intersection point in the direction of + * a specified input line segment + * + * @param segmentIndex is 0 or 1 + * @param intIndex is 0 or 1 + * + * @return the intIndex'th intersection point in the direction of the specified input line segment + */ + public Coordinate getIntersectionAlongSegment(int segmentIndex, int intIndex) { + // lazily compute int line array + computeIntLineIndex(); + return intPt[intLineIndex[segmentIndex][intIndex]]; + } + + /** + * Computes the index (order) of the intIndex'th intersection point in the direction of + * a specified input line segment + * + * @param segmentIndex is 0 or 1 + * @param intIndex is 0 or 1 + * + * @return the index of the intersection point along the input segment (0 or 1) + */ + public int getIndexAlongSegment(int segmentIndex, int intIndex) { + computeIntLineIndex(); + return intLineIndex[segmentIndex][intIndex]; + } + + protected void computeIntLineIndex(int segmentIndex) { + double dist0 = getEdgeDistance(segmentIndex, 0); + double dist1 = getEdgeDistance(segmentIndex, 1); + if (dist0 > dist1) { + intLineIndex[segmentIndex][0] = 0; + intLineIndex[segmentIndex][1] = 1; + } + else { + intLineIndex[segmentIndex][0] = 1; + intLineIndex[segmentIndex][1] = 0; + } + } + + /** + * Computes the "edge distance" of an intersection point along the specified input line segment. + * + * @param segmentIndex is 0 or 1 + * @param intIndex is 0 or 1 + * + * @return the edge distance of the intersection point + */ + public double getEdgeDistance(int segmentIndex, int intIndex) { + double dist = computeEdgeDistance(intPt[intIndex], inputLines[segmentIndex][0], + inputLines[segmentIndex][1]); + return dist; + } +} diff --git a/planar/algorithm/robustlineintersector.go b/planar/algorithm/robustlineintersector.go new file mode 100644 index 00000000..627521fe --- /dev/null +++ b/planar/algorithm/robustlineintersector.go @@ -0,0 +1,450 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package algorithm + +type robustLineIntersector struct {} + +/* +RobustLineIntersector is a robust version of LineIntersector. + +(Taken from JTS) +*/ +var RobustLineIntersector robustLineIntersector + + + public void computeIntersection(Coordinate p, Coordinate p1, Coordinate p2) { + isProper = false; + // do between check first, since it is faster than the orientation test + if (Envelope.intersects(p1, p2, p)) { + if ((Orientation.index(p1, p2, p) == 0) + && (Orientation.index(p2, p1, p) == 0)) { + isProper = true; + if (p.equals(p1) || p.equals(p2)) { + isProper = false; + } + result = POINT_INTERSECTION; + return; + } + } + result = NO_INTERSECTION; + } + + protected int computeIntersect( + Coordinate p1, Coordinate p2, + Coordinate q1, Coordinate q2 ) { + isProper = false; + + // first try a fast test to see if the envelopes of the lines intersect + if (! Envelope.intersects(p1, p2, q1, q2)) + return NO_INTERSECTION; + + // for each endpoint, compute which side of the other segment it lies + // if both endpoints lie on the same side of the other segment, + // the segments do not intersect + int Pq1 = Orientation.index(p1, p2, q1); + int Pq2 = Orientation.index(p1, p2, q2); + + if ((Pq1>0 && Pq2>0) || (Pq1<0 && Pq2<0)) { + return NO_INTERSECTION; + } + + int Qp1 = Orientation.index(q1, q2, p1); + int Qp2 = Orientation.index(q1, q2, p2); + + if ((Qp1>0 && Qp2>0) || (Qp1<0 && Qp2<0)) { + return NO_INTERSECTION; + } + + boolean collinear = Pq1 == 0 + && Pq2 == 0 + && Qp1 == 0 + && Qp2 == 0; + if (collinear) { + return computeCollinearIntersection(p1, p2, q1, q2); + } + + /** + * At this point we know that there is a single intersection point + * (since the lines are not collinear). + */ + + /** + * Check if the intersection is an endpoint. If it is, copy the endpoint as + * the intersection point. Copying the point rather than computing it + * ensures the point has the exact value, which is important for + * robustness. It is sufficient to simply check for an endpoint which is on + * the other line, since at this point we know that the inputLines must + * intersect. + */ + if (Pq1 == 0 || Pq2 == 0 || Qp1 == 0 || Qp2 == 0) { + isProper = false; + + /** + * Check for two equal endpoints. + * This is done explicitly rather than by the orientation tests + * below in order to improve robustness. + * + * [An example where the orientation tests fail to be consistent is + * the following (where the true intersection is at the shared endpoint + * POINT (19.850257749638203 46.29709338043669) + * + * LINESTRING ( 19.850257749638203 46.29709338043669, 20.31970698357233 46.76654261437082 ) + * and + * LINESTRING ( -48.51001596420236 -22.063180333403878, 19.850257749638203 46.29709338043669 ) + * + * which used to produce the INCORRECT result: (20.31970698357233, 46.76654261437082, NaN) + * + */ + if (p1.equals2D(q1) + || p1.equals2D(q2)) { + intPt[0] = p1; + } + else if (p2.equals2D(q1) + || p2.equals2D(q2)) { + intPt[0] = p2; + } + + /** + * Now check to see if any endpoint lies on the interior of the other segment. + */ + else if (Pq1 == 0) { + intPt[0] = new Coordinate(q1); + } + else if (Pq2 == 0) { + intPt[0] = new Coordinate(q2); + } + else if (Qp1 == 0) { + intPt[0] = new Coordinate(p1); + } + else if (Qp2 == 0) { + intPt[0] = new Coordinate(p2); + } + } + else { + isProper = true; + intPt[0] = intersection(p1, p2, q1, q2); + } + return POINT_INTERSECTION; + } + + private int computeCollinearIntersection(Coordinate p1, Coordinate p2, + Coordinate q1, Coordinate q2) { + boolean p1q1p2 = Envelope.intersects(p1, p2, q1); + boolean p1q2p2 = Envelope.intersects(p1, p2, q2); + boolean q1p1q2 = Envelope.intersects(q1, q2, p1); + boolean q1p2q2 = Envelope.intersects(q1, q2, p2); + + if (p1q1p2 && p1q2p2) { + intPt[0] = q1; + intPt[1] = q2; + return COLLINEAR_INTERSECTION; + } + if (q1p1q2 && q1p2q2) { + intPt[0] = p1; + intPt[1] = p2; + return COLLINEAR_INTERSECTION; + } + if (p1q1p2 && q1p1q2) { + intPt[0] = q1; + intPt[1] = p1; + return q1.equals(p1) && !p1q2p2 && !q1p2q2 ? POINT_INTERSECTION : COLLINEAR_INTERSECTION; + } + if (p1q1p2 && q1p2q2) { + intPt[0] = q1; + intPt[1] = p2; + return q1.equals(p2) && !p1q2p2 && !q1p1q2 ? POINT_INTERSECTION : COLLINEAR_INTERSECTION; + } + if (p1q2p2 && q1p1q2) { + intPt[0] = q2; + intPt[1] = p1; + return q2.equals(p1) && !p1q1p2 && !q1p2q2 ? POINT_INTERSECTION : COLLINEAR_INTERSECTION; + } + if (p1q2p2 && q1p2q2) { + intPt[0] = q2; + intPt[1] = p2; + return q2.equals(p2) && !p1q1p2 && !q1p1q2 ? POINT_INTERSECTION : COLLINEAR_INTERSECTION; + } + return NO_INTERSECTION; + } + + /** + * This method computes the actual value of the intersection point. + * To obtain the maximum precision from the intersection calculation, + * the coordinates are normalized by subtracting the minimum + * ordinate values (in absolute value). This has the effect of + * removing common significant digits from the calculation to + * maintain more bits of precision. + */ + private Coordinate intersection( + Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) + { + Coordinate intPt = intersectionWithNormalization(p1, p2, q1, q2); + + /* + // TESTING ONLY + Coordinate intPtDD = CGAlgorithmsDD.intersection(p1, p2, q1, q2); + double dist = intPt.distance(intPtDD); + System.out.println(intPt + " - " + intPtDD + " dist = " + dist); + //intPt = intPtDD; + */ + + /** + * Due to rounding it can happen that the computed intersection is + * outside the envelopes of the input segments. Clearly this + * is inconsistent. + * This code checks this condition and forces a more reasonable answer + * + * MD - May 4 2005 - This is still a problem. Here is a failure case: + * + * LINESTRING (2089426.5233462777 1180182.3877339689, 2085646.6891757075 1195618.7333999649) + * LINESTRING (1889281.8148903656 1997547.0560044837, 2259977.3672235999 483675.17050843034) + * int point = (2097408.2633752143,1144595.8008114607) + * + * MD - Dec 14 2006 - This does not seem to be a failure case any longer + */ + if (! isInSegmentEnvelopes(intPt)) { +// System.out.println("Intersection outside segment envelopes: " + intPt); + + // compute a safer result + // copy the coordinate, since it may be rounded later + intPt = new Coordinate(nearestEndpoint(p1, p2, q1, q2)); +// intPt = CentralEndpointIntersector.getIntersection(p1, p2, q1, q2); + +// System.out.println("Segments: " + this); +// System.out.println("Snapped to " + intPt); +// checkDD(p1, p2, q1, q2, intPt); + } + if (precisionModel != null) { + precisionModel.makePrecise(intPt); + } + return intPt; + } + + private void checkDD(Coordinate p1, Coordinate p2, Coordinate q1, + Coordinate q2, Coordinate intPt) + { + Coordinate intPtDD = CGAlgorithmsDD.intersection(p1, p2, q1, q2); + boolean isIn = isInSegmentEnvelopes(intPtDD); + System.out.println( "DD in env = " + isIn + " --------------------- " + intPtDD); + if (intPt.distance(intPtDD) > 0.0001) { + System.out.println("Distance = " + intPt.distance(intPtDD)); + } + } + + private Coordinate intersectionWithNormalization( + Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) + { + Coordinate n1 = new Coordinate(p1); + Coordinate n2 = new Coordinate(p2); + Coordinate n3 = new Coordinate(q1); + Coordinate n4 = new Coordinate(q2); + Coordinate normPt = new Coordinate(); + normalizeToEnvCentre(n1, n2, n3, n4, normPt); + + Coordinate intPt = safeHCoordinateIntersection(n1, n2, n3, n4); + + intPt.x += normPt.x; + intPt.y += normPt.y; + + return intPt; + } + + /** + * Computes a segment intersection using homogeneous coordinates. + * Round-off error can cause the raw computation to fail, + * (usually due to the segments being approximately parallel). + * If this happens, a reasonable approximation is computed instead. + * + * @param p1 a segment endpoint + * @param p2 a segment endpoint + * @param q1 a segment endpoint + * @param q2 a segment endpoint + * @return the computed intersection point + */ + private Coordinate safeHCoordinateIntersection(Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) + { + Coordinate intPt = null; + try { + intPt = HCoordinate.intersection(p1, p2, q1, q2); + } + catch (NotRepresentableException e) { +// System.out.println("Not calculable: " + this); + // compute an approximate result +// intPt = CentralEndpointIntersector.getIntersection(p1, p2, q1, q2); + intPt = nearestEndpoint(p1, p2, q1, q2); + // System.out.println("Snapped to " + intPt); + } + return intPt; + } + + /** + * Normalize the supplied coordinates so that + * their minimum ordinate values lie at the origin. + * NOTE: this normalization technique appears to cause + * large errors in the position of the intersection point for some cases. + * + * @param n1 + * @param n2 + * @param n3 + * @param n4 + * @param normPt + */ + private void normalizeToMinimum( + Coordinate n1, + Coordinate n2, + Coordinate n3, + Coordinate n4, + Coordinate normPt) + { + normPt.x = smallestInAbsValue(n1.x, n2.x, n3.x, n4.x); + normPt.y = smallestInAbsValue(n1.y, n2.y, n3.y, n4.y); + n1.x -= normPt.x; n1.y -= normPt.y; + n2.x -= normPt.x; n2.y -= normPt.y; + n3.x -= normPt.x; n3.y -= normPt.y; + n4.x -= normPt.x; n4.y -= normPt.y; + } + + /** + * Normalize the supplied coordinates to + * so that the midpoint of their intersection envelope + * lies at the origin. + * + * @param n00 + * @param n01 + * @param n10 + * @param n11 + * @param normPt + */ + private void normalizeToEnvCentre( + Coordinate n00, + Coordinate n01, + Coordinate n10, + Coordinate n11, + Coordinate normPt) + { + double minX0 = n00.x < n01.x ? n00.x : n01.x; + double minY0 = n00.y < n01.y ? n00.y : n01.y; + double maxX0 = n00.x > n01.x ? n00.x : n01.x; + double maxY0 = n00.y > n01.y ? n00.y : n01.y; + + double minX1 = n10.x < n11.x ? n10.x : n11.x; + double minY1 = n10.y < n11.y ? n10.y : n11.y; + double maxX1 = n10.x > n11.x ? n10.x : n11.x; + double maxY1 = n10.y > n11.y ? n10.y : n11.y; + + double intMinX = minX0 > minX1 ? minX0 : minX1; + double intMaxX = maxX0 < maxX1 ? maxX0 : maxX1; + double intMinY = minY0 > minY1 ? minY0 : minY1; + double intMaxY = maxY0 < maxY1 ? maxY0 : maxY1; + + double intMidX = (intMinX + intMaxX) / 2.0; + double intMidY = (intMinY + intMaxY) / 2.0; + normPt.x = intMidX; + normPt.y = intMidY; + + /* + // equilavalent code using more modular but slower method + Envelope env0 = new Envelope(n00, n01); + Envelope env1 = new Envelope(n10, n11); + Envelope intEnv = env0.intersection(env1); + Coordinate intMidPt = intEnv.centre(); + + normPt.x = intMidPt.x; + normPt.y = intMidPt.y; + */ + + n00.x -= normPt.x; n00.y -= normPt.y; + n01.x -= normPt.x; n01.y -= normPt.y; + n10.x -= normPt.x; n10.y -= normPt.y; + n11.x -= normPt.x; n11.y -= normPt.y; + } + + private double smallestInAbsValue(double x1, double x2, double x3, double x4) + { + double x = x1; + double xabs = Math.abs(x); + if (Math.abs(x2) < xabs) { + x = x2; + xabs = Math.abs(x2); + } + if (Math.abs(x3) < xabs) { + x = x3; + xabs = Math.abs(x3); + } + if (Math.abs(x4) < xabs) { + x = x4; + } + return x; + } + + /** + * Tests whether a point lies in the envelopes of both input segments. + * A correctly computed intersection point should return true + * for this test. + * Since this test is for debugging purposes only, no attempt is + * made to optimize the envelope test. + * + * @return true if the input point lies within both input segment envelopes + */ + private boolean isInSegmentEnvelopes(Coordinate intPt) + { + Envelope env0 = new Envelope(inputLines[0][0], inputLines[0][1]); + Envelope env1 = new Envelope(inputLines[1][0], inputLines[1][1]); + return env0.contains(intPt) && env1.contains(intPt); + } + + /** + * Finds the endpoint of the segments P and Q which + * is closest to the other segment. + * This is a reasonable surrogate for the true + * intersection points in ill-conditioned cases + * (e.g. where two segments are nearly coincident, + * or where the endpoint of one segment lies almost on the other segment). + *

+ * This replaces the older CentralEndpoint heuristic, + * which chose the wrong endpoint in some cases + * where the segments had very distinct slopes + * and one endpoint lay almost on the other segment. + * + * @param p1 an endpoint of segment P + * @param p2 an endpoint of segment P + * @param q1 an endpoint of segment Q + * @param q2 an endpoint of segment Q + * @return the nearest endpoint to the other segment + */ + private static Coordinate nearestEndpoint(Coordinate p1, Coordinate p2, + Coordinate q1, Coordinate q2) + { + Coordinate nearestPt = p1; + double minDist = Distance.pointToSegment(p1, q1, q2); + + double dist = Distance.pointToSegment(p2, q1, q2); + if (dist < minDist) { + minDist = dist; + nearestPt = p2; + } + dist = Distance.pointToSegment(q1, p1, p2); + if (dist < minDist) { + minDist = dist; + nearestPt = q1; + } + dist = Distance.pointToSegment(q2, p1, p2); + if (dist < minDist) { + minDist = dist; + nearestPt = q2; + } + return nearestPt; + } + + +} diff --git a/planar/triangulate/constraineddelaunay/LICENSE b/planar/triangulate/constraineddelaunay/LICENSE new file mode 100644 index 00000000..cbb04dd1 --- /dev/null +++ b/planar/triangulate/constraineddelaunay/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 go-spatial + +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. diff --git a/planar/triangulate/constraineddelaunay/README.md b/planar/triangulate/constraineddelaunay/README.md new file mode 100644 index 00000000..794219e6 --- /dev/null +++ b/planar/triangulate/constraineddelaunay/README.md @@ -0,0 +1,12 @@ + + +This constrained delaunay triangulation implementation is based on: + +Domiter, Vid. "Constrained Delaunay triangulation using plane subdivision." +Proceedings of the 8th central European seminar on computer graphics. +Budmerice. 2004. +http://old.cescg.org/CESCG-2004/web/Domiter-Vid/CDT.pdf + +While this code makes heavy use of the JTS port, it is all new code. As such +it can fall under the MIT license. + diff --git a/planar/triangulate/constraineddelaunay/triangle.go b/planar/triangulate/constraineddelaunay/triangle.go new file mode 100644 index 00000000..d7561793 --- /dev/null +++ b/planar/triangulate/constraineddelaunay/triangle.go @@ -0,0 +1,225 @@ + + +package constraineddelaunay + +import ( + "errors" + "fmt" + "log" + + "github.com/go-spatial/geom/planar/triangulate/quadedge" +) + + +var ErrInvalidVertex = errors.New("invalid vertex") +var ErrNoMatchingEdgeFound = errors.New("no matching edge found") + +/* +Triangle provides operations on a triangle within a +quadedge.QuadEdgeSubdivision. + +This is outside the quadedge package to avoid making changes to the original +JTS port. +*/ +type Triangle struct { + // the triangle referenced is to the right of this edge + qe *quadedge.QuadEdge +} + +/* +IntersectsPoint returns true if the vertex intersects the given triangle. This +includes falling on an edge. +*/ +func (tri *Triangle) IntersectsPoint(v quadedge.Vertex) bool { + e := tri.qe + + for i := 0; i < 3; i++ { + lc := v.Classify(e.Orig(), e.Dest()) + switch lc { + // return true if v is on the edge + case quadedge.ORIGIN: + return true + case quadedge.DESTINATION: + return true + case quadedge.BETWEEN: + return true + // return false if v is well outside the triangle + case quadedge.LEFT: + return false + case quadedge.BEHIND: + return false + case quadedge.BEYOND: + return false + } + // go to the next edge of the triangle. + e = e.RNext() + } + + // if v is to the right of all edges, it is inside the triangle. + return true +} + +/* +opposedTriangle returns the triangle opposite to the vertex v. + + + /|\ + / | \ + / | \ +v1 + a | b + + \ | / + \ | / + \|/ + + + +If this method is called on triangle a with v1 as the vertex, the result will be triangle b. +*/ +func (tri *Triangle) opposedTriangle(v quadedge.Vertex) (*Triangle, error) { + qe := tri.qe + for qe.Orig().Equals(v) == false { + + qe = qe.RNext() + + if qe == tri.qe { + return nil, ErrInvalidVertex + } + } + + return &Triangle{qe.RNext().RNext().Sym()}, nil +} + +/* +opposedVertex returns the vertex opposite to this triangle. + + + /|\ + / | \ + / | \ +v1 + a | b + v2 + \ | / + \ | / + \|/ + + + +If this method is called as a.opposedVertex(b), the result will be vertex v2. +*/ +func (tri *Triangle) opposedVertex(other *Triangle) (quadedge.Vertex, error) { + ae, err := tri.sharedEdge(other) + if err != nil { + return quadedge.Vertex{}, err + } + log.Printf("ae: %v", ae) + + // using the matching edge in triangle a, find the opposed vertex in b. + return ae.Sym().ONext().Dest(), nil +} + +/* +sharedEdge returns the edge that is shared by both a and b. The edge is +returned with triangle a on the left. + + + l + /|\ + / | \ + / | \ + + a | b + + \ | / + \ | / + \|/ + + r + +If this method is called as a.sharedEdge(b), the result will be edge lr. +*/ +func (tri *Triangle) sharedEdge(other *Triangle) (*quadedge.QuadEdge, error) { + ae := tri.qe + be := other.qe + foundMatch := false + + // search for the matching edge between both triangles + for ai := 0; ai < 3; ai++ { + for bi := 0; bi < 3; bi++ { + if ae.Orig().Equals(be.Dest()) && ae.Dest().Equals(be.Orig()) { + foundMatch = true + break + } + be = be.RNext(); + } + + if foundMatch { + break + } + ae = ae.RNext() + } + + if foundMatch == false { + // if there wasn't a matching edge + return nil, ErrNoMatchingEdgeFound + } + + // return the matching edge in triangle a + return ae, nil +} + +/* +sharedVertexLeft returns the left vertex that is shared by both triangles. + + l + /|\ + / | \ + / | \ + + a | b + + \ | / + \ | / + \|/ + + r + +If this method is called as a.sharedVertexLeft(b), the result will be vertex +l. +*/ +func (tri *Triangle) sharedVertexLeft(other *Triangle) (quadedge.Vertex, error) { + ae, err := tri.sharedEdge(other) + if err != nil { + return quadedge.Vertex{}, err + } + + // using the matching edge in triangle a, find the opposed vertex in b. + return ae.Orig(), nil +} + +/* +sharedVertexRight returns the right vertex that is shared by both triangles. + + l + /|\ + / | \ + / | \ + + a | b + + \ | / + \ | / + \|/ + + r + +If this method is called as a.sharedVertexRight(b), the result will be vertex +r. +*/ +func (tri *Triangle) sharedVertexRight(other *Triangle) (quadedge.Vertex, error) { + ae, err := tri.sharedEdge(other) + if err != nil { + return quadedge.Vertex{}, err + } + + // using the matching edge in triangle a, find the opposed vertex in b. + return ae.Dest(), nil +} + +func (tri *Triangle) String() string { + str := "[" + e := tri.qe + comma := "" + for true { + str += comma + fmt.Sprintf("%v", e.Orig()) + comma = "," + e = e.RPrev() + if e.Orig().Equals(tri.qe.Orig()) { + break + } + } + str = str + "]" + return str +} diff --git a/planar/triangulate/constraineddelaunay/triangle_test.go b/planar/triangulate/constraineddelaunay/triangle_test.go new file mode 100644 index 00000000..516128d5 --- /dev/null +++ b/planar/triangulate/constraineddelaunay/triangle_test.go @@ -0,0 +1,3 @@ + +package constraineddelaunay + diff --git a/planar/triangulate/constraineddelaunay/triangulator.go b/planar/triangulate/constraineddelaunay/triangulator.go new file mode 100644 index 00000000..e121c3b1 --- /dev/null +++ b/planar/triangulate/constraineddelaunay/triangulator.go @@ -0,0 +1,591 @@ +package constraineddelaunay + +import ( + "errors" + "fmt" + "log" + + "github.com/go-spatial/geom" + "github.com/go-spatial/geom/cmp" + "github.com/go-spatial/geom/planar/triangulate" + "github.com/go-spatial/geom/planar/triangulate/quadedge" +) + +/* + +TODO: + +* Start w/ basic constraint implementation (no intersections) + // Start at the origin point + // Search around the origin point for the containing triangle. To determine containment, check two line segments for intersection, if they intersect, it is contained. + // assert for now if the intersected line segment is a constraint + // assert for now if we fall on an existing edge + // Create methods OpposedTriangle and OpposedVertex (?) + // Loop through finding opposed triangles removing edges, keep a set of all edges that will need to be revisited. + +* When constraints are introduced: + + if there is an intersection between line segments + + Calculate the intersection and divide each line segment to use that point + + The inserted segment should then be inserted again as two different segments, recurse +* Edge conditions: + + a constrained edge that lies on top of an existing edge + + a constrained edge that lies on top of another constrained edge + +*/ + +var ErrInvalidPointClassification = errors.New("invalid point classification") +var ErrUnsupportedCoincidentEdges = errors.New("unsupported coincident edges") + +/* +Triangulator provides methods for performing a constrainted delaunay +triangulation. + +Domiter, Vid. "Constrained Delaunay triangulation using plane subdivision." +Proceedings of the 8th central European seminar on computer graphics. +Budmerice. 2004. +http://old.cescg.org/CESCG-2004/web/Domiter-Vid/CDT.pdf +*/ +type Triangulator struct { + builder *triangulate.DelaunayTriangulationBuilder + // a map of constraints where the segments have the lesser point first. + constraints map[triangulate.Segment]bool + subdiv *quadedge.QuadEdgeSubdivision + tolerance float64 + // maintain an index of vertices to quad edges. Each vertex will point to + // one quad edge that has the vertex as an origin. The other quad edges + // that point to this vertex can be reached from there. + vertexIndex map[quadedge.Vertex]*quadedge.QuadEdge +} + +/* +appendNonRepeat only appends the provided value if it does not repeat the last +value that was appended onto the array. +*/ +func appendNonRepeat(arr []quadedge.Vertex, v quadedge.Vertex) []quadedge.Vertex { + if len(arr) == 0 || arr[len(arr) - 1].Equals(v) == false { + arr = append(arr, v) + } + return arr +} + +/* +createSegment creates a segment with vertices a & b, if it doesn't already +exist. All the vertices must already exist in the triangulator. +*/ +func (tri *Triangulator) createSegment(s triangulate.Segment) error { + qe, err := tri.LocateSegment(s.GetStart(), s.GetEnd()) + if err != nil && err != quadedge.ErrLocateFailure { + return err + } + if qe != nil { + // if the segment already exists + return nil + } + + ct, err := tri.findContainingTriangle(s) + if err != nil { + return err + } + from := ct.qe.Sym() + + ct, err = tri.findContainingTriangle(triangulate.NewSegment(geom.Line{s.GetEnd(), s.GetStart()})) + if err != nil { + return err + } + to := ct.qe.OPrev() + + quadedge.Connect(from, to) + // since we aren't adding any vertices we don't need to modify the vertex + // index. + return nil +} + +/* +createTriangle creates a triangle with vertices a, b and c. All the vertices +must already exist in the triangulator. Any existing edges that make up the triangle will not be recreated. + +This method makes no effort to ensure the resulting changes are a valid +triangulation. +*/ +func (tri *Triangulator) createTriangle(a, b, c quadedge.Vertex) error { + log.Printf("a: %v b: %v c: %v", a, b, c) + if err := tri.createSegment(triangulate.NewSegment(geom.Line{a, b})); err != nil { + return err + } + + if err := tri.createSegment(triangulate.NewSegment(geom.Line{b, c})); err != nil { + return err + } + + if err := tri.createSegment(triangulate.NewSegment(geom.Line{c, a})); err != nil { + return err + } + + return nil +} + +/* +deleteEdge deletes the specified edge and updates all associated neighbors to +reflect the removal. The local vertex index is also updated to reflect the +deletion. + +It is invalid to call this method on the last edge that links to a vertex. +*/ +func (tri *Triangulator) deleteEdge(e *quadedge.QuadEdge) { + + toRemove := make(map[*quadedge.QuadEdge]bool, 4) + + eSym := e.Sym() + eRot := e.Rot() + eRotSym := e.Rot().Sym() + + // a set of all the edges that will be removed. + toRemove[e] = true + toRemove[eSym] = true + toRemove[eRot] = true + toRemove[eRotSym] = true + + updateVertexIndex := func(v quadedge.Vertex) { + ve := tri.vertexIndex[v] + if toRemove[ve] { + log.Printf("Removing from vertex index: %v", ve) + for testEdge := ve.ONext(); ; testEdge = testEdge.ONext() { + if testEdge == ve { + log.Fatal("unable to update vertex index") + } + if toRemove[testEdge] == false { + log.Printf("Replacing %v with %v", ve, testEdge) + tri.vertexIndex[v] = testEdge + break + } + } + } + } + + // remove this edge from the vertex index. + updateVertexIndex(e.Orig()) + updateVertexIndex(e.Dest()) + quadedge.Splice(e.OPrev(), e) + quadedge.Splice(eSym.OPrev(), eSym) + + tri.subdiv.Delete(e) +} + +/* +findContainingTriangle finds the triangle that contains the vertex s.GetStart() +and contains at least part of the edge that extends from s.GetStart(). + +Returns a quadedge that has s.GetStart() as the origin and the right face is +the desired triangle. +*/ +func (tri *Triangulator) findContainingTriangle(s triangulate.Segment) (*Triangle, error) { + + qe, err := tri.locateEdgeByVertex(s.GetStart()) + if err != nil { + return nil, err + } + + left := qe + + // walk around all the triangles that share qe.Orig() + for true { + if left.IsLive() == false { + log.Fatalf("unexpected dead node: %v", left) + } + // create the two quad edges around s + right := left.OPrev() + + lc := s.GetEnd().Classify(left.Orig(), left.Dest()) + rc := s.GetEnd().Classify(right.Orig(), right.Dest()) + + if lc == quadedge.RIGHT && rc == quadedge.LEFT { + // if s is between the two edges, we found our triangle. + return &Triangle{left}, nil + } else if lc != quadedge.RIGHT && lc != quadedge.LEFT && rc != quadedge.LEFT && rc != quadedge.RIGHT { + // if s falls on lc or rc, then throw an error (for now) + // TODO: Handle this case + return nil, ErrUnsupportedCoincidentEdges + } + left = right + + if left == qe { + // if we've walked all the way around the vertex. + return nil, fmt.Errorf("no containing triangle: %v", s) + } + } + + return nil, fmt.Errorf("no containing triangle: %v", s) +} + +/* +GetEdges gets the edges of the computed triangulation as a MultiLineString. + +returns the edges of the triangulation +*/ +func (tri *Triangulator) GetEdges() geom.MultiLineString { + return tri.builder.GetEdges() +} + +/* +GetTriangles Gets the faces of the computed triangulation as a +MultiPolygon. +*/ +func (tri *Triangulator) GetTriangles() (geom.MultiPolygon, error) { + return tri.builder.GetTriangles() +} + +/* +InsertSegments inserts the line segments in the specified geometry and builds +a triangulation. The line segments are used as constraints in the +triangulation. If the geometry is made up solely of points, then no +constraints will be used. +*/ +func (tri *Triangulator) InsertSegments(g geom.Geometry) error { + err := tri.insertSites(g) + if err != nil { + return err + } + + err = tri.insertConstraints(g) + if err != nil { + return err + } + + return nil +} + +func (tri *Triangulator) insertSites(g geom.Geometry) error { + tri.builder = triangulate.NewDelaunayTriangulationBuilder(tri.tolerance) + err := tri.builder.SetSites(g) + if err != nil { + return err + } + tri.subdiv = tri.builder.GetSubdivision() + + // Add all the edges to a constant time lookup + tri.vertexIndex = make(map[quadedge.Vertex]*quadedge.QuadEdge) + edges := tri.subdiv.GetEdges() + for i := range(edges) { + e := edges[i] + if _, ok := tri.vertexIndex[e.Orig()]; ok == false { + tri.vertexIndex[e.Orig()] = e + } + if _, ok := tri.vertexIndex[e.Dest()]; ok == false { + tri.vertexIndex[e.Dest()] = e.Sym() + } + } + + return nil +} + +func (tri *Triangulator) insertConstraints(g geom.Geometry) error { + tri.constraints = make(map[triangulate.Segment]bool) + + lines, err := geom.ExtractLines(g) + if err != nil { + return fmt.Errorf("error adding constraint: %v", err) + } + for _, l := range(lines) { + // make the line ordering consistent + if !cmp.PointLess(l[0], l[1]) { + l[0], l[1] = l[1], l[0] + } + + seg := triangulate.NewSegment(l) + // this maintains the constraints and de-dupes + tri.constraints[seg] = true + } + + log.Printf("tri.constraints: %v", tri.constraints) + for seg := range tri.constraints { + qe, err := tri.LocateSegment(seg.GetStart(), seg.GetEnd()) + if qe != nil && err != nil { + return fmt.Errorf("error adding constraint: %v", err) + } + + if qe == nil { + err := tri.insertEdgeCDT(&seg) + if err != nil { + return fmt.Errorf("error adding constraint: %v", err) + } + } + if err = tri.Validate(); err != nil { + log.Fatalf("validate failed: %v", err) + } + } + + return nil +} + +func (tri *Triangulator) IsConstraint(s triangulate.Segment) bool { + _, ok := tri.constraints[s] + return ok +} + +// Procedure InsertEdgeCDT(T:CDT, ab:Edge) +func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { + log.Printf("ab: %v", *ab) + // Precondition: a,b in T and ab not in T + // Find the triangle t ∈ T that contains a and is cut by ab + at, err := tri.findContainingTriangle(*ab) + if err != nil { + return err + } + be, err := tri.locateEdgeByVertex(ab.GetEnd()) + if err != nil { + return err + } + log.Printf("be: %v", be) + log.Printf("at: %v", at) + t := at + + removalList := make([]*quadedge.QuadEdge, 0) + + // PU:=EmptyList + pu := make([]quadedge.Vertex, 0) + // PL:=EmptyList + pl := make([]quadedge.Vertex, 0) + // v:=a + v := ab.GetStart() + b := ab.GetEnd() + + // While v not in t do -- should this be 'b not in t'!? -JRS + for t.IntersectsPoint(b) == false { + // tseq:=OpposedTriangle(t,v) + tseq, err := t.opposedTriangle(v) + if err != nil { + return err + } + // vseq:=OpposesdVertex(tseq,t) + vseq, err := tseq.opposedVertex(t) + if err != nil { + return err + } + log.Printf("t: %v", t) + log.Printf("v: %v", v) + log.Printf("tseq: %v", tseq) + log.Printf("vseq: %v", vseq) + shared, err := t.sharedEdge(tseq) + if err != nil { + return err + } + log.Printf("shared: %v", shared) + + c := vseq.Classify(ab.GetStart(), ab.GetEnd()) + + switch c { + // If vseq above the edge ab then + case quadedge.LEFT: + // v:=Vertex shared by t and tseq above ab + v = shared.Orig() + pu = appendNonRepeat(pu, v) + // AddList(PU ,vseq) + pu = appendNonRepeat(pu, vseq) + // Else If vseq below the edge ab + case quadedge.RIGHT: + // v:=Vertex shared by t and tseq below ab + v = shared.Dest() + pl = appendNonRepeat(pl, v) + // AddList(PL, vseq) + pl = appendNonRepeat(pl, vseq) + // NOTE: You may be able to use this same mechanism to handle edges that overlap/intersect + // Else vseq on the edge ab + case quadedge.BETWEEN: + // InsertEdgeCDT(T, vseqb) + // a:=vseq + // break + case quadedge.DESTINATION: + // nothing left to do + default: + log.Printf("c: %v", c) + return ErrInvalidPointClassification + } + + // "Remove t from T" -- We are just removing the edge intersected by + // ab, which in effect removes the triangle. + removalList = append(removalList, shared) + + t = tseq + } + // EndWhile + + // remove the previously marked edges + // TODO Inefficient + for i := range(removalList) { + tri.deleteEdge(removalList[i]) + } + if err := tri.Validate(); err != nil { + log.Fatalf("validate failed: %v", err) + } + + // TriangulatePseudoPolygon(PU,ab,T) + log.Printf("pu: %v", pu) + tri.triangulatePseudoPolygon(pu, *ab) + // TriangulatePseudoPolygon(PL,ab,T) + log.Printf("pl: %v", pl) + tri.triangulatePseudoPolygon(pl, *ab) + + // Reconstitute the triangle adjacencies of T + // bt, err := tri.findContainingTriangle(triangulate.NewSegment(geom.Line{ab.GetEnd(), ab.GetStart()})) + // if err != nil { + // return err + // } + + // // Add edge ab to T + // log.Printf("at.qe.Sym(): %v", at.qe.Sym()) + // log.Printf("bt.qe.OPrev(): %v", bt.qe.OPrev()) + // quadedge.Connect(at.qe.Sym(), bt.qe.OPrev()) + tri.createSegment(*ab) + + return nil +} + +/* +locateEdgeByVertex finds a quad edge that has this vertex as Orig(). This will +not be a unique edge. + +This is looking for an exact match and tolerance will not be considered. +*/ +func (tri *Triangulator) locateEdgeByVertex(v quadedge.Vertex) (*quadedge.QuadEdge, error) { + qe := tri.vertexIndex[v] + + if qe == nil { + return nil, quadedge.ErrLocateFailure + } + return qe, nil +} + +/* +locateEdgeByVertex finds a quad edge that has this vertex as Orig(). This will +not be a unique edge. + +This is looking for an exact match and tolerance will not be considered. +*/ +func (tri *Triangulator) LocateSegment(v1 quadedge.Vertex, v2 quadedge.Vertex) (*quadedge.QuadEdge, error) { + qe := tri.vertexIndex[v1] + + if qe == nil { + return nil, quadedge.ErrLocateFailure + } + if err := tri.Validate(); err != nil { + log.Fatalf("validate failed: %v", err) + } + + start := qe + for true { + if qe == nil || qe.IsLive() == false { + log.Fatalf("unexpected dead node: %v", qe) + return nil, fmt.Errorf("nil or dead qe when locating segment %v %v", v1, v2) + } + if v2.Equals(qe.Dest()) { + return qe, nil + } + + qe = qe.ONext() + if qe == start { + return nil, quadedge.ErrLocateFailure + } + } + + return qe, nil +} + + +// TriangulatePseudoPolygon +// Pseudocode taken from Figure 10 +// http://old.cescg.org/CESCG-2004/web/Domiter-Vid/CDT.pdf +func (tri *Triangulator) triangulatePseudoPolygon(p []quadedge.Vertex, ab triangulate.Segment) error { + a := ab.GetStart() + b := ab.GetEnd() + var c quadedge.Vertex + // If P has more than one element then + if len(p) > 1 { + // c:=First vertex of P + c = p[0] + ci := 0 + // For each vertex v in P do + for i, v := range p { + // If v ∈ CircumCircle (a, b, c) then + if quadedge.TrianglePredicate.IsInCircleRobust(a, b, c, v) { + c = v + ci = i + } + } + // Divide P into PE and PD giving P=PE+c+PD + pe := p[0:ci] + pd := p[ci+1:] + // TriangulatePseudoPolygon(PE, ac, T) + if err := tri.triangulatePseudoPolygon(pe, triangulate.NewSegment(geom.Line{a, c})); err != nil { + return err + } + // TriangulatePseudoPolygon(PD, cd, T) (cb instead of cd? -JRS) + if err := tri.triangulatePseudoPolygon(pd, triangulate.NewSegment(geom.Line{c, b})); err != nil { + return err + } + } else if len(p) == 1 { + c = p[0] + } + + // If P is not empty then + if len(p) > 0 { + // Add triangle with vertices a, b, c into T + if err := tri.createTriangle(a, c, b); err != nil { + return err + } + } + + return nil +} + +/* +validate runs a number of self consistency checks against a triangulation and +reports the first error. + +This is most useful when testing/debugging. +*/ +func (tri *Triangulator) Validate() error { + err := tri.subdiv.Validate() + if err != nil { + return err + } + return tri.validateVertexIndex() +} + +/* +validateVertexIndex self consistency checks against a triangulation and the +subdiv and reports the first error. +*/ +func (tri *Triangulator) validateVertexIndex() error { + // collect a set of all edges + edgeSet := make(map[*quadedge.QuadEdge]bool) + vertexSet := make(map[quadedge.Vertex]bool) + edges := tri.subdiv.GetEdges() + for i := range(edges) { + edgeSet[edges[i]] = true + edgeSet[edges[i].Sym()] = true + vertexSet[edges[i].Orig()] = true + vertexSet[edges[i].Dest()] = true + } + + // verify the vertex index points to appropriate edges and vertices + for v, e := range tri.vertexIndex { + if _, ok := vertexSet[v]; ok == false { + return fmt.Errorf("vertex index contains an unexpected vertex: %v", v) + } + if _, ok := edgeSet[e]; ok == false { + return fmt.Errorf("vertex index contains an unexpected edge: %v", e) + } + if v.Equals(e.Orig()) == false { + return fmt.Errorf("vertex index points to an incorrect edge, expected %v got %v", e.Orig(), v) + } + } + + // verify all vertices are in the vertex index + for v, _ := range vertexSet { + if _, ok := tri.vertexIndex[v]; ok == false { + return fmt.Errorf("vertex index is missing a vertex: %v", v) + } + } + + return nil +} diff --git a/planar/triangulate/constraineddelaunay/triangulator_test.go b/planar/triangulate/constraineddelaunay/triangulator_test.go new file mode 100644 index 00000000..21023681 --- /dev/null +++ b/planar/triangulate/constraineddelaunay/triangulator_test.go @@ -0,0 +1,248 @@ + +package constraineddelaunay + +import ( + "encoding/hex" + "fmt" + "strconv" + "testing" + + "github.com/go-spatial/geom" + "github.com/go-spatial/geom/encoding/wkb" + "github.com/go-spatial/geom/encoding/wkt" + "github.com/go-spatial/geom/planar/triangulate" + "github.com/go-spatial/geom/planar/triangulate/quadedge" +) + +func TestFindContainingTriangle(t *testing.T) { + type tcase struct { + // provided for readability + inputWKT string + // this can be removed if/when geom has a WKT decoder. + // A simple website for performing conversions: + // https://rodic.fr/blog/online-conversion-between-geometric-formats/ + inputWKB string + searchFrom geom.Line + expectedEdge string + } + + fn := func(t *testing.T, tc tcase) { + bytes, err := hex.DecodeString(tc.inputWKB) + if err != nil { + t.Fatalf("error decoding hex string: %v", err) + return + } + g, err := wkb.DecodeBytes(bytes) + if err != nil { + t.Fatalf("error decoding WKB: %v", err) + return + } + + uut := new(Triangulator) + uut.tolerance = 1e-6 + uut.insertSites(g) + + tri, err := uut.findContainingTriangle(triangulate.NewSegment(tc.searchFrom)) + if err != nil { + t.Fatalf("error, expected nil got %v", err) + return + } + qeStr := fmt.Sprintf("%v -> %v", tri.qe.Orig(), tri.qe.Dest()) + if qeStr != tc.expectedEdge { + t.Fatalf("error, expected %v got %v", tc.expectedEdge, qeStr) + } + + } + testcases := []tcase{ + { + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + searchFrom: geom.Line{{0,0}, {10, 10}}, + expectedEdge: `[0 0] -> [0 10]`, + }, + { + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + searchFrom: geom.Line{{10,0}, {0, 20}}, + expectedEdge: `[10 0] -> [0 10]`, + }, + { + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + searchFrom: geom.Line{{10,10}, {0, 0}}, + expectedEdge: `[10 10] -> [10 0]`, + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +func TestDeleteEdge(t *testing.T) { + type tcase struct { + // provided for readability + inputWKT string + // this can be removed if/when geom has a WKT decoder. + // A simple website for performing conversions: + // https://rodic.fr/blog/online-conversion-between-geometric-formats/ + inputWKB string + deleteMe geom.Line + } + + fn := func(t *testing.T, tc tcase) { + bytes, err := hex.DecodeString(tc.inputWKB) + if err != nil { + t.Fatalf("error decoding hex string, expected nil got %v", err) + return + } + g, err := wkb.DecodeBytes(bytes) + if err != nil { + t.Fatalf("error decoding WKB, expected nil got %v", err) + return + } + + uut := new(Triangulator) + uut.tolerance = 1e-6 + uut.InsertSegments(g) + e, err := uut.LocateSegment(quadedge.Vertex(tc.deleteMe[0]), quadedge.Vertex(tc.deleteMe[1])) + if err != nil { + t.Fatalf("error locating segment, expected nil got %v", err) + return + } + + err = uut.Validate() + if err != nil { + t.Errorf("error validating triangulation, expected nil got %v", err) + return + } + + uut.deleteEdge(e) + err = uut.Validate() + if err != nil { + t.Errorf("error validating triangulation after delete, expected nil got %v", err) + return + } + + // this edge shouldn't exist anymore. + _, err = uut.LocateSegment(quadedge.Vertex(tc.deleteMe[0]), quadedge.Vertex(tc.deleteMe[1])) + if err == nil { + t.Fatalf("error locating segment, expected %v got nil", quadedge.ErrLocateFailure) + return + } + + } + testcases := []tcase{ + { + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + deleteMe: geom.Line{{0,10}, {10, 0}}, + }, + { + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + deleteMe: geom.Line{{0,10}, {10, 0}}, + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +/* +TestTriangulation test cases test for small constrained triangulations and +edge cases +*/ +func TestTriangulation(t *testing.T) { + type tcase struct { + // provided for readability + inputWKT string + // this can be removed if/when geom has a WKT decoder. + // A simple website for performing conversions: + // https://rodic.fr/blog/online-conversion-between-geometric-formats/ + inputWKB string + expectedEdges string + expectedTris string + } + + fn := func(t *testing.T, tc tcase) { + bytes, err := hex.DecodeString(tc.inputWKB) + if err != nil { + t.Fatalf("error decoding hex string: %v", err) + return + } + g, err := wkb.DecodeBytes(bytes) + if err != nil { + t.Fatalf("error decoding WKB: %v", err) + return + } + + uut := new(Triangulator) + uut.tolerance = 1e-6 + err = uut.InsertSegments(g) + if err != nil { + t.Fatalf("error inserting segments, expected nil got %v", err) + } + + edges := uut.GetEdges() + edgesWKT, err := wkt.Encode(edges) + if err != nil { + t.Errorf("error, expected nil got %v", err) + return + } + if edgesWKT != tc.expectedEdges { + t.Errorf("error, expected %v got %v", tc.expectedEdges, edgesWKT) + return + } + + tris, err := uut.GetTriangles() + if err != nil { + t.Errorf("error, expected nil got %v", err) + return + } + trisWKT, err := wkt.Encode(tris) + if err != nil { + t.Errorf("error, expected nil got %v", err) + return + } + if trisWKT != tc.expectedTris { + t.Errorf("error, expected %v got %v", tc.expectedTris, trisWKT) + return + } + } + testcases := []tcase{ + { + // should create a triangulation w/ a vertical line (2 5, 2 -5). + // The unconstrained version has a horizontal line + inputWKT: `LINESTRING(0 0, 2 5, 2 -5, 5 0)`, + inputWKB: `0102000000040000000000000000000000000000000000000000000000000000400000000000001440000000000000004000000000000014c000000000000014400000000000000000`, + expectedEdges: `MULTILINESTRING ((2 5,5 0),(0 0,2 5),(0 0,2 -5),(2 -5,5 0),(2 -5,2 5))`, + expectedTris: `MULTIPOLYGON (((0 0,2 -5,2 5,0 0)),((2 5,2 -5,5 0,2 5)))`, + }, + { + // a horizontal rectangle w/ one diagonal line. The diagonal line + // should be maintained and the top/bottom re-triangulated. + inputWKT: `MULTILINESTRING ((0 0,0 1,1 1.1,2 1,2 0,1 -0.1,0 0),(0 0,2 1))`, + inputWKB: `010500000002000000010200000007000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f9a9999999999f13f0000000000000040000000000000f03f00000000000000400000000000000000000000000000f03f9a9999999999b93f000000000000000000000000000000000102000000020000000000000000000000000000000000f03f00000000000000400000000000000000`, + expectedEdges: `MULTILINESTRING ((1 1.1,2 1),(0 1,1 1.1),(0 0,0 1),(0 0,2 0),(2 0,2 1),(1 1.1,2 0),(0 1,2 0),(1 0.1,2 0),(0 1,1 0.1),(0 0,1 0.1))`, + expectedTris: `MULTIPOLYGON (((0 1,0 0,1 0.1,0 1)),((0 1,1 0.1,2 0,0 1)),((0 1,2 0,1 1.1,0 1)),((1 1.1,2 0,2 1,1 1.1)),((0 0,2 0,1 0.1,0 0)))`, + }, + { + // a horizontal rectangle w/ one diagonal line. The diagonal line + // should be maintained and the top/bottom re-triangulated. + inputWKT: `MULTILINESTRING((0 0,-0.1 0.5,0 1,0.5 1.2,1 1.3,1.5 1.2,2 1,2.1 0.5,2 0,1.5 -0.2,1 -0.3,0.5 -0.2,0 0),(-0.1 0.5,2.1 0.5))`, + inputWKB: `01050000000200000001020000000d000000000000000000000000000000000000009a9999999999b9bf000000000000e03f0000000000000000000000000000f03f000000000000e03f333333333333f33f000000000000f03fcdccccccccccf43f000000000000f83f333333333333f33f0000000000000040000000000000f03fcdcccccccccc0040000000000000e03f00000000000000400000000000000000000000000000f83f9a9999999999c9bf000000000000f03f333333333333d3bf000000000000e03f9a9999999999c9bf000000000000000000000000000000000102000000020000009a9999999999b9bf000000000000e03fcdcccccccccc0040000000000000e03f`, + expectedEdges: `MULTILINESTRING ((1.5 1.2,2 1),(1 1.3,1.5 1.2),(0.5 1.2,1 1.3),(0 1,0.5 1.2),(-0.1 0.5,0 1),(-0.1 0.5,0 0),(0 0,0.5 -0.2),(0.5 -0.2,1 -0.3),(1 -0.3,1.5 -0.2),(1.5 -0.2,2 0),(2 0,2.1 0.5),(2 1,2.1 0.5),(1.5 1.2,2.1 0.5),(1 1.3,2.1 0.5),(-0.1 0.5,2.1 0.5),(-0.1 0.5,1 1.3),(-0.1 0.5,0.5 1.2),(1.5 -0.2,2.1 0.5),(-0.1 0.5,1.5 -0.2),(0.5 -0.2,1.5 -0.2),(-0.1 0.5,0.5 -0.2))`, + expectedTris: `MULTIPOLYGON (((0 1,-0.1 0.5,0.5 1.2,0 1)),((0.5 1.2,-0.1 0.5,1 1.3,0.5 1.2)),((1 1.3,-0.1 0.5,2.1 0.5,1 1.3)),((1 1.3,2.1 0.5,1.5 1.2,1 1.3)),((1.5 1.2,2.1 0.5,2 1,1.5 1.2)),((1 -0.3,1.5 -0.2,0.5 -0.2,1 -0.3)),((0.5 -0.2,1.5 -0.2,-0.1 0.5,0.5 -0.2)),((0.5 -0.2,-0.1 0.5,0 0,0.5 -0.2)),((-0.1 0.5,1.5 -0.2,2.1 0.5,-0.1 0.5)),((2.1 0.5,1.5 -0.2,2 0,2.1 0.5)))`, + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + diff --git a/planar/triangulate/delaunay_test.go b/planar/triangulate/delaunay_test.go index f6abe1d7..e95e8e61 100644 --- a/planar/triangulate/delaunay_test.go +++ b/planar/triangulate/delaunay_test.go @@ -22,14 +22,15 @@ import ( ) /* -TestDelaunayTriangulation test cases were taken from JTS and converted to -GeoJSON. +TestDelaunayTriangulation test cases were taken from JTS. */ func TestDelaunayTriangulation(t *testing.T) { type tcase struct { // provided for readability inputWKT string // this can be removed if/when geom has a WKT decoder. + // A simple website for performing conversions: + // https://rodic.fr/blog/online-conversion-between-geometric-formats/ inputWKB string expectedEdges string expectedTris string @@ -50,8 +51,16 @@ func TestDelaunayTriangulation(t *testing.T) { builder := new(DelaunayTriangulationBuilder) builder.tolerance = 1e-6 builder.SetSites(sites) + if builder.create() == false { + t.Errorf("error building triangulation, expected true got false"); + } + err = builder.subdiv.Validate() + if err != nil { + t.Errorf("error, expected nil got %v", err) + } + - edges := builder.getEdges() + edges := builder.GetEdges() edgesWKT, err := wkt.Encode(edges) if err != nil { t.Errorf("error, expected nil got %v", err) diff --git a/planar/triangulate/delaunaytriangulationbuilder.go b/planar/triangulate/delaunaytriangulationbuilder.go index 01b3e6f4..c6a2ba9f 100644 --- a/planar/triangulate/delaunaytriangulationbuilder.go +++ b/planar/triangulate/delaunaytriangulationbuilder.go @@ -40,6 +40,10 @@ func (xy PointByXY) Less(i, j int) bool { return cmp.XYLessPoint(xy[i], xy[j]) } func (xy PointByXY) Swap(i, j int) { xy[i], xy[j] = xy[j], xy[i] } func (xy PointByXY) Len() int { return len(xy) } +func NewDelaunayTriangulationBuilder(tolerance float64) *DelaunayTriangulationBuilder { + return &DelaunayTriangulationBuilder{tolerance: tolerance} +} + /* extractUniqueCoordinates extracts the unique points from the given Geometry. @@ -176,23 +180,23 @@ func (dtb *DelaunayTriangulationBuilder) create() bool { return true } -/** - * Gets the {@link QuadEdgeSubdivision} which models the computed triangulation. - * - * @return the subdivision containing the triangulation -public QuadEdgeSubdivision getSubdivision() -{ - create(); - return subdiv; -} +/* +Gets the QuadEdgeSubdivision which models the computed triangulation. + +Returns the subdivision containing the triangulation or nil if it has +not been created. */ +func (dtb *DelaunayTriangulationBuilder) GetSubdivision() *quadedge.QuadEdgeSubdivision { + dtb.create(); + return dtb.subdiv; +} /* GetEdges gets the edges of the computed triangulation as a MultiLineString. returns the edges of the triangulation */ -func (dtb *DelaunayTriangulationBuilder) getEdges() geom.MultiLineString { +func (dtb *DelaunayTriangulationBuilder) GetEdges() geom.MultiLineString { if !dtb.create() { return geom.MultiLineString{} } @@ -201,7 +205,7 @@ func (dtb *DelaunayTriangulationBuilder) getEdges() geom.MultiLineString { /* GetTriangles Gets the faces of the computed triangulation as a -GeometryCollection Polygons. +MultiPolygon. Unlike JTS, this method returns a MultiPolygon. I found not all viewers like displaying collections. -JRS diff --git a/planar/triangulate/incrementaldelaunaytriangulator.go b/planar/triangulate/incrementaldelaunaytriangulator.go index 0ed54e79..24583fcb 100644 --- a/planar/triangulate/incrementaldelaunaytriangulator.go +++ b/planar/triangulate/incrementaldelaunaytriangulator.go @@ -96,12 +96,11 @@ func (idt *IncrementalDelaunayTriangulator) InsertSite(v quadedge.Vertex) (*quad // log.Printf("Made Edge: %v -> %v", base.Orig(), base.Dest()); quadedge.Splice(base, e) startEdge := base - done := false - for !done { + for true { base = idt.subdiv.Connect(e, base.Sym()) e = base.OPrev() if e.LNext() == startEdge { - done = true + break } } diff --git a/planar/triangulate/quadedge/debug.go b/planar/triangulate/quadedge/debug.go new file mode 100644 index 00000000..9a6d3488 --- /dev/null +++ b/planar/triangulate/quadedge/debug.go @@ -0,0 +1,84 @@ + +package quadedge + +import ( + "fmt" + + "github.com/go-spatial/geom/encoding/wkt" +) + +// DebugDumpEdges returns a string with the WKT representation of the +// edges. On error, an error string is returned. +// +// This is intended for debug purposes only. +func (qes *QuadEdgeSubdivision) DebugDumpEdges() string { + edges := qes.GetEdgesAsMultiLineString() + edgesWKT, err := wkt.Encode(edges) + if err != nil { + return fmt.Sprintf("error formatting as WKT: %v", err) + } + return edgesWKT +} + +/* +Validate runs a self consistency checks and reports the first error. + +This is not part of the original JTS code. +*/ +func (qes *QuadEdgeSubdivision) Validate() error { + // collect a set of all edges + edgeSet := make(map[*QuadEdge]bool) + edges := qes.GetEdges() + for i := range edges { + if _, ok := edgeSet[edges[i]]; ok == true { + return fmt.Errorf("edge reported multiple times in subdiv: %v", edges[i]) + } + if edges[i].IsLive() == false { + return fmt.Errorf("a deleted edge is still in subdiv: %v", edges[i]) + } + if edges[i].Sym().IsLive() == false { + return fmt.Errorf("a deleted edge is still in subdiv: %v", edges[i].Sym()) + } + edgeSet[edges[i]] = true + } + + return qes.validateONext() +} + +/* +validateONext validates that each QuadEdge's ONext() goes to the next edge that +shares an origin point in CCW order. + +This is not part of the original JTS code. +*/ +func (qes *QuadEdgeSubdivision) validateONext() error { + + edgeSet := make(map[*QuadEdge]bool) + edges := qes.GetEdges() + for _, e := range edges { + if _, ok := edgeSet[e]; ok == false { + // if we haven't checked this edge already + n := e + for true { + ccw := n.ONext() + if n.Orig().Equals(e.Orig()) == false { + return fmt.Errorf("edge in ONext() doesn't share an origin: between %v and %v", e, n) + } + // this will only work if the angles between edges are < 180deg + // if both edges are frame edges then the CCW rule may not + // be easily detectable. (think angles > 180deg) + if (qes.isFrameEdge(n) == false || qes.isFrameEdge(ccw) == false) && n.Orig().IsCCW(n.Dest(), ccw.Dest()) == false { + return fmt.Errorf("edges are not CCW, expected %v to be CCW of %v", ccw, n) + } + edgeSet[n] = true + n = ccw + if (n == e) { + break + } + } + } + } + + return nil +} + diff --git a/planar/triangulate/quadedge/quadedge.go b/planar/triangulate/quadedge/quadedge.go index cdaf103c..5de11f2c 100644 --- a/planar/triangulate/quadedge/quadedge.go +++ b/planar/triangulate/quadedge/quadedge.go @@ -13,6 +13,8 @@ http://www.eclipse.org/org/documents/edl-v10.php. package quadedge import ( + "fmt" + "github.com/go-spatial/geom/cmp" ) @@ -69,6 +71,8 @@ func MakeEdge(o Vertex, d Vertex) *QuadEdge { base := q0 base.setOrig(o) base.setDest(d) + base.rot.setOrig(o) + base.rot.setDest(d) return base } @@ -392,13 +396,16 @@ public LineSegment toLineSegment() */ /* -Converts this edge to a WKT two-point LINESTRING indicating +String Converts this edge to a WKT two-point LINESTRING indicating the geometry of this edge. -@return a String representing this edge's geometry -public String toString() { - Coordinate p0 = vertex.getCoordinate(); - Coordinate p1 = dest().getCoordinate(); - return WKTWriter.toLineString(p0, p1); -} +Unlike JTS, if IsLive() is false, a deleted string is returned. + +return a String representing this edge's geometry */ +func (qe *QuadEdge) String() string { + if qe.IsLive() == false { + return fmt.Sprintf("", qe.Orig()) + } + return fmt.Sprintf("LINESTRING (%v %v, %v %v)", qe.Orig().X(), qe.Orig().Y(), qe.Dest().X(), qe.Dest().Y()) +} diff --git a/planar/triangulate/quadedge/quadedgesubdivision.go b/planar/triangulate/quadedge/quadedgesubdivision.go index 30782f0c..d0b2cb84 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision.go +++ b/planar/triangulate/quadedge/quadedgesubdivision.go @@ -15,6 +15,7 @@ package quadedge import ( "errors" "fmt" + "log" "github.com/go-spatial/geom" "github.com/go-spatial/geom/planar" @@ -204,19 +205,32 @@ func (qes *QuadEdgeSubdivision) Delete(e *QuadEdge) { eRot := e.Rot() eRotSym := e.Rot().Sym() + e.Delete() + eSym.Delete() + eRot.Delete() + eRotSym.Delete() + // this is inefficient on an array, but this method should be called // infrequently newArray := make([]*QuadEdge, 0, len(qes.quadEdges)) for _, ele := range qes.quadEdges { - if ele != e && ele != eSym && ele != eRot && ele != eRotSym { + if ele.IsLive() { newArray = append(newArray, ele) + + if ele.next.IsLive() == false { + log.Fatal("a dead edge is still linked: %v", ele) + } } } + qes.quadEdges = newArray - e.Delete() - eSym.Delete() - eRot.Delete() - eRotSym.Delete() + if qes.startingEdge.IsLive() == false { + if len(qes.quadEdges) > 0 { + qes.startingEdge = qes.quadEdges[0] + } else { + qes.startingEdge = nil + } + } } /* @@ -292,34 +306,42 @@ func (qes *QuadEdgeSubdivision) Locate(v Vertex) (*QuadEdge, error) { return qes.locator.Locate(v) } -// /** -// * Locates the edge between the given vertices, if it exists in the -// * subdivision. -// * -// * @param p0 a coordinate -// * @param p1 another coordinate -// * @return the edge joining the coordinates, if present -// * or null if no such edge exists -// */ -// public QuadEdge locate(Coordinate p0, Coordinate p1) { -// // find an edge containing one of the points -// QuadEdge e = locator.locate(new Vertex(p0)); -// if (e == null) -// return null; - -// // normalize so that p0 is origin of base edge -// QuadEdge base = e; -// if (e.dest().getCoordinate().equals2D(p0)) -// base = e.sym(); -// // check all edges around origin of base edge -// QuadEdge locEdge = base; -// do { -// if (locEdge.dest().getCoordinate().equals2D(p1)) -// return locEdge; -// locEdge = locEdge.oNext(); -// } while (locEdge != base); -// return null; -// } +/* +Locates the edge between the given vertices, if it exists in the +subdivision. + +p0 a coordinate +p1 another coordinate +Return the edge joining the coordinates, if present or null if no such edge +exists +*/ +func (qes *QuadEdgeSubdivision) LocateSegment(p0 Vertex, p1 Vertex) (*QuadEdge, error) { + // find an edge containing one of the points + e, err := qes.locator.Locate(p0); + if err != nil || e == nil { + return nil, err + } + + // normalize so that p0 is origin of base edge + base := e; + if (e.Dest().EqualsTolerance(p0, qes.tolerance)) { + base = e.Sym(); + } + // check all edges around origin of base edge + locEdge := base; + done := false + for !done { + if locEdge.Dest().EqualsTolerance(p1, qes.tolerance) { + return locEdge, nil + } + locEdge = locEdge.ONext(); + + if locEdge == base { + done = true + } + } + return nil, nil +} /** * Inserts a new site into the Subdivision, connecting it to the vertices of @@ -572,6 +594,9 @@ func (qes *QuadEdgeSubdivision) GetPrimaryEdges(includeFrame bool) []*QuadEdge { qes.visitedKey++ var edges []*QuadEdge + if qes.startingEdge == nil { + return edges + } var stack edgeStack stack.push(qes.startingEdge) @@ -636,7 +661,10 @@ func (qes *QuadEdgeSubdivision) visitTriangles(triVisitor func(triEdges []*QuadE // visited flag is used to record visited edges of triangles // setVisitedAll(false); var stack *edgeStack = new(edgeStack) - stack.push(qes.startingEdge) + log.Printf("startingEdge: %v", qes.startingEdge) + if qes.startingEdge != nil { + stack.push(qes.startingEdge) + } visitedEdges := make(edgeSet) @@ -647,7 +675,7 @@ func (qes *QuadEdgeSubdivision) visitTriangles(triVisitor func(triEdges []*QuadE if triEdges != nil { triVisitor(triEdges) } - } + } } } @@ -666,10 +694,12 @@ func (qes *QuadEdgeSubdivision) fetchTriangleToVisit(edge *QuadEdge, stack *edge triEdges := make([]*QuadEdge, 0, 3) curr := edge var isFrame bool - var done bool - for !done { + for true { triEdges = append(triEdges, curr) + if curr.IsLive() == false { + log.Fatal("traversing dead edge") + } if qes.isFrameEdge(curr) { isFrame = true } @@ -686,7 +716,7 @@ func (qes *QuadEdgeSubdivision) fetchTriangleToVisit(edge *QuadEdge, stack *edge curr = curr.LNext() if curr == edge { - done = true + break } } @@ -942,3 +972,6 @@ func (qes *QuadEdgeSubdivision) GetTriangles() (geom.MultiPolygon, error) { // } // } + + + diff --git a/planar/triangulate/quadedge/quadedgesubdivision_test.go b/planar/triangulate/quadedge/quadedgesubdivision_test.go index 497ed6c9..fc5974f0 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision_test.go +++ b/planar/triangulate/quadedge/quadedgesubdivision_test.go @@ -236,3 +236,4 @@ func TestQuadEdgeSubdivisionLocate(t *testing.T) { t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) } } + diff --git a/planar/triangulate/segment.go b/planar/triangulate/segment.go new file mode 100644 index 00000000..e573d260 --- /dev/null +++ b/planar/triangulate/segment.go @@ -0,0 +1,196 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package triangulate + +import ( + "github.com/go-spatial/geom" + "github.com/go-spatial/geom/planar/triangulate/quadedge" +) + +/* +Segment models a constraint segment in a triangulation. + +A constraint segment is an oriented straight line segment between a start point +and an end point. + +Author David Skea +Author Martin Davis +Ported to Go by Jason R. Surratt +*/ +type Segment struct { + ls geom.Line + data interface{} +} + +func NewSegment(l geom.Line) Segment { + return Segment{ls: l} +} + + /** + * Creates a new instance for the given ordinates. + public Segment(double x1, double y1, double z1, double x2, double y2, double z2) { + this(new Coordinate(x1, y1, z1), new Coordinate(x2, y2, z2)); + } + */ + + /** + * Creates a new instance for the given ordinates, with associated external data. + public Segment(double x1, double y1, double z1, double x2, double y2, double z2, Object data) { + this(new Coordinate(x1, y1, z1), new Coordinate(x2, y2, z2), data); + } + */ + + /** + * Creates a new instance for the given points. + * + * @param p0 the start point + * @param p1 the end point + public Segment(Coordinate p0, Coordinate p1) { + ls = new LineSegment(p0, p1); + } + */ + +/* +Gets the start coordinate of the segment + +Returns the starting vertex +*/ +func (seg *Segment) GetStart() quadedge.Vertex { + return quadedge.Vertex(seg.ls[0]) +} + +/* +Gets the end coordinate of the segment + +Return a Coordinate +*/ +func (seg *Segment) GetEnd() quadedge.Vertex { + return quadedge.Vertex(seg.ls[1]) +} + + /** + * Gets the start X ordinate of the segment + * + * @return the X ordinate value + public double getStartX() { + Coordinate p = ls.getCoordinate(0); + return p.x; + } + */ + + /** + * Gets the start Y ordinate of the segment + * + * @return the Y ordinate value + public double getStartY() { + Coordinate p = ls.getCoordinate(0); + return p.y; + } + */ + + /** + * Gets the start Z ordinate of the segment + * + * @return the Z ordinate value + public double getStartZ() { + Coordinate p = ls.getCoordinate(0); + return p.z; + } + */ + + /** + * Gets the end X ordinate of the segment + * + * @return the X ordinate value + public double getEndX() { + Coordinate p = ls.getCoordinate(1); + return p.x; + } + */ + + /** + * Gets the end Y ordinate of the segment + * + * @return the Y ordinate value + public double getEndY() { + Coordinate p = ls.getCoordinate(1); + return p.y; + } + */ + + /** + * Gets the end Z ordinate of the segment + * + * @return the Z ordinate value + public double getEndZ() { + Coordinate p = ls.getCoordinate(1); + return p.z; + } + */ + + /** + * Gets a LineSegment modelling this segment. + * + * @return a LineSegment + public LineSegment getLineSegment() { + return ls; + } + */ + + /** + * Gets the external data associated with this segment + * + * @return a data object + public Object getData() { + return data; + } + */ + + /** + * Sets the external data to be associated with this segment + * + * @param data a data object + public void setData(Object data) { + this.data = data; + } + */ + + /** + * Determines whether two segments are topologically equal. + * I.e. equal up to orientation. + * + * @param s a segment + * @return true if the segments are topologically equal + public boolean equalsTopo(Segment s) { + return ls.equalsTopo(s.getLineSegment()); + } + */ + + /** + * Computes the intersection point between this segment and another one. + * + * @param s a segment + * @return the intersection point, or null if there is none + public Coordinate intersection(Segment s) { + return ls.intersection(s.getLineSegment()); + } + */ + + /** + * Computes a string representation of this segment. + * + * @return a string + public String toString() { + return ls.toString(); + } + */ From 8995fdf4d782d0f187bce73254207e0bd2dc13c9 Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Thu, 17 May 2018 13:46:20 -0600 Subject: [PATCH 04/12] Simple test cases in constrained all working 1. Haven't run any real world data through it yet, but the test cases are all passing as expected. --- planar/algorithm/LICENSE | 30 -- planar/algorithm/lineintersector.go | 401 ---------------- planar/algorithm/robustlineintersector.go | 450 ------------------ .../constraineddelaunay/triangulator.go | 271 +++++++++-- .../constraineddelaunay/triangulator_test.go | 106 ++++- planar/triangulate/quadedge/debug.go | 31 +- .../quadedge/quadedgesubdivision.go | 13 + planar/triangulate/quadedge/vertex.go | 23 +- planar/triangulate/segment.go | 12 +- 9 files changed, 384 insertions(+), 953 deletions(-) delete mode 100644 planar/algorithm/LICENSE delete mode 100644 planar/algorithm/lineintersector.go delete mode 100644 planar/algorithm/robustlineintersector.go diff --git a/planar/algorithm/LICENSE b/planar/algorithm/LICENSE deleted file mode 100644 index 1071fed9..00000000 --- a/planar/algorithm/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ -Eclipse Distribution License - v 1.0 - -Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - Neither the name of the Eclipse Foundation, Inc. nor the names of its - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/planar/algorithm/lineintersector.go b/planar/algorithm/lineintersector.go deleted file mode 100644 index b9237a71..00000000 --- a/planar/algorithm/lineintersector.go +++ /dev/null @@ -1,401 +0,0 @@ -/* -Copyright (c) 2016 Vivid Solutions. - -All rights reserved. This program and the accompanying materials -are made available under the terms of the Eclipse Public License v1.0 -and Eclipse Distribution License v. 1.0 which accompanies this distribution. -The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html -and the Eclipse Distribution License is available at - -http://www.eclipse.org/org/documents/edl-v10.php. -*/ - -package algorithm - -const ( - // Indicates that line segments do not intersect - NO_INTERSECTION = 0 - // Indicates that line segments intersect in a single point - POINT_INTERSECTION = 1 - // Indicates that line segments intersect in a line segment - COLLINEAR_INTERSECTION = 2 -) - -/* -LineIntersector is an algorithm that can both test whether -two line segments intersect and compute the intersection point(s) -if they do. - -There are three possible outcomes when determining whether two line segments intersect: - -NO_INTERSECTION - the segments do not intersect -POINT_INTERSECTION - the segments intersect in a single point -COLLINEAR_INTERSECTION - the segments are collinear and they intersect in a line segment - -For segments which intersect in a single point, the point may be either an endpoint -or in the interior of each segment. -If the point lies in the interior of both segments, -this is termed a proper intersection. -The method isProper() test for this situation. - -The intersection point(s) may be computed in a precise or non-precise manner. -Computing an intersection point precisely involves rounding it -via a supplied PrecisionModel. - -LineIntersectors do not perform an initial envelope intersection test -to determine if the segments are disjoint. -This is because this class is likely to be used in a context where -envelope overlap is already known to occur (or be likely). -*/ -type lineIntersector struct { - -} - -public abstract class LineIntersector -{ - - /** - * Computes the "edge distance" of an intersection point p along a segment. - * The edge distance is a metric of the point along the edge. - * The metric used is a robust and easy to compute metric function. - * It is not equivalent to the usual Euclidean metric. - * It relies on the fact that either the x or the y ordinates of the - * points in the edge are unique, depending on whether the edge is longer in - * the horizontal or vertical direction. - *

- * NOTE: This function may produce incorrect distances - * for inputs where p is not precisely on p1-p2 - * (E.g. p = (139,9) p1 = (139,10), p2 = (280,1) produces distance 0.0, which is incorrect. - *

- * My hypothesis is that the function is safe to use for points which are the - * result of rounding points which lie on the line, - * but not safe to use for truncated points. - */ - public static double computeEdgeDistance( - Coordinate p, - Coordinate p0, - Coordinate p1) - { - double dx = Math.abs(p1.x - p0.x); - double dy = Math.abs(p1.y - p0.y); - - double dist = -1.0; // sentinel value - if (p.equals(p0)) { - dist = 0.0; - } - else if (p.equals(p1)) { - if (dx > dy) - dist = dx; - else - dist = dy; - } - else { - double pdx = Math.abs(p.x - p0.x); - double pdy = Math.abs(p.y - p0.y); - if (dx > dy) - dist = pdx; - else - dist = pdy; - // - // hack to ensure that non-endpoints always have a non-zero distance - if (dist == 0.0 && ! p.equals(p0)) - { - dist = Math.max(pdx, pdy); - } - } - Assert.isTrue(! (dist == 0.0 && ! p.equals(p0)), "Bad distance calculation"); - return dist; - } - - /** - * This function is non-robust, since it may compute the square of large numbers. - * Currently not sure how to improve this. - */ - public static double nonRobustComputeEdgeDistance( - Coordinate p, - Coordinate p1, - Coordinate p2) - { - double dx = p.x - p1.x; - double dy = p.y - p1.y; - double dist = Math.sqrt(dx * dx + dy * dy); // dummy value - Assert.isTrue(! (dist == 0.0 && ! p.equals(p1)), "Invalid distance calculation"); - return dist; - } - - protected int result; - protected Coordinate[][] inputLines = new Coordinate[2][2]; - protected Coordinate[] intPt = new Coordinate[2]; - /** - * The indexes of the endpoints of the intersection lines, in order along - * the corresponding line - */ - protected int[][] intLineIndex; - protected boolean isProper; - protected Coordinate pa; - protected Coordinate pb; - /** - * If makePrecise is true, computed intersection coordinates will be made precise - * using Coordinate#makePrecise - */ - protected PrecisionModel precisionModel = null; -//public int numIntersects = 0; - - public LineIntersector() { - intPt[0] = new Coordinate(); - intPt[1] = new Coordinate(); - // alias the intersection points for ease of reference - pa = intPt[0]; - pb = intPt[1]; - result = 0; - } - - /** - * Force computed intersection to be rounded to a given precision model - * @param precisionModel - * @deprecated use setPrecisionModel instead - */ - public void setMakePrecise(PrecisionModel precisionModel) - { - this.precisionModel = precisionModel; - } - - /** - * Force computed intersection to be rounded to a given precision model. - * No getter is provided, because the precision model is not required to be specified. - * @param precisionModel - */ - public void setPrecisionModel(PrecisionModel precisionModel) - { - this.precisionModel = precisionModel; - } - - /** - * Gets an endpoint of an input segment. - * - * @param segmentIndex the index of the input segment (0 or 1) - * @param ptIndex the index of the endpoint (0 or 1) - * @return the specified endpoint - */ - public Coordinate getEndpoint(int segmentIndex, int ptIndex) - { - return inputLines[segmentIndex][ptIndex]; - } - - /** - * Compute the intersection of a point p and the line p1-p2. - * This function computes the boolean value of the hasIntersection test. - * The actual value of the intersection (if there is one) - * is equal to the value of p. - */ - public abstract void computeIntersection( - Coordinate p, - Coordinate p1, Coordinate p2); - - protected boolean isCollinear() { - return result == COLLINEAR_INTERSECTION; - } - - /** - * Computes the intersection of the lines p1-p2 and p3-p4. - * This function computes both the boolean value of the hasIntersection test - * and the (approximate) value of the intersection point itself (if there is one). - */ - public void computeIntersection( - Coordinate p1, Coordinate p2, - Coordinate p3, Coordinate p4) { - inputLines[0][0] = p1; - inputLines[0][1] = p2; - inputLines[1][0] = p3; - inputLines[1][1] = p4; - result = computeIntersect(p1, p2, p3, p4); -//numIntersects++; - } - - protected abstract int computeIntersect( - Coordinate p1, Coordinate p2, - Coordinate q1, Coordinate q2); - -/* - public String toString() { - String str = inputLines[0][0] + "-" - + inputLines[0][1] + " " - + inputLines[1][0] + "-" - + inputLines[1][1] + " : " - + getTopologySummary(); - return str; - } -*/ - - public String toString() { - return WKTWriter.toLineString(inputLines[0][0], inputLines[0][1]) + " - " - + WKTWriter.toLineString(inputLines[1][0], inputLines[1][1]) - + getTopologySummary(); - } - - private String getTopologySummary() - { - StringBuilder catBuilder = new StringBuilder(); - if (isEndPoint()) catBuilder.append(" endpoint"); - if (isProper) catBuilder.append(" proper"); - if (isCollinear()) catBuilder.append(" collinear"); - return catBuilder.toString(); - } - - protected boolean isEndPoint() { - return hasIntersection() && !isProper; - } - - /** - * Tests whether the input geometries intersect. - * - * @return true if the input geometries intersect - */ - public boolean hasIntersection() { - return result != NO_INTERSECTION; - } - - /** - * Returns the number of intersection points found. This will be either 0, 1 or 2. - * - * @return the number of intersection points found (0, 1, or 2) - */ - public int getIntersectionNum() { return result; } - - /** - * Returns the intIndex'th intersection point - * - * @param intIndex is 0 or 1 - * - * @return the intIndex'th intersection point - */ - public Coordinate getIntersection(int intIndex) { return intPt[intIndex]; } - - protected void computeIntLineIndex() { - if (intLineIndex == null) { - intLineIndex = new int[2][2]; - computeIntLineIndex(0); - computeIntLineIndex(1); - } - } - - /** - * Test whether a point is a intersection point of two line segments. - * Note that if the intersection is a line segment, this method only tests for - * equality with the endpoints of the intersection segment. - * It does not return true if - * the input point is internal to the intersection segment. - * - * @return true if the input point is one of the intersection points. - */ - public boolean isIntersection(Coordinate pt) { - for (int i = 0; i < result; i++) { - if (intPt[i].equals2D(pt)) { - return true; - } - } - return false; - } - - /** - * Tests whether either intersection point is an interior point of one of the input segments. - * - * @return true if either intersection point is in the interior of one of the input segments - */ - public boolean isInteriorIntersection() - { - if (isInteriorIntersection(0)) return true; - if (isInteriorIntersection(1)) return true; - return false; - } - - /** - * Tests whether either intersection point is an interior point of the specified input segment. - * - * @return true if either intersection point is in the interior of the input segment - */ - public boolean isInteriorIntersection(int inputLineIndex) - { - for (int i = 0; i < result; i++) { - if (! ( intPt[i].equals2D(inputLines[inputLineIndex][0]) - || intPt[i].equals2D(inputLines[inputLineIndex][1]) )) { - return true; - } - } - return false; - } - - /** - * Tests whether an intersection is proper. - *
- * The intersection between two line segments is considered proper if - * they intersect in a single point in the interior of both segments - * (e.g. the intersection is a single point and is not equal to any of the - * endpoints). - *

- * The intersection between a point and a line segment is considered proper - * if the point lies in the interior of the segment (e.g. is not equal to - * either of the endpoints). - * - * @return true if the intersection is proper - */ - public boolean isProper() { - return hasIntersection() && isProper; - } - - /** - * Computes the intIndex'th intersection point in the direction of - * a specified input line segment - * - * @param segmentIndex is 0 or 1 - * @param intIndex is 0 or 1 - * - * @return the intIndex'th intersection point in the direction of the specified input line segment - */ - public Coordinate getIntersectionAlongSegment(int segmentIndex, int intIndex) { - // lazily compute int line array - computeIntLineIndex(); - return intPt[intLineIndex[segmentIndex][intIndex]]; - } - - /** - * Computes the index (order) of the intIndex'th intersection point in the direction of - * a specified input line segment - * - * @param segmentIndex is 0 or 1 - * @param intIndex is 0 or 1 - * - * @return the index of the intersection point along the input segment (0 or 1) - */ - public int getIndexAlongSegment(int segmentIndex, int intIndex) { - computeIntLineIndex(); - return intLineIndex[segmentIndex][intIndex]; - } - - protected void computeIntLineIndex(int segmentIndex) { - double dist0 = getEdgeDistance(segmentIndex, 0); - double dist1 = getEdgeDistance(segmentIndex, 1); - if (dist0 > dist1) { - intLineIndex[segmentIndex][0] = 0; - intLineIndex[segmentIndex][1] = 1; - } - else { - intLineIndex[segmentIndex][0] = 1; - intLineIndex[segmentIndex][1] = 0; - } - } - - /** - * Computes the "edge distance" of an intersection point along the specified input line segment. - * - * @param segmentIndex is 0 or 1 - * @param intIndex is 0 or 1 - * - * @return the edge distance of the intersection point - */ - public double getEdgeDistance(int segmentIndex, int intIndex) { - double dist = computeEdgeDistance(intPt[intIndex], inputLines[segmentIndex][0], - inputLines[segmentIndex][1]); - return dist; - } -} diff --git a/planar/algorithm/robustlineintersector.go b/planar/algorithm/robustlineintersector.go deleted file mode 100644 index 627521fe..00000000 --- a/planar/algorithm/robustlineintersector.go +++ /dev/null @@ -1,450 +0,0 @@ -/* -Copyright (c) 2016 Vivid Solutions. - -All rights reserved. This program and the accompanying materials -are made available under the terms of the Eclipse Public License v1.0 -and Eclipse Distribution License v. 1.0 which accompanies this distribution. -The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html -and the Eclipse Distribution License is available at - -http://www.eclipse.org/org/documents/edl-v10.php. -*/ - -package algorithm - -type robustLineIntersector struct {} - -/* -RobustLineIntersector is a robust version of LineIntersector. - -(Taken from JTS) -*/ -var RobustLineIntersector robustLineIntersector - - - public void computeIntersection(Coordinate p, Coordinate p1, Coordinate p2) { - isProper = false; - // do between check first, since it is faster than the orientation test - if (Envelope.intersects(p1, p2, p)) { - if ((Orientation.index(p1, p2, p) == 0) - && (Orientation.index(p2, p1, p) == 0)) { - isProper = true; - if (p.equals(p1) || p.equals(p2)) { - isProper = false; - } - result = POINT_INTERSECTION; - return; - } - } - result = NO_INTERSECTION; - } - - protected int computeIntersect( - Coordinate p1, Coordinate p2, - Coordinate q1, Coordinate q2 ) { - isProper = false; - - // first try a fast test to see if the envelopes of the lines intersect - if (! Envelope.intersects(p1, p2, q1, q2)) - return NO_INTERSECTION; - - // for each endpoint, compute which side of the other segment it lies - // if both endpoints lie on the same side of the other segment, - // the segments do not intersect - int Pq1 = Orientation.index(p1, p2, q1); - int Pq2 = Orientation.index(p1, p2, q2); - - if ((Pq1>0 && Pq2>0) || (Pq1<0 && Pq2<0)) { - return NO_INTERSECTION; - } - - int Qp1 = Orientation.index(q1, q2, p1); - int Qp2 = Orientation.index(q1, q2, p2); - - if ((Qp1>0 && Qp2>0) || (Qp1<0 && Qp2<0)) { - return NO_INTERSECTION; - } - - boolean collinear = Pq1 == 0 - && Pq2 == 0 - && Qp1 == 0 - && Qp2 == 0; - if (collinear) { - return computeCollinearIntersection(p1, p2, q1, q2); - } - - /** - * At this point we know that there is a single intersection point - * (since the lines are not collinear). - */ - - /** - * Check if the intersection is an endpoint. If it is, copy the endpoint as - * the intersection point. Copying the point rather than computing it - * ensures the point has the exact value, which is important for - * robustness. It is sufficient to simply check for an endpoint which is on - * the other line, since at this point we know that the inputLines must - * intersect. - */ - if (Pq1 == 0 || Pq2 == 0 || Qp1 == 0 || Qp2 == 0) { - isProper = false; - - /** - * Check for two equal endpoints. - * This is done explicitly rather than by the orientation tests - * below in order to improve robustness. - * - * [An example where the orientation tests fail to be consistent is - * the following (where the true intersection is at the shared endpoint - * POINT (19.850257749638203 46.29709338043669) - * - * LINESTRING ( 19.850257749638203 46.29709338043669, 20.31970698357233 46.76654261437082 ) - * and - * LINESTRING ( -48.51001596420236 -22.063180333403878, 19.850257749638203 46.29709338043669 ) - * - * which used to produce the INCORRECT result: (20.31970698357233, 46.76654261437082, NaN) - * - */ - if (p1.equals2D(q1) - || p1.equals2D(q2)) { - intPt[0] = p1; - } - else if (p2.equals2D(q1) - || p2.equals2D(q2)) { - intPt[0] = p2; - } - - /** - * Now check to see if any endpoint lies on the interior of the other segment. - */ - else if (Pq1 == 0) { - intPt[0] = new Coordinate(q1); - } - else if (Pq2 == 0) { - intPt[0] = new Coordinate(q2); - } - else if (Qp1 == 0) { - intPt[0] = new Coordinate(p1); - } - else if (Qp2 == 0) { - intPt[0] = new Coordinate(p2); - } - } - else { - isProper = true; - intPt[0] = intersection(p1, p2, q1, q2); - } - return POINT_INTERSECTION; - } - - private int computeCollinearIntersection(Coordinate p1, Coordinate p2, - Coordinate q1, Coordinate q2) { - boolean p1q1p2 = Envelope.intersects(p1, p2, q1); - boolean p1q2p2 = Envelope.intersects(p1, p2, q2); - boolean q1p1q2 = Envelope.intersects(q1, q2, p1); - boolean q1p2q2 = Envelope.intersects(q1, q2, p2); - - if (p1q1p2 && p1q2p2) { - intPt[0] = q1; - intPt[1] = q2; - return COLLINEAR_INTERSECTION; - } - if (q1p1q2 && q1p2q2) { - intPt[0] = p1; - intPt[1] = p2; - return COLLINEAR_INTERSECTION; - } - if (p1q1p2 && q1p1q2) { - intPt[0] = q1; - intPt[1] = p1; - return q1.equals(p1) && !p1q2p2 && !q1p2q2 ? POINT_INTERSECTION : COLLINEAR_INTERSECTION; - } - if (p1q1p2 && q1p2q2) { - intPt[0] = q1; - intPt[1] = p2; - return q1.equals(p2) && !p1q2p2 && !q1p1q2 ? POINT_INTERSECTION : COLLINEAR_INTERSECTION; - } - if (p1q2p2 && q1p1q2) { - intPt[0] = q2; - intPt[1] = p1; - return q2.equals(p1) && !p1q1p2 && !q1p2q2 ? POINT_INTERSECTION : COLLINEAR_INTERSECTION; - } - if (p1q2p2 && q1p2q2) { - intPt[0] = q2; - intPt[1] = p2; - return q2.equals(p2) && !p1q1p2 && !q1p1q2 ? POINT_INTERSECTION : COLLINEAR_INTERSECTION; - } - return NO_INTERSECTION; - } - - /** - * This method computes the actual value of the intersection point. - * To obtain the maximum precision from the intersection calculation, - * the coordinates are normalized by subtracting the minimum - * ordinate values (in absolute value). This has the effect of - * removing common significant digits from the calculation to - * maintain more bits of precision. - */ - private Coordinate intersection( - Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) - { - Coordinate intPt = intersectionWithNormalization(p1, p2, q1, q2); - - /* - // TESTING ONLY - Coordinate intPtDD = CGAlgorithmsDD.intersection(p1, p2, q1, q2); - double dist = intPt.distance(intPtDD); - System.out.println(intPt + " - " + intPtDD + " dist = " + dist); - //intPt = intPtDD; - */ - - /** - * Due to rounding it can happen that the computed intersection is - * outside the envelopes of the input segments. Clearly this - * is inconsistent. - * This code checks this condition and forces a more reasonable answer - * - * MD - May 4 2005 - This is still a problem. Here is a failure case: - * - * LINESTRING (2089426.5233462777 1180182.3877339689, 2085646.6891757075 1195618.7333999649) - * LINESTRING (1889281.8148903656 1997547.0560044837, 2259977.3672235999 483675.17050843034) - * int point = (2097408.2633752143,1144595.8008114607) - * - * MD - Dec 14 2006 - This does not seem to be a failure case any longer - */ - if (! isInSegmentEnvelopes(intPt)) { -// System.out.println("Intersection outside segment envelopes: " + intPt); - - // compute a safer result - // copy the coordinate, since it may be rounded later - intPt = new Coordinate(nearestEndpoint(p1, p2, q1, q2)); -// intPt = CentralEndpointIntersector.getIntersection(p1, p2, q1, q2); - -// System.out.println("Segments: " + this); -// System.out.println("Snapped to " + intPt); -// checkDD(p1, p2, q1, q2, intPt); - } - if (precisionModel != null) { - precisionModel.makePrecise(intPt); - } - return intPt; - } - - private void checkDD(Coordinate p1, Coordinate p2, Coordinate q1, - Coordinate q2, Coordinate intPt) - { - Coordinate intPtDD = CGAlgorithmsDD.intersection(p1, p2, q1, q2); - boolean isIn = isInSegmentEnvelopes(intPtDD); - System.out.println( "DD in env = " + isIn + " --------------------- " + intPtDD); - if (intPt.distance(intPtDD) > 0.0001) { - System.out.println("Distance = " + intPt.distance(intPtDD)); - } - } - - private Coordinate intersectionWithNormalization( - Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) - { - Coordinate n1 = new Coordinate(p1); - Coordinate n2 = new Coordinate(p2); - Coordinate n3 = new Coordinate(q1); - Coordinate n4 = new Coordinate(q2); - Coordinate normPt = new Coordinate(); - normalizeToEnvCentre(n1, n2, n3, n4, normPt); - - Coordinate intPt = safeHCoordinateIntersection(n1, n2, n3, n4); - - intPt.x += normPt.x; - intPt.y += normPt.y; - - return intPt; - } - - /** - * Computes a segment intersection using homogeneous coordinates. - * Round-off error can cause the raw computation to fail, - * (usually due to the segments being approximately parallel). - * If this happens, a reasonable approximation is computed instead. - * - * @param p1 a segment endpoint - * @param p2 a segment endpoint - * @param q1 a segment endpoint - * @param q2 a segment endpoint - * @return the computed intersection point - */ - private Coordinate safeHCoordinateIntersection(Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) - { - Coordinate intPt = null; - try { - intPt = HCoordinate.intersection(p1, p2, q1, q2); - } - catch (NotRepresentableException e) { -// System.out.println("Not calculable: " + this); - // compute an approximate result -// intPt = CentralEndpointIntersector.getIntersection(p1, p2, q1, q2); - intPt = nearestEndpoint(p1, p2, q1, q2); - // System.out.println("Snapped to " + intPt); - } - return intPt; - } - - /** - * Normalize the supplied coordinates so that - * their minimum ordinate values lie at the origin. - * NOTE: this normalization technique appears to cause - * large errors in the position of the intersection point for some cases. - * - * @param n1 - * @param n2 - * @param n3 - * @param n4 - * @param normPt - */ - private void normalizeToMinimum( - Coordinate n1, - Coordinate n2, - Coordinate n3, - Coordinate n4, - Coordinate normPt) - { - normPt.x = smallestInAbsValue(n1.x, n2.x, n3.x, n4.x); - normPt.y = smallestInAbsValue(n1.y, n2.y, n3.y, n4.y); - n1.x -= normPt.x; n1.y -= normPt.y; - n2.x -= normPt.x; n2.y -= normPt.y; - n3.x -= normPt.x; n3.y -= normPt.y; - n4.x -= normPt.x; n4.y -= normPt.y; - } - - /** - * Normalize the supplied coordinates to - * so that the midpoint of their intersection envelope - * lies at the origin. - * - * @param n00 - * @param n01 - * @param n10 - * @param n11 - * @param normPt - */ - private void normalizeToEnvCentre( - Coordinate n00, - Coordinate n01, - Coordinate n10, - Coordinate n11, - Coordinate normPt) - { - double minX0 = n00.x < n01.x ? n00.x : n01.x; - double minY0 = n00.y < n01.y ? n00.y : n01.y; - double maxX0 = n00.x > n01.x ? n00.x : n01.x; - double maxY0 = n00.y > n01.y ? n00.y : n01.y; - - double minX1 = n10.x < n11.x ? n10.x : n11.x; - double minY1 = n10.y < n11.y ? n10.y : n11.y; - double maxX1 = n10.x > n11.x ? n10.x : n11.x; - double maxY1 = n10.y > n11.y ? n10.y : n11.y; - - double intMinX = minX0 > minX1 ? minX0 : minX1; - double intMaxX = maxX0 < maxX1 ? maxX0 : maxX1; - double intMinY = minY0 > minY1 ? minY0 : minY1; - double intMaxY = maxY0 < maxY1 ? maxY0 : maxY1; - - double intMidX = (intMinX + intMaxX) / 2.0; - double intMidY = (intMinY + intMaxY) / 2.0; - normPt.x = intMidX; - normPt.y = intMidY; - - /* - // equilavalent code using more modular but slower method - Envelope env0 = new Envelope(n00, n01); - Envelope env1 = new Envelope(n10, n11); - Envelope intEnv = env0.intersection(env1); - Coordinate intMidPt = intEnv.centre(); - - normPt.x = intMidPt.x; - normPt.y = intMidPt.y; - */ - - n00.x -= normPt.x; n00.y -= normPt.y; - n01.x -= normPt.x; n01.y -= normPt.y; - n10.x -= normPt.x; n10.y -= normPt.y; - n11.x -= normPt.x; n11.y -= normPt.y; - } - - private double smallestInAbsValue(double x1, double x2, double x3, double x4) - { - double x = x1; - double xabs = Math.abs(x); - if (Math.abs(x2) < xabs) { - x = x2; - xabs = Math.abs(x2); - } - if (Math.abs(x3) < xabs) { - x = x3; - xabs = Math.abs(x3); - } - if (Math.abs(x4) < xabs) { - x = x4; - } - return x; - } - - /** - * Tests whether a point lies in the envelopes of both input segments. - * A correctly computed intersection point should return true - * for this test. - * Since this test is for debugging purposes only, no attempt is - * made to optimize the envelope test. - * - * @return true if the input point lies within both input segment envelopes - */ - private boolean isInSegmentEnvelopes(Coordinate intPt) - { - Envelope env0 = new Envelope(inputLines[0][0], inputLines[0][1]); - Envelope env1 = new Envelope(inputLines[1][0], inputLines[1][1]); - return env0.contains(intPt) && env1.contains(intPt); - } - - /** - * Finds the endpoint of the segments P and Q which - * is closest to the other segment. - * This is a reasonable surrogate for the true - * intersection points in ill-conditioned cases - * (e.g. where two segments are nearly coincident, - * or where the endpoint of one segment lies almost on the other segment). - *

- * This replaces the older CentralEndpoint heuristic, - * which chose the wrong endpoint in some cases - * where the segments had very distinct slopes - * and one endpoint lay almost on the other segment. - * - * @param p1 an endpoint of segment P - * @param p2 an endpoint of segment P - * @param q1 an endpoint of segment Q - * @param q2 an endpoint of segment Q - * @return the nearest endpoint to the other segment - */ - private static Coordinate nearestEndpoint(Coordinate p1, Coordinate p2, - Coordinate q1, Coordinate q2) - { - Coordinate nearestPt = p1; - double minDist = Distance.pointToSegment(p1, q1, q2); - - double dist = Distance.pointToSegment(p2, q1, q2); - if (dist < minDist) { - minDist = dist; - nearestPt = p2; - } - dist = Distance.pointToSegment(q1, p1, p2); - if (dist < minDist) { - minDist = dist; - nearestPt = q1; - } - dist = Distance.pointToSegment(q2, p1, p2); - if (dist < minDist) { - minDist = dist; - nearestPt = q2; - } - return nearestPt; - } - - -} diff --git a/planar/triangulate/constraineddelaunay/triangulator.go b/planar/triangulate/constraineddelaunay/triangulator.go index e121c3b1..adaf1cdc 100644 --- a/planar/triangulate/constraineddelaunay/triangulator.go +++ b/planar/triangulate/constraineddelaunay/triangulator.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "math" "github.com/go-spatial/geom" "github.com/go-spatial/geom/cmp" @@ -34,6 +35,9 @@ TODO: */ var ErrInvalidPointClassification = errors.New("invalid point classification") +var ErrLinesDoNotIntersect = errors.New("line segments do not intersect") +// these errors indicate a problem with the algorithm. +var ErrUnexpectedDeadNode = errors.New("unexpected dead node") var ErrUnsupportedCoincidentEdges = errors.New("unsupported coincident edges") /* @@ -82,13 +86,13 @@ func (tri *Triangulator) createSegment(s triangulate.Segment) error { return nil } - ct, err := tri.findContainingTriangle(s) + ct, err := tri.findIntersectingTriangle(s) if err != nil { return err } from := ct.qe.Sym() - ct, err = tri.findContainingTriangle(triangulate.NewSegment(geom.Line{s.GetEnd(), s.GetStart()})) + ct, err = tri.findIntersectingTriangle(triangulate.NewSegment(geom.Line{s.GetEnd(), s.GetStart()})) if err != nil { return err } @@ -172,13 +176,16 @@ func (tri *Triangulator) deleteEdge(e *quadedge.QuadEdge) { } /* -findContainingTriangle finds the triangle that contains the vertex s.GetStart() -and contains at least part of the edge that extends from s.GetStart(). +findIntersectingTriangle finds the triangle that shares the vertex s.GetStart() +and intersects at least part of the edge that extends from s.GetStart(). + +Tolerance is not considered when determining if vertices are the same. Returns a quadedge that has s.GetStart() as the origin and the right face is -the desired triangle. +the desired triangle. If the segment falls on an edge, the triangle to the +right of the segment is returned. */ -func (tri *Triangulator) findContainingTriangle(s triangulate.Segment) (*Triangle, error) { +func (tri *Triangulator) findIntersectingTriangle(s triangulate.Segment) (*Triangle, error) { qe, err := tri.locateEdgeByVertex(s.GetStart()) if err != nil { @@ -186,19 +193,22 @@ func (tri *Triangulator) findContainingTriangle(s triangulate.Segment) (*Triangl } left := qe + log.Printf("s: %v", s) // walk around all the triangles that share qe.Orig() for true { if left.IsLive() == false { - log.Fatalf("unexpected dead node: %v", left) + return nil, ErrUnexpectedDeadNode } // create the two quad edges around s right := left.OPrev() lc := s.GetEnd().Classify(left.Orig(), left.Dest()) rc := s.GetEnd().Classify(right.Orig(), right.Dest()) - - if lc == quadedge.RIGHT && rc == quadedge.LEFT { + + log.Printf("left: %v right: %v", left, right) + log.Printf("lc: %v rc: %v", lc, rc) + if (lc == quadedge.RIGHT && rc == quadedge.LEFT) || lc == quadedge.BETWEEN || lc == quadedge.DESTINATION || lc == quadedge.BEYOND { // if s is between the two edges, we found our triangle. return &Triangle{left}, nil } else if lc != quadedge.RIGHT && lc != quadedge.LEFT && rc != quadedge.LEFT && rc != quadedge.RIGHT { @@ -210,11 +220,11 @@ func (tri *Triangulator) findContainingTriangle(s triangulate.Segment) (*Triangl if left == qe { // if we've walked all the way around the vertex. - return nil, fmt.Errorf("no containing triangle: %v", s) + return nil, fmt.Errorf("no intersecting triangle: %v", s) } } - return nil, fmt.Errorf("no containing triangle: %v", s) + return nil, fmt.Errorf("no intersecting triangle: %v", s) } /* @@ -285,6 +295,7 @@ func (tri *Triangulator) insertConstraints(g geom.Geometry) error { if err != nil { return fmt.Errorf("error adding constraint: %v", err) } + constraints := make(map[triangulate.Segment]bool) for _, l := range(lines) { // make the line ordering consistent if !cmp.PointLess(l[0], l[1]) { @@ -293,22 +304,16 @@ func (tri *Triangulator) insertConstraints(g geom.Geometry) error { seg := triangulate.NewSegment(l) // this maintains the constraints and de-dupes + constraints[seg] = true tri.constraints[seg] = true } log.Printf("tri.constraints: %v", tri.constraints) - for seg := range tri.constraints { - qe, err := tri.LocateSegment(seg.GetStart(), seg.GetEnd()) - if qe != nil && err != nil { + for seg := range constraints { + err := tri.insertEdgeCDT(&seg) + if err != nil { return fmt.Errorf("error adding constraint: %v", err) } - - if qe == nil { - err := tri.insertEdgeCDT(&seg) - if err != nil { - return fmt.Errorf("error adding constraint: %v", err) - } - } if err = tri.Validate(); err != nil { log.Fatalf("validate failed: %v", err) } @@ -317,27 +322,88 @@ func (tri *Triangulator) insertConstraints(g geom.Geometry) error { return nil } -func (tri *Triangulator) IsConstraint(s triangulate.Segment) bool { - _, ok := tri.constraints[s] +/* +intersection calculates the intersection between two line segments. When the +rest of geom is ported over from spatial, this can be replaced with a more +generic call. + +The tolerance here only acts by extending the lines by tolerance. E.g. if the +tolerance is 0.1 and you have two lines {{0, 0}, {1, 0}} and +{{0, 0.01}, {1, 0.01}} then these will not be marked as intersecting lines. + +If tolerance is used to mark two lines as intersecting, you are still +guaranteed that the intersecting point will fall _on_ one of the lines, not in +the extended region of the line. + +Taken from: https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect +*/ +func (tri *Triangulator) intersection(l1, l2 triangulate.Segment) (quadedge.Vertex, error) { + p := l1.GetStart() + r := l1.GetEnd().Sub(p) + q := l2.GetStart() + s := l2.GetEnd().Sub(q) + + rs := r.CrossProduct(s) + log.Printf("rs: %v", rs) + + if rs == 0 { + return quadedge.Vertex{}, ErrLinesDoNotIntersect + } + t := q.Sub(p).CrossProduct(s.Divide(r.CrossProduct(s))) + u := p.Sub(q).CrossProduct(r.Divide(s.CrossProduct(r))) + + // calculate the acceptable range of values for t + ttolerance := tri.tolerance / r.Magn() + tlow := -ttolerance + thigh := 1 + ttolerance + + // calculate the acceptable range of values for u + utolerance := tri.tolerance / s.Magn() + ulow := -utolerance + uhigh := 1 + utolerance + log.Printf("t: %v u: %v", t, u) + + if t < tlow || t > thigh || u < ulow || u > uhigh { + return quadedge.Vertex{}, ErrLinesDoNotIntersect + } + // if t is just out of range, but within the acceptable tolerance, snap + // it back to the beginning/end of the line. + t = math.Min(1, math.Max(t, 0)) + + return p.Sum(r.Times(t)), nil +} + +func (tri *Triangulator) IsConstraint(e *quadedge.QuadEdge) bool { + + _, ok := tri.constraints[triangulate.NewSegment(geom.Line{e.Orig(), e.Dest()})] + if ok { + return true + } + _, ok = tri.constraints[triangulate.NewSegment(geom.Line{e.Dest(), e.Orig()})] return ok } // Procedure InsertEdgeCDT(T:CDT, ab:Edge) func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { log.Printf("ab: %v", *ab) + log.Print(tri.subdiv.DebugDumpEdges()) + + + qe, err := tri.LocateSegment(ab.GetStart(), ab.GetEnd()) + if qe != nil && err != nil { + return fmt.Errorf("error inserting constraint: %v", err) + } + if qe != nil { + // nothing to do, the edge already exists. + return nil + } + // Precondition: a,b in T and ab not in T // Find the triangle t ∈ T that contains a and is cut by ab - at, err := tri.findContainingTriangle(*ab) - if err != nil { - return err - } - be, err := tri.locateEdgeByVertex(ab.GetEnd()) + t, err := tri.findIntersectingTriangle(*ab) if err != nil { return err } - log.Printf("be: %v", be) - log.Printf("at: %v", at) - t := at removalList := make([]*quadedge.QuadEdge, 0) @@ -372,51 +438,108 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { log.Printf("shared: %v", shared) c := vseq.Classify(ab.GetStart(), ab.GetEnd()) + cv := v.Classify(ab.GetStart(), ab.GetEnd()) + log.Printf("c: %v", c) + log.Printf("cv: %v", cv) + // should we remove the edge shared between t & tseq? + flagEdgeForRemoval := false - switch c { + switch { + + case tri.subdiv.IsOnLine(ab.GetLineSegment(), shared.Orig()): + // InsertEdgeCDT(T, vseqb) + vb := triangulate.NewSegment(geom.Line{shared.Orig(), ab.GetEnd()}) + tri.insertEdgeCDT(&vb) + // a:=vseq -- Should this be b:=vseq!? -JRS + b = shared.Orig() + *ab = triangulate.NewSegment(geom.Line{ab.GetStart(), b}) + + case tri.subdiv.IsOnLine(ab.GetLineSegment(), shared.Dest()): + // InsertEdgeCDT(T, vseqb) + vb := triangulate.NewSegment(geom.Line{shared.Dest(), ab.GetEnd()}) + tri.insertEdgeCDT(&vb) + // a:=vseq -- Should this be b:=vseq!? -JRS + b = shared.Dest() + *ab = triangulate.NewSegment(geom.Line{ab.GetStart(), b}) + + // if the constrained edge is passing through another constrained edge + case tri.IsConstraint(shared): + // find the point of intersection + iv, err := tri.intersection(*ab, triangulate.NewSegment(geom.Line{shared.Orig(), shared.Dest()})) + if err != nil { + return err + } + log.Printf("Intersection: %v", iv) + // split the constrained edge we interesect + if err := tri.splitEdge(shared, iv); err != nil { + return err + } + tri.deleteEdge(shared) + tseq, err = t.opposedTriangle(v) + if err != nil { + return err + } + // create a new edge for the rest of this segment and recursively + // insert the new edge. + vb := triangulate.NewSegment(geom.Line{iv, ab.GetEnd()}) + tri.insertEdgeCDT(&vb) + // the current insertion will stop at the interesction point + b = iv + *ab = triangulate.NewSegment(geom.Line{ab.GetStart(), iv}) + //flagEdgeForRemoval = true + // If vseq above the edge ab then - case quadedge.LEFT: + case c == quadedge.LEFT: // v:=Vertex shared by t and tseq above ab v = shared.Orig() pu = appendNonRepeat(pu, v) // AddList(PU ,vseq) pu = appendNonRepeat(pu, vseq) + flagEdgeForRemoval = true + // Else If vseq below the edge ab - case quadedge.RIGHT: + case c == quadedge.RIGHT: // v:=Vertex shared by t and tseq below ab v = shared.Dest() pl = appendNonRepeat(pl, v) // AddList(PL, vseq) pl = appendNonRepeat(pl, vseq) - // NOTE: You may be able to use this same mechanism to handle edges that overlap/intersect - // Else vseq on the edge ab - case quadedge.BETWEEN: - // InsertEdgeCDT(T, vseqb) - // a:=vseq - // break - case quadedge.DESTINATION: - // nothing left to do + flagEdgeForRemoval = true + + // // NOTE: You may be able to use this same mechanism to handle edges that overlap/intersect + // // Else vseq on the edge ab + // case c == quadedge.BETWEEN: + // log.Printf("Between: %v", vseq) + // // InsertEdgeCDT(T, vseqb) + // vseqb := triangulate.NewSegment(geom.Line{vseq, ab.GetEnd()}) + // tri.insertEdgeCDT(&vseqb) + // // a:=vseq -- Should this be b:=vseq!? -JRS + // b = vseq + // *ab = triangulate.NewSegment(geom.Line{ab.GetStart(), b}) + case c == quadedge.DESTINATION: + flagEdgeForRemoval = true + default: log.Printf("c: %v", c) return ErrInvalidPointClassification } - // "Remove t from T" -- We are just removing the edge intersected by - // ab, which in effect removes the triangle. - removalList = append(removalList, shared) + if flagEdgeForRemoval { + // "Remove t from T" -- We are just removing the edge intersected + // by ab, which in effect removes the triangle. + removalList = append(removalList, shared) + } t = tseq } // EndWhile + log.Printf("removalList: %v", removalList) // remove the previously marked edges // TODO Inefficient for i := range(removalList) { tri.deleteEdge(removalList[i]) } - if err := tri.Validate(); err != nil { - log.Fatalf("validate failed: %v", err) - } // TriangulatePseudoPolygon(PU,ab,T) log.Printf("pu: %v", pu) @@ -425,8 +548,14 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { log.Printf("pl: %v", pl) tri.triangulatePseudoPolygon(pl, *ab) + log.Print(tri.subdiv.DebugDumpEdges()) + + if err := tri.Validate(); err != nil { + log.Fatalf("validate failed: %v", err) + } + // Reconstitute the triangle adjacencies of T - // bt, err := tri.findContainingTriangle(triangulate.NewSegment(geom.Line{ab.GetEnd(), ab.GetStart()})) + // bt, err := tri.findIntersectingTriangle(triangulate.NewSegment(geom.Line{ab.GetEnd(), ab.GetStart()})) // if err != nil { // return err // } @@ -467,9 +596,6 @@ func (tri *Triangulator) LocateSegment(v1 quadedge.Vertex, v2 quadedge.Vertex) ( if qe == nil { return nil, quadedge.ErrLocateFailure } - if err := tri.Validate(); err != nil { - log.Fatalf("validate failed: %v", err) - } start := qe for true { @@ -490,6 +616,47 @@ func (tri *Triangulator) LocateSegment(v1 quadedge.Vertex, v2 quadedge.Vertex) ( return qe, nil } +/* +removeConstraintEdge removes any constraints that share the same Orig() and Dest() as the edge provided. If there are none, no changes are made. +*/ +func (tri *Triangulator) removeConstraintEdge(e *quadedge.QuadEdge) { + delete(tri.constraints, triangulate.NewSegment(geom.Line{e.Orig(), e.Dest()})) + delete(tri.constraints, triangulate.NewSegment(geom.Line{e.Dest(), e.Orig()})) +} + +func (tri *Triangulator) splitEdge(e *quadedge.QuadEdge, v quadedge.Vertex) error { + constraint := tri.IsConstraint(e) + + ePrev := e.OPrev() + eSym := e.Sym() + eSymPrev := eSym.OPrev() + + tri.removeConstraintEdge(e) + + e1 := tri.subdiv.MakeEdge(e.Orig(), v) + e2 := tri.subdiv.MakeEdge(e.Dest(), v) + + if _, ok := tri.vertexIndex[v]; ok == false { + tri.vertexIndex[v] = e1.Sym() + } + + // splice e1 on + quadedge.Splice(ePrev, e1) + // splice e2 on + quadedge.Splice(eSymPrev, e2) + + // splice e1 and e2 together + quadedge.Splice(e1.Sym(), e2.Sym()) + + if constraint { + tri.constraints[triangulate.NewSegment(geom.Line{e1.Orig(), e1.Dest()})] = true + tri.constraints[triangulate.NewSegment(geom.Line{e2.Dest(), e2.Orig()})] = true + } + + // since we aren't adding any vertices we don't need to modify the vertex + // index. + return nil +} // TriangulatePseudoPolygon // Pseudocode taken from Figure 10 diff --git a/planar/triangulate/constraineddelaunay/triangulator_test.go b/planar/triangulate/constraineddelaunay/triangulator_test.go index 21023681..15826999 100644 --- a/planar/triangulate/constraineddelaunay/triangulator_test.go +++ b/planar/triangulate/constraineddelaunay/triangulator_test.go @@ -4,6 +4,7 @@ package constraineddelaunay import ( "encoding/hex" "fmt" + "log" "strconv" "testing" @@ -14,13 +15,13 @@ import ( "github.com/go-spatial/geom/planar/triangulate/quadedge" ) -func TestFindContainingTriangle(t *testing.T) { +func TestFindIntersectingTriangle(t *testing.T) { type tcase struct { // provided for readability inputWKT string // this can be removed if/when geom has a WKT decoder. - // A simple website for performing conversions: - // https://rodic.fr/blog/online-conversion-between-geometric-formats/ + // A simple website for performing conversions: + // https://rodic.fr/blog/online-conversion-between-geometric-formats/ inputWKB string searchFrom geom.Line expectedEdge string @@ -42,7 +43,7 @@ func TestFindContainingTriangle(t *testing.T) { uut.tolerance = 1e-6 uut.insertSites(g) - tri, err := uut.findContainingTriangle(triangulate.NewSegment(tc.searchFrom)) + tri, err := uut.findIntersectingTriangle(triangulate.NewSegment(tc.searchFrom)) if err != nil { t.Fatalf("error, expected nil got %v", err) return @@ -72,6 +73,12 @@ func TestFindContainingTriangle(t *testing.T) { searchFrom: geom.Line{{10,10}, {0, 0}}, expectedEdge: `[10 10] -> [10 0]`, }, + { + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + searchFrom: geom.Line{{10,10}, {10, 20}}, + expectedEdge: `[10 10] -> [10 20]`, + }, } for i, tc := range testcases { @@ -152,6 +159,60 @@ func TestDeleteEdge(t *testing.T) { } } +func TestIntersection(t *testing.T) { + type tcase struct { + l1 triangulate.Segment + l2 triangulate.Segment + intersection quadedge.Vertex + expectedError error + } + + fn := func(t *testing.T, tc tcase) { + uut := new(Triangulator) + uut.tolerance = 1e-2 + v, err := uut.intersection(tc.l1, tc.l2) + if err != tc.expectedError { + t.Errorf("error intersecting line segments, expected %v got %v", tc.expectedError, err) + return + } + + if v.Equals(tc.intersection) == false { + t.Errorf("error validating intersection, expected %v got %v", tc.intersection, v) + } + } + testcases := []tcase{ + { + l1: triangulate.NewSegment(geom.Line{{0, 1}, {2, 3}}), + l2: triangulate.NewSegment(geom.Line{{1, 1}, {0, 2}}), + intersection: quadedge.Vertex{0.5, 1.5}, + expectedError: nil, + }, + { + l1: triangulate.NewSegment(geom.Line{{0, 1}, {2, 4}}), + l2: triangulate.NewSegment(geom.Line{{1, 1}, {0, 2}}), + intersection: quadedge.Vertex{0.4, 1.6}, + expectedError: nil, + }, + { + l1: triangulate.NewSegment(geom.Line{{0, 1}, {2, 3}}), + l2: triangulate.NewSegment(geom.Line{{1, 1}, {2, 2}}), + intersection: quadedge.Vertex{0, 0}, + expectedError: ErrLinesDoNotIntersect, + }, + { + l1: triangulate.NewSegment(geom.Line{{3, 5}, {3, 6}}), + l2: triangulate.NewSegment(geom.Line{{1, 4.995}, {4, 4.995}}), + intersection: quadedge.Vertex{3, 5}, + expectedError: nil, + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + /* TestTriangulation test cases test for small constrained triangulations and edge cases @@ -168,6 +229,9 @@ func TestTriangulation(t *testing.T) { expectedTris string } + // to change the flags on the default logger + log.SetFlags(log.LstdFlags | log.Lshortfile) + fn := func(t *testing.T, tc tcase) { bytes, err := hex.DecodeString(tc.inputWKB) if err != nil { @@ -225,19 +289,49 @@ func TestTriangulation(t *testing.T) { { // a horizontal rectangle w/ one diagonal line. The diagonal line // should be maintained and the top/bottom re-triangulated. - inputWKT: `MULTILINESTRING ((0 0,0 1,1 1.1,2 1,2 0,1 -0.1,0 0),(0 0,2 1))`, + inputWKT: `MULTILINESTRING((0 0,0 1,1 1.1,2 1,2 0,1 0.1,0 0),(0 1,2 0))`, inputWKB: `010500000002000000010200000007000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f9a9999999999f13f0000000000000040000000000000f03f00000000000000400000000000000000000000000000f03f9a9999999999b93f000000000000000000000000000000000102000000020000000000000000000000000000000000f03f00000000000000400000000000000000`, expectedEdges: `MULTILINESTRING ((1 1.1,2 1),(0 1,1 1.1),(0 0,0 1),(0 0,2 0),(2 0,2 1),(1 1.1,2 0),(0 1,2 0),(1 0.1,2 0),(0 1,1 0.1),(0 0,1 0.1))`, expectedTris: `MULTIPOLYGON (((0 1,0 0,1 0.1,0 1)),((0 1,1 0.1,2 0,0 1)),((0 1,2 0,1 1.1,0 1)),((1 1.1,2 0,2 1,1 1.1)),((0 0,2 0,1 0.1,0 0)))`, }, { - // a horizontal rectangle w/ one diagonal line. The diagonal line + // an egg shape with one horizontal line. The horizontal line // should be maintained and the top/bottom re-triangulated. inputWKT: `MULTILINESTRING((0 0,-0.1 0.5,0 1,0.5 1.2,1 1.3,1.5 1.2,2 1,2.1 0.5,2 0,1.5 -0.2,1 -0.3,0.5 -0.2,0 0),(-0.1 0.5,2.1 0.5))`, inputWKB: `01050000000200000001020000000d000000000000000000000000000000000000009a9999999999b9bf000000000000e03f0000000000000000000000000000f03f000000000000e03f333333333333f33f000000000000f03fcdccccccccccf43f000000000000f83f333333333333f33f0000000000000040000000000000f03fcdcccccccccc0040000000000000e03f00000000000000400000000000000000000000000000f83f9a9999999999c9bf000000000000f03f333333333333d3bf000000000000e03f9a9999999999c9bf000000000000000000000000000000000102000000020000009a9999999999b9bf000000000000e03fcdcccccccccc0040000000000000e03f`, expectedEdges: `MULTILINESTRING ((1.5 1.2,2 1),(1 1.3,1.5 1.2),(0.5 1.2,1 1.3),(0 1,0.5 1.2),(-0.1 0.5,0 1),(-0.1 0.5,0 0),(0 0,0.5 -0.2),(0.5 -0.2,1 -0.3),(1 -0.3,1.5 -0.2),(1.5 -0.2,2 0),(2 0,2.1 0.5),(2 1,2.1 0.5),(1.5 1.2,2.1 0.5),(1 1.3,2.1 0.5),(-0.1 0.5,2.1 0.5),(-0.1 0.5,1 1.3),(-0.1 0.5,0.5 1.2),(1.5 -0.2,2.1 0.5),(-0.1 0.5,1.5 -0.2),(0.5 -0.2,1.5 -0.2),(-0.1 0.5,0.5 -0.2))`, expectedTris: `MULTIPOLYGON (((0 1,-0.1 0.5,0.5 1.2,0 1)),((0.5 1.2,-0.1 0.5,1 1.3,0.5 1.2)),((1 1.3,-0.1 0.5,2.1 0.5,1 1.3)),((1 1.3,2.1 0.5,1.5 1.2,1 1.3)),((1.5 1.2,2.1 0.5,2 1,1.5 1.2)),((1 -0.3,1.5 -0.2,0.5 -0.2,1 -0.3)),((0.5 -0.2,1.5 -0.2,-0.1 0.5,0.5 -0.2)),((0.5 -0.2,-0.1 0.5,0 0,0.5 -0.2)),((-0.1 0.5,1.5 -0.2,2.1 0.5,-0.1 0.5)),((2.1 0.5,1.5 -0.2,2 0,2.1 0.5)))`, }, + { + // a triangle with a line intersecting the top vertex. Where the + // line intersects the vertex, the line should be broken into two + // pieces and triangulated properly. + inputWKT: `MULTILINESTRING((0 0,-0.1 0.5,0 1,0.5 1.2,1 1.3,1.5 1.2,2 1,2.1 0.5,2 0,1.5 -0.2,1 -0.3,0.5 -0.2,0 0),(-0.1 0.5,2.1 0.5))`, + inputWKB: `01050000000200000001020000000400000000000000000000000000000000000000000000000000f03f000000000000f03f00000000000000400000000000000000000000000000000000000000000000000102000000020000000000000000000000000000000000f03f0000000000000040000000000000f03f`, + expectedEdges: `MULTILINESTRING ((1 1,2 1),(0 1,1 1),(0 0,0 1),(0 0,2 0),(2 0,2 1),(1 1,2 0),(0 0,1 1))`, + expectedTris: `MULTIPOLYGON (((0 1,0 0,1 1,0 1)),((1 1,0 0,2 0,1 1)),((1 1,2 0,2 1,1 1)))`, + }, + { + // a figure eight with a duplicate constrained line. + inputWKT: `MULTIPOLYGON (((0 0,0 1,1 1,1 0,0 0,0 -1,1 -1,1 0,0 0)))`, + inputWKB: `01060000000100000001030000000100000009000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f0000000000000000000000000000000000000000000000000000000000000000000000000000f0bf000000000000f03f000000000000f0bf000000000000f03f000000000000000000000000000000000000000000000000`, + expectedEdges: `MULTILINESTRING ((0 1,1 1),(0 0,0 1),(0 -1,0 0),(0 -1,1 -1),(1 -1,1 0),(1 0,1 1),(0 1,1 0),(0 0,1 0),(0 0,1 -1))`, + expectedTris: `MULTIPOLYGON (((0 1,0 0,1 0,0 1)),((0 1,1 0,1 1,0 1)),((0 -1,1 -1,0 0,0 -1)),((0 0,1 -1,1 0,0 0)))`, + }, + { + // A constraint line that overlaps with another edge + inputWKT: `MULTIPOLYGON (((0 0,1 1,2 1,3 0,3 1,0 1,0 0)))`, + inputWKB: `0106000000010000000103000000010000000700000000000000000000000000000000000000000000000000f03f000000000000f03f0000000000000040000000000000f03f000000000000084000000000000000000000000000000840000000000000f03f0000000000000000000000000000f03f00000000000000000000000000000000`, + expectedEdges: `MULTILINESTRING ((2 1,3 1),(1 1,2 1),(0 1,1 1),(0 0,0 1),(0 0,3 0),(3 0,3 1),(2 1,3 0),(0 0,2 1),(0 0,1 1))`, + expectedTris: `MULTIPOLYGON (((0 1,0 0,1 1,0 1)),((1 1,0 0,2 1,1 1)),((2 1,0 0,3 0,2 1)),((2 1,3 0,3 1,2 1)))`, + }, + { + // bow-tie + inputWKT: `MULTIPOLYGON (((0 0,1 1,1 0,0 1,0 0)))`, + inputWKB: `0106000000010000000103000000010000000500000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f00000000000000000000000000000000000000000000f03f00000000000000000000000000000000`, + expectedEdges: `MULTILINESTRING ((0 1,1 1),(0 0,0 1),(0 0,1 0),(1 0,1 1),(0.5 0.5,1 0),(0.5 0.5,1 1),(0 1,0.5 0.5),(0 0,0.5 0.5))`, + expectedTris: `MULTIPOLYGON (((0 1,0 0,0.5 0.5,0 1)),((0 1,0.5 0.5,1 1,0 1)),((1 1,0.5 0.5,1 0,1 1)),((0 0,1 0,0.5 0.5,0 0)))`, + }, } for i, tc := range testcases { diff --git a/planar/triangulate/quadedge/debug.go b/planar/triangulate/quadedge/debug.go index 9a6d3488..719f9ed7 100644 --- a/planar/triangulate/quadedge/debug.go +++ b/planar/triangulate/quadedge/debug.go @@ -20,6 +20,28 @@ func (qes *QuadEdgeSubdivision) DebugDumpEdges() string { return edgesWKT } +// hasCCWNeighbor returns true if n has at least one neighbor that is < 180deg +// angle and is counter clockwise. +func (qes *QuadEdgeSubdivision) hasCCWNeighbor(e *QuadEdge) bool { + n := e + // if we haven't checked this edge already + for true { + ccw := n.ONext() + // this will only work if the angles between edges are < 180deg + // if both edges are frame edges then the CCW rule may not + // be easily detectable. (think angles > 180deg) + if n.Orig().IsCCW(n.Dest(), ccw.Dest()) == true { + return true + } + n = ccw + if n == e { + return false + } + } + return false +} + + /* Validate runs a self consistency checks and reports the first error. @@ -45,6 +67,8 @@ func (qes *QuadEdgeSubdivision) Validate() error { return qes.validateONext() } + + /* validateONext validates that each QuadEdge's ONext() goes to the next edge that shares an origin point in CCW order. @@ -64,10 +88,9 @@ func (qes *QuadEdgeSubdivision) validateONext() error { if n.Orig().Equals(e.Orig()) == false { return fmt.Errorf("edge in ONext() doesn't share an origin: between %v and %v", e, n) } - // this will only work if the angles between edges are < 180deg - // if both edges are frame edges then the CCW rule may not - // be easily detectable. (think angles > 180deg) - if (qes.isFrameEdge(n) == false || qes.isFrameEdge(ccw) == false) && n.Orig().IsCCW(n.Dest(), ccw.Dest()) == false { + // this isn't a perfect check for CCW, but it should work well + // enough in most cases and shouldn't produce false positives. + if (qes.isFrameEdge(n) == false || qes.isFrameEdge(ccw) == false) && n.Orig().IsCCW(n.Dest(), ccw.Dest()) == false && qes.hasCCWNeighbor(n) == true { return fmt.Errorf("edges are not CCW, expected %v to be CCW of %v", ccw, n) } edgeSet[n] = true diff --git a/planar/triangulate/quadedge/quadedgesubdivision.go b/planar/triangulate/quadedge/quadedgesubdivision.go index d0b2cb84..38775932 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision.go +++ b/planar/triangulate/quadedge/quadedgesubdivision.go @@ -458,6 +458,19 @@ func (qes *QuadEdgeSubdivision) IsOnEdge(e *QuadEdge, p geom.Pointer) bool { return dist < qes.edgeCoincidenceTolerance } +/* +IsOnSegment Tests whether a point lies on a segment, up to a tolerance +determined by the subdivision tolerance. + +Returns true if the vertex lies on the edge +*/ +func (qes *QuadEdgeSubdivision) IsOnLine(l geom.Line, p geom.Pointer) bool { + dist := planar.DistanceToLineSegment(p, geom.Point(l[0]), geom.Point(l[1])) + + // heuristic (hack?) + return dist < qes.edgeCoincidenceTolerance +} + /* IsVertexOfEdge tests whether a {@link Vertex} is the start or end vertex of a QuadEdge, up to the subdivision tolerance distance. diff --git a/planar/triangulate/quadedge/vertex.go b/planar/triangulate/quadedge/vertex.go index ead0b383..8b7bea73 100644 --- a/planar/triangulate/quadedge/vertex.go +++ b/planar/triangulate/quadedge/vertex.go @@ -120,13 +120,23 @@ func (u Vertex) Dot(v Vertex) float64 { /* Times computes the scalar product c(v) -@param v a vertex -@return returns the scaled vector +Return the scaled vector */ func (u Vertex) Times(c float64) Vertex { return Vertex{u.X() * c, u.Y() * c} } +/* +Divide computes the scalar division v / c + +Returns the scaled vector + +This is not part of the original JTS code. +*/ +func (u Vertex) Divide(c float64) Vertex { + return Vertex{u.X() / c, u.Y() / c} +} + // Sum u + v and return the new Vertex func (u Vertex) Sum(v Vertex) Vertex { return Vertex{u.X() + v.X(), u.Y() + v.Y()} @@ -142,6 +152,15 @@ func (u Vertex) Magn() float64 { return math.Sqrt(u.X()*u.X() + u.Y()*u.Y()) } +/* +Normalize scales the vector so the length is one. + +This is not part of the original JTS code. +*/ +func (u Vertex) Normalize() Vertex { + return u.Divide(u.Magn()) +} + /* returns k X v (cross product). this is a vector perpendicular to v */ func (u Vertex) Cross() Vertex { return Vertex{u.Y(), -u.X()} diff --git a/planar/triangulate/segment.go b/planar/triangulate/segment.go index e573d260..0760ec32 100644 --- a/planar/triangulate/segment.go +++ b/planar/triangulate/segment.go @@ -138,14 +138,10 @@ func (seg *Segment) GetEnd() quadedge.Vertex { } */ - /** - * Gets a LineSegment modelling this segment. - * - * @return a LineSegment - public LineSegment getLineSegment() { - return ls; - } - */ +// GetLineSegment gets a Line modelling this segment. +func (seg *Segment) GetLineSegment() geom.Line { + return seg.ls; +} /** * Gets the external data associated with this segment From 76b82d9ff2de0afc1017ff96eff75e8074fa2ba1 Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Thu, 17 May 2018 14:12:23 -0600 Subject: [PATCH 05/12] Increase test coverage in vertex.go --- planar/triangulate/quadedge/vertex_test.go | 120 +++++++++++++++++---- 1 file changed, 102 insertions(+), 18 deletions(-) diff --git a/planar/triangulate/quadedge/vertex_test.go b/planar/triangulate/quadedge/vertex_test.go index f42d4e9e..197be5e8 100644 --- a/planar/triangulate/quadedge/vertex_test.go +++ b/planar/triangulate/quadedge/vertex_test.go @@ -1,10 +1,47 @@ package quadedge import ( + "encoding/json" "strconv" "testing" ) +/* +TestVertexClassify tests for basic classification values. The test is quite simplistic +in that it only tests against a vertical vector, but should be good enough for a sniff +test. +*/ +func TestVertexClassify(t *testing.T) { + type tcase struct { + u Vertex + p0 Vertex + p1 Vertex + expected int + } + + fn := func(t *testing.T, tc tcase) { + r := tc.u.Classify(tc.p0, tc.p1) + if r != tc.expected { + t.Errorf("error, expected %v got %v", tc.expected, r) + return + } + } + testcases := []tcase{ + {Vertex{1.1, 2.5}, Vertex{1, 2}, Vertex{1, 3}, RIGHT}, + {Vertex{0.9, 2.5}, Vertex{1, 2}, Vertex{1, 3}, LEFT}, + {Vertex{1, 1}, Vertex{1, 2}, Vertex{1, 3}, BEHIND}, + {Vertex{1, 4}, Vertex{1, 2}, Vertex{1, 3}, BEYOND}, + {Vertex{1, 2}, Vertex{1, 2}, Vertex{1, 3}, ORIGIN}, + {Vertex{1, 3}, Vertex{1, 2}, Vertex{1, 3}, DESTINATION}, + {Vertex{1, 2.5}, Vertex{1, 2}, Vertex{1, 3}, BETWEEN}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + func TestVertexEquals(t *testing.T) { type tcase struct { v1 Vertex @@ -65,34 +102,22 @@ func TestVertexEqualsTolerance(t *testing.T) { } } -/* -TestVertexClassify tests for basic classification values. The test is quite simplistic -in that it only tests against a vertical vector, but should be good enough for a sniff -test. -*/ -func TestVertexClassify(t *testing.T) { +func TestVertexIsInCircle(t *testing.T) { type tcase struct { - u Vertex - p0 Vertex - p1 Vertex - expected int + v1 Vertex + expected bool } fn := func(t *testing.T, tc tcase) { - r := tc.u.Classify(tc.p0, tc.p1) + r := tc.v1.IsInCircle(Vertex{0, 0}, Vertex{2,0}, Vertex{1, 1}) if r != tc.expected { t.Errorf("error, expected %v got %v", tc.expected, r) return } } testcases := []tcase{ - {Vertex{1.1, 2.5}, Vertex{1, 2}, Vertex{1, 3}, RIGHT}, - {Vertex{0.9, 2.5}, Vertex{1, 2}, Vertex{1, 3}, LEFT}, - {Vertex{1, 1}, Vertex{1, 2}, Vertex{1, 3}, BEHIND}, - {Vertex{1, 4}, Vertex{1, 2}, Vertex{1, 3}, BEYOND}, - {Vertex{1, 2}, Vertex{1, 2}, Vertex{1, 3}, ORIGIN}, - {Vertex{1, 3}, Vertex{1, 2}, Vertex{1, 3}, DESTINATION}, - {Vertex{1, 2.5}, Vertex{1, 2}, Vertex{1, 3}, BETWEEN}, + {Vertex{.5, .5}, true}, + {Vertex{-1, 0}, false}, } for i, tc := range testcases { @@ -100,3 +125,62 @@ func TestVertexClassify(t *testing.T) { t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) } } + +func TestVertexMarshalJSON(t *testing.T) { + type tcase struct { + v1 Vertex + expected string + } + + fn := func(t *testing.T, tc tcase) { + r, err := json.Marshal(tc.v1) + if err != nil { + t.Errorf("error, expected nil got %v", err) + } + if string(r) != tc.expected { + t.Errorf("error, expected %v got %v", tc.expected, string(r)) + return + } + } + testcases := []tcase{ + {Vertex{1, 2}, `{"X":1,"Y":2}`}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +func TestVertexScalar(t *testing.T) { + type tcase struct { + v Vertex + scalar float64 + times Vertex + } + + fn := func(t *testing.T, tc tcase) { + r := tc.v.Times(tc.scalar) + if r.Equals(tc.times) == false { + t.Errorf("error, expected %v got %v", tc.times, r) + return + } + + r = tc.v.Cross() + c := Vertex{tc.v.Y(), -tc.v.X()} + if c.Equals(r) == false { + t.Errorf("error, expected %v got %v", c, r) + return + } + + } + testcases := []tcase{ + {Vertex{1, 2}, 3, Vertex{3, 6}}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + From 3aea88e4352128317a9a351cb8f09d18fc78243d Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Tue, 29 May 2018 09:04:41 -0600 Subject: [PATCH 06/12] Cleanup: comments, remove logs, add err checks 1. General house keeping in the constrained triangulation code. --- .../constraineddelaunay/triangle.go | 52 ----- .../constraineddelaunay/triangulator.go | 195 +++++++++--------- .../constraineddelaunay/triangulator_test.go | 29 ++- .../quadedge/quadedgesubdivision.go | 1 - 4 files changed, 116 insertions(+), 161 deletions(-) diff --git a/planar/triangulate/constraineddelaunay/triangle.go b/planar/triangulate/constraineddelaunay/triangle.go index d7561793..f9376708 100644 --- a/planar/triangulate/constraineddelaunay/triangle.go +++ b/planar/triangulate/constraineddelaunay/triangle.go @@ -5,7 +5,6 @@ package constraineddelaunay import ( "errors" "fmt" - "log" "github.com/go-spatial/geom/planar/triangulate/quadedge" ) @@ -106,7 +105,6 @@ func (tri *Triangle) opposedVertex(other *Triangle) (quadedge.Vertex, error) { if err != nil { return quadedge.Vertex{}, err } - log.Printf("ae: %v", ae) // using the matching edge in triangle a, find the opposed vertex in b. return ae.Sym().ONext().Dest(), nil @@ -158,56 +156,6 @@ func (tri *Triangle) sharedEdge(other *Triangle) (*quadedge.QuadEdge, error) { return ae, nil } -/* -sharedVertexLeft returns the left vertex that is shared by both triangles. - + l - /|\ - / | \ - / | \ - + a | b + - \ | / - \ | / - \|/ - + r - -If this method is called as a.sharedVertexLeft(b), the result will be vertex -l. -*/ -func (tri *Triangle) sharedVertexLeft(other *Triangle) (quadedge.Vertex, error) { - ae, err := tri.sharedEdge(other) - if err != nil { - return quadedge.Vertex{}, err - } - - // using the matching edge in triangle a, find the opposed vertex in b. - return ae.Orig(), nil -} - -/* -sharedVertexRight returns the right vertex that is shared by both triangles. - + l - /|\ - / | \ - / | \ - + a | b + - \ | / - \ | / - \|/ - + r - -If this method is called as a.sharedVertexRight(b), the result will be vertex -r. -*/ -func (tri *Triangle) sharedVertexRight(other *Triangle) (quadedge.Vertex, error) { - ae, err := tri.sharedEdge(other) - if err != nil { - return quadedge.Vertex{}, err - } - - // using the matching edge in triangle a, find the opposed vertex in b. - return ae.Dest(), nil -} - func (tri *Triangle) String() string { str := "[" e := tri.qe diff --git a/planar/triangulate/constraineddelaunay/triangulator.go b/planar/triangulate/constraineddelaunay/triangulator.go index adaf1cdc..eaf31bf0 100644 --- a/planar/triangulate/constraineddelaunay/triangulator.go +++ b/planar/triangulate/constraineddelaunay/triangulator.go @@ -12,31 +12,10 @@ import ( "github.com/go-spatial/geom/planar/triangulate/quadedge" ) -/* - -TODO: - -* Start w/ basic constraint implementation (no intersections) - // Start at the origin point - // Search around the origin point for the containing triangle. To determine containment, check two line segments for intersection, if they intersect, it is contained. - // assert for now if the intersected line segment is a constraint - // assert for now if we fall on an existing edge - // Create methods OpposedTriangle and OpposedVertex (?) - // Loop through finding opposed triangles removing edges, keep a set of all edges that will need to be revisited. - -* When constraints are introduced: - + if there is an intersection between line segments - + Calculate the intersection and divide each line segment to use that point - + The inserted segment should then be inserted again as two different segments, recurse -* Edge conditions: - + a constrained edge that lies on top of an existing edge - + a constrained edge that lies on top of another constrained edge - -*/ - var ErrInvalidPointClassification = errors.New("invalid point classification") var ErrLinesDoNotIntersect = errors.New("line segments do not intersect") // these errors indicate a problem with the algorithm. +var ErrUnableToUpdateVertexIndex = errors.New("unable to update vertex index") var ErrUnexpectedDeadNode = errors.New("unexpected dead node") var ErrUnsupportedCoincidentEdges = errors.New("unsupported coincident edges") @@ -55,6 +34,9 @@ type Triangulator struct { constraints map[triangulate.Segment]bool subdiv *quadedge.QuadEdgeSubdivision tolerance float64 + // run validation after many modification operations. This is expensive, + // but very useful when debugging. + validate bool // maintain an index of vertices to quad edges. Each vertex will point to // one quad edge that has the vertex as an origin. The other quad edges // that point to this vertex can be reached from there. @@ -112,7 +94,6 @@ This method makes no effort to ensure the resulting changes are a valid triangulation. */ func (tri *Triangulator) createTriangle(a, b, c quadedge.Vertex) error { - log.Printf("a: %v b: %v c: %v", a, b, c) if err := tri.createSegment(triangulate.NewSegment(geom.Line{a, b})); err != nil { return err } @@ -135,7 +116,7 @@ deletion. It is invalid to call this method on the last edge that links to a vertex. */ -func (tri *Triangulator) deleteEdge(e *quadedge.QuadEdge) { +func (tri *Triangulator) deleteEdge(e *quadedge.QuadEdge) error { toRemove := make(map[*quadedge.QuadEdge]bool, 4) @@ -149,30 +130,20 @@ func (tri *Triangulator) deleteEdge(e *quadedge.QuadEdge) { toRemove[eRot] = true toRemove[eRotSym] = true - updateVertexIndex := func(v quadedge.Vertex) { - ve := tri.vertexIndex[v] - if toRemove[ve] { - log.Printf("Removing from vertex index: %v", ve) - for testEdge := ve.ONext(); ; testEdge = testEdge.ONext() { - if testEdge == ve { - log.Fatal("unable to update vertex index") - } - if toRemove[testEdge] == false { - log.Printf("Replacing %v with %v", ve, testEdge) - tri.vertexIndex[v] = testEdge - break - } - } - } - } - // remove this edge from the vertex index. - updateVertexIndex(e.Orig()) - updateVertexIndex(e.Dest()) + if err := tri.removeEdgesFromVertexIndex(toRemove, e.Orig()); err != nil { + return err + } + if err := tri.removeEdgesFromVertexIndex(toRemove, e.Dest()); err != nil { + return err + } quadedge.Splice(e.OPrev(), e) quadedge.Splice(eSym.OPrev(), eSym) + // TODO: this call is horribly inefficient and should be optimized. tri.subdiv.Delete(e) + + return nil } /* @@ -193,7 +164,6 @@ func (tri *Triangulator) findIntersectingTriangle(s triangulate.Segment) (*Trian } left := qe - log.Printf("s: %v", s) // walk around all the triangles that share qe.Orig() for true { @@ -206,8 +176,6 @@ func (tri *Triangulator) findIntersectingTriangle(s triangulate.Segment) (*Trian lc := s.GetEnd().Classify(left.Orig(), left.Dest()) rc := s.GetEnd().Classify(right.Orig(), right.Dest()) - log.Printf("left: %v right: %v", left, right) - log.Printf("lc: %v rc: %v", lc, rc) if (lc == quadedge.RIGHT && rc == quadedge.LEFT) || lc == quadedge.BETWEEN || lc == quadedge.DESTINATION || lc == quadedge.BEYOND { // if s is between the two edges, we found our triangle. return &Triangle{left}, nil @@ -264,6 +232,11 @@ func (tri *Triangulator) InsertSegments(g geom.Geometry) error { return nil } +/* +insertSites inserts all of the vertices found in g into a Delaunay +triangulation. Other steps will modify the Delaunay Triangulation to create +the constrained Delaunay triangulation. +*/ func (tri *Triangulator) insertSites(g geom.Geometry) error { tri.builder = triangulate.NewDelaunayTriangulationBuilder(tri.tolerance) err := tri.builder.SetSites(g) @@ -288,6 +261,13 @@ func (tri *Triangulator) insertSites(g geom.Geometry) error { return nil } +/* +insertConstraints modifies the triangulation by incrementally using the +line segements in g as constraints in the triangulation. After this step +the triangulation is no longer a proper Delaunay triangulation, but the +constraints are guaranteed. Some constraints may need to be split (think +about the case when two constraints intersect). +*/ func (tri *Triangulator) insertConstraints(g geom.Geometry) error { tri.constraints = make(map[triangulate.Segment]bool) @@ -308,14 +288,12 @@ func (tri *Triangulator) insertConstraints(g geom.Geometry) error { tri.constraints[seg] = true } - log.Printf("tri.constraints: %v", tri.constraints) for seg := range constraints { - err := tri.insertEdgeCDT(&seg) - if err != nil { + if err := tri.insertEdgeCDT(&seg); err != nil { return fmt.Errorf("error adding constraint: %v", err) } if err = tri.Validate(); err != nil { - log.Fatalf("validate failed: %v", err) + return err } } @@ -344,7 +322,6 @@ func (tri *Triangulator) intersection(l1, l2 triangulate.Segment) (quadedge.Vert s := l2.GetEnd().Sub(q) rs := r.CrossProduct(s) - log.Printf("rs: %v", rs) if rs == 0 { return quadedge.Vertex{}, ErrLinesDoNotIntersect @@ -361,7 +338,6 @@ func (tri *Triangulator) intersection(l1, l2 triangulate.Segment) (quadedge.Vert utolerance := tri.tolerance / s.Magn() ulow := -utolerance uhigh := 1 + utolerance - log.Printf("t: %v u: %v", t, u) if t < tlow || t > thigh || u < ulow || u > uhigh { return quadedge.Vertex{}, ErrLinesDoNotIntersect @@ -383,11 +359,20 @@ func (tri *Triangulator) IsConstraint(e *quadedge.QuadEdge) bool { return ok } -// Procedure InsertEdgeCDT(T:CDT, ab:Edge) -func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { - log.Printf("ab: %v", *ab) - log.Print(tri.subdiv.DebugDumpEdges()) +/* +insertEdgeCDT attempts to follow the pseudo code in Domiter. + +Procedure InsertEdgeCDT(T:CDT, ab:Edge) +There are some deviations that are also mentioned inline in the comments + + - Some aparrent typos that are resolved to give consistent results + - Modifications to work with the planar subdivision representation of + a triangulation (QuadEdge) + - Modification to support the case when two constrained edges intersect + at more than the end points. +*/ +func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { qe, err := tri.LocateSegment(ab.GetStart(), ab.GetEnd()) if qe != nil && err != nil { @@ -427,20 +412,14 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { if err != nil { return err } - log.Printf("t: %v", t) - log.Printf("v: %v", v) - log.Printf("tseq: %v", tseq) - log.Printf("vseq: %v", vseq) + shared, err := t.sharedEdge(tseq) if err != nil { return err } - log.Printf("shared: %v", shared) c := vseq.Classify(ab.GetStart(), ab.GetEnd()) - cv := v.Classify(ab.GetStart(), ab.GetEnd()) - log.Printf("c: %v", c) - log.Printf("cv: %v", cv) + // should we remove the edge shared between t & tseq? flagEdgeForRemoval := false @@ -469,7 +448,7 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { if err != nil { return err } - log.Printf("Intersection: %v", iv) + // split the constrained edge we interesect if err := tri.splitEdge(shared, iv); err != nil { return err @@ -479,14 +458,15 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { if err != nil { return err } + // create a new edge for the rest of this segment and recursively // insert the new edge. vb := triangulate.NewSegment(geom.Line{iv, ab.GetEnd()}) tri.insertEdgeCDT(&vb) + // the current insertion will stop at the interesction point b = iv *ab = triangulate.NewSegment(geom.Line{ab.GetStart(), iv}) - //flagEdgeForRemoval = true // If vseq above the edge ab then case c == quadedge.LEFT: @@ -506,21 +486,10 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { pl = appendNonRepeat(pl, vseq) flagEdgeForRemoval = true - // // NOTE: You may be able to use this same mechanism to handle edges that overlap/intersect - // // Else vseq on the edge ab - // case c == quadedge.BETWEEN: - // log.Printf("Between: %v", vseq) - // // InsertEdgeCDT(T, vseqb) - // vseqb := triangulate.NewSegment(geom.Line{vseq, ab.GetEnd()}) - // tri.insertEdgeCDT(&vseqb) - // // a:=vseq -- Should this be b:=vseq!? -JRS - // b = vseq - // *ab = triangulate.NewSegment(geom.Line{ab.GetStart(), b}) case c == quadedge.DESTINATION: flagEdgeForRemoval = true default: - log.Printf("c: %v", c) return ErrInvalidPointClassification } @@ -534,7 +503,6 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { } // EndWhile - log.Printf("removalList: %v", removalList) // remove the previously marked edges // TODO Inefficient for i := range(removalList) { @@ -542,29 +510,22 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { } // TriangulatePseudoPolygon(PU,ab,T) - log.Printf("pu: %v", pu) - tri.triangulatePseudoPolygon(pu, *ab) + if err := tri.triangulatePseudoPolygon(pu, *ab); err != nil { + return err + } // TriangulatePseudoPolygon(PL,ab,T) - log.Printf("pl: %v", pl) - tri.triangulatePseudoPolygon(pl, *ab) - - log.Print(tri.subdiv.DebugDumpEdges()) + if err := tri.triangulatePseudoPolygon(pl, *ab); err != nil { + return err + } if err := tri.Validate(); err != nil { - log.Fatalf("validate failed: %v", err) + return err } - // Reconstitute the triangle adjacencies of T - // bt, err := tri.findIntersectingTriangle(triangulate.NewSegment(geom.Line{ab.GetEnd(), ab.GetStart()})) - // if err != nil { - // return err - // } - - // // Add edge ab to T - // log.Printf("at.qe.Sym(): %v", at.qe.Sym()) - // log.Printf("bt.qe.OPrev(): %v", bt.qe.OPrev()) - // quadedge.Connect(at.qe.Sym(), bt.qe.OPrev()) - tri.createSegment(*ab) + // Add edge ab to T + if err := tri.createSegment(*ab); err != nil { + return err + } return nil } @@ -600,7 +561,7 @@ func (tri *Triangulator) LocateSegment(v1 quadedge.Vertex, v2 quadedge.Vertex) ( start := qe for true { if qe == nil || qe.IsLive() == false { - log.Fatalf("unexpected dead node: %v", qe) + log.Printf("unexpected dead node: %v", qe) return nil, fmt.Errorf("nil or dead qe when locating segment %v %v", v1, v2) } if v2.Equals(qe.Dest()) { @@ -617,13 +578,48 @@ func (tri *Triangulator) LocateSegment(v1 quadedge.Vertex, v2 quadedge.Vertex) ( } /* -removeConstraintEdge removes any constraints that share the same Orig() and Dest() as the edge provided. If there are none, no changes are made. +removeConstraintEdge removes any constraints that share the same Orig() and +Dest() as the edge provided. If there are none, no changes are made. */ func (tri *Triangulator) removeConstraintEdge(e *quadedge.QuadEdge) { delete(tri.constraints, triangulate.NewSegment(geom.Line{e.Orig(), e.Dest()})) delete(tri.constraints, triangulate.NewSegment(geom.Line{e.Dest(), e.Orig()})) } +/* +removeEdgesFromVertexIndex will remove a set of QuadEdges from the vertex index +for the specified vertex. If the operation cannot be completed an error will be +returned and the index will not be modified. + +The vertex index maps from a vertex to an arbitrary QuadEdges. This method is +helpful in modifying the index after an edge has been deleted. + +toRemove - a set of QuadEdges that should be removed from the index. These +QuadEdges don't necessarily have to link to the provided vertex. +v - The vertex to modify in the index. +*/ +func (tri *Triangulator) removeEdgesFromVertexIndex(toRemove map[*quadedge.QuadEdge]bool, v quadedge.Vertex) error { + ve := tri.vertexIndex[v] + if toRemove[ve] { + for testEdge := ve.ONext(); ; testEdge = testEdge.ONext() { + if testEdge == ve { + // if we made it all the way around the vertex without finding + // a valid edge to reference from this vertex + return ErrUnableToUpdateVertexIndex + } + if toRemove[testEdge] == false { + tri.vertexIndex[v] = testEdge + return nil + } + } + } + // this should happen if the vertex doesn't need to be updated. + return nil +} + +/* +splitEdge splits the given edge at the vertex v. +*/ func (tri *Triangulator) splitEdge(e *quadedge.QuadEdge, v quadedge.Vertex) error { constraint := tri.IsConstraint(e) @@ -711,6 +707,9 @@ reports the first error. This is most useful when testing/debugging. */ func (tri *Triangulator) Validate() error { + if tri.validate == false { + return nil + } err := tri.subdiv.Validate() if err != nil { return err diff --git a/planar/triangulate/constraineddelaunay/triangulator_test.go b/planar/triangulate/constraineddelaunay/triangulator_test.go index 15826999..980f01d7 100644 --- a/planar/triangulate/constraineddelaunay/triangulator_test.go +++ b/planar/triangulate/constraineddelaunay/triangulator_test.go @@ -3,7 +3,6 @@ package constraineddelaunay import ( "encoding/hex" - "fmt" "log" "strconv" "testing" @@ -24,7 +23,7 @@ func TestFindIntersectingTriangle(t *testing.T) { // https://rodic.fr/blog/online-conversion-between-geometric-formats/ inputWKB string searchFrom geom.Line - expectedEdge string + expectedTriangle string } fn := func(t *testing.T, tc tcase) { @@ -41,16 +40,21 @@ func TestFindIntersectingTriangle(t *testing.T) { uut := new(Triangulator) uut.tolerance = 1e-6 + // perform self consistency validation while building the + // triangulation. + uut.validate = true uut.insertSites(g) + // find the triangle tri, err := uut.findIntersectingTriangle(triangulate.NewSegment(tc.searchFrom)) if err != nil { t.Fatalf("error, expected nil got %v", err) return } - qeStr := fmt.Sprintf("%v -> %v", tri.qe.Orig(), tri.qe.Dest()) - if qeStr != tc.expectedEdge { - t.Fatalf("error, expected %v got %v", tc.expectedEdge, qeStr) + + qeStr := tri.String() + if qeStr != tc.expectedTriangle { + t.Fatalf("error, expected %v got %v", tc.expectedTriangle, qeStr) } } @@ -59,25 +63,25 @@ func TestFindIntersectingTriangle(t *testing.T) { inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, searchFrom: geom.Line{{0,0}, {10, 10}}, - expectedEdge: `[0 0] -> [0 10]`, + expectedTriangle: `[[0 0],[0 10],[10 0]]`, }, { inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, searchFrom: geom.Line{{10,0}, {0, 20}}, - expectedEdge: `[10 0] -> [0 10]`, + expectedTriangle: `[[10 0],[0 10],[10 10]]`, }, { inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, searchFrom: geom.Line{{10,10}, {0, 0}}, - expectedEdge: `[10 10] -> [10 0]`, + expectedTriangle: `[[10 10],[10 0],[0 10]]`, }, { inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, searchFrom: geom.Line{{10,10}, {10, 20}}, - expectedEdge: `[10 10] -> [10 20]`, + expectedTriangle: `[[10 10],[10 20],[20 10]]`, }, } @@ -112,6 +116,9 @@ func TestDeleteEdge(t *testing.T) { uut := new(Triangulator) uut.tolerance = 1e-6 + // perform self consistency validation while building the + // triangulation. + uut.validate = true uut.InsertSegments(g) e, err := uut.LocateSegment(quadedge.Vertex(tc.deleteMe[0]), quadedge.Vertex(tc.deleteMe[1])) if err != nil { @@ -125,7 +132,9 @@ func TestDeleteEdge(t *testing.T) { return } - uut.deleteEdge(e) + if err = uut.deleteEdge(e); err != nil { + t.Errorf("error deleting edge, expected nil got %v", err) + } err = uut.Validate() if err != nil { t.Errorf("error validating triangulation after delete, expected nil got %v", err) diff --git a/planar/triangulate/quadedge/quadedgesubdivision.go b/planar/triangulate/quadedge/quadedgesubdivision.go index 38775932..9d28c786 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision.go +++ b/planar/triangulate/quadedge/quadedgesubdivision.go @@ -674,7 +674,6 @@ func (qes *QuadEdgeSubdivision) visitTriangles(triVisitor func(triEdges []*QuadE // visited flag is used to record visited edges of triangles // setVisitedAll(false); var stack *edgeStack = new(edgeStack) - log.Printf("startingEdge: %v", qes.startingEdge) if qes.startingEdge != nil { stack.push(qes.startingEdge) } From c0c7038d2f5760b019a5715ee2a5a960cfbf989a Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Tue, 29 May 2018 09:15:45 -0600 Subject: [PATCH 07/12] Run gofmt for triangulation changes --- geom.go | 8 +- .../constraineddelaunay/triangle.go | 11 +- .../constraineddelaunay/triangle_test.go | 2 - .../constraineddelaunay/triangulator.go | 69 ++--- .../constraineddelaunay/triangulator_test.go | 114 ++++---- planar/triangulate/delaunay_test.go | 43 ++- .../delaunaytriangulationbuilder.go | 4 +- planar/triangulate/quadedge/debug.go | 11 +- .../quadedge/quadedgesubdivision.go | 19 +- .../quadedge/quadedgesubdivision_test.go | 1 - planar/triangulate/quadedge/vertex.go | 4 +- planar/triangulate/quadedge/vertex_test.go | 13 +- planar/triangulate/segment.go | 266 +++++++++--------- 13 files changed, 274 insertions(+), 291 deletions(-) diff --git a/geom.go b/geom.go index 2e2cccf6..808df49f 100644 --- a/geom.go +++ b/geom.go @@ -154,8 +154,8 @@ func extractLines(g Geometry, lines *[]Line) error { case LineStringer: v := gg.Verticies() - for i := 0; i < len(v) - 1; i++ { - *lines = append(*lines, Line{v[i], v[i + 1]}) + for i := 0; i < len(v)-1; i++ { + *lines = append(*lines, Line{v[i], v[i+1]}) } return nil @@ -199,8 +199,8 @@ func extractLines(g Geometry, lines *[]Line) error { } /* -ExtractLines extracts all linear components from a geometry (line segements). -If the geometry contains no line segements (e.g. empty geometry or +ExtractLines extracts all linear components from a geometry (line segements). +If the geometry contains no line segements (e.g. empty geometry or point), then an empty array will be returned. Duplicate lines will not be removed. diff --git a/planar/triangulate/constraineddelaunay/triangle.go b/planar/triangulate/constraineddelaunay/triangle.go index f9376708..b8708e85 100644 --- a/planar/triangulate/constraineddelaunay/triangle.go +++ b/planar/triangulate/constraineddelaunay/triangle.go @@ -1,5 +1,3 @@ - - package constraineddelaunay import ( @@ -9,12 +7,11 @@ import ( "github.com/go-spatial/geom/planar/triangulate/quadedge" ) - var ErrInvalidVertex = errors.New("invalid vertex") var ErrNoMatchingEdgeFound = errors.New("no matching edge found") /* -Triangle provides operations on a triangle within a +Triangle provides operations on a triangle within a quadedge.QuadEdgeSubdivision. This is outside the quadedge package to avoid making changes to the original @@ -26,7 +23,7 @@ type Triangle struct { } /* -IntersectsPoint returns true if the vertex intersects the given triangle. This +IntersectsPoint returns true if the vertex intersects the given triangle. This includes falling on an edge. */ func (tri *Triangle) IntersectsPoint(v quadedge.Vertex) bool { @@ -111,7 +108,7 @@ func (tri *Triangle) opposedVertex(other *Triangle) (quadedge.Vertex, error) { } /* -sharedEdge returns the edge that is shared by both a and b. The edge is +sharedEdge returns the edge that is shared by both a and b. The edge is returned with triangle a on the left. + l @@ -138,7 +135,7 @@ func (tri *Triangle) sharedEdge(other *Triangle) (*quadedge.QuadEdge, error) { foundMatch = true break } - be = be.RNext(); + be = be.RNext() } if foundMatch { diff --git a/planar/triangulate/constraineddelaunay/triangle_test.go b/planar/triangulate/constraineddelaunay/triangle_test.go index 516128d5..c4ca4beb 100644 --- a/planar/triangulate/constraineddelaunay/triangle_test.go +++ b/planar/triangulate/constraineddelaunay/triangle_test.go @@ -1,3 +1 @@ - package constraineddelaunay - diff --git a/planar/triangulate/constraineddelaunay/triangulator.go b/planar/triangulate/constraineddelaunay/triangulator.go index eaf31bf0..86e1fe5a 100644 --- a/planar/triangulate/constraineddelaunay/triangulator.go +++ b/planar/triangulate/constraineddelaunay/triangulator.go @@ -14,17 +14,18 @@ import ( var ErrInvalidPointClassification = errors.New("invalid point classification") var ErrLinesDoNotIntersect = errors.New("line segments do not intersect") + // these errors indicate a problem with the algorithm. var ErrUnableToUpdateVertexIndex = errors.New("unable to update vertex index") var ErrUnexpectedDeadNode = errors.New("unexpected dead node") var ErrUnsupportedCoincidentEdges = errors.New("unsupported coincident edges") /* -Triangulator provides methods for performing a constrainted delaunay +Triangulator provides methods for performing a constrainted delaunay triangulation. -Domiter, Vid. "Constrained Delaunay triangulation using plane subdivision." -Proceedings of the 8th central European seminar on computer graphics. +Domiter, Vid. "Constrained Delaunay triangulation using plane subdivision." +Proceedings of the 8th central European seminar on computer graphics. Budmerice. 2004. http://old.cescg.org/CESCG-2004/web/Domiter-Vid/CDT.pdf */ @@ -32,13 +33,13 @@ type Triangulator struct { builder *triangulate.DelaunayTriangulationBuilder // a map of constraints where the segments have the lesser point first. constraints map[triangulate.Segment]bool - subdiv *quadedge.QuadEdgeSubdivision - tolerance float64 - // run validation after many modification operations. This is expensive, + subdiv *quadedge.QuadEdgeSubdivision + tolerance float64 + // run validation after many modification operations. This is expensive, // but very useful when debugging. validate bool // maintain an index of vertices to quad edges. Each vertex will point to - // one quad edge that has the vertex as an origin. The other quad edges + // one quad edge that has the vertex as an origin. The other quad edges // that point to this vertex can be reached from there. vertexIndex map[quadedge.Vertex]*quadedge.QuadEdge } @@ -48,14 +49,14 @@ appendNonRepeat only appends the provided value if it does not repeat the last value that was appended onto the array. */ func appendNonRepeat(arr []quadedge.Vertex, v quadedge.Vertex) []quadedge.Vertex { - if len(arr) == 0 || arr[len(arr) - 1].Equals(v) == false { + if len(arr) == 0 || arr[len(arr)-1].Equals(v) == false { arr = append(arr, v) } return arr } /* -createSegment creates a segment with vertices a & b, if it doesn't already +createSegment creates a segment with vertices a & b, if it doesn't already exist. All the vertices must already exist in the triangulator. */ func (tri *Triangulator) createSegment(s triangulate.Segment) error { @@ -81,16 +82,16 @@ func (tri *Triangulator) createSegment(s triangulate.Segment) error { to := ct.qe.OPrev() quadedge.Connect(from, to) - // since we aren't adding any vertices we don't need to modify the vertex + // since we aren't adding any vertices we don't need to modify the vertex // index. return nil } /* -createTriangle creates a triangle with vertices a, b and c. All the vertices +createTriangle creates a triangle with vertices a, b and c. All the vertices must already exist in the triangulator. Any existing edges that make up the triangle will not be recreated. -This method makes no effort to ensure the resulting changes are a valid +This method makes no effort to ensure the resulting changes are a valid triangulation. */ func (tri *Triangulator) createTriangle(a, b, c quadedge.Vertex) error { @@ -111,7 +112,7 @@ func (tri *Triangulator) createTriangle(a, b, c quadedge.Vertex) error { /* deleteEdge deletes the specified edge and updates all associated neighbors to -reflect the removal. The local vertex index is also updated to reflect the +reflect the removal. The local vertex index is also updated to reflect the deletion. It is invalid to call this method on the last edge that links to a vertex. @@ -152,8 +153,8 @@ and intersects at least part of the edge that extends from s.GetStart(). Tolerance is not considered when determining if vertices are the same. -Returns a quadedge that has s.GetStart() as the origin and the right face is -the desired triangle. If the segment falls on an edge, the triangle to the +Returns a quadedge that has s.GetStart() as the origin and the right face is +the desired triangle. If the segment falls on an edge, the triangle to the right of the segment is returned. */ func (tri *Triangulator) findIntersectingTriangle(s triangulate.Segment) (*Triangle, error) { @@ -214,8 +215,8 @@ func (tri *Triangulator) GetTriangles() (geom.MultiPolygon, error) { /* InsertSegments inserts the line segments in the specified geometry and builds -a triangulation. The line segments are used as constraints in the -triangulation. If the geometry is made up solely of points, then no +a triangulation. The line segments are used as constraints in the +triangulation. If the geometry is made up solely of points, then no constraints will be used. */ func (tri *Triangulator) InsertSegments(g geom.Geometry) error { @@ -233,7 +234,7 @@ func (tri *Triangulator) InsertSegments(g geom.Geometry) error { } /* -insertSites inserts all of the vertices found in g into a Delaunay +insertSites inserts all of the vertices found in g into a Delaunay triangulation. Other steps will modify the Delaunay Triangulation to create the constrained Delaunay triangulation. */ @@ -248,7 +249,7 @@ func (tri *Triangulator) insertSites(g geom.Geometry) error { // Add all the edges to a constant time lookup tri.vertexIndex = make(map[quadedge.Vertex]*quadedge.QuadEdge) edges := tri.subdiv.GetEdges() - for i := range(edges) { + for i := range edges { e := edges[i] if _, ok := tri.vertexIndex[e.Orig()]; ok == false { tri.vertexIndex[e.Orig()] = e @@ -276,7 +277,7 @@ func (tri *Triangulator) insertConstraints(g geom.Geometry) error { return fmt.Errorf("error adding constraint: %v", err) } constraints := make(map[triangulate.Segment]bool) - for _, l := range(lines) { + for _, l := range lines { // make the line ordering consistent if !cmp.PointLess(l[0], l[1]) { l[0], l[1] = l[1], l[0] @@ -306,10 +307,10 @@ rest of geom is ported over from spatial, this can be replaced with a more generic call. The tolerance here only acts by extending the lines by tolerance. E.g. if the -tolerance is 0.1 and you have two lines {{0, 0}, {1, 0}} and +tolerance is 0.1 and you have two lines {{0, 0}, {1, 0}} and {{0, 0.01}, {1, 0.01}} then these will not be marked as intersecting lines. -If tolerance is used to mark two lines as intersecting, you are still +If tolerance is used to mark two lines as intersecting, you are still guaranteed that the intersecting point will fall _on_ one of the lines, not in the extended region of the line. @@ -342,7 +343,7 @@ func (tri *Triangulator) intersection(l1, l2 triangulate.Segment) (quadedge.Vert if t < tlow || t > thigh || u < ulow || u > uhigh { return quadedge.Vertex{}, ErrLinesDoNotIntersect } - // if t is just out of range, but within the acceptable tolerance, snap + // if t is just out of range, but within the acceptable tolerance, snap // it back to the beginning/end of the line. t = math.Min(1, math.Max(t, 0)) @@ -466,8 +467,8 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { // the current insertion will stop at the interesction point b = iv - *ab = triangulate.NewSegment(geom.Line{ab.GetStart(), iv}) - + *ab = triangulate.NewSegment(geom.Line{ab.GetStart(), iv}) + // If vseq above the edge ab then case c == quadedge.LEFT: // v:=Vertex shared by t and tseq above ab @@ -494,7 +495,7 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { } if flagEdgeForRemoval { - // "Remove t from T" -- We are just removing the edge intersected + // "Remove t from T" -- We are just removing the edge intersected // by ab, which in effect removes the triangle. removalList = append(removalList, shared) } @@ -505,7 +506,7 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { // remove the previously marked edges // TODO Inefficient - for i := range(removalList) { + for i := range removalList { tri.deleteEdge(removalList[i]) } @@ -531,7 +532,7 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { } /* -locateEdgeByVertex finds a quad edge that has this vertex as Orig(). This will +locateEdgeByVertex finds a quad edge that has this vertex as Orig(). This will not be a unique edge. This is looking for an exact match and tolerance will not be considered. @@ -546,7 +547,7 @@ func (tri *Triangulator) locateEdgeByVertex(v quadedge.Vertex) (*quadedge.QuadEd } /* -locateEdgeByVertex finds a quad edge that has this vertex as Orig(). This will +locateEdgeByVertex finds a quad edge that has this vertex as Orig(). This will not be a unique edge. This is looking for an exact match and tolerance will not be considered. @@ -578,7 +579,7 @@ func (tri *Triangulator) LocateSegment(v1 quadedge.Vertex, v2 quadedge.Vertex) ( } /* -removeConstraintEdge removes any constraints that share the same Orig() and +removeConstraintEdge removes any constraints that share the same Orig() and Dest() as the edge provided. If there are none, no changes are made. */ func (tri *Triangulator) removeConstraintEdge(e *quadedge.QuadEdge) { @@ -603,7 +604,7 @@ func (tri *Triangulator) removeEdgesFromVertexIndex(toRemove map[*quadedge.QuadE if toRemove[ve] { for testEdge := ve.ONext(); ; testEdge = testEdge.ONext() { if testEdge == ve { - // if we made it all the way around the vertex without finding + // if we made it all the way around the vertex without finding // a valid edge to reference from this vertex return ErrUnableToUpdateVertexIndex } @@ -649,7 +650,7 @@ func (tri *Triangulator) splitEdge(e *quadedge.QuadEdge, v quadedge.Vertex) erro tri.constraints[triangulate.NewSegment(geom.Line{e2.Dest(), e2.Orig()})] = true } - // since we aren't adding any vertices we don't need to modify the vertex + // since we aren't adding any vertices we don't need to modify the vertex // index. return nil } @@ -718,7 +719,7 @@ func (tri *Triangulator) Validate() error { } /* -validateVertexIndex self consistency checks against a triangulation and the +validateVertexIndex self consistency checks against a triangulation and the subdiv and reports the first error. */ func (tri *Triangulator) validateVertexIndex() error { @@ -726,7 +727,7 @@ func (tri *Triangulator) validateVertexIndex() error { edgeSet := make(map[*quadedge.QuadEdge]bool) vertexSet := make(map[quadedge.Vertex]bool) edges := tri.subdiv.GetEdges() - for i := range(edges) { + for i := range edges { edgeSet[edges[i]] = true edgeSet[edges[i].Sym()] = true vertexSet[edges[i].Orig()] = true diff --git a/planar/triangulate/constraineddelaunay/triangulator_test.go b/planar/triangulate/constraineddelaunay/triangulator_test.go index 980f01d7..0aaabae7 100644 --- a/planar/triangulate/constraineddelaunay/triangulator_test.go +++ b/planar/triangulate/constraineddelaunay/triangulator_test.go @@ -1,4 +1,3 @@ - package constraineddelaunay import ( @@ -19,11 +18,11 @@ func TestFindIntersectingTriangle(t *testing.T) { // provided for readability inputWKT string // this can be removed if/when geom has a WKT decoder. - // A simple website for performing conversions: - // https://rodic.fr/blog/online-conversion-between-geometric-formats/ - inputWKB string - searchFrom geom.Line - expectedTriangle string + // A simple website for performing conversions: + // https://rodic.fr/blog/online-conversion-between-geometric-formats/ + inputWKB string + searchFrom geom.Line + expectedTriangle string } fn := func(t *testing.T, tc tcase) { @@ -40,7 +39,7 @@ func TestFindIntersectingTriangle(t *testing.T) { uut := new(Triangulator) uut.tolerance = 1e-6 - // perform self consistency validation while building the + // perform self consistency validation while building the // triangulation. uut.validate = true uut.insertSites(g) @@ -60,27 +59,27 @@ func TestFindIntersectingTriangle(t *testing.T) { } testcases := []tcase{ { - inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, - inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, - searchFrom: geom.Line{{0,0}, {10, 10}}, + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + searchFrom: geom.Line{{0, 0}, {10, 10}}, expectedTriangle: `[[0 0],[0 10],[10 0]]`, }, { - inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, - inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, - searchFrom: geom.Line{{10,0}, {0, 20}}, + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + searchFrom: geom.Line{{10, 0}, {0, 20}}, expectedTriangle: `[[10 0],[0 10],[10 10]]`, }, { - inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, - inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, - searchFrom: geom.Line{{10,10}, {0, 0}}, + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + searchFrom: geom.Line{{10, 10}, {0, 0}}, expectedTriangle: `[[10 10],[10 0],[0 10]]`, }, { - inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, - inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, - searchFrom: geom.Line{{10,10}, {10, 20}}, + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + searchFrom: geom.Line{{10, 10}, {10, 20}}, expectedTriangle: `[[10 10],[10 20],[20 10]]`, }, } @@ -96,10 +95,10 @@ func TestDeleteEdge(t *testing.T) { // provided for readability inputWKT string // this can be removed if/when geom has a WKT decoder. - // A simple website for performing conversions: - // https://rodic.fr/blog/online-conversion-between-geometric-formats/ - inputWKB string - deleteMe geom.Line + // A simple website for performing conversions: + // https://rodic.fr/blog/online-conversion-between-geometric-formats/ + inputWKB string + deleteMe geom.Line } fn := func(t *testing.T, tc tcase) { @@ -116,7 +115,7 @@ func TestDeleteEdge(t *testing.T) { uut := new(Triangulator) uut.tolerance = 1e-6 - // perform self consistency validation while building the + // perform self consistency validation while building the // triangulation. uut.validate = true uut.InsertSegments(g) @@ -151,14 +150,14 @@ func TestDeleteEdge(t *testing.T) { } testcases := []tcase{ { - inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, - inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, - deleteMe: geom.Line{{0,10}, {10, 0}}, + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + deleteMe: geom.Line{{0, 10}, {10, 0}}, }, { - inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, - inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, - deleteMe: geom.Line{{0,10}, {10, 0}}, + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + deleteMe: geom.Line{{0, 10}, {10, 0}}, }, } @@ -170,9 +169,9 @@ func TestDeleteEdge(t *testing.T) { func TestIntersection(t *testing.T) { type tcase struct { - l1 triangulate.Segment - l2 triangulate.Segment - intersection quadedge.Vertex + l1 triangulate.Segment + l2 triangulate.Segment + intersection quadedge.Vertex expectedError error } @@ -191,27 +190,27 @@ func TestIntersection(t *testing.T) { } testcases := []tcase{ { - l1: triangulate.NewSegment(geom.Line{{0, 1}, {2, 3}}), - l2: triangulate.NewSegment(geom.Line{{1, 1}, {0, 2}}), - intersection: quadedge.Vertex{0.5, 1.5}, + l1: triangulate.NewSegment(geom.Line{{0, 1}, {2, 3}}), + l2: triangulate.NewSegment(geom.Line{{1, 1}, {0, 2}}), + intersection: quadedge.Vertex{0.5, 1.5}, expectedError: nil, }, { - l1: triangulate.NewSegment(geom.Line{{0, 1}, {2, 4}}), - l2: triangulate.NewSegment(geom.Line{{1, 1}, {0, 2}}), - intersection: quadedge.Vertex{0.4, 1.6}, + l1: triangulate.NewSegment(geom.Line{{0, 1}, {2, 4}}), + l2: triangulate.NewSegment(geom.Line{{1, 1}, {0, 2}}), + intersection: quadedge.Vertex{0.4, 1.6}, expectedError: nil, }, { - l1: triangulate.NewSegment(geom.Line{{0, 1}, {2, 3}}), - l2: triangulate.NewSegment(geom.Line{{1, 1}, {2, 2}}), - intersection: quadedge.Vertex{0, 0}, + l1: triangulate.NewSegment(geom.Line{{0, 1}, {2, 3}}), + l2: triangulate.NewSegment(geom.Line{{1, 1}, {2, 2}}), + intersection: quadedge.Vertex{0, 0}, expectedError: ErrLinesDoNotIntersect, }, { - l1: triangulate.NewSegment(geom.Line{{3, 5}, {3, 6}}), - l2: triangulate.NewSegment(geom.Line{{1, 4.995}, {4, 4.995}}), - intersection: quadedge.Vertex{3, 5}, + l1: triangulate.NewSegment(geom.Line{{3, 5}, {3, 6}}), + l2: triangulate.NewSegment(geom.Line{{1, 4.995}, {4, 4.995}}), + intersection: quadedge.Vertex{3, 5}, expectedError: nil, }, } @@ -223,7 +222,7 @@ func TestIntersection(t *testing.T) { } /* -TestTriangulation test cases test for small constrained triangulations and +TestTriangulation test cases test for small constrained triangulations and edge cases */ func TestTriangulation(t *testing.T) { @@ -231,8 +230,8 @@ func TestTriangulation(t *testing.T) { // provided for readability inputWKT string // this can be removed if/when geom has a WKT decoder. - // A simple website for performing conversions: - // https://rodic.fr/blog/online-conversion-between-geometric-formats/ + // A simple website for performing conversions: + // https://rodic.fr/blog/online-conversion-between-geometric-formats/ inputWKB string expectedEdges string expectedTris string @@ -288,12 +287,12 @@ func TestTriangulation(t *testing.T) { } testcases := []tcase{ { - // should create a triangulation w/ a vertical line (2 5, 2 -5). + // should create a triangulation w/ a vertical line (2 5, 2 -5). // The unconstrained version has a horizontal line inputWKT: `LINESTRING(0 0, 2 5, 2 -5, 5 0)`, inputWKB: `0102000000040000000000000000000000000000000000000000000000000000400000000000001440000000000000004000000000000014c000000000000014400000000000000000`, expectedEdges: `MULTILINESTRING ((2 5,5 0),(0 0,2 5),(0 0,2 -5),(2 -5,5 0),(2 -5,2 5))`, - expectedTris: `MULTIPOLYGON (((0 0,2 -5,2 5,0 0)),((2 5,2 -5,5 0,2 5)))`, + expectedTris: `MULTIPOLYGON (((0 0,2 -5,2 5,0 0)),((2 5,2 -5,5 0,2 5)))`, }, { // a horizontal rectangle w/ one diagonal line. The diagonal line @@ -301,7 +300,7 @@ func TestTriangulation(t *testing.T) { inputWKT: `MULTILINESTRING((0 0,0 1,1 1.1,2 1,2 0,1 0.1,0 0),(0 1,2 0))`, inputWKB: `010500000002000000010200000007000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f9a9999999999f13f0000000000000040000000000000f03f00000000000000400000000000000000000000000000f03f9a9999999999b93f000000000000000000000000000000000102000000020000000000000000000000000000000000f03f00000000000000400000000000000000`, expectedEdges: `MULTILINESTRING ((1 1.1,2 1),(0 1,1 1.1),(0 0,0 1),(0 0,2 0),(2 0,2 1),(1 1.1,2 0),(0 1,2 0),(1 0.1,2 0),(0 1,1 0.1),(0 0,1 0.1))`, - expectedTris: `MULTIPOLYGON (((0 1,0 0,1 0.1,0 1)),((0 1,1 0.1,2 0,0 1)),((0 1,2 0,1 1.1,0 1)),((1 1.1,2 0,2 1,1 1.1)),((0 0,2 0,1 0.1,0 0)))`, + expectedTris: `MULTIPOLYGON (((0 1,0 0,1 0.1,0 1)),((0 1,1 0.1,2 0,0 1)),((0 1,2 0,1 1.1,0 1)),((1 1.1,2 0,2 1,1 1.1)),((0 0,2 0,1 0.1,0 0)))`, }, { // an egg shape with one horizontal line. The horizontal line @@ -309,37 +308,37 @@ func TestTriangulation(t *testing.T) { inputWKT: `MULTILINESTRING((0 0,-0.1 0.5,0 1,0.5 1.2,1 1.3,1.5 1.2,2 1,2.1 0.5,2 0,1.5 -0.2,1 -0.3,0.5 -0.2,0 0),(-0.1 0.5,2.1 0.5))`, inputWKB: `01050000000200000001020000000d000000000000000000000000000000000000009a9999999999b9bf000000000000e03f0000000000000000000000000000f03f000000000000e03f333333333333f33f000000000000f03fcdccccccccccf43f000000000000f83f333333333333f33f0000000000000040000000000000f03fcdcccccccccc0040000000000000e03f00000000000000400000000000000000000000000000f83f9a9999999999c9bf000000000000f03f333333333333d3bf000000000000e03f9a9999999999c9bf000000000000000000000000000000000102000000020000009a9999999999b9bf000000000000e03fcdcccccccccc0040000000000000e03f`, expectedEdges: `MULTILINESTRING ((1.5 1.2,2 1),(1 1.3,1.5 1.2),(0.5 1.2,1 1.3),(0 1,0.5 1.2),(-0.1 0.5,0 1),(-0.1 0.5,0 0),(0 0,0.5 -0.2),(0.5 -0.2,1 -0.3),(1 -0.3,1.5 -0.2),(1.5 -0.2,2 0),(2 0,2.1 0.5),(2 1,2.1 0.5),(1.5 1.2,2.1 0.5),(1 1.3,2.1 0.5),(-0.1 0.5,2.1 0.5),(-0.1 0.5,1 1.3),(-0.1 0.5,0.5 1.2),(1.5 -0.2,2.1 0.5),(-0.1 0.5,1.5 -0.2),(0.5 -0.2,1.5 -0.2),(-0.1 0.5,0.5 -0.2))`, - expectedTris: `MULTIPOLYGON (((0 1,-0.1 0.5,0.5 1.2,0 1)),((0.5 1.2,-0.1 0.5,1 1.3,0.5 1.2)),((1 1.3,-0.1 0.5,2.1 0.5,1 1.3)),((1 1.3,2.1 0.5,1.5 1.2,1 1.3)),((1.5 1.2,2.1 0.5,2 1,1.5 1.2)),((1 -0.3,1.5 -0.2,0.5 -0.2,1 -0.3)),((0.5 -0.2,1.5 -0.2,-0.1 0.5,0.5 -0.2)),((0.5 -0.2,-0.1 0.5,0 0,0.5 -0.2)),((-0.1 0.5,1.5 -0.2,2.1 0.5,-0.1 0.5)),((2.1 0.5,1.5 -0.2,2 0,2.1 0.5)))`, + expectedTris: `MULTIPOLYGON (((0 1,-0.1 0.5,0.5 1.2,0 1)),((0.5 1.2,-0.1 0.5,1 1.3,0.5 1.2)),((1 1.3,-0.1 0.5,2.1 0.5,1 1.3)),((1 1.3,2.1 0.5,1.5 1.2,1 1.3)),((1.5 1.2,2.1 0.5,2 1,1.5 1.2)),((1 -0.3,1.5 -0.2,0.5 -0.2,1 -0.3)),((0.5 -0.2,1.5 -0.2,-0.1 0.5,0.5 -0.2)),((0.5 -0.2,-0.1 0.5,0 0,0.5 -0.2)),((-0.1 0.5,1.5 -0.2,2.1 0.5,-0.1 0.5)),((2.1 0.5,1.5 -0.2,2 0,2.1 0.5)))`, }, { - // a triangle with a line intersecting the top vertex. Where the + // a triangle with a line intersecting the top vertex. Where the // line intersects the vertex, the line should be broken into two // pieces and triangulated properly. inputWKT: `MULTILINESTRING((0 0,-0.1 0.5,0 1,0.5 1.2,1 1.3,1.5 1.2,2 1,2.1 0.5,2 0,1.5 -0.2,1 -0.3,0.5 -0.2,0 0),(-0.1 0.5,2.1 0.5))`, inputWKB: `01050000000200000001020000000400000000000000000000000000000000000000000000000000f03f000000000000f03f00000000000000400000000000000000000000000000000000000000000000000102000000020000000000000000000000000000000000f03f0000000000000040000000000000f03f`, expectedEdges: `MULTILINESTRING ((1 1,2 1),(0 1,1 1),(0 0,0 1),(0 0,2 0),(2 0,2 1),(1 1,2 0),(0 0,1 1))`, - expectedTris: `MULTIPOLYGON (((0 1,0 0,1 1,0 1)),((1 1,0 0,2 0,1 1)),((1 1,2 0,2 1,1 1)))`, + expectedTris: `MULTIPOLYGON (((0 1,0 0,1 1,0 1)),((1 1,0 0,2 0,1 1)),((1 1,2 0,2 1,1 1)))`, }, { // a figure eight with a duplicate constrained line. inputWKT: `MULTIPOLYGON (((0 0,0 1,1 1,1 0,0 0,0 -1,1 -1,1 0,0 0)))`, inputWKB: `01060000000100000001030000000100000009000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f0000000000000000000000000000000000000000000000000000000000000000000000000000f0bf000000000000f03f000000000000f0bf000000000000f03f000000000000000000000000000000000000000000000000`, expectedEdges: `MULTILINESTRING ((0 1,1 1),(0 0,0 1),(0 -1,0 0),(0 -1,1 -1),(1 -1,1 0),(1 0,1 1),(0 1,1 0),(0 0,1 0),(0 0,1 -1))`, - expectedTris: `MULTIPOLYGON (((0 1,0 0,1 0,0 1)),((0 1,1 0,1 1,0 1)),((0 -1,1 -1,0 0,0 -1)),((0 0,1 -1,1 0,0 0)))`, + expectedTris: `MULTIPOLYGON (((0 1,0 0,1 0,0 1)),((0 1,1 0,1 1,0 1)),((0 -1,1 -1,0 0,0 -1)),((0 0,1 -1,1 0,0 0)))`, }, { // A constraint line that overlaps with another edge inputWKT: `MULTIPOLYGON (((0 0,1 1,2 1,3 0,3 1,0 1,0 0)))`, inputWKB: `0106000000010000000103000000010000000700000000000000000000000000000000000000000000000000f03f000000000000f03f0000000000000040000000000000f03f000000000000084000000000000000000000000000000840000000000000f03f0000000000000000000000000000f03f00000000000000000000000000000000`, expectedEdges: `MULTILINESTRING ((2 1,3 1),(1 1,2 1),(0 1,1 1),(0 0,0 1),(0 0,3 0),(3 0,3 1),(2 1,3 0),(0 0,2 1),(0 0,1 1))`, - expectedTris: `MULTIPOLYGON (((0 1,0 0,1 1,0 1)),((1 1,0 0,2 1,1 1)),((2 1,0 0,3 0,2 1)),((2 1,3 0,3 1,2 1)))`, + expectedTris: `MULTIPOLYGON (((0 1,0 0,1 1,0 1)),((1 1,0 0,2 1,1 1)),((2 1,0 0,3 0,2 1)),((2 1,3 0,3 1,2 1)))`, }, { // bow-tie inputWKT: `MULTIPOLYGON (((0 0,1 1,1 0,0 1,0 0)))`, inputWKB: `0106000000010000000103000000010000000500000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f00000000000000000000000000000000000000000000f03f00000000000000000000000000000000`, expectedEdges: `MULTILINESTRING ((0 1,1 1),(0 0,0 1),(0 0,1 0),(1 0,1 1),(0.5 0.5,1 0),(0.5 0.5,1 1),(0 1,0.5 0.5),(0 0,0.5 0.5))`, - expectedTris: `MULTIPOLYGON (((0 1,0 0,0.5 0.5,0 1)),((0 1,0.5 0.5,1 1,0 1)),((1 1,0.5 0.5,1 0,1 1)),((0 0,1 0,0.5 0.5,0 0)))`, + expectedTris: `MULTIPOLYGON (((0 1,0 0,0.5 0.5,0 1)),((0 1,0.5 0.5,1 1,0 1)),((1 1,0.5 0.5,1 0,1 1)),((0 0,1 0,0.5 0.5,0 0)))`, }, } @@ -348,4 +347,3 @@ func TestTriangulation(t *testing.T) { t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) } } - diff --git a/planar/triangulate/delaunay_test.go b/planar/triangulate/delaunay_test.go index e95e8e61..f66d527b 100644 --- a/planar/triangulate/delaunay_test.go +++ b/planar/triangulate/delaunay_test.go @@ -29,8 +29,8 @@ func TestDelaunayTriangulation(t *testing.T) { // provided for readability inputWKT string // this can be removed if/when geom has a WKT decoder. - // A simple website for performing conversions: - // https://rodic.fr/blog/online-conversion-between-geometric-formats/ + // A simple website for performing conversions: + // https://rodic.fr/blog/online-conversion-between-geometric-formats/ inputWKB string expectedEdges string expectedTris string @@ -52,14 +52,13 @@ func TestDelaunayTriangulation(t *testing.T) { builder.tolerance = 1e-6 builder.SetSites(sites) if builder.create() == false { - t.Errorf("error building triangulation, expected true got false"); + t.Errorf("error building triangulation, expected true got false") } err = builder.subdiv.Validate() if err != nil { t.Errorf("error, expected nil got %v", err) } - edges := builder.GetEdges() edgesWKT, err := wkt.Encode(edges) if err != nil { @@ -111,24 +110,24 @@ func TestDelaunayTriangulation(t *testing.T) { // MULTIPOLYGON expectedTris: "MULTIPOLYGON (((0 20,0 10,10 10,0 20)),((0 20,10 10,10 20,0 20)),((10 20,10 10,20 10,10 20)),((10 20,20 10,20 20,10 20)),((10 0,20 0,10 10,10 0)),((10 0,10 10,0 10,10 0)),((10 0,0 10,0 0,10 0)),((10 10,20 0,20 10,10 10)))", }, - { - inputWKT: "MULTIPOINT ((50 40),(140 70),(80 100),(130 140),(30 150),(70 180),(190 110),(120 20))", - inputWKB: "01040000000800000001010000000000000000004940000000000000444001010000000000000000806140000000000080514001010000000000000000005440000000000000594001010000000000000000406040000000000080614001010000000000000000003e400000000000c0624001010000000000000000805140000000000080664001010000000000000000c067400000000000805b4001010000000000000000005e400000000000003440", - expectedEdges: "MULTILINESTRING ((70 180,190 110),(30 150,70 180),(30 150,50 40),(50 40,120 20),(120 20,190 110),(120 20,140 70),(140 70,190 110),(130 140,140 70),(130 140,190 110),(70 180,130 140),(80 100,130 140),(70 180,80 100),(30 150,80 100),(50 40,80 100),(80 100,120 20),(80 100,140 70))", - expectedTris: "MULTIPOLYGON (((30 150,50 40,80 100,30 150)),((30 150,80 100,70 180,30 150)),((70 180,80 100,130 140,70 180)),((70 180,130 140,190 110,70 180)),((190 110,130 140,140 70,190 110)),((190 110,140 70,120 20,190 110)),((120 20,140 70,80 100,120 20)),((120 20,80 100,50 40,120 20)),((80 100,140 70,130 140,80 100)))", - }, - { - inputWKT: "POLYGON ((42 30, 41.96 29.61, 41.85 29.23, 41.66 28.89, 41.41 28.59, 41.11 28.34, 40.77 28.15, 40.39 28.04, 40 28, 39.61 28.04, 39.23 28.15, 38.89 28.34, 38.59 28.59, 38.34 28.89, 38.15 29.23, 38.04 29.61, 38 30, 38.04 30.39, 38.15 30.77, 38.34 31.11, 38.59 31.41, 38.89 31.66, 39.23 31.85, 39.61 31.96, 40 32, 40.39 31.96, 40.77 31.85, 41.11 31.66, 41.41 31.41, 41.66 31.11, 41.85 30.77, 41.96 30.39, 42 30))", - inputWKB: "0103000000010000002100000000000000000045400000000000003e407b14ae47e1fa44405c8fc2f5289c3d40cdccccccccec44407b14ae47e13a3d4014ae47e17ad44440a4703d0ad7e33c4014ae47e17ab44440d7a3703d0a973c40ae47e17a148e4440d7a3703d0a573c40c3f5285c8f6244406666666666263c4052b81e85eb3144400ad7a3703d0a3c4000000000000044400000000000003c40ae47e17a14ce43400ad7a3703d0a3c403d0ad7a3709d43406666666666263c4052b81e85eb714340d7a3703d0a573c40ec51b81e854b4340d7a3703d0a973c40ec51b81e852b4340a4703d0ad7e33c4033333333331343407b14ae47e13a3d4085eb51b81e0543405c8fc2f5289c3d4000000000000043400000000000003e4085eb51b81e054340a4703d0ad7633e40333333333313434085eb51b81ec53e40ec51b81e852b43405c8fc2f5281c3f40ec51b81e854b4340295c8fc2f5683f4052b81e85eb714340295c8fc2f5a83f403d0ad7a3709d43409a99999999d93f40ae47e17a14ce4340f6285c8fc2f53f400000000000004440000000000000404052b81e85eb314440f6285c8fc2f53f40c3f5285c8f6244409a99999999d93f40ae47e17a148e4440295c8fc2f5a83f4014ae47e17ab44440295c8fc2f5683f4014ae47e17ad444405c8fc2f5281c3f40cdccccccccec444085eb51b81ec53e407b14ae47e1fa4440a4703d0ad7633e4000000000000045400000000000003e40", - expectedEdges: "MULTILINESTRING ((41.66 31.11,41.85 30.77),(41.41 31.41,41.66 31.11),(41.11 31.66,41.41 31.41),(40.77 31.85,41.11 31.66),(40.39 31.96,40.77 31.85),(40 32,40.39 31.96),(39.61 31.96,40 32),(39.23 31.85,39.61 31.96),(38.89 31.66,39.23 31.85),(38.59 31.41,38.89 31.66),(38.34 31.11,38.59 31.41),(38.15 30.77,38.34 31.11),(38.04 30.39,38.15 30.77),(38 30,38.04 30.39),(38 30,38.04 29.61),(38.04 29.61,38.15 29.23),(38.15 29.23,38.34 28.89),(38.34 28.89,38.59 28.59),(38.59 28.59,38.89 28.34),(38.89 28.34,39.23 28.15),(39.23 28.15,39.61 28.04),(39.61 28.04,40 28),(40 28,40.39 28.04),(40.39 28.04,40.77 28.15),(40.77 28.15,41.11 28.34),(41.11 28.34,41.41 28.59),(41.41 28.59,41.66 28.89),(41.66 28.89,41.85 29.23),(41.85 29.23,41.96 29.61),(41.96 29.61,42 30),(41.96 30.39,42 30),(41.85 30.77,41.96 30.39),(41.66 31.11,41.96 30.39),(41.41 31.41,41.96 30.39),(41.41 28.59,41.96 30.39),(41.41 28.59,41.41 31.41),(38.59 28.59,41.41 28.59),(38.59 28.59,41.41 31.41),(38.59 28.59,38.59 31.41),(38.59 31.41,41.41 31.41),(38.59 31.41,39.61 31.96),(39.61 31.96,41.41 31.41),(39.61 31.96,40.39 31.96),(40.39 31.96,41.41 31.41),(40.39 31.96,41.11 31.66),(38.04 30.39,38.59 28.59),(38.04 30.39,38.59 31.41),(38.04 30.39,38.34 31.11),(38.04 29.61,38.59 28.59),(38.04 29.61,38.04 30.39),(39.61 28.04,41.41 28.59),(38.59 28.59,39.61 28.04),(38.89 28.34,39.61 28.04),(40.39 28.04,41.41 28.59),(39.61 28.04,40.39 28.04),(41.96 29.61,41.96 30.39),(41.41 28.59,41.96 29.61),(41.66 28.89,41.96 29.61),(40.39 28.04,41.11 28.34),(38.04 29.61,38.34 28.89),(38.89 31.66,39.61 31.96))", - expectedTris: "MULTIPOLYGON (((38.15 30.77,38.04 30.39,38.34 31.11,38.15 30.77)),((38.34 31.11,38.04 30.39,38.59 31.41,38.34 31.11)),((38.59 31.41,38.04 30.39,38.59 28.59,38.59 31.41)),((38.59 31.41,38.59 28.59,41.41 31.41,38.59 31.41)),((38.59 31.41,41.41 31.41,39.61 31.96,38.59 31.41)),((38.59 31.41,39.61 31.96,38.89 31.66,38.59 31.41)),((38.89 31.66,39.61 31.96,39.23 31.85,38.89 31.66)),((39.61 31.96,41.41 31.41,40.39 31.96,39.61 31.96)),((39.61 31.96,40.39 31.96,40 32,39.61 31.96)),((40.39 31.96,41.41 31.41,41.11 31.66,40.39 31.96)),((40.39 31.96,41.11 31.66,40.77 31.85,40.39 31.96)),((41.41 31.41,38.59 28.59,41.41 28.59,41.41 31.41)),((41.41 31.41,41.41 28.59,41.96 30.39,41.41 31.41)),((41.41 31.41,41.96 30.39,41.66 31.11,41.41 31.41)),((41.66 31.11,41.96 30.39,41.85 30.77,41.66 31.11)),((40 28,40.39 28.04,39.61 28.04,40 28)),((39.61 28.04,40.39 28.04,41.41 28.59,39.61 28.04)),((39.61 28.04,41.41 28.59,38.59 28.59,39.61 28.04)),((39.61 28.04,38.59 28.59,38.89 28.34,39.61 28.04)),((39.61 28.04,38.89 28.34,39.23 28.15,39.61 28.04)),((41.41 28.59,40.39 28.04,41.11 28.34,41.41 28.59)),((41.11 28.34,40.39 28.04,40.77 28.15,41.11 28.34)),((41.41 28.59,41.66 28.89,41.96 29.61,41.41 28.59)),((41.41 28.59,41.96 29.61,41.96 30.39,41.41 28.59)),((41.96 30.39,41.96 29.61,42 30,41.96 30.39)),((41.96 29.61,41.66 28.89,41.85 29.23,41.96 29.61)),((38.59 28.59,38.04 30.39,38.04 29.61,38.59 28.59)),((38.59 28.59,38.04 29.61,38.34 28.89,38.59 28.59)),((38.34 28.89,38.04 29.61,38.15 29.23,38.34 28.89)),((38.04 29.61,38.04 30.39,38 30,38.04 29.61)))", - }, - { - inputWKT: "POLYGON ((0 0, 0 200, 180 200, 180 0, 0 0), (20 180, 160 180, 160 20, 152.625 146.75, 20 180), (30 160, 150 30, 70 90, 30 160))", - inputWKB: "010300000003000000050000000000000000000000000000000000000000000000000000000000000000006940000000000080664000000000000069400000000000806640000000000000000000000000000000000000000000000000050000000000000000003440000000000080664000000000000064400000000000806640000000000000644000000000000034400000000000146340000000000058624000000000000034400000000000806640040000000000000000003e4000000000000064400000000000c062400000000000003e40000000000080514000000000008056400000000000003e400000000000006440", - expectedEdges: "MULTILINESTRING ((0 200,180 200),(0 0,0 200),(0 0,180 0),(180 0,180 200),(152.625 146.75,180 0),(152.625 146.75,180 200),(152.625 146.75,160 180),(160 180,180 200),(0 200,160 180),(20 180,160 180),(0 200,20 180),(20 180,30 160),(0 200,30 160),(0 0,30 160),(30 160,70 90),(0 0,70 90),(70 90,150 30),(0 0,150 30),(150 30,160 20),(0 0,160 20),(160 20,180 0),(152.625 146.75,160 20),(150 30,152.625 146.75),(70 90,152.625 146.75),(30 160,152.625 146.75),(30 160,160 180))", - expectedTris: "MULTIPOLYGON (((0 200,0 0,30 160,0 200)),((0 200,30 160,20 180,0 200)),((0 200,20 180,160 180,0 200)),((0 200,160 180,180 200,0 200)),((180 200,160 180,152.625 146.75,180 200)),((180 200,152.625 146.75,180 0,180 200)),((0 0,180 0,160 20,0 0)),((0 0,160 20,150 30,0 0)),((0 0,150 30,70 90,0 0)),((0 0,70 90,30 160,0 0)),((30 160,70 90,152.625 146.75,30 160)),((30 160,152.625 146.75,160 180,30 160)),((30 160,160 180,20 180,30 160)),((152.625 146.75,70 90,150 30,152.625 146.75)),((152.625 146.75,150 30,160 20,152.625 146.75)),((152.625 146.75,160 20,180 0,152.625 146.75)))", - }, + { + inputWKT: "MULTIPOINT ((50 40),(140 70),(80 100),(130 140),(30 150),(70 180),(190 110),(120 20))", + inputWKB: "01040000000800000001010000000000000000004940000000000000444001010000000000000000806140000000000080514001010000000000000000005440000000000000594001010000000000000000406040000000000080614001010000000000000000003e400000000000c0624001010000000000000000805140000000000080664001010000000000000000c067400000000000805b4001010000000000000000005e400000000000003440", + expectedEdges: "MULTILINESTRING ((70 180,190 110),(30 150,70 180),(30 150,50 40),(50 40,120 20),(120 20,190 110),(120 20,140 70),(140 70,190 110),(130 140,140 70),(130 140,190 110),(70 180,130 140),(80 100,130 140),(70 180,80 100),(30 150,80 100),(50 40,80 100),(80 100,120 20),(80 100,140 70))", + expectedTris: "MULTIPOLYGON (((30 150,50 40,80 100,30 150)),((30 150,80 100,70 180,30 150)),((70 180,80 100,130 140,70 180)),((70 180,130 140,190 110,70 180)),((190 110,130 140,140 70,190 110)),((190 110,140 70,120 20,190 110)),((120 20,140 70,80 100,120 20)),((120 20,80 100,50 40,120 20)),((80 100,140 70,130 140,80 100)))", + }, + { + inputWKT: "POLYGON ((42 30, 41.96 29.61, 41.85 29.23, 41.66 28.89, 41.41 28.59, 41.11 28.34, 40.77 28.15, 40.39 28.04, 40 28, 39.61 28.04, 39.23 28.15, 38.89 28.34, 38.59 28.59, 38.34 28.89, 38.15 29.23, 38.04 29.61, 38 30, 38.04 30.39, 38.15 30.77, 38.34 31.11, 38.59 31.41, 38.89 31.66, 39.23 31.85, 39.61 31.96, 40 32, 40.39 31.96, 40.77 31.85, 41.11 31.66, 41.41 31.41, 41.66 31.11, 41.85 30.77, 41.96 30.39, 42 30))", + inputWKB: "0103000000010000002100000000000000000045400000000000003e407b14ae47e1fa44405c8fc2f5289c3d40cdccccccccec44407b14ae47e13a3d4014ae47e17ad44440a4703d0ad7e33c4014ae47e17ab44440d7a3703d0a973c40ae47e17a148e4440d7a3703d0a573c40c3f5285c8f6244406666666666263c4052b81e85eb3144400ad7a3703d0a3c4000000000000044400000000000003c40ae47e17a14ce43400ad7a3703d0a3c403d0ad7a3709d43406666666666263c4052b81e85eb714340d7a3703d0a573c40ec51b81e854b4340d7a3703d0a973c40ec51b81e852b4340a4703d0ad7e33c4033333333331343407b14ae47e13a3d4085eb51b81e0543405c8fc2f5289c3d4000000000000043400000000000003e4085eb51b81e054340a4703d0ad7633e40333333333313434085eb51b81ec53e40ec51b81e852b43405c8fc2f5281c3f40ec51b81e854b4340295c8fc2f5683f4052b81e85eb714340295c8fc2f5a83f403d0ad7a3709d43409a99999999d93f40ae47e17a14ce4340f6285c8fc2f53f400000000000004440000000000000404052b81e85eb314440f6285c8fc2f53f40c3f5285c8f6244409a99999999d93f40ae47e17a148e4440295c8fc2f5a83f4014ae47e17ab44440295c8fc2f5683f4014ae47e17ad444405c8fc2f5281c3f40cdccccccccec444085eb51b81ec53e407b14ae47e1fa4440a4703d0ad7633e4000000000000045400000000000003e40", + expectedEdges: "MULTILINESTRING ((41.66 31.11,41.85 30.77),(41.41 31.41,41.66 31.11),(41.11 31.66,41.41 31.41),(40.77 31.85,41.11 31.66),(40.39 31.96,40.77 31.85),(40 32,40.39 31.96),(39.61 31.96,40 32),(39.23 31.85,39.61 31.96),(38.89 31.66,39.23 31.85),(38.59 31.41,38.89 31.66),(38.34 31.11,38.59 31.41),(38.15 30.77,38.34 31.11),(38.04 30.39,38.15 30.77),(38 30,38.04 30.39),(38 30,38.04 29.61),(38.04 29.61,38.15 29.23),(38.15 29.23,38.34 28.89),(38.34 28.89,38.59 28.59),(38.59 28.59,38.89 28.34),(38.89 28.34,39.23 28.15),(39.23 28.15,39.61 28.04),(39.61 28.04,40 28),(40 28,40.39 28.04),(40.39 28.04,40.77 28.15),(40.77 28.15,41.11 28.34),(41.11 28.34,41.41 28.59),(41.41 28.59,41.66 28.89),(41.66 28.89,41.85 29.23),(41.85 29.23,41.96 29.61),(41.96 29.61,42 30),(41.96 30.39,42 30),(41.85 30.77,41.96 30.39),(41.66 31.11,41.96 30.39),(41.41 31.41,41.96 30.39),(41.41 28.59,41.96 30.39),(41.41 28.59,41.41 31.41),(38.59 28.59,41.41 28.59),(38.59 28.59,41.41 31.41),(38.59 28.59,38.59 31.41),(38.59 31.41,41.41 31.41),(38.59 31.41,39.61 31.96),(39.61 31.96,41.41 31.41),(39.61 31.96,40.39 31.96),(40.39 31.96,41.41 31.41),(40.39 31.96,41.11 31.66),(38.04 30.39,38.59 28.59),(38.04 30.39,38.59 31.41),(38.04 30.39,38.34 31.11),(38.04 29.61,38.59 28.59),(38.04 29.61,38.04 30.39),(39.61 28.04,41.41 28.59),(38.59 28.59,39.61 28.04),(38.89 28.34,39.61 28.04),(40.39 28.04,41.41 28.59),(39.61 28.04,40.39 28.04),(41.96 29.61,41.96 30.39),(41.41 28.59,41.96 29.61),(41.66 28.89,41.96 29.61),(40.39 28.04,41.11 28.34),(38.04 29.61,38.34 28.89),(38.89 31.66,39.61 31.96))", + expectedTris: "MULTIPOLYGON (((38.15 30.77,38.04 30.39,38.34 31.11,38.15 30.77)),((38.34 31.11,38.04 30.39,38.59 31.41,38.34 31.11)),((38.59 31.41,38.04 30.39,38.59 28.59,38.59 31.41)),((38.59 31.41,38.59 28.59,41.41 31.41,38.59 31.41)),((38.59 31.41,41.41 31.41,39.61 31.96,38.59 31.41)),((38.59 31.41,39.61 31.96,38.89 31.66,38.59 31.41)),((38.89 31.66,39.61 31.96,39.23 31.85,38.89 31.66)),((39.61 31.96,41.41 31.41,40.39 31.96,39.61 31.96)),((39.61 31.96,40.39 31.96,40 32,39.61 31.96)),((40.39 31.96,41.41 31.41,41.11 31.66,40.39 31.96)),((40.39 31.96,41.11 31.66,40.77 31.85,40.39 31.96)),((41.41 31.41,38.59 28.59,41.41 28.59,41.41 31.41)),((41.41 31.41,41.41 28.59,41.96 30.39,41.41 31.41)),((41.41 31.41,41.96 30.39,41.66 31.11,41.41 31.41)),((41.66 31.11,41.96 30.39,41.85 30.77,41.66 31.11)),((40 28,40.39 28.04,39.61 28.04,40 28)),((39.61 28.04,40.39 28.04,41.41 28.59,39.61 28.04)),((39.61 28.04,41.41 28.59,38.59 28.59,39.61 28.04)),((39.61 28.04,38.59 28.59,38.89 28.34,39.61 28.04)),((39.61 28.04,38.89 28.34,39.23 28.15,39.61 28.04)),((41.41 28.59,40.39 28.04,41.11 28.34,41.41 28.59)),((41.11 28.34,40.39 28.04,40.77 28.15,41.11 28.34)),((41.41 28.59,41.66 28.89,41.96 29.61,41.41 28.59)),((41.41 28.59,41.96 29.61,41.96 30.39,41.41 28.59)),((41.96 30.39,41.96 29.61,42 30,41.96 30.39)),((41.96 29.61,41.66 28.89,41.85 29.23,41.96 29.61)),((38.59 28.59,38.04 30.39,38.04 29.61,38.59 28.59)),((38.59 28.59,38.04 29.61,38.34 28.89,38.59 28.59)),((38.34 28.89,38.04 29.61,38.15 29.23,38.34 28.89)),((38.04 29.61,38.04 30.39,38 30,38.04 29.61)))", + }, + { + inputWKT: "POLYGON ((0 0, 0 200, 180 200, 180 0, 0 0), (20 180, 160 180, 160 20, 152.625 146.75, 20 180), (30 160, 150 30, 70 90, 30 160))", + inputWKB: "010300000003000000050000000000000000000000000000000000000000000000000000000000000000006940000000000080664000000000000069400000000000806640000000000000000000000000000000000000000000000000050000000000000000003440000000000080664000000000000064400000000000806640000000000000644000000000000034400000000000146340000000000058624000000000000034400000000000806640040000000000000000003e4000000000000064400000000000c062400000000000003e40000000000080514000000000008056400000000000003e400000000000006440", + expectedEdges: "MULTILINESTRING ((0 200,180 200),(0 0,0 200),(0 0,180 0),(180 0,180 200),(152.625 146.75,180 0),(152.625 146.75,180 200),(152.625 146.75,160 180),(160 180,180 200),(0 200,160 180),(20 180,160 180),(0 200,20 180),(20 180,30 160),(0 200,30 160),(0 0,30 160),(30 160,70 90),(0 0,70 90),(70 90,150 30),(0 0,150 30),(150 30,160 20),(0 0,160 20),(160 20,180 0),(152.625 146.75,160 20),(150 30,152.625 146.75),(70 90,152.625 146.75),(30 160,152.625 146.75),(30 160,160 180))", + expectedTris: "MULTIPOLYGON (((0 200,0 0,30 160,0 200)),((0 200,30 160,20 180,0 200)),((0 200,20 180,160 180,0 200)),((0 200,160 180,180 200,0 200)),((180 200,160 180,152.625 146.75,180 200)),((180 200,152.625 146.75,180 0,180 200)),((0 0,180 0,160 20,0 0)),((0 0,160 20,150 30,0 0)),((0 0,150 30,70 90,0 0)),((0 0,70 90,30 160,0 0)),((30 160,70 90,152.625 146.75,30 160)),((30 160,152.625 146.75,160 180,30 160)),((30 160,160 180,20 180,30 160)),((152.625 146.75,70 90,150 30,152.625 146.75)),((152.625 146.75,150 30,160 20,152.625 146.75)),((152.625 146.75,160 20,180 0,152.625 146.75)))", + }, } for i, tc := range testcases { diff --git a/planar/triangulate/delaunaytriangulationbuilder.go b/planar/triangulate/delaunaytriangulationbuilder.go index c6a2ba9f..5955854e 100644 --- a/planar/triangulate/delaunaytriangulationbuilder.go +++ b/planar/triangulate/delaunaytriangulationbuilder.go @@ -187,8 +187,8 @@ Returns the subdivision containing the triangulation or nil if it has not been created. */ func (dtb *DelaunayTriangulationBuilder) GetSubdivision() *quadedge.QuadEdgeSubdivision { - dtb.create(); - return dtb.subdiv; + dtb.create() + return dtb.subdiv } /* diff --git a/planar/triangulate/quadedge/debug.go b/planar/triangulate/quadedge/debug.go index 719f9ed7..09d717e9 100644 --- a/planar/triangulate/quadedge/debug.go +++ b/planar/triangulate/quadedge/debug.go @@ -1,4 +1,3 @@ - package quadedge import ( @@ -28,7 +27,7 @@ func (qes *QuadEdgeSubdivision) hasCCWNeighbor(e *QuadEdge) bool { for true { ccw := n.ONext() // this will only work if the angles between edges are < 180deg - // if both edges are frame edges then the CCW rule may not + // if both edges are frame edges then the CCW rule may not // be easily detectable. (think angles > 180deg) if n.Orig().IsCCW(n.Dest(), ccw.Dest()) == true { return true @@ -41,7 +40,6 @@ func (qes *QuadEdgeSubdivision) hasCCWNeighbor(e *QuadEdge) bool { return false } - /* Validate runs a self consistency checks and reports the first error. @@ -67,8 +65,6 @@ func (qes *QuadEdgeSubdivision) Validate() error { return qes.validateONext() } - - /* validateONext validates that each QuadEdge's ONext() goes to the next edge that shares an origin point in CCW order. @@ -88,14 +84,14 @@ func (qes *QuadEdgeSubdivision) validateONext() error { if n.Orig().Equals(e.Orig()) == false { return fmt.Errorf("edge in ONext() doesn't share an origin: between %v and %v", e, n) } - // this isn't a perfect check for CCW, but it should work well + // this isn't a perfect check for CCW, but it should work well // enough in most cases and shouldn't produce false positives. if (qes.isFrameEdge(n) == false || qes.isFrameEdge(ccw) == false) && n.Orig().IsCCW(n.Dest(), ccw.Dest()) == false && qes.hasCCWNeighbor(n) == true { return fmt.Errorf("edges are not CCW, expected %v to be CCW of %v", ccw, n) } edgeSet[n] = true n = ccw - if (n == e) { + if n == e { break } } @@ -104,4 +100,3 @@ func (qes *QuadEdgeSubdivision) validateONext() error { return nil } - diff --git a/planar/triangulate/quadedge/quadedgesubdivision.go b/planar/triangulate/quadedge/quadedgesubdivision.go index 9d28c786..eabfb0f1 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision.go +++ b/planar/triangulate/quadedge/quadedgesubdivision.go @@ -312,29 +312,29 @@ subdivision. p0 a coordinate p1 another coordinate -Return the edge joining the coordinates, if present or null if no such edge +Return the edge joining the coordinates, if present or null if no such edge exists */ func (qes *QuadEdgeSubdivision) LocateSegment(p0 Vertex, p1 Vertex) (*QuadEdge, error) { // find an edge containing one of the points - e, err := qes.locator.Locate(p0); + e, err := qes.locator.Locate(p0) if err != nil || e == nil { return nil, err } // normalize so that p0 is origin of base edge - base := e; - if (e.Dest().EqualsTolerance(p0, qes.tolerance)) { - base = e.Sym(); + base := e + if e.Dest().EqualsTolerance(p0, qes.tolerance) { + base = e.Sym() } // check all edges around origin of base edge - locEdge := base; + locEdge := base done := false for !done { if locEdge.Dest().EqualsTolerance(p1, qes.tolerance) { return locEdge, nil } - locEdge = locEdge.ONext(); + locEdge = locEdge.ONext() if locEdge == base { done = true @@ -687,7 +687,7 @@ func (qes *QuadEdgeSubdivision) visitTriangles(triVisitor func(triEdges []*QuadE if triEdges != nil { triVisitor(triEdges) } - } + } } } @@ -984,6 +984,3 @@ func (qes *QuadEdgeSubdivision) GetTriangles() (geom.MultiPolygon, error) { // } // } - - - diff --git a/planar/triangulate/quadedge/quadedgesubdivision_test.go b/planar/triangulate/quadedge/quadedgesubdivision_test.go index fc5974f0..497ed6c9 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision_test.go +++ b/planar/triangulate/quadedge/quadedgesubdivision_test.go @@ -236,4 +236,3 @@ func TestQuadEdgeSubdivisionLocate(t *testing.T) { t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) } } - diff --git a/planar/triangulate/quadedge/vertex.go b/planar/triangulate/quadedge/vertex.go index 8b7bea73..f2ca4ae5 100644 --- a/planar/triangulate/quadedge/vertex.go +++ b/planar/triangulate/quadedge/vertex.go @@ -134,7 +134,7 @@ Returns the scaled vector This is not part of the original JTS code. */ func (u Vertex) Divide(c float64) Vertex { - return Vertex{u.X() / c, u.Y() / c} + return Vertex{u.X() / c, u.Y() / c} } // Sum u + v and return the new Vertex @@ -158,7 +158,7 @@ Normalize scales the vector so the length is one. This is not part of the original JTS code. */ func (u Vertex) Normalize() Vertex { - return u.Divide(u.Magn()) + return u.Divide(u.Magn()) } /* returns k X v (cross product). this is a vector perpendicular to v */ diff --git a/planar/triangulate/quadedge/vertex_test.go b/planar/triangulate/quadedge/vertex_test.go index 197be5e8..466b0ce6 100644 --- a/planar/triangulate/quadedge/vertex_test.go +++ b/planar/triangulate/quadedge/vertex_test.go @@ -104,12 +104,12 @@ func TestVertexEqualsTolerance(t *testing.T) { func TestVertexIsInCircle(t *testing.T) { type tcase struct { - v1 Vertex - expected bool + v1 Vertex + expected bool } fn := func(t *testing.T, tc tcase) { - r := tc.v1.IsInCircle(Vertex{0, 0}, Vertex{2,0}, Vertex{1, 1}) + r := tc.v1.IsInCircle(Vertex{0, 0}, Vertex{2, 0}, Vertex{1, 1}) if r != tc.expected { t.Errorf("error, expected %v got %v", tc.expected, r) return @@ -128,8 +128,8 @@ func TestVertexIsInCircle(t *testing.T) { func TestVertexMarshalJSON(t *testing.T) { type tcase struct { - v1 Vertex - expected string + v1 Vertex + expected string } fn := func(t *testing.T, tc tcase) { @@ -154,7 +154,7 @@ func TestVertexMarshalJSON(t *testing.T) { func TestVertexScalar(t *testing.T) { type tcase struct { - v Vertex + v Vertex scalar float64 times Vertex } @@ -183,4 +183,3 @@ func TestVertexScalar(t *testing.T) { t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) } } - diff --git a/planar/triangulate/segment.go b/planar/triangulate/segment.go index 0760ec32..2c77e3df 100644 --- a/planar/triangulate/segment.go +++ b/planar/triangulate/segment.go @@ -28,7 +28,7 @@ Author Martin Davis Ported to Go by Jason R. Surratt */ type Segment struct { - ls geom.Line + ls geom.Line data interface{} } @@ -36,29 +36,29 @@ func NewSegment(l geom.Line) Segment { return Segment{ls: l} } - /** - * Creates a new instance for the given ordinates. - public Segment(double x1, double y1, double z1, double x2, double y2, double z2) { - this(new Coordinate(x1, y1, z1), new Coordinate(x2, y2, z2)); - } - */ - - /** - * Creates a new instance for the given ordinates, with associated external data. - public Segment(double x1, double y1, double z1, double x2, double y2, double z2, Object data) { - this(new Coordinate(x1, y1, z1), new Coordinate(x2, y2, z2), data); - } - */ - - /** - * Creates a new instance for the given points. - * - * @param p0 the start point - * @param p1 the end point - public Segment(Coordinate p0, Coordinate p1) { - ls = new LineSegment(p0, p1); - } - */ +/** + * Creates a new instance for the given ordinates. + public Segment(double x1, double y1, double z1, double x2, double y2, double z2) { + this(new Coordinate(x1, y1, z1), new Coordinate(x2, y2, z2)); + } +*/ + +/** + * Creates a new instance for the given ordinates, with associated external data. + public Segment(double x1, double y1, double z1, double x2, double y2, double z2, Object data) { + this(new Coordinate(x1, y1, z1), new Coordinate(x2, y2, z2), data); + } +*/ + +/** + * Creates a new instance for the given points. + * + * @param p0 the start point + * @param p1 the end point + public Segment(Coordinate p0, Coordinate p1) { + ls = new LineSegment(p0, p1); + } +*/ /* Gets the start coordinate of the segment @@ -66,7 +66,7 @@ Gets the start coordinate of the segment Returns the starting vertex */ func (seg *Segment) GetStart() quadedge.Vertex { - return quadedge.Vertex(seg.ls[0]) + return quadedge.Vertex(seg.ls[0]) } /* @@ -75,118 +75,118 @@ Gets the end coordinate of the segment Return a Coordinate */ func (seg *Segment) GetEnd() quadedge.Vertex { - return quadedge.Vertex(seg.ls[1]) + return quadedge.Vertex(seg.ls[1]) } - /** - * Gets the start X ordinate of the segment - * - * @return the X ordinate value - public double getStartX() { - Coordinate p = ls.getCoordinate(0); - return p.x; - } - */ - - /** - * Gets the start Y ordinate of the segment - * - * @return the Y ordinate value - public double getStartY() { - Coordinate p = ls.getCoordinate(0); - return p.y; - } - */ - - /** - * Gets the start Z ordinate of the segment - * - * @return the Z ordinate value - public double getStartZ() { - Coordinate p = ls.getCoordinate(0); - return p.z; - } - */ - - /** - * Gets the end X ordinate of the segment - * - * @return the X ordinate value - public double getEndX() { - Coordinate p = ls.getCoordinate(1); - return p.x; - } - */ - - /** - * Gets the end Y ordinate of the segment - * - * @return the Y ordinate value - public double getEndY() { - Coordinate p = ls.getCoordinate(1); - return p.y; - } - */ - - /** - * Gets the end Z ordinate of the segment - * - * @return the Z ordinate value - public double getEndZ() { - Coordinate p = ls.getCoordinate(1); - return p.z; - } - */ +/** + * Gets the start X ordinate of the segment + * + * @return the X ordinate value + public double getStartX() { + Coordinate p = ls.getCoordinate(0); + return p.x; + } +*/ + +/** + * Gets the start Y ordinate of the segment + * + * @return the Y ordinate value + public double getStartY() { + Coordinate p = ls.getCoordinate(0); + return p.y; + } +*/ + +/** + * Gets the start Z ordinate of the segment + * + * @return the Z ordinate value + public double getStartZ() { + Coordinate p = ls.getCoordinate(0); + return p.z; + } +*/ + +/** + * Gets the end X ordinate of the segment + * + * @return the X ordinate value + public double getEndX() { + Coordinate p = ls.getCoordinate(1); + return p.x; + } +*/ + +/** + * Gets the end Y ordinate of the segment + * + * @return the Y ordinate value + public double getEndY() { + Coordinate p = ls.getCoordinate(1); + return p.y; + } +*/ + +/** + * Gets the end Z ordinate of the segment + * + * @return the Z ordinate value + public double getEndZ() { + Coordinate p = ls.getCoordinate(1); + return p.z; + } +*/ // GetLineSegment gets a Line modelling this segment. func (seg *Segment) GetLineSegment() geom.Line { - return seg.ls; + return seg.ls } - /** - * Gets the external data associated with this segment - * - * @return a data object - public Object getData() { - return data; - } - */ - - /** - * Sets the external data to be associated with this segment - * - * @param data a data object - public void setData(Object data) { - this.data = data; - } - */ - - /** - * Determines whether two segments are topologically equal. - * I.e. equal up to orientation. - * - * @param s a segment - * @return true if the segments are topologically equal - public boolean equalsTopo(Segment s) { - return ls.equalsTopo(s.getLineSegment()); - } - */ - - /** - * Computes the intersection point between this segment and another one. - * - * @param s a segment - * @return the intersection point, or null if there is none - public Coordinate intersection(Segment s) { - return ls.intersection(s.getLineSegment()); - } - */ - - /** - * Computes a string representation of this segment. - * - * @return a string - public String toString() { - return ls.toString(); - } - */ +/** + * Gets the external data associated with this segment + * + * @return a data object + public Object getData() { + return data; + } +*/ + +/** + * Sets the external data to be associated with this segment + * + * @param data a data object + public void setData(Object data) { + this.data = data; + } +*/ + +/** + * Determines whether two segments are topologically equal. + * I.e. equal up to orientation. + * + * @param s a segment + * @return true if the segments are topologically equal + public boolean equalsTopo(Segment s) { + return ls.equalsTopo(s.getLineSegment()); + } +*/ + +/** + * Computes the intersection point between this segment and another one. + * + * @param s a segment + * @return the intersection point, or null if there is none + public Coordinate intersection(Segment s) { + return ls.intersection(s.getLineSegment()); + } +*/ + +/** + * Computes a string representation of this segment. + * + * @return a string + public String toString() { + return ls.toString(); + } +*/ From a0166d47e377402c53461753bbd92bad8ada3395 Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Tue, 29 May 2018 16:03:51 -0600 Subject: [PATCH 08/12] Fix ErrUnknownGeometry error --- geom.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geom.go b/geom.go index d6a1e5f6..eb8e93ab 100644 --- a/geom.go +++ b/geom.go @@ -136,7 +136,7 @@ func extractLines(g Geometry, lines *[]Line) error { default: - return ErrUnknownGeometry + return ErrUnknownGeometry{g} case Pointer: From f962da5cf4f40206852e47da0c9cd00e0680dd58 Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Tue, 29 May 2018 17:04:03 -0600 Subject: [PATCH 09/12] Add segment_test to increase coverage. --- planar/triangulate/segment_test.go | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 planar/triangulate/segment_test.go diff --git a/planar/triangulate/segment_test.go b/planar/triangulate/segment_test.go new file mode 100644 index 00000000..38c95a52 --- /dev/null +++ b/planar/triangulate/segment_test.go @@ -0,0 +1,52 @@ +/* +Copyright (c) 2016 Vivid Solutions. + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v. 1.0 which accompanies this distribution. +The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + +http://www.eclipse.org/org/documents/edl-v10.php. +*/ + +package triangulate + +import ( + "strconv" + "testing" + + "github.com/go-spatial/geom" +) + +/* +TestSegmentDummy keeps coveralls from complaining. Not really necessary tests. +*/ +func TestSegmentDummy(t *testing.T) { + type tcase struct { + line geom.Line + } + + fn := func(t *testing.T, tc tcase) { + s := NewSegment(tc.line) + if s.GetStart().Equals(tc.line[0]) == false { + t.Errorf("error, expected %v got %v", tc.line[0], s.GetStart()) + } + if s.GetEnd().Equals(tc.line[1]) == false { + t.Errorf("error, expected %v got %v", tc.line[1], s.GetEnd()) + } + if s.GetLineSegment() != tc.line { + t.Errorf("error, expected %v got %v", tc.line, s.GetLineSegment()) + } + } + testcases := []tcase{ + { + line: geom.Line{{1,2},{3,4}}, + }, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} From 1115e681ca8613d924a895ca3058890c71d4cf53 Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Tue, 29 May 2018 17:09:51 -0600 Subject: [PATCH 10/12] Incorporate review requests 1. Modify comments to start with function name 2. Note in comments that calling with a nil object will cause a panic. --- planar/triangulate/delaunay_test.go | 36 ++-- .../delaunaytriangulationbuilder.go | 47 +++-- .../incrementaldelaunaytriangulator.go | 10 +- planar/triangulate/quadedge/debug.go | 114 ++++++++++ .../quadedge/lastfoundquadedgelocator.go | 7 + planar/triangulate/quadedge/quadedge.go | 121 +++++++---- .../triangulate/quadedge/quadedgelocator.go | 6 +- .../quadedge/quadedgesubdivision.go | 195 +++++++++++------- .../quadedge/quadedgesubdivision_test.go | 2 + .../triangulate/quadedge/trianglepredicate.go | 27 ++- planar/triangulate/quadedge/vertex.go | 6 +- planar/triangulate/quadedge/vertex_test.go | 13 +- 12 files changed, 415 insertions(+), 169 deletions(-) create mode 100644 planar/triangulate/quadedge/debug.go diff --git a/planar/triangulate/delaunay_test.go b/planar/triangulate/delaunay_test.go index f6abe1d7..a96c8061 100644 --- a/planar/triangulate/delaunay_test.go +++ b/planar/triangulate/delaunay_test.go @@ -102,24 +102,24 @@ func TestDelaunayTriangulation(t *testing.T) { // MULTIPOLYGON expectedTris: "MULTIPOLYGON (((0 20,0 10,10 10,0 20)),((0 20,10 10,10 20,0 20)),((10 20,10 10,20 10,10 20)),((10 20,20 10,20 20,10 20)),((10 0,20 0,10 10,10 0)),((10 0,10 10,0 10,10 0)),((10 0,0 10,0 0,10 0)),((10 10,20 0,20 10,10 10)))", }, - { - inputWKT: "MULTIPOINT ((50 40),(140 70),(80 100),(130 140),(30 150),(70 180),(190 110),(120 20))", - inputWKB: "01040000000800000001010000000000000000004940000000000000444001010000000000000000806140000000000080514001010000000000000000005440000000000000594001010000000000000000406040000000000080614001010000000000000000003e400000000000c0624001010000000000000000805140000000000080664001010000000000000000c067400000000000805b4001010000000000000000005e400000000000003440", - expectedEdges: "MULTILINESTRING ((70 180,190 110),(30 150,70 180),(30 150,50 40),(50 40,120 20),(120 20,190 110),(120 20,140 70),(140 70,190 110),(130 140,140 70),(130 140,190 110),(70 180,130 140),(80 100,130 140),(70 180,80 100),(30 150,80 100),(50 40,80 100),(80 100,120 20),(80 100,140 70))", - expectedTris: "MULTIPOLYGON (((30 150,50 40,80 100,30 150)),((30 150,80 100,70 180,30 150)),((70 180,80 100,130 140,70 180)),((70 180,130 140,190 110,70 180)),((190 110,130 140,140 70,190 110)),((190 110,140 70,120 20,190 110)),((120 20,140 70,80 100,120 20)),((120 20,80 100,50 40,120 20)),((80 100,140 70,130 140,80 100)))", - }, - { - inputWKT: "POLYGON ((42 30, 41.96 29.61, 41.85 29.23, 41.66 28.89, 41.41 28.59, 41.11 28.34, 40.77 28.15, 40.39 28.04, 40 28, 39.61 28.04, 39.23 28.15, 38.89 28.34, 38.59 28.59, 38.34 28.89, 38.15 29.23, 38.04 29.61, 38 30, 38.04 30.39, 38.15 30.77, 38.34 31.11, 38.59 31.41, 38.89 31.66, 39.23 31.85, 39.61 31.96, 40 32, 40.39 31.96, 40.77 31.85, 41.11 31.66, 41.41 31.41, 41.66 31.11, 41.85 30.77, 41.96 30.39, 42 30))", - inputWKB: "0103000000010000002100000000000000000045400000000000003e407b14ae47e1fa44405c8fc2f5289c3d40cdccccccccec44407b14ae47e13a3d4014ae47e17ad44440a4703d0ad7e33c4014ae47e17ab44440d7a3703d0a973c40ae47e17a148e4440d7a3703d0a573c40c3f5285c8f6244406666666666263c4052b81e85eb3144400ad7a3703d0a3c4000000000000044400000000000003c40ae47e17a14ce43400ad7a3703d0a3c403d0ad7a3709d43406666666666263c4052b81e85eb714340d7a3703d0a573c40ec51b81e854b4340d7a3703d0a973c40ec51b81e852b4340a4703d0ad7e33c4033333333331343407b14ae47e13a3d4085eb51b81e0543405c8fc2f5289c3d4000000000000043400000000000003e4085eb51b81e054340a4703d0ad7633e40333333333313434085eb51b81ec53e40ec51b81e852b43405c8fc2f5281c3f40ec51b81e854b4340295c8fc2f5683f4052b81e85eb714340295c8fc2f5a83f403d0ad7a3709d43409a99999999d93f40ae47e17a14ce4340f6285c8fc2f53f400000000000004440000000000000404052b81e85eb314440f6285c8fc2f53f40c3f5285c8f6244409a99999999d93f40ae47e17a148e4440295c8fc2f5a83f4014ae47e17ab44440295c8fc2f5683f4014ae47e17ad444405c8fc2f5281c3f40cdccccccccec444085eb51b81ec53e407b14ae47e1fa4440a4703d0ad7633e4000000000000045400000000000003e40", - expectedEdges: "MULTILINESTRING ((41.66 31.11,41.85 30.77),(41.41 31.41,41.66 31.11),(41.11 31.66,41.41 31.41),(40.77 31.85,41.11 31.66),(40.39 31.96,40.77 31.85),(40 32,40.39 31.96),(39.61 31.96,40 32),(39.23 31.85,39.61 31.96),(38.89 31.66,39.23 31.85),(38.59 31.41,38.89 31.66),(38.34 31.11,38.59 31.41),(38.15 30.77,38.34 31.11),(38.04 30.39,38.15 30.77),(38 30,38.04 30.39),(38 30,38.04 29.61),(38.04 29.61,38.15 29.23),(38.15 29.23,38.34 28.89),(38.34 28.89,38.59 28.59),(38.59 28.59,38.89 28.34),(38.89 28.34,39.23 28.15),(39.23 28.15,39.61 28.04),(39.61 28.04,40 28),(40 28,40.39 28.04),(40.39 28.04,40.77 28.15),(40.77 28.15,41.11 28.34),(41.11 28.34,41.41 28.59),(41.41 28.59,41.66 28.89),(41.66 28.89,41.85 29.23),(41.85 29.23,41.96 29.61),(41.96 29.61,42 30),(41.96 30.39,42 30),(41.85 30.77,41.96 30.39),(41.66 31.11,41.96 30.39),(41.41 31.41,41.96 30.39),(41.41 28.59,41.96 30.39),(41.41 28.59,41.41 31.41),(38.59 28.59,41.41 28.59),(38.59 28.59,41.41 31.41),(38.59 28.59,38.59 31.41),(38.59 31.41,41.41 31.41),(38.59 31.41,39.61 31.96),(39.61 31.96,41.41 31.41),(39.61 31.96,40.39 31.96),(40.39 31.96,41.41 31.41),(40.39 31.96,41.11 31.66),(38.04 30.39,38.59 28.59),(38.04 30.39,38.59 31.41),(38.04 30.39,38.34 31.11),(38.04 29.61,38.59 28.59),(38.04 29.61,38.04 30.39),(39.61 28.04,41.41 28.59),(38.59 28.59,39.61 28.04),(38.89 28.34,39.61 28.04),(40.39 28.04,41.41 28.59),(39.61 28.04,40.39 28.04),(41.96 29.61,41.96 30.39),(41.41 28.59,41.96 29.61),(41.66 28.89,41.96 29.61),(40.39 28.04,41.11 28.34),(38.04 29.61,38.34 28.89),(38.89 31.66,39.61 31.96))", - expectedTris: "MULTIPOLYGON (((38.15 30.77,38.04 30.39,38.34 31.11,38.15 30.77)),((38.34 31.11,38.04 30.39,38.59 31.41,38.34 31.11)),((38.59 31.41,38.04 30.39,38.59 28.59,38.59 31.41)),((38.59 31.41,38.59 28.59,41.41 31.41,38.59 31.41)),((38.59 31.41,41.41 31.41,39.61 31.96,38.59 31.41)),((38.59 31.41,39.61 31.96,38.89 31.66,38.59 31.41)),((38.89 31.66,39.61 31.96,39.23 31.85,38.89 31.66)),((39.61 31.96,41.41 31.41,40.39 31.96,39.61 31.96)),((39.61 31.96,40.39 31.96,40 32,39.61 31.96)),((40.39 31.96,41.41 31.41,41.11 31.66,40.39 31.96)),((40.39 31.96,41.11 31.66,40.77 31.85,40.39 31.96)),((41.41 31.41,38.59 28.59,41.41 28.59,41.41 31.41)),((41.41 31.41,41.41 28.59,41.96 30.39,41.41 31.41)),((41.41 31.41,41.96 30.39,41.66 31.11,41.41 31.41)),((41.66 31.11,41.96 30.39,41.85 30.77,41.66 31.11)),((40 28,40.39 28.04,39.61 28.04,40 28)),((39.61 28.04,40.39 28.04,41.41 28.59,39.61 28.04)),((39.61 28.04,41.41 28.59,38.59 28.59,39.61 28.04)),((39.61 28.04,38.59 28.59,38.89 28.34,39.61 28.04)),((39.61 28.04,38.89 28.34,39.23 28.15,39.61 28.04)),((41.41 28.59,40.39 28.04,41.11 28.34,41.41 28.59)),((41.11 28.34,40.39 28.04,40.77 28.15,41.11 28.34)),((41.41 28.59,41.66 28.89,41.96 29.61,41.41 28.59)),((41.41 28.59,41.96 29.61,41.96 30.39,41.41 28.59)),((41.96 30.39,41.96 29.61,42 30,41.96 30.39)),((41.96 29.61,41.66 28.89,41.85 29.23,41.96 29.61)),((38.59 28.59,38.04 30.39,38.04 29.61,38.59 28.59)),((38.59 28.59,38.04 29.61,38.34 28.89,38.59 28.59)),((38.34 28.89,38.04 29.61,38.15 29.23,38.34 28.89)),((38.04 29.61,38.04 30.39,38 30,38.04 29.61)))", - }, - { - inputWKT: "POLYGON ((0 0, 0 200, 180 200, 180 0, 0 0), (20 180, 160 180, 160 20, 152.625 146.75, 20 180), (30 160, 150 30, 70 90, 30 160))", - inputWKB: "010300000003000000050000000000000000000000000000000000000000000000000000000000000000006940000000000080664000000000000069400000000000806640000000000000000000000000000000000000000000000000050000000000000000003440000000000080664000000000000064400000000000806640000000000000644000000000000034400000000000146340000000000058624000000000000034400000000000806640040000000000000000003e4000000000000064400000000000c062400000000000003e40000000000080514000000000008056400000000000003e400000000000006440", - expectedEdges: "MULTILINESTRING ((0 200,180 200),(0 0,0 200),(0 0,180 0),(180 0,180 200),(152.625 146.75,180 0),(152.625 146.75,180 200),(152.625 146.75,160 180),(160 180,180 200),(0 200,160 180),(20 180,160 180),(0 200,20 180),(20 180,30 160),(0 200,30 160),(0 0,30 160),(30 160,70 90),(0 0,70 90),(70 90,150 30),(0 0,150 30),(150 30,160 20),(0 0,160 20),(160 20,180 0),(152.625 146.75,160 20),(150 30,152.625 146.75),(70 90,152.625 146.75),(30 160,152.625 146.75),(30 160,160 180))", - expectedTris: "MULTIPOLYGON (((0 200,0 0,30 160,0 200)),((0 200,30 160,20 180,0 200)),((0 200,20 180,160 180,0 200)),((0 200,160 180,180 200,0 200)),((180 200,160 180,152.625 146.75,180 200)),((180 200,152.625 146.75,180 0,180 200)),((0 0,180 0,160 20,0 0)),((0 0,160 20,150 30,0 0)),((0 0,150 30,70 90,0 0)),((0 0,70 90,30 160,0 0)),((30 160,70 90,152.625 146.75,30 160)),((30 160,152.625 146.75,160 180,30 160)),((30 160,160 180,20 180,30 160)),((152.625 146.75,70 90,150 30,152.625 146.75)),((152.625 146.75,150 30,160 20,152.625 146.75)),((152.625 146.75,160 20,180 0,152.625 146.75)))", - }, + { + inputWKT: "MULTIPOINT ((50 40),(140 70),(80 100),(130 140),(30 150),(70 180),(190 110),(120 20))", + inputWKB: "01040000000800000001010000000000000000004940000000000000444001010000000000000000806140000000000080514001010000000000000000005440000000000000594001010000000000000000406040000000000080614001010000000000000000003e400000000000c0624001010000000000000000805140000000000080664001010000000000000000c067400000000000805b4001010000000000000000005e400000000000003440", + expectedEdges: "MULTILINESTRING ((70 180,190 110),(30 150,70 180),(30 150,50 40),(50 40,120 20),(120 20,190 110),(120 20,140 70),(140 70,190 110),(130 140,140 70),(130 140,190 110),(70 180,130 140),(80 100,130 140),(70 180,80 100),(30 150,80 100),(50 40,80 100),(80 100,120 20),(80 100,140 70))", + expectedTris: "MULTIPOLYGON (((30 150,50 40,80 100,30 150)),((30 150,80 100,70 180,30 150)),((70 180,80 100,130 140,70 180)),((70 180,130 140,190 110,70 180)),((190 110,130 140,140 70,190 110)),((190 110,140 70,120 20,190 110)),((120 20,140 70,80 100,120 20)),((120 20,80 100,50 40,120 20)),((80 100,140 70,130 140,80 100)))", + }, + { + inputWKT: "POLYGON ((42 30, 41.96 29.61, 41.85 29.23, 41.66 28.89, 41.41 28.59, 41.11 28.34, 40.77 28.15, 40.39 28.04, 40 28, 39.61 28.04, 39.23 28.15, 38.89 28.34, 38.59 28.59, 38.34 28.89, 38.15 29.23, 38.04 29.61, 38 30, 38.04 30.39, 38.15 30.77, 38.34 31.11, 38.59 31.41, 38.89 31.66, 39.23 31.85, 39.61 31.96, 40 32, 40.39 31.96, 40.77 31.85, 41.11 31.66, 41.41 31.41, 41.66 31.11, 41.85 30.77, 41.96 30.39, 42 30))", + inputWKB: "0103000000010000002100000000000000000045400000000000003e407b14ae47e1fa44405c8fc2f5289c3d40cdccccccccec44407b14ae47e13a3d4014ae47e17ad44440a4703d0ad7e33c4014ae47e17ab44440d7a3703d0a973c40ae47e17a148e4440d7a3703d0a573c40c3f5285c8f6244406666666666263c4052b81e85eb3144400ad7a3703d0a3c4000000000000044400000000000003c40ae47e17a14ce43400ad7a3703d0a3c403d0ad7a3709d43406666666666263c4052b81e85eb714340d7a3703d0a573c40ec51b81e854b4340d7a3703d0a973c40ec51b81e852b4340a4703d0ad7e33c4033333333331343407b14ae47e13a3d4085eb51b81e0543405c8fc2f5289c3d4000000000000043400000000000003e4085eb51b81e054340a4703d0ad7633e40333333333313434085eb51b81ec53e40ec51b81e852b43405c8fc2f5281c3f40ec51b81e854b4340295c8fc2f5683f4052b81e85eb714340295c8fc2f5a83f403d0ad7a3709d43409a99999999d93f40ae47e17a14ce4340f6285c8fc2f53f400000000000004440000000000000404052b81e85eb314440f6285c8fc2f53f40c3f5285c8f6244409a99999999d93f40ae47e17a148e4440295c8fc2f5a83f4014ae47e17ab44440295c8fc2f5683f4014ae47e17ad444405c8fc2f5281c3f40cdccccccccec444085eb51b81ec53e407b14ae47e1fa4440a4703d0ad7633e4000000000000045400000000000003e40", + expectedEdges: "MULTILINESTRING ((41.66 31.11,41.85 30.77),(41.41 31.41,41.66 31.11),(41.11 31.66,41.41 31.41),(40.77 31.85,41.11 31.66),(40.39 31.96,40.77 31.85),(40 32,40.39 31.96),(39.61 31.96,40 32),(39.23 31.85,39.61 31.96),(38.89 31.66,39.23 31.85),(38.59 31.41,38.89 31.66),(38.34 31.11,38.59 31.41),(38.15 30.77,38.34 31.11),(38.04 30.39,38.15 30.77),(38 30,38.04 30.39),(38 30,38.04 29.61),(38.04 29.61,38.15 29.23),(38.15 29.23,38.34 28.89),(38.34 28.89,38.59 28.59),(38.59 28.59,38.89 28.34),(38.89 28.34,39.23 28.15),(39.23 28.15,39.61 28.04),(39.61 28.04,40 28),(40 28,40.39 28.04),(40.39 28.04,40.77 28.15),(40.77 28.15,41.11 28.34),(41.11 28.34,41.41 28.59),(41.41 28.59,41.66 28.89),(41.66 28.89,41.85 29.23),(41.85 29.23,41.96 29.61),(41.96 29.61,42 30),(41.96 30.39,42 30),(41.85 30.77,41.96 30.39),(41.66 31.11,41.96 30.39),(41.41 31.41,41.96 30.39),(41.41 28.59,41.96 30.39),(41.41 28.59,41.41 31.41),(38.59 28.59,41.41 28.59),(38.59 28.59,41.41 31.41),(38.59 28.59,38.59 31.41),(38.59 31.41,41.41 31.41),(38.59 31.41,39.61 31.96),(39.61 31.96,41.41 31.41),(39.61 31.96,40.39 31.96),(40.39 31.96,41.41 31.41),(40.39 31.96,41.11 31.66),(38.04 30.39,38.59 28.59),(38.04 30.39,38.59 31.41),(38.04 30.39,38.34 31.11),(38.04 29.61,38.59 28.59),(38.04 29.61,38.04 30.39),(39.61 28.04,41.41 28.59),(38.59 28.59,39.61 28.04),(38.89 28.34,39.61 28.04),(40.39 28.04,41.41 28.59),(39.61 28.04,40.39 28.04),(41.96 29.61,41.96 30.39),(41.41 28.59,41.96 29.61),(41.66 28.89,41.96 29.61),(40.39 28.04,41.11 28.34),(38.04 29.61,38.34 28.89),(38.89 31.66,39.61 31.96))", + expectedTris: "MULTIPOLYGON (((38.15 30.77,38.04 30.39,38.34 31.11,38.15 30.77)),((38.34 31.11,38.04 30.39,38.59 31.41,38.34 31.11)),((38.59 31.41,38.04 30.39,38.59 28.59,38.59 31.41)),((38.59 31.41,38.59 28.59,41.41 31.41,38.59 31.41)),((38.59 31.41,41.41 31.41,39.61 31.96,38.59 31.41)),((38.59 31.41,39.61 31.96,38.89 31.66,38.59 31.41)),((38.89 31.66,39.61 31.96,39.23 31.85,38.89 31.66)),((39.61 31.96,41.41 31.41,40.39 31.96,39.61 31.96)),((39.61 31.96,40.39 31.96,40 32,39.61 31.96)),((40.39 31.96,41.41 31.41,41.11 31.66,40.39 31.96)),((40.39 31.96,41.11 31.66,40.77 31.85,40.39 31.96)),((41.41 31.41,38.59 28.59,41.41 28.59,41.41 31.41)),((41.41 31.41,41.41 28.59,41.96 30.39,41.41 31.41)),((41.41 31.41,41.96 30.39,41.66 31.11,41.41 31.41)),((41.66 31.11,41.96 30.39,41.85 30.77,41.66 31.11)),((40 28,40.39 28.04,39.61 28.04,40 28)),((39.61 28.04,40.39 28.04,41.41 28.59,39.61 28.04)),((39.61 28.04,41.41 28.59,38.59 28.59,39.61 28.04)),((39.61 28.04,38.59 28.59,38.89 28.34,39.61 28.04)),((39.61 28.04,38.89 28.34,39.23 28.15,39.61 28.04)),((41.41 28.59,40.39 28.04,41.11 28.34,41.41 28.59)),((41.11 28.34,40.39 28.04,40.77 28.15,41.11 28.34)),((41.41 28.59,41.66 28.89,41.96 29.61,41.41 28.59)),((41.41 28.59,41.96 29.61,41.96 30.39,41.41 28.59)),((41.96 30.39,41.96 29.61,42 30,41.96 30.39)),((41.96 29.61,41.66 28.89,41.85 29.23,41.96 29.61)),((38.59 28.59,38.04 30.39,38.04 29.61,38.59 28.59)),((38.59 28.59,38.04 29.61,38.34 28.89,38.59 28.59)),((38.34 28.89,38.04 29.61,38.15 29.23,38.34 28.89)),((38.04 29.61,38.04 30.39,38 30,38.04 29.61)))", + }, + { + inputWKT: "POLYGON ((0 0, 0 200, 180 200, 180 0, 0 0), (20 180, 160 180, 160 20, 152.625 146.75, 20 180), (30 160, 150 30, 70 90, 30 160))", + inputWKB: "010300000003000000050000000000000000000000000000000000000000000000000000000000000000006940000000000080664000000000000069400000000000806640000000000000000000000000000000000000000000000000050000000000000000003440000000000080664000000000000064400000000000806640000000000000644000000000000034400000000000146340000000000058624000000000000034400000000000806640040000000000000000003e4000000000000064400000000000c062400000000000003e40000000000080514000000000008056400000000000003e400000000000006440", + expectedEdges: "MULTILINESTRING ((0 200,180 200),(0 0,0 200),(0 0,180 0),(180 0,180 200),(152.625 146.75,180 0),(152.625 146.75,180 200),(152.625 146.75,160 180),(160 180,180 200),(0 200,160 180),(20 180,160 180),(0 200,20 180),(20 180,30 160),(0 200,30 160),(0 0,30 160),(30 160,70 90),(0 0,70 90),(70 90,150 30),(0 0,150 30),(150 30,160 20),(0 0,160 20),(160 20,180 0),(152.625 146.75,160 20),(150 30,152.625 146.75),(70 90,152.625 146.75),(30 160,152.625 146.75),(30 160,160 180))", + expectedTris: "MULTIPOLYGON (((0 200,0 0,30 160,0 200)),((0 200,30 160,20 180,0 200)),((0 200,20 180,160 180,0 200)),((0 200,160 180,180 200,0 200)),((180 200,160 180,152.625 146.75,180 200)),((180 200,152.625 146.75,180 0,180 200)),((0 0,180 0,160 20,0 0)),((0 0,160 20,150 30,0 0)),((0 0,150 30,70 90,0 0)),((0 0,70 90,30 160,0 0)),((30 160,70 90,152.625 146.75,30 160)),((30 160,152.625 146.75,160 180,30 160)),((30 160,160 180,20 180,30 160)),((152.625 146.75,70 90,150 30,152.625 146.75)),((152.625 146.75,150 30,160 20,152.625 146.75)),((152.625 146.75,160 20,180 0,152.625 146.75)))", + }, } for i, tc := range testcases { diff --git a/planar/triangulate/delaunaytriangulationbuilder.go b/planar/triangulate/delaunaytriangulationbuilder.go index 01b3e6f4..441e9dc2 100644 --- a/planar/triangulate/delaunaytriangulationbuilder.go +++ b/planar/triangulate/delaunaytriangulationbuilder.go @@ -21,8 +21,8 @@ import ( ) /* -A utility class which creates Delaunay Triangulations -from collections of points and extract the resulting +DelaunayTriangulationBuilder is a utility class which creates Delaunay +Triangulations from collections of points and extract the resulting triangulation edges or triangles as geometries. Author Martin Davis @@ -45,6 +45,8 @@ extractUniqueCoordinates extracts the unique points from the given Geometry. geom - the geometry to extract from Returns a List of the unique Coordinates + +If dtb is nil a panic will occur. */ func (dtb *DelaunayTriangulationBuilder) extractUniqueCoordinates(g geom.Geometry) ([]quadedge.Vertex, error) { if g == nil { @@ -64,6 +66,11 @@ func (dtb *DelaunayTriangulationBuilder) extractUniqueCoordinates(g geom.Geometr return dtb.unique(vertices), nil } +/* +unique returns a list of unique vertices. + +If dtb is nil a panic will occur. +*/ func (dtb *DelaunayTriangulationBuilder) unique(points []quadedge.Vertex) []quadedge.Vertex { sort.Sort(PointByXY(points)) @@ -111,10 +118,12 @@ public static Envelope envelope(Collection coords) */ /* -Sets the sites (vertices) which will be triangulated. -All vertices of the given geometry will be used as sites. +SetSites sets the vertices which will be triangulated. All vertices of the +given geometry will be used as sites. geom - the geometry from which the sites will be extracted. + +If dtb is nil a panic will occur. */ func (dtb *DelaunayTriangulationBuilder) SetSites(g geom.Geometry) error { // remove any duplicate points (they will cause the triangulation to fail) @@ -151,6 +160,8 @@ public void setTolerance(double tolerance) create will create the triangulation. return true on success, false on failure. + +If dtb is nil a panic will occur. */ func (dtb *DelaunayTriangulationBuilder) create() bool { if dtb.subdiv != nil { @@ -176,21 +187,26 @@ func (dtb *DelaunayTriangulationBuilder) create() bool { return true } -/** - * Gets the {@link QuadEdgeSubdivision} which models the computed triangulation. - * - * @return the subdivision containing the triangulation -public QuadEdgeSubdivision getSubdivision() -{ - create(); - return subdiv; -} +/* +GetSubdivision gets the QuadEdgeSubdivision which models the computed +triangulation. + +Returns the subdivision containing the triangulation or nil if it has +not been created. + +If dtb is nil a panic will occur. */ +func (dtb *DelaunayTriangulationBuilder) GetSubdivision() *quadedge.QuadEdgeSubdivision { + dtb.create() + return dtb.subdiv +} /* GetEdges gets the edges of the computed triangulation as a MultiLineString. returns the edges of the triangulation + +If dtb is nil a panic will occur. */ func (dtb *DelaunayTriangulationBuilder) getEdges() geom.MultiLineString { if !dtb.create() { @@ -200,11 +216,12 @@ func (dtb *DelaunayTriangulationBuilder) getEdges() geom.MultiLineString { } /* -GetTriangles Gets the faces of the computed triangulation as a -GeometryCollection Polygons. +GetTriangles Gets the faces of the computed triangulation as a MultiPolygon. Unlike JTS, this method returns a MultiPolygon. I found not all viewers like displaying collections. -JRS + +If dtb is nil a panic will occur. */ func (dtb *DelaunayTriangulationBuilder) GetTriangles() (geom.MultiPolygon, error) { if !dtb.create() { diff --git a/planar/triangulate/incrementaldelaunaytriangulator.go b/planar/triangulate/incrementaldelaunaytriangulator.go index 0ed54e79..f4441817 100644 --- a/planar/triangulate/incrementaldelaunaytriangulator.go +++ b/planar/triangulate/incrementaldelaunaytriangulator.go @@ -17,8 +17,8 @@ import ( ) /* -Computes a Delaunay Triangulation of a set of {@link Vertex}es, using an -incremental insertion algorithm. +IncrementalDelaunayTriangulator computes a Delaunay Triangulation of a set of +{@link Vertex}es, using an incremental insertion algorithm. Author Martin Davis Ported to Go by Jason R. Surratt @@ -37,6 +37,8 @@ vertices - a Collection of Vertex Returns ErrLocateFailure if the location algorithm fails to converge in a reasonable number of iterations. If this occurs the triangulator is left in an unknown state with 0 or more of the vertices inserted. + +If idt is nil a panic will occur. */ func (idt *IncrementalDelaunayTriangulator) InsertSites(vertices []quadedge.Vertex) error { for _, v := range vertices { @@ -50,13 +52,15 @@ func (idt *IncrementalDelaunayTriangulator) InsertSites(vertices []quadedge.Vert } /* -Inserts a new point into a subdivision representing a Delaunay +InsertSite inserts a new point into a subdivision representing a Delaunay triangulation, and fixes the affected edges so that the result is still a Delaunay triangulation. Returns a tuple with a quadedge containing the inserted vertex and an error code. If there is an error then the vertex will not be inserted and the triangulator will still be in a consistent state. + +If idt is nil a panic will occur. */ func (idt *IncrementalDelaunayTriangulator) InsertSite(v quadedge.Vertex) (*quadedge.QuadEdge, error) { diff --git a/planar/triangulate/quadedge/debug.go b/planar/triangulate/quadedge/debug.go new file mode 100644 index 00000000..5f912715 --- /dev/null +++ b/planar/triangulate/quadedge/debug.go @@ -0,0 +1,114 @@ +package quadedge + +import ( + "fmt" + + "github.com/go-spatial/geom/encoding/wkt" +) + +/* +DebugDumpEdges returns a string with the WKT representation of the +edges. On error, an error string is returned. + +This is intended for debug purposes only. + +If qes is nil a panic will occur. +*/ +func (qes *QuadEdgeSubdivision) DebugDumpEdges() string { + edges := qes.GetEdgesAsMultiLineString() + edgesWKT, err := wkt.Encode(edges) + if err != nil { + return fmt.Sprintf("error formatting as WKT: %v", err) + } + return edgesWKT +} + +/* +hasCCWNeighbor returns true if n has at least one neighbor that is < 180deg +angle and is counter clockwise. + +If qes is nil a panic will occur. +*/ +func (qes *QuadEdgeSubdivision) hasCCWNeighbor(e *QuadEdge) bool { + n := e + // if we haven't checked this edge already + for true { + ccw := n.ONext() + // this will only work if the angles between edges are < 180deg + // if both edges are frame edges then the CCW rule may not + // be easily detectable. (think angles > 180deg) + if n.Orig().IsCCW(n.Dest(), ccw.Dest()) == true { + return true + } + n = ccw + if n == e { + return false + } + } + return false +} + +/* +Validate runs a self consistency checks and reports the first error. + +This is not part of the original JTS code. + +If qes is nil a panic will occur. +*/ +func (qes *QuadEdgeSubdivision) Validate() error { + // collect a set of all edges + edgeSet := make(map[*QuadEdge]bool) + edges := qes.GetEdges() + for i := range edges { + if _, ok := edgeSet[edges[i]]; ok == true { + return fmt.Errorf("edge reported multiple times in subdiv: %v", edges[i]) + } + if edges[i].IsLive() == false { + return fmt.Errorf("a deleted edge is still in subdiv: %v", edges[i]) + } + if edges[i].Sym().IsLive() == false { + return fmt.Errorf("a deleted edge is still in subdiv: %v", edges[i].Sym()) + } + edgeSet[edges[i]] = true + } + + return qes.validateONext() +} + +/* +validateONext validates that each QuadEdge's ONext() goes to the next edge that +shares an origin point in CCW order. + +This is not part of the original JTS code. + +If qes is nil a panic will occur. +*/ +func (qes *QuadEdgeSubdivision) validateONext() error { + + edgeSet := make(map[*QuadEdge]bool) + edges := qes.GetEdges() + for _, e := range edges { + if _, ok := edgeSet[e]; ok == false { + // if we haven't checked this edge already + n := e + for true { + ccw := n.ONext() + if n.Orig().Equals(e.Orig()) == false { + return fmt.Errorf("edge in ONext() doesn't share an origin: between %v and %v", e, n) + } + // this isn't a perfect check for CCW, but it should work well + // enough in most cases and shouldn't produce false positives. + if (qes.isFrameEdge(n) == false || qes.isFrameEdge(ccw) == false) && n.Orig().IsCCW(n.Dest(), ccw.Dest()) == false && qes.hasCCWNeighbor(n) == true { + return fmt.Errorf("edges are not CCW, expected %v to be CCW of %v", ccw, n) + } + edgeSet[n] = true + n = ccw + if n == e { + break + } + } + } + } + + return nil +} diff --git a/planar/triangulate/quadedge/lastfoundquadedgelocator.go b/planar/triangulate/quadedge/lastfoundquadedgelocator.go index 76327cdc..463fb3a6 100644 --- a/planar/triangulate/quadedge/lastfoundquadedgelocator.go +++ b/planar/triangulate/quadedge/lastfoundquadedgelocator.go @@ -29,11 +29,18 @@ type LastFoundQuadEdgeLocator struct { func NewLastFoundQuadEdgeLocator(subdiv *QuadEdgeSubdivision) *LastFoundQuadEdgeLocator { var lf LastFoundQuadEdgeLocator + if subdiv == nil { + return nil + } + lf.subdiv = subdiv lf.init() return &lf } +/* +If lf is nil a panic will occur. +*/ func (lf *LastFoundQuadEdgeLocator) init() { lf.lastEdge = lf.findEdge() } diff --git a/planar/triangulate/quadedge/quadedge.go b/planar/triangulate/quadedge/quadedge.go index cdaf103c..8a90b6ea 100644 --- a/planar/triangulate/quadedge/quadedge.go +++ b/planar/triangulate/quadedge/quadedge.go @@ -13,6 +13,8 @@ http://www.eclipse.org/org/documents/edl-v10.php. package quadedge import ( + "fmt" + "github.com/go-spatial/geom/cmp" ) @@ -145,6 +147,8 @@ edge is the one for which the origin and destination coordinates are ordered according to the standard Point ordering. Returns the primary quadedge + +If qe is nil a panic will occur. */ func (qe *QuadEdge) GetPrimary() *QuadEdge { v1 := qe.Orig() @@ -174,11 +178,11 @@ public Object getData() { */ /* -Marks this quadedge as being deleted. -This does not free the memory used by -this quadedge quartet, but indicates -that this edge no longer participates -in a subdivision. +Delete marks this quadedge as being deleted. This does not free the memory +used by this quadedge quartet, but indicates that this edge no longer +participates in a subdivision. + +If qe is nil a panic will occur. */ func (qe *QuadEdge) Delete() { qe.rot = nil @@ -188,12 +192,18 @@ func (qe *QuadEdge) Delete() { IsLive tests whether this edge has been deleted. Returns true if this edge has not been deleted. + +If qe is nil a panic will occur. */ func (qe *QuadEdge) IsLive() bool { return qe.rot != nil } -// SetNext sets the connected edge +/* +SetNext sets the connected edge + +If qe is nil a panic will occur. +*/ func (qe *QuadEdge) SetNext(next *QuadEdge) { qe.next = next } @@ -204,99 +214,121 @@ QuadEdge Algebra */ /* -Gets the dual of this edge, directed from its right to its left. +Rot gets the dual of this edge, directed from its right to its left. -@return the rotated edge +Return the rotated edge + +If qe is nil a panic will occur. */ func (qe *QuadEdge) Rot() *QuadEdge { return qe.rot } /* -Gets the dual of this edge, directed from its left to its right. +InvRot gets the dual of this edge, directed from its left to its right. + +Return the inverse rotated edge. -@return the inverse rotated edge. +If qe is nil a panic will occur. */ func (qe *QuadEdge) InvRot() *QuadEdge { return qe.rot.Sym() } /* -Gets the edge from the destination to the origin of this edge. +Sym gets the edge from the destination to the origin of this edge. -@return the sym of the edge +Return the sym of the edge + +If qe is nil a panic will occur. */ func (qe *QuadEdge) Sym() *QuadEdge { return qe.rot.rot } /* -Gets the next CCW edge around the origin of this edge. +ONext gets the next CCW edge around the origin of this edge. + +Return the next linked edge. -@return the next linked edge. +If qe is nil a panic will occur. */ func (qe *QuadEdge) ONext() *QuadEdge { return qe.next } /* -Gets the next CW edge around (from) the origin of this edge. +OPrev gets the next CW edge around (from) the origin of this edge. -@return the previous edge. +Return the previous edge. + +If qe is nil a panic will occur. */ func (qe *QuadEdge) OPrev() *QuadEdge { return qe.rot.next.rot } /* -Gets the next CCW edge around (into) the destination of this edge. +DNext gets the next CCW edge around (into) the destination of this edge. + +Return the next destination edge. -@return the next destination edge. +If qe is nil a panic will occur. */ func (qe *QuadEdge) DNext() *QuadEdge { return qe.Sym().ONext().Sym() } /* -Gets the next CW edge around (into) the destination of this edge. +DPrev gets the next CW edge around (into) the destination of this edge. + +Return the previous destination edge. -@return the previous destination edge. +If qe is nil a panic will occur. */ func (qe *QuadEdge) DPrev() *QuadEdge { return qe.InvRot().ONext().InvRot() } /* -Gets the CCW edge around the left face following this edge. +LNext gets the CCW edge around the left face following this edge. -@return the next left face edge. +Return the next left face edge. + +If qe is nil a panic will occur. */ func (qe *QuadEdge) LNext() *QuadEdge { return qe.InvRot().ONext().Rot() } /* -Gets the CCW edge around the left face before this edge. +LPrev gets the CCW edge around the left face before this edge. + +Return the previous left face edge. -@return the previous left face edge. +If qe is nil a panic will occur. */ func (qe *QuadEdge) LPrev() *QuadEdge { return qe.next.Sym() } /* -Gets the edge around the right face ccw following this edge. +RNext gets the edge around the right face ccw following this edge. -@return the next right face edge. +Return the next right face edge. + +If qe is nil a panic will occur. */ func (qe *QuadEdge) RNext() *QuadEdge { return qe.rot.next.InvRot() } /* -Gets the edge around the right face ccw before this edge. +RPrev gets the edge around the right face ccw before this edge. + +Return the previous right face edge. -@return the previous right face edge. +If qe is nil a panic will occur. */ func (qe *QuadEdge) RPrev() *QuadEdge { return qe.Sym().ONext() @@ -310,6 +342,8 @@ Data Access SetOrig sets the vertex for this edge's origin o - the origin vertex + +If qe is nil a panic will occur. */ func (qe *QuadEdge) setOrig(o Vertex) { qe.vertex = o @@ -319,24 +353,30 @@ func (qe *QuadEdge) setOrig(o Vertex) { SetDest sets the vertex for this edge's destination d - the destination vertex + +If qe is nil a panic will occur. */ func (qe *QuadEdge) setDest(d Vertex) { qe.Sym().setOrig(d) } /* -Gets the vertex for the edge's origin +Orig gets the vertex for the edge's origin + +Returns the origin vertex -returns the origin vertex +If qe is nil a panic will occur. */ func (qe *QuadEdge) Orig() Vertex { return qe.vertex } /* -dest Gets the vertex for the edge's destination +Dest gets the vertex for the edge's destination -returns the destination vertex +Returns the destination vertex + +If qe is nil a panic will occur. */ func (qe *QuadEdge) Dest() Vertex { return qe.Sym().Orig() @@ -395,10 +435,15 @@ public LineSegment toLineSegment() Converts this edge to a WKT two-point LINESTRING indicating the geometry of this edge. -@return a String representing this edge's geometry -public String toString() { - Coordinate p0 = vertex.getCoordinate(); - Coordinate p1 = dest().getCoordinate(); - return WKTWriter.toLineString(p0, p1); -} +Unlike JTS, if IsLive() is false, a deleted string is returned. + +return a String representing this edge's geometry + +If qe is nil a panic will occur. */ +func (qe *QuadEdge) String() string { + if qe.IsLive() == false { + return fmt.Sprintf("", qe.Orig()) + } + return fmt.Sprintf("LINESTRING (%v %v, %v %v)", qe.Orig().X(), qe.Orig().Y(), qe.Dest().X(), qe.Dest().Y()) +} diff --git a/planar/triangulate/quadedge/quadedgelocator.go b/planar/triangulate/quadedge/quadedgelocator.go index 48b2b332..0346d2a5 100644 --- a/planar/triangulate/quadedge/quadedgelocator.go +++ b/planar/triangulate/quadedge/quadedgelocator.go @@ -13,9 +13,9 @@ http://www.eclipse.org/org/documents/edl-v10.php. package quadedge /* -An interface for classes which locate an edge in a {@link QuadEdgeSubdivision} -which either contains a given {@link Vertex} V or is an edge of a triangle -which contains V. Implementors may utilized different strategies for +QuadEdgeLocator is an interface for classes which locate an edge in a +QuadEdgeSubdivision which either contains a given Vertex V or is an edge of a +triangle which contains V. Implementors may utilized different strategies for optimizing locating containing edges/triangles. Author Martin Davis diff --git a/planar/triangulate/quadedge/quadedgesubdivision.go b/planar/triangulate/quadedge/quadedgesubdivision.go index 30782f0c..c2c43e5d 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision.go +++ b/planar/triangulate/quadedge/quadedgesubdivision.go @@ -23,12 +23,13 @@ import ( var ErrLocateFailure = errors.New("failure locating edge") /* -A class that contains the QuadEdges representing a planar subdivision that -models a triangulation. The subdivision is constructed using the quadedge -algebra defined in the class QuadEdge. All metric calculations are done in the -Vertex class. In addition to a triangulation, subdivisions support extraction -of Voronoi diagrams. This is easily accomplished, since the Voronoi diagram is -the dual of the Delaunay triangulation. +QuadEdgeSubdivision is a class that contains the QuadEdges representing a +planar subdivision that models a triangulation. The subdivision is constructed +using the quadedge algebra defined in the class QuadEdge. All metric +calculations are done in the Vertex class. In addition to a triangulation, +subdivisions support extraction of Voronoi diagrams. This is easily +accomplished, since the Voronoi diagram is the dual of the Delaunay +triangulation. Subdivisions can be provided with a tolerance value. Inserted vertices which are closer than this value to vertices already in the subdivision will be @@ -77,9 +78,9 @@ var EDGE_COINCIDENCE_TOL_FACTOR float64 = 1000 // } /* -Creates a new instance of a quad-edge subdivision based on a frame triangle -that encloses a supplied bounding box. A new super-bounding box that -contains the triangle is computed and stored. +NewQuadEdgeSubdivision creates a new instance of a quad-edge subdivision based +on a frame triangle that encloses a supplied bounding box. A new +super-bounding box that contains the triangle is computed and stored. env - the bounding box to surround tolerance - the tolerance value for determining if two sites are equal @@ -95,6 +96,11 @@ func NewQuadEdgeSubdivision(env geom.Extent, tolerance float64) *QuadEdgeSubdivi return &qes } +/* +createFrame creates the frame of a triangulation around the given extent. + +If qes is nil a panic will occur. +*/ func (qes *QuadEdgeSubdivision) createFrame(env geom.Extent) { deltaX := env.XSpan() deltaY := env.YSpan() @@ -112,6 +118,11 @@ func (qes *QuadEdgeSubdivision) createFrame(env geom.Extent) { qes.frameEnv = *geom.NewExtent(qes.frameVertex[0], qes.frameVertex[1], qes.frameVertex[2]) } +/* +initSubdiv initializes a subdivision from the frame. + +If qes is nil a panic will occur. +*/ func (qes *QuadEdgeSubdivision) initSubdiv() *QuadEdge { // build initial subdivision from frame ea := qes.MakeEdge(qes.frameVertex[0], qes.frameVertex[1]) @@ -124,19 +135,22 @@ func (qes *QuadEdgeSubdivision) initSubdiv() *QuadEdge { } /* -Gets the vertex-equality tolerance value -used in this subdivision +GetTolerance gets the vertex-equality tolerance value used in this subdivision return the tolerance value + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) GetTolerance() float64 { return qes.tolerance } /* -Gets the envelope of the Subdivision (including the frame). +GetEnvelope gets the envelope of the Subdivision (including the frame). -@return the envelope +Return the envelope + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) GetEnvelope() geom.Extent { // returns a deep copy to avoid modification by caller @@ -148,6 +162,8 @@ GetEdges gets the collection of base {@link QuadEdge}s (one for every pair of vertices which is connected). return a collection of QuadEdges + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) GetEdges() []*QuadEdge { return qes.quadEdges @@ -168,6 +184,8 @@ func (qes *QuadEdgeSubdivision) GetEdges() []*QuadEdge { MakeEdge creates a new quadedge, recording it in the edges list. return a new quadedge + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) MakeEdge(o Vertex, d Vertex) *QuadEdge { q := MakeEdge(o, d) @@ -180,9 +198,9 @@ Connect creates a new QuadEdge connecting the destination of a to the origin of b, in such a way that all three have the same left face after the connection is complete. The quadedge is recorded in the edges list. -@param a -@param b -@return a quadedge +Return a quadedge + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) Connect(a *QuadEdge, b *QuadEdge) *QuadEdge { q := Connect(a, b) @@ -191,10 +209,12 @@ func (qes *QuadEdgeSubdivision) Connect(a *QuadEdge, b *QuadEdge) *QuadEdge { } /* -Deletes a quadedge from the subdivision. Linked quadedges are updated to +Delete a quadedge from the subdivision. Linked quadedges are updated to reflect the deletion. e - the quadedge to delete + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) Delete(e *QuadEdge) { Splice(e, e.OPrev()) @@ -220,10 +240,10 @@ func (qes *QuadEdgeSubdivision) Delete(e *QuadEdge) { } /* -Locates an edge of a triangle which contains a location specified by a Vertex -v. The edge returned has the property that either v is on e, or e is an edge -of a triangle containing v. The search starts from startEdge amd proceeds on -the general direction of v. +LocateFromEdge locates an edge of a triangle which contains a location +specified by a Vertex v. The edge returned has the property that either v is +on e, or e is an edge of a triangle containing v. The search starts from +startEdge amd proceeds on the general direction of v. This locate algorithm relies on the subdivision being Delaunay. For non-Delaunay subdivisions, this may loop for ever. @@ -235,6 +255,8 @@ v If the location algorithm fails to converge in a reasonable number of iterations a ErrLocateFailure will be returned. + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) LocateFromEdge(v Vertex, startEdge *QuadEdge) (*QuadEdge, error) { iter := 0 @@ -281,45 +303,56 @@ func (qes *QuadEdgeSubdivision) LocateFromEdge(v Vertex, startEdge *QuadEdge) (* } /* -Finds a quadedge of a triangle containing a location -specified by a {@link Vertex}, if one exists. +Locate Finds a quadedge of a triangle containing a location specified by a Vertex, if one exists. v - the vertex to locate Return a quadedge on the edge of a triangle which touches or contains the location or nil if no such triangle exists + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) Locate(v Vertex) (*QuadEdge, error) { return qes.locator.Locate(v) } -// /** -// * Locates the edge between the given vertices, if it exists in the -// * subdivision. -// * -// * @param p0 a coordinate -// * @param p1 another coordinate -// * @return the edge joining the coordinates, if present -// * or null if no such edge exists -// */ -// public QuadEdge locate(Coordinate p0, Coordinate p1) { -// // find an edge containing one of the points -// QuadEdge e = locator.locate(new Vertex(p0)); -// if (e == null) -// return null; - -// // normalize so that p0 is origin of base edge -// QuadEdge base = e; -// if (e.dest().getCoordinate().equals2D(p0)) -// base = e.sym(); -// // check all edges around origin of base edge -// QuadEdge locEdge = base; -// do { -// if (locEdge.dest().getCoordinate().equals2D(p1)) -// return locEdge; -// locEdge = locEdge.oNext(); -// } while (locEdge != base); -// return null; -// } +/* +LocateSegment locates the edge between the given vertices, if it exists in the +subdivision. + +p0 a coordinate +p1 another coordinate +Return the edge joining the coordinates, if present or null if no such edge +exists + +If qes is nil a panic will occur. +*/ +func (qes *QuadEdgeSubdivision) LocateSegment(p0 Vertex, p1 Vertex) (*QuadEdge, error) { + // find an edge containing one of the points + e, err := qes.locator.Locate(p0) + if err != nil || e == nil { + return nil, err + } + + // normalize so that p0 is origin of base edge + base := e + if e.Dest().EqualsTolerance(p0, qes.tolerance) { + base = e.Sym() + } + // check all edges around origin of base edge + locEdge := base + done := false + for !done { + if locEdge.Dest().EqualsTolerance(p1, qes.tolerance) { + return locEdge, nil + } + locEdge = locEdge.ONext() + + if locEdge == base { + done = true + } + } + return nil, nil +} /** * Inserts a new site into the Subdivision, connecting it to the vertices of @@ -364,6 +397,8 @@ vertex. e - the edge to test return true if the edge is connected to the frame triangle + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) isFrameEdge(e *QuadEdge) bool { if qes.isFrameVertex(e.Orig()) || qes.isFrameVertex(e.Dest()) { @@ -407,6 +442,8 @@ isFrameVertex tests whether a vertex is a vertex of the outer triangle. v - the vertex to test returns true if the vertex is an outer triangle vertex + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) isFrameVertex(v Vertex) bool { if v.Equals(qes.frameVertex[0]) { @@ -428,6 +465,8 @@ IsOnEdge Tests whether a point lies on a QuadEdge, up to a tolerance determined by the subdivision tolerance. Returns true if the vertex lies on the edge + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) IsOnEdge(e *QuadEdge, p geom.Pointer) bool { dist := planar.DistanceToLineSegment(p, e.Orig(), e.Dest()) @@ -441,6 +480,8 @@ IsVertexOfEdge tests whether a {@link Vertex} is the start or end vertex of a QuadEdge, up to the subdivision tolerance distance. Returns true if the vertex is a endpoint of the edge + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) IsVertexOfEdge(e *QuadEdge, v Vertex) bool { if (v.EqualsTolerance(e.Orig(), qes.tolerance)) || (v.EqualsTolerance(e.Dest(), qes.tolerance)) { @@ -535,10 +576,20 @@ func (qes *QuadEdgeSubdivision) IsVertexOfEdge(e *QuadEdge, v Vertex) bool { type edgeStack []*QuadEdge type edgeSet map[*QuadEdge]bool +/* +push pushes an edge onto the edgeStack + +If es is nil a panic will occur. +*/ func (es *edgeStack) push(edge *QuadEdge) { *es = append(*es, edge) } +/* +pop pops an edge off the edgeStack + +If es is nil a panic will occur. +*/ func (es *edgeStack) pop() *QuadEdge { if len(*es) == 0 { return nil @@ -553,20 +604,23 @@ contains returns true if edge is in the map. This just isn't natural for me yet... if _, ok := es[edge]; ok { + +If es is nil a panic will occur. */ func (es *edgeSet) contains(edge *QuadEdge) bool { _, ok := (*es)[edge] return ok } -/** - * Gets all primary quadedges in the subdivision. -* A primary edge is a {@link QuadEdge} - * which occupies the 0'th position in its array of associated quadedges. - * These provide the unique geometric edges of the triangulation. - * - * @param includeFrame true if the frame edges are to be included - * @return a List of QuadEdges +/* +GetPrimaryEdges gets all primary quadedges in the subdivision. A primary edge +is a QuadEdge which occupies the 0'th position in its array of associated +quadedges. These provide the unique geometric edges of the triangulation. + +includeFrame true if the frame edges are to be included +Return a List of QuadEdges + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) GetPrimaryEdges(includeFrame bool) []*QuadEdge { qes.visitedKey++ @@ -655,12 +709,10 @@ func (qes *QuadEdgeSubdivision) visitTriangles(triVisitor func(triEdges []*QuadE Stores the edges for a visited triangle. Also pushes sym (neighbour) edges on stack to visit later. -@param edge -@param edgeStack -@param includeFrame -@return the visited triangle edges -or null if the triangle should not be visited (for instance, if it is - outer) +Return the visited triangle edges or null if the triangle should not be +visited (for instance, if it is outer) + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) fetchTriangleToVisit(edge *QuadEdge, stack *edgeStack, includeFrame bool, visitedEdges edgeSet) []*QuadEdge { triEdges := make([]*QuadEdge, 0, 3) @@ -750,12 +802,13 @@ func (qes *QuadEdgeSubdivision) fetchTriangleToVisit(edge *QuadEdge, stack *edge // } // } -/** +/* Gets the coordinates for each triangle in the subdivision as an array. -@param includeFrame - true if the frame triangles should be included -@return a list of Coordinate[4] representing each triangle +includeFrame true if the frame triangles should be included +Return a list of Coordinate[4] representing each triangle + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) GetTriangleCoordinates(includeFrame bool) ([]geom.Polygon, error) { var visitor TriangleCoordinatesVisitor @@ -818,6 +871,8 @@ GetEdgesAsMultiLineString gets the geometry for the edges in the subdivision as a MultiLineString containing 2-point lines. returns a MultiLineString + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) GetEdgesAsMultiLineString() geom.MultiLineString { quadEdges := qes.GetPrimaryEdges(false) @@ -838,6 +893,8 @@ Unlike JTS, this method returns a MultiPolygon. I found not all viewers like displaying collections. -JRS Returns a MultiPolygon of triangular Polygons + +If qes is nil a panic will occur. */ func (qes *QuadEdgeSubdivision) GetTriangles() (geom.MultiPolygon, error) { tris, err := qes.GetTriangleCoordinates(false) diff --git a/planar/triangulate/quadedge/quadedgesubdivision_test.go b/planar/triangulate/quadedge/quadedgesubdivision_test.go index 497ed6c9..faacd094 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision_test.go +++ b/planar/triangulate/quadedge/quadedgesubdivision_test.go @@ -61,8 +61,10 @@ func TestQuadEdgeSubdivisionDelete(t *testing.T) { uut.Connect(uut.startingEdge, e1) e2 := uut.MakeEdge(tc.b, tc.c) uut.Connect(e2, e1) + uut.Validate() uut.Delete(e2) + uut.Validate() edges := uut.GetEdgesAsMultiLineString() edgesWKT, err := wkt.Encode(edges) diff --git a/planar/triangulate/quadedge/trianglepredicate.go b/planar/triangulate/quadedge/trianglepredicate.go index 50b62457..90d4b3de 100644 --- a/planar/triangulate/quadedge/trianglepredicate.go +++ b/planar/triangulate/quadedge/trianglepredicate.go @@ -17,11 +17,11 @@ import ( ) /* -Algorithms for computing values and predicates associated with triangles. -For some algorithms extended-precision implementations are provided, which are -more robust (i.e. they produce correct answers in more cases). Also, some more -robust formulations of some algorithms are provided, which utilize -normalization to the origin. +TrianglePredicate contains algorithms for computing values and predicates +associated with triangles. For some algorithms extended-precision +implementations are provided, which are more robust (i.e. they produce correct +answers in more cases). Also, some more robust formulations of some algorithms +are provided, which utilize normalization to the origin. Author Martin Davis Ported to Go by Jason R. Surratt @@ -57,12 +57,11 @@ var TrianglePredicate trianglePredicate */ /* -Tests if a point is inside the circle defined by -the triangle with vertices a, b, c (oriented counter-clockwise). -This test uses simple -double-precision arithmetic, and thus is not 100% robust. -However, by using normalization to the origin -it provides improved robustness and increased performance. +IsInCircleNormalized Tests if a point is inside the circle defined by the +triangle with vertices a, b, c (oriented counter-clockwise). This test uses +simple double-precision arithmetic, and thus is not 100% robust. However, by +using normalization to the origin it provides improved robustness and +increased performance. Based on code by J.R.Shewchuk. @@ -105,9 +104,9 @@ func (_ trianglePredicate) IsInCircleNormalized(a geom.Pointer, b geom.Pointer, */ /* -Tests if a point is inside the circle defined by -the triangle with vertices a, b, c (oriented counter-clockwise). -This method uses more robust computation. +IsInCircleRobust Tests if a point is inside the circle defined by the triangle +with vertices a, b, c (oriented counter-clockwise). This method uses more +robust computation. a - a vertex of the triangle b - a vertex of the triangle diff --git a/planar/triangulate/quadedge/vertex.go b/planar/triangulate/quadedge/vertex.go index ead0b383..c7acda40 100644 --- a/planar/triangulate/quadedge/vertex.go +++ b/planar/triangulate/quadedge/vertex.go @@ -108,7 +108,7 @@ func (u Vertex) CrossProduct(v Vertex) float64 { } /* -Computes the inner or dot product +Dot computes the inner or dot product @param v a vertex @return returns the dot product u.v @@ -142,7 +142,9 @@ func (u Vertex) Magn() float64 { return math.Sqrt(u.X()*u.X() + u.Y()*u.Y()) } -/* returns k X v (cross product). this is a vector perpendicular to v */ +/* +Cross returns k X v (cross product). this is a vector perpendicular to v +*/ func (u Vertex) Cross() Vertex { return Vertex{u.Y(), -u.X()} } diff --git a/planar/triangulate/quadedge/vertex_test.go b/planar/triangulate/quadedge/vertex_test.go index 197be5e8..466b0ce6 100644 --- a/planar/triangulate/quadedge/vertex_test.go +++ b/planar/triangulate/quadedge/vertex_test.go @@ -104,12 +104,12 @@ func TestVertexEqualsTolerance(t *testing.T) { func TestVertexIsInCircle(t *testing.T) { type tcase struct { - v1 Vertex - expected bool + v1 Vertex + expected bool } fn := func(t *testing.T, tc tcase) { - r := tc.v1.IsInCircle(Vertex{0, 0}, Vertex{2,0}, Vertex{1, 1}) + r := tc.v1.IsInCircle(Vertex{0, 0}, Vertex{2, 0}, Vertex{1, 1}) if r != tc.expected { t.Errorf("error, expected %v got %v", tc.expected, r) return @@ -128,8 +128,8 @@ func TestVertexIsInCircle(t *testing.T) { func TestVertexMarshalJSON(t *testing.T) { type tcase struct { - v1 Vertex - expected string + v1 Vertex + expected string } fn := func(t *testing.T, tc tcase) { @@ -154,7 +154,7 @@ func TestVertexMarshalJSON(t *testing.T) { func TestVertexScalar(t *testing.T) { type tcase struct { - v Vertex + v Vertex scalar float64 times Vertex } @@ -183,4 +183,3 @@ func TestVertexScalar(t *testing.T) { t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) } } - From 1d476c0a5529bf15f0cff4b60aa4e3b9c215495b Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Tue, 29 May 2018 17:28:26 -0600 Subject: [PATCH 11/12] Increase test coverage. --- planar/triangulate/quadedge/quadedgesubdivision_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/planar/triangulate/quadedge/quadedgesubdivision_test.go b/planar/triangulate/quadedge/quadedgesubdivision_test.go index faacd094..e3ca3b49 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision_test.go +++ b/planar/triangulate/quadedge/quadedgesubdivision_test.go @@ -65,6 +65,7 @@ func TestQuadEdgeSubdivisionDelete(t *testing.T) { uut.Delete(e2) uut.Validate() + uut.DebugDumpEdges(); edges := uut.GetEdgesAsMultiLineString() edgesWKT, err := wkt.Encode(edges) @@ -72,6 +73,14 @@ func TestQuadEdgeSubdivisionDelete(t *testing.T) { t.Fatalf("expected nil got %v", err) } + qe, err := uut.LocateSegment(tc.a, tc.b) + if err != nil { + t.Errorf("expected nil got %v", err) + } + if qe.Orig().Equals(tc.a) == false || qe.Dest().Equals(tc.b) == false { + t.Errorf("expected true got false") + } + if edgesWKT != tc.expected { t.Errorf("expected %v got %v", tc.expected, edgesWKT) } From 4b14c8fc87bba5504b652b6269f19f772026cce7 Mon Sep 17 00:00:00 2001 From: "Jason R. Surratt" Date: Wed, 30 May 2018 09:57:36 -0600 Subject: [PATCH 12/12] Increase test coverage & add comments. --- geom_test.go | 10 +++ .../constraineddelaunay/triangle.go | 13 ++++ .../constraineddelaunay/triangle_test.go | 1 - .../constraineddelaunay/triangulator.go | 51 +++++++++++- .../constraineddelaunay/triangulator_test.go | 21 +++-- planar/triangulate/delaunay_test.go | 3 +- .../delaunaytriangulationbuilder_test.go | 4 + .../quadedge/quadedgesubdivision.go | 19 ++--- .../quadedge/quadedgesubdivision_test.go | 12 +++ planar/triangulate/quadedge/vertex_test.go | 77 +++++++++++++++++-- 10 files changed, 181 insertions(+), 30 deletions(-) delete mode 100644 planar/triangulate/constraineddelaunay/triangle_test.go diff --git a/geom_test.go b/geom_test.go index 1644c8ba..90443c5f 100644 --- a/geom_test.go +++ b/geom_test.go @@ -24,6 +24,11 @@ func TestGetCoordinates(t *testing.T) { } } testcases := []tcase{ + { + geom: Extent{}, + expected: nil, + err: ErrUnknownGeometry{Extent{}}, + }, { geom: Point{10, 20}, expected: []Point{{10, 20}}, @@ -143,6 +148,11 @@ func TestExtractLines(t *testing.T) { } } testcases := []tcase{ + { + geom: Extent{}, + expected: nil, + err: ErrUnknownGeometry{Extent{}}, + }, { geom: Point{10, 20}, expected: []Line{}, diff --git a/planar/triangulate/constraineddelaunay/triangle.go b/planar/triangulate/constraineddelaunay/triangle.go index b8708e85..820bc1d9 100644 --- a/planar/triangulate/constraineddelaunay/triangle.go +++ b/planar/triangulate/constraineddelaunay/triangle.go @@ -25,6 +25,8 @@ type Triangle struct { /* IntersectsPoint returns true if the vertex intersects the given triangle. This includes falling on an edge. + +If tri is nil a panic will occur. */ func (tri *Triangle) IntersectsPoint(v quadedge.Vertex) bool { e := tri.qe @@ -68,6 +70,8 @@ v1 + a | b + + If this method is called on triangle a with v1 as the vertex, the result will be triangle b. + +If tri is nil a panic will occur. */ func (tri *Triangle) opposedTriangle(v quadedge.Vertex) (*Triangle, error) { qe := tri.qe @@ -96,6 +100,8 @@ v1 + a | b + v2 + If this method is called as a.opposedVertex(b), the result will be vertex v2. + +If tri is nil a panic will occur. */ func (tri *Triangle) opposedVertex(other *Triangle) (quadedge.Vertex, error) { ae, err := tri.sharedEdge(other) @@ -122,6 +128,8 @@ returned with triangle a on the left. + r If this method is called as a.sharedEdge(b), the result will be edge lr. + +If tri is nil a panic will occur. */ func (tri *Triangle) sharedEdge(other *Triangle) (*quadedge.QuadEdge, error) { ae := tri.qe @@ -153,6 +161,11 @@ func (tri *Triangle) sharedEdge(other *Triangle) (*quadedge.QuadEdge, error) { return ae, nil } +/* +String returns a string representation of triangle. + +If tri is nil a panic will occur. +*/ func (tri *Triangle) String() string { str := "[" e := tri.qe diff --git a/planar/triangulate/constraineddelaunay/triangle_test.go b/planar/triangulate/constraineddelaunay/triangle_test.go deleted file mode 100644 index c4ca4beb..00000000 --- a/planar/triangulate/constraineddelaunay/triangle_test.go +++ /dev/null @@ -1 +0,0 @@ -package constraineddelaunay diff --git a/planar/triangulate/constraineddelaunay/triangulator.go b/planar/triangulate/constraineddelaunay/triangulator.go index 86e1fe5a..9eb18b96 100644 --- a/planar/triangulate/constraineddelaunay/triangulator.go +++ b/planar/triangulate/constraineddelaunay/triangulator.go @@ -58,6 +58,8 @@ func appendNonRepeat(arr []quadedge.Vertex, v quadedge.Vertex) []quadedge.Vertex /* createSegment creates a segment with vertices a & b, if it doesn't already exist. All the vertices must already exist in the triangulator. + +If tri is nil a panic will occur. */ func (tri *Triangulator) createSegment(s triangulate.Segment) error { qe, err := tri.LocateSegment(s.GetStart(), s.GetEnd()) @@ -93,6 +95,8 @@ must already exist in the triangulator. Any existing edges that make up the tria This method makes no effort to ensure the resulting changes are a valid triangulation. + +If tri is nil a panic will occur. */ func (tri *Triangulator) createTriangle(a, b, c quadedge.Vertex) error { if err := tri.createSegment(triangulate.NewSegment(geom.Line{a, b})); err != nil { @@ -116,6 +120,8 @@ reflect the removal. The local vertex index is also updated to reflect the deletion. It is invalid to call this method on the last edge that links to a vertex. + +If tri is nil a panic will occur. */ func (tri *Triangulator) deleteEdge(e *quadedge.QuadEdge) error { @@ -156,6 +162,8 @@ Tolerance is not considered when determining if vertices are the same. Returns a quadedge that has s.GetStart() as the origin and the right face is the desired triangle. If the segment falls on an edge, the triangle to the right of the segment is returned. + +If tri is nil a panic will occur. */ func (tri *Triangulator) findIntersectingTriangle(s triangulate.Segment) (*Triangle, error) { @@ -200,6 +208,8 @@ func (tri *Triangulator) findIntersectingTriangle(s triangulate.Segment) (*Trian GetEdges gets the edges of the computed triangulation as a MultiLineString. returns the edges of the triangulation + +If tri is nil a panic will occur. */ func (tri *Triangulator) GetEdges() geom.MultiLineString { return tri.builder.GetEdges() @@ -208,6 +218,8 @@ func (tri *Triangulator) GetEdges() geom.MultiLineString { /* GetTriangles Gets the faces of the computed triangulation as a MultiPolygon. + +If tri is nil a panic will occur. */ func (tri *Triangulator) GetTriangles() (geom.MultiPolygon, error) { return tri.builder.GetTriangles() @@ -218,6 +230,8 @@ InsertSegments inserts the line segments in the specified geometry and builds a triangulation. The line segments are used as constraints in the triangulation. If the geometry is made up solely of points, then no constraints will be used. + +If tri is nil a panic will occur. */ func (tri *Triangulator) InsertSegments(g geom.Geometry) error { err := tri.insertSites(g) @@ -237,6 +251,8 @@ func (tri *Triangulator) InsertSegments(g geom.Geometry) error { insertSites inserts all of the vertices found in g into a Delaunay triangulation. Other steps will modify the Delaunay Triangulation to create the constrained Delaunay triangulation. + +If tri is nil a panic will occur. */ func (tri *Triangulator) insertSites(g geom.Geometry) error { tri.builder = triangulate.NewDelaunayTriangulationBuilder(tri.tolerance) @@ -268,6 +284,8 @@ line segements in g as constraints in the triangulation. After this step the triangulation is no longer a proper Delaunay triangulation, but the constraints are guaranteed. Some constraints may need to be split (think about the case when two constraints intersect). + +If tri is nil a panic will occur. */ func (tri *Triangulator) insertConstraints(g geom.Geometry) error { tri.constraints = make(map[triangulate.Segment]bool) @@ -315,6 +333,8 @@ guaranteed that the intersecting point will fall _on_ one of the lines, not in the extended region of the line. Taken from: https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect + +If tri is nil a panic will occur. */ func (tri *Triangulator) intersection(l1, l2 triangulate.Segment) (quadedge.Vertex, error) { p := l1.GetStart() @@ -350,6 +370,11 @@ func (tri *Triangulator) intersection(l1, l2 triangulate.Segment) (quadedge.Vert return p.Sum(r.Times(t)), nil } +/* +IsConstraint returns true if e is a constrained edge. + +If tri is nil a panic will occur. +*/ func (tri *Triangulator) IsConstraint(e *quadedge.QuadEdge) bool { _, ok := tri.constraints[triangulate.NewSegment(geom.Line{e.Orig(), e.Dest()})] @@ -372,6 +397,8 @@ There are some deviations that are also mentioned inline in the comments a triangulation (QuadEdge) - Modification to support the case when two constrained edges intersect at more than the end points. + +If tri is nil a panic will occur. */ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { @@ -505,7 +532,6 @@ func (tri *Triangulator) insertEdgeCDT(ab *triangulate.Segment) error { // EndWhile // remove the previously marked edges - // TODO Inefficient for i := range removalList { tri.deleteEdge(removalList[i]) } @@ -536,6 +562,8 @@ locateEdgeByVertex finds a quad edge that has this vertex as Orig(). This will not be a unique edge. This is looking for an exact match and tolerance will not be considered. + +If tri is nil a panic will occur. */ func (tri *Triangulator) locateEdgeByVertex(v quadedge.Vertex) (*quadedge.QuadEdge, error) { qe := tri.vertexIndex[v] @@ -551,6 +579,8 @@ locateEdgeByVertex finds a quad edge that has this vertex as Orig(). This will not be a unique edge. This is looking for an exact match and tolerance will not be considered. + +If tri is nil a panic will occur. */ func (tri *Triangulator) LocateSegment(v1 quadedge.Vertex, v2 quadedge.Vertex) (*quadedge.QuadEdge, error) { qe := tri.vertexIndex[v1] @@ -581,6 +611,8 @@ func (tri *Triangulator) LocateSegment(v1 quadedge.Vertex, v2 quadedge.Vertex) ( /* removeConstraintEdge removes any constraints that share the same Orig() and Dest() as the edge provided. If there are none, no changes are made. + +If tri is nil a panic will occur. */ func (tri *Triangulator) removeConstraintEdge(e *quadedge.QuadEdge) { delete(tri.constraints, triangulate.NewSegment(geom.Line{e.Orig(), e.Dest()})) @@ -598,6 +630,8 @@ helpful in modifying the index after an edge has been deleted. toRemove - a set of QuadEdges that should be removed from the index. These QuadEdges don't necessarily have to link to the provided vertex. v - The vertex to modify in the index. + +If tri is nil a panic will occur. */ func (tri *Triangulator) removeEdgesFromVertexIndex(toRemove map[*quadedge.QuadEdge]bool, v quadedge.Vertex) error { ve := tri.vertexIndex[v] @@ -620,6 +654,8 @@ func (tri *Triangulator) removeEdgesFromVertexIndex(toRemove map[*quadedge.QuadE /* splitEdge splits the given edge at the vertex v. + +If tri is nil a panic will occur. */ func (tri *Triangulator) splitEdge(e *quadedge.QuadEdge, v quadedge.Vertex) error { constraint := tri.IsConstraint(e) @@ -655,9 +691,12 @@ func (tri *Triangulator) splitEdge(e *quadedge.QuadEdge, v quadedge.Vertex) erro return nil } -// TriangulatePseudoPolygon -// Pseudocode taken from Figure 10 -// http://old.cescg.org/CESCG-2004/web/Domiter-Vid/CDT.pdf +/* +triangulatePseudoPolygon is taken from the pseudocode TriangulatePseudoPolygon +from Figure 10 in Domiter. + +If tri is nil a panic will occur. +*/ func (tri *Triangulator) triangulatePseudoPolygon(p []quadedge.Vertex, ab triangulate.Segment) error { a := ab.GetStart() b := ab.GetEnd() @@ -706,6 +745,8 @@ validate runs a number of self consistency checks against a triangulation and reports the first error. This is most useful when testing/debugging. + +If tri is nil a panic will occur. */ func (tri *Triangulator) Validate() error { if tri.validate == false { @@ -721,6 +762,8 @@ func (tri *Triangulator) Validate() error { /* validateVertexIndex self consistency checks against a triangulation and the subdiv and reports the first error. + +If tri is nil a panic will occur. */ func (tri *Triangulator) validateVertexIndex() error { // collect a set of all edges diff --git a/planar/triangulate/constraineddelaunay/triangulator_test.go b/planar/triangulate/constraineddelaunay/triangulator_test.go index 0aaabae7..e7baf6c9 100644 --- a/planar/triangulate/constraineddelaunay/triangulator_test.go +++ b/planar/triangulate/constraineddelaunay/triangulator_test.go @@ -23,6 +23,7 @@ func TestFindIntersectingTriangle(t *testing.T) { inputWKB string searchFrom geom.Line expectedTriangle string + err error } fn := func(t *testing.T, tc tcase) { @@ -46,16 +47,17 @@ func TestFindIntersectingTriangle(t *testing.T) { // find the triangle tri, err := uut.findIntersectingTriangle(triangulate.NewSegment(tc.searchFrom)) - if err != nil { - t.Fatalf("error, expected nil got %v", err) + if err != tc.err { + t.Fatalf("error, expected %v got %v", tc.err, err) return } - qeStr := tri.String() - if qeStr != tc.expectedTriangle { - t.Fatalf("error, expected %v got %v", tc.expectedTriangle, qeStr) + if tc.err == nil { + qeStr := tri.String() + if qeStr != tc.expectedTriangle { + t.Fatalf("error, expected %v got %v", tc.expectedTriangle, qeStr) + } } - } testcases := []tcase{ { @@ -82,6 +84,13 @@ func TestFindIntersectingTriangle(t *testing.T) { searchFrom: geom.Line{{10, 10}, {10, 20}}, expectedTriangle: `[[10 10],[10 20],[20 10]]`, }, + { + inputWKT: `MULTIPOINT (10 10, 10 20, 20 20, 20 10, 20 0, 10 0, 0 0, 0 10, 0 20)`, + inputWKB: `010400000009000000010100000000000000000024400000000000002440010100000000000000000024400000000000003440010100000000000000000034400000000000003440010100000000000000000034400000000000002440010100000000000000000034400000000000000000010100000000000000000024400000000000000000010100000000000000000000000000000000000000010100000000000000000000000000000000002440010100000000000000000000000000000000003440`, + searchFrom: geom.Line{{1000, 1000}, {10000, 20000}}, + expectedTriangle: ``, + err: quadedge.ErrLocateFailure, + }, } for i, tc := range testcases { diff --git a/planar/triangulate/delaunay_test.go b/planar/triangulate/delaunay_test.go index 34c5a713..af8d69fb 100644 --- a/planar/triangulate/delaunay_test.go +++ b/planar/triangulate/delaunay_test.go @@ -49,8 +49,7 @@ func TestDelaunayTriangulation(t *testing.T) { return } - builder := new(DelaunayTriangulationBuilder) - builder.tolerance = 1e-6 + builder := NewDelaunayTriangulationBuilder(1e-6) builder.SetSites(sites) if builder.create() == false { t.Errorf("error building triangulation, expected true got false") diff --git a/planar/triangulate/delaunaytriangulationbuilder_test.go b/planar/triangulate/delaunaytriangulationbuilder_test.go index 8a70465e..dfcc4cc7 100644 --- a/planar/triangulate/delaunaytriangulationbuilder_test.go +++ b/planar/triangulate/delaunaytriangulationbuilder_test.go @@ -32,6 +32,10 @@ func TestUnique(t *testing.T) { if reflect.DeepEqual(result, tc.expected) == false { t.Errorf("error, expected %v got %v", tc.expected, result) } + // This shouldn't exist with no data + if uut.GetSubdivision() != nil { + t.Errorf("error, expected nil got not nil") + } } testcases := []tcase{ { diff --git a/planar/triangulate/quadedge/quadedgesubdivision.go b/planar/triangulate/quadedge/quadedgesubdivision.go index d33ff903..69921429 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision.go +++ b/planar/triangulate/quadedge/quadedgesubdivision.go @@ -238,7 +238,7 @@ func (qes *QuadEdgeSubdivision) Delete(e *QuadEdge) { newArray = append(newArray, ele) if ele.next.IsLive() == false { - log.Fatal("a dead edge is still linked: %v", ele) + log.Fatalf("a dead edge is still linked: %v", ele) } } } @@ -282,21 +282,16 @@ func (qes *QuadEdgeSubdivision) LocateFromEdge(v Vertex, startEdge *QuadEdge) (* iter++ /* - So far it has always been the case that failure to locate indicates an - invalid subdivision. So just fail completely. (An alternative would be - to perform an exhaustive search for the containing triangle, but this - would mask errors in the subdivision topology) + So far it has always been the case that failure to locate indicates an + invalid subdivision. So just fail completely. (An alternative would be + to perform an exhaustive search for the containing triangle, but this + would mask errors in the subdivision topology) - This can also happen if two vertices are located very close together, - since the orientation predicates may experience precision failures. + This can also happen if two vertices are located very close together, + since the orientation predicates may experience precision failures. */ if iter > maxIter { return nil, ErrLocateFailure - // String msg = "Locate failed to converge (at edge: " + e + "). - // Possible causes include invalid Subdivision topology or very close - // sites"; - // System.err.println(msg); - // dumpTriangles(); } if v.Equals(e.Orig()) || v.Equals(e.Dest()) { diff --git a/planar/triangulate/quadedge/quadedgesubdivision_test.go b/planar/triangulate/quadedge/quadedgesubdivision_test.go index e3ca3b49..de489eb3 100644 --- a/planar/triangulate/quadedge/quadedgesubdivision_test.go +++ b/planar/triangulate/quadedge/quadedgesubdivision_test.go @@ -77,6 +77,12 @@ func TestQuadEdgeSubdivisionDelete(t *testing.T) { if err != nil { t.Errorf("expected nil got %v", err) } + + _, err = uut.LocateSegment(Vertex{100,1000}, tc.b) + if err == nil { + t.Errorf("expected %v got %v", ErrLocateFailure, err) + } + if qe.Orig().Equals(tc.a) == false || qe.Dest().Equals(tc.b) == false { t.Errorf("expected true got false") } @@ -166,6 +172,12 @@ func TestQuadEdgeSubdivisionIsOnEdge(t *testing.T) { if onEdge != tc.expected { t.Fatalf("expected %v got %v", tc.expected, onEdge) } + + onLine := uut.IsOnLine(geom.Line{tc.e1, tc.e2}, tc.p) + + if onLine != tc.expected { + t.Fatalf("expected %v got %v", tc.expected, onEdge) + } } testcases := []tcase{ {Vertex{0, 0}, Vertex{5, 5}, Vertex{3, 3}, true}, diff --git a/planar/triangulate/quadedge/vertex_test.go b/planar/triangulate/quadedge/vertex_test.go index 466b0ce6..60e3bf5d 100644 --- a/planar/triangulate/quadedge/vertex_test.go +++ b/planar/triangulate/quadedge/vertex_test.go @@ -157,6 +157,7 @@ func TestVertexScalar(t *testing.T) { v Vertex scalar float64 times Vertex + divide Vertex } fn := func(t *testing.T, tc tcase) { @@ -166,16 +167,82 @@ func TestVertexScalar(t *testing.T) { return } - r = tc.v.Cross() - c := Vertex{tc.v.Y(), -tc.v.X()} - if c.Equals(r) == false { - t.Errorf("error, expected %v got %v", c, r) + r = tc.v.Divide(tc.scalar) + if r.Equals(tc.divide) == false { + t.Errorf("error, expected %v got %v", tc.divide, r) + return + } + } + testcases := []tcase{ + {Vertex{1, 2}, 3, Vertex{3, 6}, Vertex{0.3333333333333333, 0.6666666666666666}}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +func TestVertexUnary(t *testing.T) { + type tcase struct { + v Vertex + cross Vertex + magn float64 + normalize Vertex + } + + fn := func(t *testing.T, tc tcase) { + r := tc.v.Cross() + if tc.cross.Equals(r) == false { + t.Errorf("error, expected %v got %v", tc.cross, r) + return + } + + m := tc.v.Magn() + if tc.magn != m { + t.Errorf("error, expected %v got %v", tc.magn, m) + return + } + + r = tc.v.Normalize() + if tc.normalize.Equals(r) == false { + t.Errorf("error, expected %v got %v", tc.normalize, r) + return + } + } + testcases := []tcase{ + {Vertex{1, 2}, Vertex{2, -1}, 2.23606797749979, Vertex{0.4472135954999579, 0.8944271909999159}}, + } + + for i, tc := range testcases { + tc := tc + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { fn(t, tc) }) + } +} + +func TestVertexVertex(t *testing.T) { + type tcase struct { + v1 Vertex + v2 Vertex + dot float64 + sum Vertex + } + + fn := func(t *testing.T, tc tcase) { + s := tc.v1.Dot(tc.v2) + if s != tc.dot { + t.Errorf("error, expected %v got %v", tc.dot, s) return } + r := tc.v1.Sum(tc.v2) + if r.Equals(tc.sum) == false { + t.Errorf("error, expected %v got %v", tc.sum, r) + return + } } testcases := []tcase{ - {Vertex{1, 2}, 3, Vertex{3, 6}}, + {Vertex{1, 2}, Vertex{3, 4}, 11, Vertex{4, 6}}, } for i, tc := range testcases {