blob: 4c937858874419a643de300b41302a5d034efcde [file] [log] [blame]
// Copyright ©2015 The gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package internal
import (
"errors"
"math"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/simple"
)
// LimitedVisionGrid is a 2D grid planar undirected graph where the capacity
// to determine the presence of edges is dependent on the current and past
// positions on the grid. In the absence of information, the grid is
// optimistic.
type LimitedVisionGrid struct {
Grid *Grid
// Location is the current
// location on the grid.
Location graph.Node
// VisionRadius specifies how far
// away edges can be detected.
VisionRadius float64
// Known holds a store of known
// nodes, if not nil.
Known map[int64]bool
}
// MoveTo moves to the node n on the grid and returns a slice of newly seen and
// already known edges. MoveTo panics if n is nil.
func (l *LimitedVisionGrid) MoveTo(n graph.Node) (new, old []graph.Edge) {
l.Location = n
row, column := l.RowCol(n.ID())
x := float64(column)
y := float64(row)
seen := make(map[[2]int64]bool)
bound := int(l.VisionRadius + 0.5)
for r := row - bound; r <= row+bound; r++ {
for c := column - bound; c <= column+bound; c++ {
u := l.NodeAt(r, c)
if u == nil {
continue
}
ux, uy := l.XY(u)
if math.Hypot(x-ux, y-uy) > l.VisionRadius {
continue
}
for _, v := range l.allPossibleFrom(u) {
if seen[[2]int64{u.ID(), v.ID()}] {
continue
}
seen[[2]int64{u.ID(), v.ID()}] = true
vx, vy := l.XY(v)
if !l.Known[v.ID()] && math.Hypot(x-vx, y-vy) > l.VisionRadius {
continue
}
e := simple.Edge{F: u, T: v}
if !l.Known[u.ID()] || !l.Known[v.ID()] {
new = append(new, e)
} else {
old = append(old, e)
}
}
}
}
if l.Known != nil {
for r := row - bound; r <= row+bound; r++ {
for c := column - bound; c <= column+bound; c++ {
u := l.NodeAt(r, c)
if u == nil {
continue
}
ux, uy := l.XY(u)
if math.Hypot(x-ux, y-uy) > l.VisionRadius {
continue
}
for _, v := range l.allPossibleFrom(u) {
vx, vy := l.XY(v)
if math.Hypot(x-vx, y-vy) > l.VisionRadius {
continue
}
l.Known[v.ID()] = true
}
l.Known[u.ID()] = true
}
}
}
return new, old
}
// allPossibleFrom returns all the nodes possibly reachable from u.
func (l *LimitedVisionGrid) allPossibleFrom(u graph.Node) []graph.Node {
if !l.Has(u) {
return nil
}
nr, nc := l.RowCol(u.ID())
var to []graph.Node
for r := nr - 1; r <= nr+1; r++ {
for c := nc - 1; c <= nc+1; c++ {
v := l.NodeAt(r, c)
if v == nil || u.ID() == v.ID() {
continue
}
ur, uc := l.RowCol(u.ID())
vr, vc := l.RowCol(v.ID())
if abs(ur-vr) > 1 || abs(uc-vc) > 1 {
continue
}
if !l.Grid.AllowDiagonal && ur != vr && uc != vc {
continue
}
to = append(to, v)
}
}
return to
}
// RowCol returns the row and column of the id. RowCol will panic if the
// node id is outside the range of the grid.
func (l *LimitedVisionGrid) RowCol(id int64) (r, c int) {
return l.Grid.RowCol(id)
}
// XY returns the cartesian coordinates of n. If n is not a node
// in the grid, (NaN, NaN) is returned.
func (l *LimitedVisionGrid) XY(n graph.Node) (x, y float64) {
if !l.Has(n) {
return math.NaN(), math.NaN()
}
r, c := l.RowCol(n.ID())
return float64(c), float64(r)
}
// Nodes returns all the nodes in the grid.
func (l *LimitedVisionGrid) Nodes() []graph.Node {
nodes := make([]graph.Node, 0, len(l.Grid.open))
for id := range l.Grid.open {
nodes = append(nodes, simple.Node(id))
}
return nodes
}
// NodeAt returns the node at (r, c). The returned node may be open or closed.
func (l *LimitedVisionGrid) NodeAt(r, c int) graph.Node {
return l.Grid.NodeAt(r, c)
}
// Has returns whether n is a node in the grid.
func (l *LimitedVisionGrid) Has(n graph.Node) bool {
return l.has(n.ID())
}
func (l *LimitedVisionGrid) has(id int64) bool {
return 0 <= id && id < int64(len(l.Grid.open))
}
// From returns nodes that are optimistically reachable from u.
func (l *LimitedVisionGrid) From(u graph.Node) []graph.Node {
if !l.Has(u) {
return nil
}
nr, nc := l.RowCol(u.ID())
var to []graph.Node
for r := nr - 1; r <= nr+1; r++ {
for c := nc - 1; c <= nc+1; c++ {
if v := l.NodeAt(r, c); v != nil && l.HasEdgeBetween(u, v) {
to = append(to, v)
}
}
}
return to
}
// HasEdgeBetween optimistically returns whether an edge is exists between u and v.
func (l *LimitedVisionGrid) HasEdgeBetween(u, v graph.Node) bool {
if u.ID() == v.ID() {
return false
}
ur, uc := l.RowCol(u.ID())
vr, vc := l.RowCol(v.ID())
if abs(ur-vr) > 1 || abs(uc-vc) > 1 {
return false
}
if !l.Grid.AllowDiagonal && ur != vr && uc != vc {
return false
}
x, y := l.XY(l.Location)
ux, uy := l.XY(u)
vx, vy := l.XY(v)
uKnown := l.Known[u.ID()] || math.Hypot(x-ux, y-uy) <= l.VisionRadius
vKnown := l.Known[v.ID()] || math.Hypot(x-vx, y-vy) <= l.VisionRadius
switch {
case uKnown && vKnown:
return l.Grid.HasEdgeBetween(u, v)
case uKnown:
return l.Grid.HasOpen(u)
case vKnown:
return l.Grid.HasOpen(v)
default:
return true
}
}
// Edge optimistically returns the edge from u to v.
func (l *LimitedVisionGrid) Edge(u, v graph.Node) graph.Edge {
return l.EdgeBetween(u, v)
}
// EdgeBetween optimistically returns the edge between u and v.
func (l *LimitedVisionGrid) EdgeBetween(u, v graph.Node) graph.Edge {
if l.HasEdgeBetween(u, v) {
if !l.Grid.AllowDiagonal || l.Grid.UnitEdgeWeight {
return simple.Edge{F: u, T: v, W: 1}
}
ux, uy := l.XY(u)
vx, vy := l.XY(v)
return simple.Edge{F: u, T: v, W: math.Hypot(ux-vx, uy-vy)}
}
return nil
}
// Weight returns the weight of the given edge.
func (l *LimitedVisionGrid) Weight(x, y graph.Node) (w float64, ok bool) {
if x.ID() == y.ID() {
return 0, true
}
if !l.HasEdgeBetween(x, y) {
return math.Inf(1), false
}
if e := l.EdgeBetween(x, y); e != nil {
if !l.Grid.AllowDiagonal || l.Grid.UnitEdgeWeight {
return 1, true
}
ux, uy := l.XY(e.From())
vx, vy := l.XY(e.To())
return math.Hypot(ux-vx, uy-vy), true
}
return math.Inf(1), true
}
// String returns a string representation of the grid.
func (l *LimitedVisionGrid) String() string {
b, _ := l.Render(nil)
return string(b)
}
// Render returns a text representation of the graph
// with the given path included. If the path is not a path
// in the grid Render returns a non-nil error and the
// path up to that point.
func (l *LimitedVisionGrid) Render(path []graph.Node) ([]byte, error) {
rows, cols := l.Grid.Dims()
b := make([]byte, rows*(cols+1)-1)
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if !l.Known[int64(r*cols+c)] {
b[r*(cols+1)+c] = Unknown
} else if l.Grid.open[r*cols+c] {
b[r*(cols+1)+c] = Open
} else {
b[r*(cols+1)+c] = Closed
}
}
if r < rows-1 {
b[r*(cols+1)+cols] = '\n'
}
}
// We don't use topo.IsPathIn at the outset because we
// want to draw as much as possible before failing.
for i, n := range path {
if !l.Has(n) || (i != 0 && !l.HasEdgeBetween(path[i-1], n)) {
id := n.ID()
if 0 <= id && id < int64(len(l.Grid.open)) {
r, c := l.RowCol(n.ID())
b[r*(cols+1)+c] = '!'
}
return b, errors.New("grid: not a path in graph")
}
r, c := l.RowCol(n.ID())
switch i {
case len(path) - 1:
b[r*(cols+1)+c] = 'G'
case 0:
b[r*(cols+1)+c] = 'S'
default:
b[r*(cols+1)+c] = 'o'
}
}
return b, nil
}