Skip to content

Commit

Permalink
simplify: Visvalingam, by default, keeps 3 points for "areas"
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmach committed Jan 8, 2024
1 parent 3ecd611 commit 7b6e2a5
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 22 deletions.
2 changes: 1 addition & 1 deletion simplify/douglas_peucker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion simplify/douglas_peucker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 7 additions & 7 deletions simplify/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion simplify/radial.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion simplify/radial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 38 additions & 8 deletions simplify/visvalingam.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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))
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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) {
Expand Down
45 changes: 42 additions & 3 deletions simplify/visvalingam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 7b6e2a5

Please sign in to comment.