-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathpathfinder.go
164 lines (148 loc) · 4.42 KB
/
pathfinder.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// Copyright 2023 Frederik Zipp. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package pathfind finds the shortest path between two points
// constrained by a set of polygons.
package pathfind
import (
"image"
"math"
"github.com/fzipp/astar"
"github.com/fzipp/geom"
"github.com/fzipp/pathfind/internal/poly"
)
// A Pathfinder is created and initialized with a set of polygons via
// NewPathfinder. Its Path method finds the shortest path between two points
// in this polygon set.
type Pathfinder struct {
polygons [][]image.Point
polygonSet poly.PolygonSet
concaveVertices []image.Point
visibilityGraph graph[image.Point]
}
// NewPathfinder creates a Pathfinder instance and initializes it with a set of
// polygons.
//
// A polygon is represented by a slice of points, i.e. []image.Point, describing
// the vertices of the polygon. Thus [][]image.Point is a slice of polygons,
// i.e. the set of polygons.
//
// Each polygon in the polygon set designates either an area that is accessible
// for path finding or a hole inside such an area, i.e. an obstacle. Nested
// polygons alternate between accessible area and inaccessible hole:
// - Polygons at the first level are area polygons.
// - Polygons contained inside an area polygon are holes.
// - Polygons contained inside a hole are area polygons again.
func NewPathfinder(polygons [][]image.Point) *Pathfinder {
polygonSet := convert(polygons, func(ps []image.Point) poly.Polygon {
return ps2vs(ps)
})
return &Pathfinder{
polygons: polygons,
polygonSet: polygonSet,
concaveVertices: concaveVertices(polygonSet),
}
}
// VisibilityGraph returns the calculated visibility graph from the last Path
// call. It is only available after Path was called, otherwise nil.
func (p *Pathfinder) VisibilityGraph() map[image.Point][]image.Point {
return p.visibilityGraph
}
// Path finds the shortest path from start to dest within the bounds of the
// polygons the Pathfinder was initialized with.
// If dest is outside the polygon set it will be clamped to the nearest
// polygon edge.
// The function returns nil if no path exists because start is outside
// the polygon set.
func (p *Pathfinder) Path(start, dest image.Point) []image.Point {
d := p2v(dest)
if !p.polygonSet.Contains(d) {
dest = ensureInside(p.polygonSet, v2p(p.polygonSet.ClosestPt(d)))
}
graphVertices := append(p.concaveVertices, start, dest)
p.visibilityGraph = visibilityGraph(p.polygonSet, graphVertices)
return astar.FindPath[image.Point](p.visibilityGraph, start, dest, nodeDist, nodeDist)
}
func ensureInside(ps poly.PolygonSet, pt image.Point) image.Point {
if ps.Contains(p2v(pt)) {
return pt
}
adjustment:
for dx := -1; dx <= 1; dx++ {
for dy := -1; dy <= 1; dy++ {
if dx == 0 && dy == 0 {
continue
}
npt := pt.Add(image.Point{X: dx, Y: dy})
if ps.Contains(p2v(npt)) {
pt = npt
break adjustment
}
}
}
return pt
}
func concaveVertices(ps poly.PolygonSet) []image.Point {
var vs []image.Point
for i, p := range ps {
t := concave
if isHole(ps, i) {
t = convex
}
vs = append(vs, verticesOfType(p, t)...)
}
return vs
}
func isHole(ps poly.PolygonSet, i int) bool {
hole := false
for j, p := range ps {
if i != j && p.Contains(ps[i][0], false) {
hole = !hole
}
}
return hole
}
type vertexType int
const (
concave = vertexType(iota)
convex
)
func verticesOfType(p poly.Polygon, t vertexType) []image.Point {
var vs []image.Point
for i, v := range p {
isConcave := p.IsConcaveAt(i)
if (t == concave && isConcave) || (t == convex && !isConcave) {
vs = append(vs, v2p(v))
}
}
return vs
}
func visibilityGraph(ps poly.PolygonSet, points []image.Point) graph[image.Point] {
vis := make(graph[image.Point])
for i, a := range points {
for j, b := range points {
if i == j {
continue
}
if inLineOfSight(ps, p2v(a), p2v(b)) {
vis.link(a, b)
}
}
}
return vis
}
func inLineOfSight(ps poly.PolygonSet, start, end geom.Vec2) bool {
lineOfSight := poly.LineSeg{A: start, B: end}
for _, p := range ps {
if p.IsCrossedBy(lineOfSight) {
return false
}
}
return ps.Contains(lineOfSight.Middle())
}
// nodeDist is the cost function for the A* algorithm. The visibility graph has
// 2d points as nodes, so we calculate the Euclidean distance.
func nodeDist(a, b image.Point) float64 {
c := a.Sub(b)
return math.Sqrt(float64(c.X*c.X + c.Y*c.Y))
}