From 7b6e2a53ddd6500eb9672841797d508ed94952c8 Mon Sep 17 00:00:00 2001 From: Paul Mach Date: Mon, 8 Jan 2024 15:24:59 -0800 Subject: [PATCH] simplify: Visvalingam, by default, keeps 3 points for "areas" --- simplify/douglas_peucker.go | 2 +- simplify/douglas_peucker_test.go | 2 +- simplify/helpers.go | 14 +++++----- simplify/radial.go | 2 +- simplify/radial_test.go | 2 +- simplify/visvalingam.go | 46 ++++++++++++++++++++++++++------ simplify/visvalingam_test.go | 45 ++++++++++++++++++++++++++++--- 7 files changed, 91 insertions(+), 22 deletions(-) diff --git a/simplify/douglas_peucker.go b/simplify/douglas_peucker.go index 0cc8380..171f308 100644 --- a/simplify/douglas_peucker.go +++ b/simplify/douglas_peucker.go @@ -19,7 +19,7 @@ func DouglasPeucker(threshold float64) *DouglasPeuckerSimplifier { } } -func (s *DouglasPeuckerSimplifier) simplify(ls orb.LineString, wim bool) (orb.LineString, []int) { +func (s *DouglasPeuckerSimplifier) simplify(ls orb.LineString, area, wim bool) (orb.LineString, []int) { mask := make([]byte, len(ls)) mask[0] = 1 mask[len(mask)-1] = 1 diff --git a/simplify/douglas_peucker_test.go b/simplify/douglas_peucker_test.go index bed7b76..75c31e1 100644 --- a/simplify/douglas_peucker_test.go +++ b/simplify/douglas_peucker_test.go @@ -40,7 +40,7 @@ func TestDouglasPeucker(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - v, im := DouglasPeucker(tc.threshold).simplify(tc.ls, true) + v, im := DouglasPeucker(tc.threshold).simplify(tc.ls, false, true) if !v.Equal(tc.expected) { t.Log(v) t.Log(tc.expected) diff --git a/simplify/helpers.go b/simplify/helpers.go index c44bb79..d48b6bd 100644 --- a/simplify/helpers.go +++ b/simplify/helpers.go @@ -4,7 +4,7 @@ package simplify import "github.com/paulmach/orb" type simplifier interface { - simplify(orb.LineString, bool) (orb.LineString, []int) + simplify(l orb.LineString, area bool, withIndexMap bool) (orb.LineString, []int) } func simplify(s simplifier, geom orb.Geometry) orb.Geometry { @@ -64,24 +64,24 @@ func simplify(s simplifier, geom orb.Geometry) orb.Geometry { } func lineString(s simplifier, ls orb.LineString) orb.LineString { - return runSimplify(s, ls) + return runSimplify(s, ls, false) } func multiLineString(s simplifier, mls orb.MultiLineString) orb.MultiLineString { for i := range mls { - mls[i] = runSimplify(s, mls[i]) + mls[i] = runSimplify(s, mls[i], false) } return mls } func ring(s simplifier, r orb.Ring) orb.Ring { - return orb.Ring(runSimplify(s, orb.LineString(r))) + return orb.Ring(runSimplify(s, orb.LineString(r), true)) } func polygon(s simplifier, p orb.Polygon) orb.Polygon { count := 0 for i := range p { - r := orb.Ring(runSimplify(s, orb.LineString(p[i]))) + r := orb.Ring(runSimplify(s, orb.LineString(p[i]), true)) if i != 0 && len(r) <= 2 { continue } @@ -113,10 +113,10 @@ func collection(s simplifier, c orb.Collection) orb.Collection { return c } -func runSimplify(s simplifier, ls orb.LineString) orb.LineString { +func runSimplify(s simplifier, ls orb.LineString, area bool) orb.LineString { if len(ls) <= 2 { return ls } - ls, _ = s.simplify(ls, false) + ls, _ = s.simplify(ls, area, false) return ls } diff --git a/simplify/radial.go b/simplify/radial.go index 71ecf1a..531e8a4 100644 --- a/simplify/radial.go +++ b/simplify/radial.go @@ -20,7 +20,7 @@ func Radial(df orb.DistanceFunc, threshold float64) *RadialSimplifier { } } -func (s *RadialSimplifier) simplify(ls orb.LineString, wim bool) (orb.LineString, []int) { +func (s *RadialSimplifier) simplify(ls orb.LineString, area, wim bool) (orb.LineString, []int) { var indexMap []int if wim { indexMap = append(indexMap, 0) diff --git a/simplify/radial_test.go b/simplify/radial_test.go index d3d7116..bf20637 100644 --- a/simplify/radial_test.go +++ b/simplify/radial_test.go @@ -41,7 +41,7 @@ func TestRadial(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - v, im := Radial(planar.Distance, tc.threshold).simplify(tc.ls, true) + v, im := Radial(planar.Distance, tc.threshold).simplify(tc.ls, false, true) if !v.Equal(tc.expected) { t.Log(v) t.Log(tc.expected) diff --git a/simplify/visvalingam.go b/simplify/visvalingam.go index f11f608..f8d9975 100644 --- a/simplify/visvalingam.go +++ b/simplify/visvalingam.go @@ -12,10 +12,17 @@ var _ orb.Simplifier = &VisvalingamSimplifier{} // performs the vivalingham algorithm. type VisvalingamSimplifier struct { Threshold float64 - ToKeep int + + // If 0 defaults to 2 for line, 3 for non-closed rings and 4 for closed rings. + // The intent is to maintain valid geometry after simplification, however it + // is still possible for the simplification to create self-intersections. + ToKeep int } // Visvalingam creates a new VisvalingamSimplifier. +// If minPointsToKeep is 0 the algorithm will keep at least 2 points for lines, +// 3 for non-closed rings and 4 for closed rings. However it is still possible +// for the simplification to create self-intersections. func Visvalingam(threshold float64, minPointsToKeep int) *VisvalingamSimplifier { return &VisvalingamSimplifier{ Threshold: threshold, @@ -25,19 +32,42 @@ func Visvalingam(threshold float64, minPointsToKeep int) *VisvalingamSimplifier // VisvalingamThreshold runs the Visvalingam-Whyatt algorithm removing // triangles whose area is below the threshold. +// Will keep at least 2 points for lines, 3 for non-closed rings and 4 for closed rings. +// The intent is to maintain valid geometry after simplification, however it +// is still possible for the simplification to create self-intersections. func VisvalingamThreshold(threshold float64) *VisvalingamSimplifier { return Visvalingam(threshold, 0) } // VisvalingamKeep runs the Visvalingam-Whyatt algorithm removing -// triangles of minimum area until we're down to `toKeep` number of points. -func VisvalingamKeep(toKeep int) *VisvalingamSimplifier { - return Visvalingam(math.MaxFloat64, toKeep) +// triangles of minimum area until we're down to `minPointsToKeep` number of points. +// If minPointsToKeep is 0 the algorithm will keep at least 2 points for lines, +// 3 for non-closed rings and 4 for closed rings. However it is still possible +// for the simplification to create self-intersections. +func VisvalingamKeep(minPointsToKeep int) *VisvalingamSimplifier { + return Visvalingam(math.MaxFloat64, minPointsToKeep) } -func (s *VisvalingamSimplifier) simplify(ls orb.LineString, wim bool) (orb.LineString, []int) { +func (s *VisvalingamSimplifier) simplify(ls orb.LineString, area, wim bool) (orb.LineString, []int) { + if len(ls) <= 1 { + return ls, nil + } + + toKeep := s.ToKeep + if toKeep == 0 { + if area { + if ls[0] == ls[len(ls)-1] { + toKeep = 4 + } else { + toKeep = 3 + } + } else { + toKeep = 2 + } + } + var indexMap []int - if len(ls) <= s.ToKeep { + if len(ls) <= toKeep { if wim { // create identify map indexMap = make([]int, len(ls)) @@ -89,7 +119,7 @@ func (s *VisvalingamSimplifier) simplify(ls orb.LineString, wim bool) (orb.LineS // run through the reduction process for len(heap) > 0 { current := heap.Pop() - if current.area > threshold || len(ls)-removed <= s.ToKeep { + if current.area > threshold || len(ls)-removed <= toKeep { break } @@ -153,7 +183,7 @@ type visItem struct { next *visItem previous *visItem - index int // interal index in heap, for removal and update + index int // internal index in heap, for removal and update } func (h *minHeap) Push(item *visItem) { diff --git a/simplify/visvalingam_test.go b/simplify/visvalingam_test.go index bd604ba..4de77af 100644 --- a/simplify/visvalingam_test.go +++ b/simplify/visvalingam_test.go @@ -33,7 +33,7 @@ func TestVisvalingamThreshold(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - v, im := VisvalingamThreshold(tc.threshold).simplify(tc.ls, true) + v, im := VisvalingamThreshold(tc.threshold).simplify(tc.ls, false, true) if !v.Equal(tc.expected) { t.Log(v) t.Log(tc.expected) @@ -49,6 +49,45 @@ func TestVisvalingamThreshold(t *testing.T) { } } +func TestVisvalingamThreshold_area(t *testing.T) { + cases := []struct { + name string + r orb.Ring + expected orb.Ring + indexMap []int + }{ + { + name: "reduction", + r: orb.Ring{{0, 0}, {1, 0}, {1, 0.5}, {1, 1}, {0.5, 1}, {0, 1}}, + expected: orb.Ring{{0, 0}, {1, 0}, {0, 1}}, + indexMap: []int{0, 1, 5}, + }, + { + name: "reduction closed", + r: orb.Ring{{0, 0}, {1, 0}, {1, 0.5}, {1, 1}, {0.5, 1}, {0, 1}, {0, 0}}, + expected: orb.Ring{{0, 0}, {1, 0}, {1, 1}, {0, 0}}, + indexMap: []int{0, 1, 3, 6}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + v, im := VisvalingamThreshold(10).simplify(orb.LineString(tc.r), true, true) + if !v.Equal(orb.LineString(tc.expected)) { + t.Log(v) + t.Log(tc.expected) + t.Errorf("incorrect ring") + } + + if !reflect.DeepEqual(im, tc.indexMap) { + t.Log(im) + t.Log(tc.indexMap) + t.Errorf("incorrect index map") + } + }) + } +} + func TestVisvalingamKeep(t *testing.T) { cases := []struct { name string @@ -96,7 +135,7 @@ func TestVisvalingamKeep(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - v, im := VisvalingamKeep(tc.keep).simplify(tc.ls, true) + v, im := VisvalingamKeep(tc.keep).simplify(tc.ls, false, true) if !v.Equal(tc.expected) { t.Log(v) t.Log(tc.expected) @@ -181,7 +220,7 @@ func TestVisvalingam(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - v, im := Visvalingam(tc.threshold, tc.keep).simplify(tc.ls, true) + v, im := Visvalingam(tc.threshold, tc.keep).simplify(tc.ls, false, true) if !v.Equal(tc.expected) { t.Log(v) t.Log(tc.expected)