Compare commits
2 commits
d6d0b03547
...
39253dfe5e
| Author | SHA1 | Date | |
|---|---|---|---|
| 39253dfe5e | |||
| f23ca49f82 |
7 changed files with 888 additions and 0 deletions
71
RouteFinding/05_tests.py
Normal file
71
RouteFinding/05_tests.py
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Define Searcher, hSLD, SearchNode.
|
||||||
|
class SearchNode:
|
||||||
|
def __init__(self, label, value):
|
||||||
|
self.label = label
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
class Searcher:
|
||||||
|
def __init__(self, mapfile):
|
||||||
|
self.mapfile = mapfile
|
||||||
|
# load the mapfile's structure
|
||||||
|
with open(self.mapfile) as mapper:
|
||||||
|
mapContents = mapper.read()
|
||||||
|
mapContents = mapContents.split("\n")
|
||||||
|
for mapIter in range(len(mapContents)):
|
||||||
|
mapContents[mapIter] = mapContents[mapIter].split(", ")
|
||||||
|
for firstDimMapIter in range(len(mapContents) - 1):
|
||||||
|
for secondDimMapIter in range(len(mapContents[firstDimMapIter])):
|
||||||
|
mapContents[firstDimMapIter][secondDimMapIter] = mapContents[firstDimMapIter][secondDimMapIter].strip("')(][")
|
||||||
|
mapContents.pop(len(mapContents) - 1)
|
||||||
|
for tupleIter in range(len(mapContents)):
|
||||||
|
mapContents[tupleIter] = tuple(mapContents[tupleIter])
|
||||||
|
# print(mapContents) # list of tuples
|
||||||
|
# print(mapContents[0]) # tuple of strings (data)
|
||||||
|
# [first node, second node, path cost, first node x, first node y, second node x, second node y]
|
||||||
|
# 0 1 2 3 4 5 6
|
||||||
|
# print(mapContents[0][0]) # data, string format
|
||||||
|
# make two dictionaries of nodes, iterating through list of tuples to make note of every node's paths and locations
|
||||||
|
nodeLoc = dict()
|
||||||
|
nodePaths = dict()
|
||||||
|
for nodeIter in range(len(mapContents)):
|
||||||
|
# these node locations are fine, because they should technically be in only one place.
|
||||||
|
nodeLoc[mapContents[nodeIter][0]] = tuple((mapContents[nodeIter][3], mapContents[nodeIter][4]))
|
||||||
|
nodeLoc[mapContents[nodeIter][1]] = tuple((mapContents[nodeIter][5], mapContents[nodeIter][6]))
|
||||||
|
# however, this is only taking the first node path it gets to...
|
||||||
|
# I probably need a list instead, that gets updated if the node is not in the node's child list.
|
||||||
|
nodePaths[mapContents[nodeIter][0]] = tuple((mapContents[nodeIter][1], mapContents[nodeIter][2]))
|
||||||
|
print(nodeLoc)
|
||||||
|
print(nodePaths)
|
||||||
|
|
||||||
|
# (b) Show your program loading in the 30-node sample file.
|
||||||
|
s=Searcher("30node.txt")
|
||||||
|
# (c) Show you program setting start node=U and end node=T.
|
||||||
|
s.setStartGoal('U','T')
|
||||||
|
# myViz should be a DRDViz instance -> save map to file on disk.
|
||||||
|
s.myViz.save("30node.png")
|
||||||
|
# (d) Show the one open node.
|
||||||
|
[n.showBasic() for n in s.open]
|
||||||
|
# (e) Show successors of only open node.
|
||||||
|
initial_children = s.successors(s.open.pop(0))
|
||||||
|
[n.showBasic() for n in initial_children]
|
||||||
|
|
||||||
|
# (f) Show three inserts: at the front, and the end, and "in order"
|
||||||
|
def reset_insert(where):
|
||||||
|
s.reset()
|
||||||
|
initial_children = s.successors(s.open.pop(0))
|
||||||
|
insert_method = getattr(s, "insert_"+where)
|
||||||
|
insert_method(initial_children)
|
||||||
|
return [n.showBasic() for n in s.open]
|
||||||
|
|
||||||
|
reset_insert("front")
|
||||||
|
reset_insert("end")
|
||||||
|
reset_insert("ordered")
|
||||||
|
|
||||||
|
# (g) INSERT (K,500), (C,91) and (J,10) and show no duplicates.
|
||||||
|
newdata = (("K",500), ("C",91), ("J",10))
|
||||||
|
newlist = [SearchNode(label=label, pathcost=pathcost) for label, pathcost in newdata]
|
||||||
|
ignored = s.insert_end(newlist)
|
||||||
|
[n.showBasic() for n in s.open]
|
||||||
|
|
||||||
|
# 3. hSLD heuritic function being called on three nodes.
|
||||||
|
[hSLD(x, s) for x in ("V", "AC", "J")]
|
||||||
54
RouteFinding/30node.txt
Normal file
54
RouteFinding/30node.txt
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
('N', 'AB', 299, [589, 414], [776, 1])
|
||||||
|
('U', 'H', 72, [227, 758], [294, 878])
|
||||||
|
('V', 'U', 185, [1, 712], [227, 758])
|
||||||
|
('S', 'N', 69, [502, 382], [589, 414])
|
||||||
|
('N', 'AA', 100, [589, 414], [481, 450])
|
||||||
|
('AA', 'S', 52, [481, 450], [502, 382])
|
||||||
|
('AA', 'B', 91, [481, 450], [394, 422])
|
||||||
|
('K', 'M', 138, [343, 838], [448, 925])
|
||||||
|
('H', 'K', 41, [294, 878], [343, 838])
|
||||||
|
('K', 'U', 78, [343, 838], [227, 758])
|
||||||
|
('U', 'C', 64, [227, 758], [316, 737])
|
||||||
|
('W', 'A', 84, [240, 428], [287, 473])
|
||||||
|
('V', 'W', 399, [1, 712], [240, 428])
|
||||||
|
('A', 'U', 337, [287, 473], [227, 758])
|
||||||
|
('Y', 'N', 166, [518, 543], [589, 414])
|
||||||
|
('I', 'Y', 272, [750, 737], [518, 543])
|
||||||
|
('I', 'F', 413, [750, 737], [470, 644])
|
||||||
|
('Y', 'AA', 62, [518, 543], [481, 450])
|
||||||
|
('P', 'F', 201, [520, 810], [470, 644])
|
||||||
|
('R', 'P', 176, [487, 971], [520, 810])
|
||||||
|
('M', 'P', 85, [448, 925], [520, 810])
|
||||||
|
('Q', 'E', 449, [569, 230], [328, 114])
|
||||||
|
('AB', 'Q', 477, [776, 1], [569, 230])
|
||||||
|
('Q', 'D', 63, [569, 230], [489, 259])
|
||||||
|
('D', 'E', 231, [489, 259], [328, 114])
|
||||||
|
('N', 'Q', 192, [589, 414], [569, 230])
|
||||||
|
('Q', 'S', 126, [569, 230], [502, 382])
|
||||||
|
('S', 'D', 102, [502, 382], [489, 259])
|
||||||
|
('AD', 'V', 599, [134, 238], [1, 712])
|
||||||
|
('AC', 'E', 227, [351, 298], [328, 114])
|
||||||
|
('D', 'AC', 134, [489, 259], [351, 298])
|
||||||
|
('S', 'AC', 131, [502, 382], [351, 298])
|
||||||
|
('B', 'AC', 119, [394, 422], [351, 298])
|
||||||
|
('B', 'AE', 48, [394, 422], [347, 470])
|
||||||
|
('AE', 'W', 84, [347, 470], [240, 428])
|
||||||
|
('K', 'X', 77, [343, 838], [418, 798])
|
||||||
|
('C', 'X', 119, [316, 737], [418, 798])
|
||||||
|
('X', 'F', 196, [418, 798], [470, 644])
|
||||||
|
('AC', 'O', 108, [351, 298], [225, 251])
|
||||||
|
('O', 'E', 219, [225, 251], [328, 114])
|
||||||
|
('C', 'J', 54, [316, 737], [359, 667])
|
||||||
|
('J', 'F', 64, [359, 667], [470, 644])
|
||||||
|
('AE', 'J', 266, [347, 470], [359, 667])
|
||||||
|
('U', 'J', 178, [227, 758], [359, 667])
|
||||||
|
('T', 'Y', 65, [455, 523], [518, 543])
|
||||||
|
('F', 'T', 99, [470, 644], [455, 523])
|
||||||
|
('J', 'T', 203, [359, 667], [455, 523])
|
||||||
|
('AE', 'T', 87, [347, 470], [455, 523])
|
||||||
|
('T', 'AA', 93, [455, 523], [481, 450])
|
||||||
|
('L', 'AC', 99, [179, 306], [351, 298])
|
||||||
|
('W', 'L', 171, [240, 428], [179, 306])
|
||||||
|
('L', 'O', 67, [179, 306], [225, 251])
|
||||||
|
('L', 'AD', 88, [179, 306], [134, 238])
|
||||||
|
('E', 'G', 100, [328, 114], [250, 75])
|
||||||
245
RouteFinding/DRDViz.py
Normal file
245
RouteFinding/DRDViz.py
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
# DRDViz is a powerful graph visualizer graph specifically for
|
||||||
|
# visualizing searches in a given search space. It it designed to be
|
||||||
|
# used an an "attached helper" to some sort of graph searching class.
|
||||||
|
# The overall idea is that you you create an instance of your graph
|
||||||
|
# searcher object, loading some map file. You then create yourself an
|
||||||
|
# instance of the DRDViz class, and load it with the same map file as
|
||||||
|
# your search class; the new DRDViz is stored in an instance variable
|
||||||
|
# of your graph searcher object. Each time your graph searcher does
|
||||||
|
# something significant e.g. setting start/edge node, exploring a new
|
||||||
|
# node in the search, or adding new siblings to the OPEN list of the
|
||||||
|
# search, it simply calls a method on the attached DRDViz to visually
|
||||||
|
# show the evolving search on the graphical map.
|
||||||
|
|
||||||
|
## Usage examples:
|
||||||
|
# x=DRDViz() # make yourself a new visualizer
|
||||||
|
# x.loadGraphFromFile('testfile.txt') # load it up from a map file
|
||||||
|
# x.plot() # make the plot of the road map appear
|
||||||
|
# x.markStart('A') # mark starting node A
|
||||||
|
# x.markGoal('D') # mark 'D' as a goal node
|
||||||
|
# x.exploreNode('J',['A','J']) # mark node 'J' as being currently explored, along with the path to it from A
|
||||||
|
# x.exploreEdges('G',['C','B']) # mark the edges coming from parent 'G' and going to children [C, B].
|
||||||
|
#
|
||||||
|
# x.reset() # reset the visualizer, e.g., in preparation for a new search.
|
||||||
|
#
|
||||||
|
# ##
|
||||||
|
|
||||||
|
__author__ = "Eck Doerry"
|
||||||
|
__copyright__ = "Copyright 2018, Northern Arizona University, Flagstaff AZ"
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import scipy.spatial as graph
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from node import Node
|
||||||
|
from edge import Edge
|
||||||
|
|
||||||
|
|
||||||
|
### GRAPHVIZ
|
||||||
|
# This class implements a graph visualizer. It can plot a graph, then provides methods to mark start/end nodes, and to
|
||||||
|
# color/re-color nodes and edges in various ways to visualize exploration of the space defined by the graph.
|
||||||
|
class DRDViz:
|
||||||
|
|
||||||
|
# to create a GraphVisualizer, you first create the blank visualizer (no args taken by INIT).
|
||||||
|
# You could then use either loadGraphFromFile() or loadGraphFromObjects() to suck in a map to visualize
|
||||||
|
# and create internal data structures for use in all other operations.
|
||||||
|
def __init__(self):
|
||||||
|
self.nodes=[]
|
||||||
|
self.edges=[]
|
||||||
|
self.nodeIndex={}
|
||||||
|
self.markedNodes=[]
|
||||||
|
self.markedEdges=[]
|
||||||
|
|
||||||
|
|
||||||
|
###### EXTERNAL INTERFACE METHODS ############
|
||||||
|
# The primary methods you call to use this visualizer
|
||||||
|
|
||||||
|
# Just resets the visualizer. Easiest to just close the graph and redraw it
|
||||||
|
def reset(self):
|
||||||
|
self.markedEdges = []
|
||||||
|
self.markedNodes = []
|
||||||
|
plt.close()
|
||||||
|
self.plot()
|
||||||
|
|
||||||
|
def save(self, out):
|
||||||
|
plt.savefig(out)
|
||||||
|
|
||||||
|
# A function to load up a graph to visualize. The filename specified by "infile" should contain a well-formed
|
||||||
|
# input file describing the graph. The format is a set of edge descriptions, one line per edge, of following format:
|
||||||
|
# (nodeLabe1, nodeLabel2, edgeLabel, [x1,y1],[x2,y2],[midx,midy])
|
||||||
|
# Reads each line and creates the appropriate node/edge objects.
|
||||||
|
def loadGraphFromFile(self, infile):
|
||||||
|
with open(infile) as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
cleanlines = [x.strip() for x in lines] #strip off any weird leading or trailing spaces.
|
||||||
|
for line in cleanlines:
|
||||||
|
line=line.replace('\'','').replace('[','').replace(']','').replace(' ','').strip('()')
|
||||||
|
rawEdge=line.split(',') # now just have nice clean comma-separated values. Make it a list
|
||||||
|
[l1,l2,label,x1,y1,x2,y2]=rawEdge # grab all the components of the edge
|
||||||
|
if l1 not in self.nodeIndex:
|
||||||
|
self.nodeIndex[l1]=(int(x1),int(y1))
|
||||||
|
self.nodes.append(Node(int(x1),int(y1),l1))
|
||||||
|
if l2 not in self.nodeIndex:
|
||||||
|
self.nodeIndex[l2] = (int(x2), int(y2))
|
||||||
|
self.nodes.append(Node(int(x2), int(y2), l2))
|
||||||
|
# Now build and Edge object for it
|
||||||
|
newEdge=Edge((int(x1),int(y1)), (int(x2),int(y2)), int(label))
|
||||||
|
newEdge.setLabels(l1,l2)
|
||||||
|
self.edges.append(newEdge)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# Function to ask a loaded graph to plot itself. If you're not in interactive Python mode, may need to call
|
||||||
|
# plot.show() to see it. Assumes that a graph has been loaded first!
|
||||||
|
def plot(self):
|
||||||
|
plt.close() # just in case previous graphs were open!
|
||||||
|
plt.ion # turn on interactive mode. Not sure if it's critical
|
||||||
|
# first plot the nodes
|
||||||
|
plt.plot([x.x for x in self.nodes], [x.y for x in self.nodes], 'ko',
|
||||||
|
color="#BBBCBD") # plot vertices circles first
|
||||||
|
for node in self.nodes: # now plot the labels
|
||||||
|
plt.text(node.x, node.y, node.label, color='b', weight='normal')
|
||||||
|
# Now plot in the edges
|
||||||
|
for edge in self.edges:
|
||||||
|
plt.plot([edge.x1, edge.x2], [edge.y1, edge.y2], 'b-', color='k', linewidth=0.5) # make the lines
|
||||||
|
plt.text(edge.midx, edge.midy, edge.label, size='x-small') # label the edge at its midpoint
|
||||||
|
self.paintGraph()
|
||||||
|
|
||||||
|
|
||||||
|
# Takes in start node label, and repaints that node as start (ie, different node shape and green)
|
||||||
|
start_color = 'c' #cyan https://matplotlib.org/stable/tutorials/colors/colors.html
|
||||||
|
def markStart(self,startLabel):
|
||||||
|
#self.paintNode(startLabel, self.start_color, weight='bold') # paint label of start node
|
||||||
|
self.plotVertex(startLabel, 'D', self.start_color) # give start node a vertex
|
||||||
|
self.paintGraph()
|
||||||
|
|
||||||
|
# Takes in end node label, and repaints that node as end (ie, different node shape and red)
|
||||||
|
def markGoal(self,goalLabel):
|
||||||
|
#self.paintNode(goalLabel, 'r', weight='bold') # paint label of endnode red
|
||||||
|
self.plotVertex(goalLabel, 'D', 'r') # give end node a different label shape/color
|
||||||
|
self.paintGraph() # force a repaint
|
||||||
|
|
||||||
|
|
||||||
|
# ExploreNode is a major method, called each time a new node is opened for exploration.
|
||||||
|
# takes in: the label of the next node to explore, and the path (list of nodelabels) to that node.
|
||||||
|
# it uses the latter to highlight the path leading to that node on the graph...could pass it an empty list
|
||||||
|
# if you don't want that function.
|
||||||
|
# First it "cleans up" the graph from the last round of exploration: it unmarks the parent node,
|
||||||
|
# and also unmarked all edges painted in the previous round by exploreEdges() and paintPath()
|
||||||
|
# It then resets self.paintedEdges and is ready to start the new cycle by painting the node being
|
||||||
|
# explored.
|
||||||
|
def exploreNode(self,nodeLabel,nodePath):
|
||||||
|
# First clean up. Unmark highlights nodes/edges from last step
|
||||||
|
for edge in self.markedEdges: # first unmark any edges painted in last steps
|
||||||
|
self.paintEdge(edge[0],edge[1],color='k')
|
||||||
|
if self.markedNodes: # if there was a previous node explored, unmark it
|
||||||
|
self.paintNode(self.markedNodes[0], color='k')
|
||||||
|
self.markedEdges = []
|
||||||
|
# Ok, now highlight the newly explored node and its path
|
||||||
|
self.paintNode(nodeLabel, color='r')
|
||||||
|
self.paintPath(nodePath, color='r') # mark path to current node in red
|
||||||
|
self.markedNodes= [nodeLabel] # add node to explored nodes list
|
||||||
|
self.paintGraph() # force a repaint
|
||||||
|
|
||||||
|
|
||||||
|
# ExploreEdges extends the explored frontier out from some parent node to its children.
|
||||||
|
# take in a parent node label, and a set of children node labels.
|
||||||
|
# it first paints all previously explored edges a neutral color, then paints the new
|
||||||
|
# edges to highlight them.
|
||||||
|
def exploreEdges(self,parentLabel, childLabelList):
|
||||||
|
for newDest in childLabelList:
|
||||||
|
#print("Painting: "+parentLabel+" to "+newDest)
|
||||||
|
self.markedEdges += [(parentLabel,newDest)] # add the new edge to explored edges
|
||||||
|
self.paintEdge(parentLabel,newDest,color='m') # paint edge to highlight
|
||||||
|
self.paintNode(newDest,color='g') # color new dest node as part of frontier
|
||||||
|
self.paintGraph() # to redraw the graph!
|
||||||
|
|
||||||
|
# paintPath takes in a contiguous path of node labels and paints it (default=red).
|
||||||
|
def paintPath(self, nodePath, color='r'):
|
||||||
|
last = ""
|
||||||
|
for node in nodePath:
|
||||||
|
if last:
|
||||||
|
self.paintEdge(last, node, color=color)
|
||||||
|
self.markedEdges += [(last, node)]
|
||||||
|
last = node
|
||||||
|
else:
|
||||||
|
last = node
|
||||||
|
|
||||||
|
|
||||||
|
############ INTERNAL UTILITY METHODS ######################
|
||||||
|
## Don't really need to call these to use DRDViz. These methods are called by the External Interface
|
||||||
|
## methods listed above to get the job done.
|
||||||
|
## Also include various little methods useful for debugging or other purposes.
|
||||||
|
|
||||||
|
# Takes in a node label and color. If node exists, paint its label that color
|
||||||
|
def paintNode(self,nodeLabel,color='r',weight='normal', size='medium'):
|
||||||
|
p1=self.nodeLoc(nodeLabel)
|
||||||
|
if p1:
|
||||||
|
plt.text(p1[0], p1[1], nodeLabel, color=color, size=size, weight=weight) # repaint the label!
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
print("PaintNode: No node labeled "+nodeLabel+" exists.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Load a graph given a list of nodes object and a list of edge objects. Useful convenience when used in
|
||||||
|
# conjunction with a GraphMaker, allow you to just grab the Edge objects directly from the GraphMaker, rather
|
||||||
|
# than exporting them to a file first and then reloading them with loadGraphFromFile()
|
||||||
|
def loadGraphFromObjects(self, nodes, edges):
|
||||||
|
self.nodes = nodes
|
||||||
|
self.edges = edges
|
||||||
|
|
||||||
|
# Useful if some outside object wants to get a list of edges in the visualization.
|
||||||
|
# Just returns a big list of edges in form (endlabel,endlabel, edgelabel,[x1,y1],[x2,y2])
|
||||||
|
def getSearchSpace(self):
|
||||||
|
result = []
|
||||||
|
for edge in self.edges:
|
||||||
|
result.append((edge.endlabel1, edge.endlabel2, edge.label, edge.p1, edge.p2))
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Just returns a graphed node object based on its label
|
||||||
|
def getNodeByLabel(self, nodeLabel):
|
||||||
|
for node in self.nodes:
|
||||||
|
if node.label == nodeLabel: return node
|
||||||
|
print("getNodebyLabel: No node labeled " + nodeLabel + " exists")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# a function that repaints (replots) an edge on the graphical plot
|
||||||
|
# take in to node labels and optionally, a color
|
||||||
|
def paintEdge(self, startLabel, endLabel, color='r'):
|
||||||
|
p1 = self.nodeLoc(startLabel)
|
||||||
|
p2 = self.nodeLoc(endLabel)
|
||||||
|
plt.plot([p1[0], p2[0]], [p1[1], p2[1]], color=color, linewidth=1) # repaint the edge
|
||||||
|
|
||||||
|
## Main External interface functions for using a RoadGraph
|
||||||
|
def paintGraph(self):
|
||||||
|
plt.pause(0.05)
|
||||||
|
|
||||||
|
# Job is to (re)plot a node vertex in different shape/color. Takes in node label, a new vertex shape code,
|
||||||
|
# and a new color, and redraws the vertex associated with that node accordingly.
|
||||||
|
def plotVertex(self,nodeLabel,mtype,color):
|
||||||
|
loc= self.nodeLoc(nodeLabel)
|
||||||
|
plt.plot(loc[0],loc[1],color+mtype)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def nodeLoc(self,label): # finds and returns the location of node labeled 'label'
|
||||||
|
for node in self.nodes:
|
||||||
|
if node.label==label: return [node.x,node.y]
|
||||||
|
print("nodeLoc: No node with label "+label+" exists")
|
||||||
|
|
||||||
|
|
||||||
|
#mainly for debugging: Functions to have the graph display its edges and nodes
|
||||||
|
def show(self):
|
||||||
|
print("Here is the graph:")
|
||||||
|
print("Nodes: ")
|
||||||
|
print([x.get() for x in self.nodes])
|
||||||
|
print("Edges: ")
|
||||||
|
print([x.get() for x in self.edges])
|
||||||
|
|
||||||
|
def die(self): # just kills any open graph
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
x=DRDViz()
|
||||||
|
x.loadGraphFromFile("test1.txt")
|
||||||
|
|
||||||
|
DRDViz_class = DRDViz
|
||||||
59
RouteFinding/edge.py
Normal file
59
RouteFinding/edge.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
## The EDGE class is just a utility class for the GraphViz and GraphMaker classes. It simply represents an edge in
|
||||||
|
# a graph being constructed, capturing the endpoints of the edge as well as its label. Its biggest active
|
||||||
|
# contribution is that it is able to compute the edge label based on some distortion percentage (between 0-1) that is
|
||||||
|
# passed into the constructor as the "label".
|
||||||
|
|
||||||
|
__author__ = "Eck Doerry"
|
||||||
|
__copyright__ = "Copyright 2018, Northern Arizona University, Flagstaff AZ"
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import scipy.spatial as graph
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
class Edge:
|
||||||
|
|
||||||
|
# Makes a new edge. You pass in two points (x,y tuples) plus a label. Label is used flexibly to determine
|
||||||
|
# how the edge is labeled. If label=0, then edge is just labeled with exact straightline distance between
|
||||||
|
# the points. If label is a value between 0-1, this is viewed as a percentage to distort the distance by.
|
||||||
|
# If label > 1, then is taken as an actual value that you want placed on that edge.
|
||||||
|
def __init__(self, p1,p2, label=0):
|
||||||
|
self.p1=p1
|
||||||
|
self.p2=p2
|
||||||
|
self.x1=p1[0]
|
||||||
|
self.y1=p1[1]
|
||||||
|
self.x2=p2[0]
|
||||||
|
self.y2=p2[1]
|
||||||
|
self.dist= int(graph.distance.euclidean(p1,p2)) # holds straight cartesian dist between points!
|
||||||
|
self.midx = abs(p1[0] - p2[0]) / 2 + min(p1[0], p2[0])
|
||||||
|
self.midy = abs(p1[1] - p2[1]) / 2 + min(p1[1], p2[1])
|
||||||
|
self.endlabel1=""
|
||||||
|
self.endlabel2=""
|
||||||
|
self.label=0 # holds the actual edge value of the edge. Calc'd by distorting the distance
|
||||||
|
|
||||||
|
# if a distortion was passed in, randomly distort edge length appropriately.
|
||||||
|
if label==0: self.label=self.dist # default. Edge label is its length
|
||||||
|
elif label<1: # label was a distortion percentage. randomly calc length based on it.
|
||||||
|
adjust=self.dist*label
|
||||||
|
distortion= np.random.random()*adjust # randomly take some percentage of the adjustment range
|
||||||
|
self.label = int(self.dist + distortion) # adds on distortion. Roads are ALWAYS longer than SLD
|
||||||
|
else: self.label=label # label was an actual number you want on the edge
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
return ([self.x1,self.y1],[self.x2,self.y2],self.label,[self.midx,self.midy])
|
||||||
|
|
||||||
|
def midpt(self):
|
||||||
|
return([self.midx,self.midy])
|
||||||
|
|
||||||
|
# Returns true if the edge connects the two labels. Have to check both directions!
|
||||||
|
def connects(self,label1,label2):
|
||||||
|
if (label1 == self.endlabel1 and label2 == self.endlabel2) or (label1 == self.endlabel2 and label2 == self.endlabel1):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def setLabels(self,l1,l2): # just stored the node labels at the ends of the edge. For convenience mostly.
|
||||||
|
self.endlabel1=l1
|
||||||
|
self.endlabel2=l2
|
||||||
|
|
||||||
|
# Unit test main fn
|
||||||
|
# x= Edge((23,56),(34,45))
|
||||||
|
# print("created an edge: "+ str(x.get()))
|
||||||
265
RouteFinding/graphmaker.py
Normal file
265
RouteFinding/graphmaker.py
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
|
||||||
|
## INTRO. Purpose of the GraphMaker class is to generate random graphs; these graphs can then also be edited, adding
|
||||||
|
# or deleting edges while visually inspecting the evolving map. This makes it quick and easy to create custom "road
|
||||||
|
# networks" to use as search spaces for developing and visualizing the behavior of searching algorithms.
|
||||||
|
#
|
||||||
|
# You ask it to create a graph, you can optionally ask it to
|
||||||
|
# plot that graph for you for visual verification purposes. You can then edit the graph as desired, quickly adding
|
||||||
|
# or deleting edge. Finall, you can export that graph as a simple text file of Edges
|
||||||
|
# for use in other programs.
|
||||||
|
#
|
||||||
|
# The class has multiple parameters that you can set to fine tune the nature of the graphs produced. For now, these are
|
||||||
|
# just set by editing the values in the __init__ function below. For instance,
|
||||||
|
# you can specify edge distance distortion, percentage of edges to drop (to make sparse graphs), min. distance between
|
||||||
|
# nodes generated, etc. See comments in the init function below
|
||||||
|
#
|
||||||
|
## Usage examples:
|
||||||
|
## x=GraphMaker(20) # create a new random 20 node map.
|
||||||
|
#
|
||||||
|
# x.plot() # now graphically show it
|
||||||
|
# x.edit() # enter edit mode. It repeatedly asks you to add/delete edges.
|
||||||
|
# x.export('outfile') # exports the current map to the file "outfile.txt" in the working dir
|
||||||
|
|
||||||
|
# TO-DO: implement a load-graph-from-file to *reload* a graph after saving...in case you want to edit and re-save
|
||||||
|
|
||||||
|
__author__ = "Eck Doerry"
|
||||||
|
__copyright__ = "Copyright 2018, Northern Arizona University, Flagstaff AZ"
|
||||||
|
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import scipy.spatial as graph
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from node import Node
|
||||||
|
from edge import Edge
|
||||||
|
|
||||||
|
class GraphMaker:
|
||||||
|
|
||||||
|
def __init__(self,nodeCount=10):
|
||||||
|
self.numNodes=nodeCount # number of nodes to create in graph.
|
||||||
|
self.expansion=2 # how wide the graph will be. Basically a factor that increases the x-y extents of plane
|
||||||
|
self.mindist = 0.3 * self.expansion # the minimum separation distance between nodes. Prevents cluttered graphs
|
||||||
|
# distDistortion. Often you might want to have a graph where the distances are *based on* the actual
|
||||||
|
# distances, but not exactly that. distDistortion is a percentage within which the cartesian distances
|
||||||
|
# between points will be randomly distorted while labeling the new edges in the graph. So at 0.3, 30% of the
|
||||||
|
# distance will be randomly distorted between 0 and 100%. So you could lose/gain 30% distance, randomly.
|
||||||
|
self.distDistortion = 0.5 # usually 0.3 is nice
|
||||||
|
# edgeLoss: in a roadmap, we don't want edges between EVERY point to EVERY other point. Hence we randomly drop some edges
|
||||||
|
# the edgeLoss specifies the percentage of edges we should randomly drop
|
||||||
|
self.edgeLoss= 0.1
|
||||||
|
self.nodes=[] # array of node objects in this graph
|
||||||
|
self.edges=[] # array of edges in this graph
|
||||||
|
self.buildGraph() # Build the new graph! Commented out. Call it separately after creation!
|
||||||
|
|
||||||
|
# if you don't like the graph generated, just try another one!
|
||||||
|
def redo(self,numNodes=-1):
|
||||||
|
self.edges=[]
|
||||||
|
self.nodes=[]
|
||||||
|
if numNodes>0: self.numNodes=numNodes
|
||||||
|
plt.close()
|
||||||
|
self.buildGraph()
|
||||||
|
self.plot()
|
||||||
|
|
||||||
|
# Lets you interactively edit a graph to whack out edges
|
||||||
|
def edit(self):
|
||||||
|
entry=""
|
||||||
|
print("Edit Mode: {k/b}a,b to kill/build an edge between a,b ; r to replot, q to quit")
|
||||||
|
while True:
|
||||||
|
plt.pause(0.05)
|
||||||
|
entry=input("What now?")
|
||||||
|
entry = entry.strip()
|
||||||
|
if entry=='r':
|
||||||
|
self.replot()
|
||||||
|
elif entry=='q': break
|
||||||
|
else:
|
||||||
|
command=entry[0]
|
||||||
|
[start,end]=entry[1:].split(',')
|
||||||
|
if command=='b': self.buildEdge(start,end)
|
||||||
|
else: self.whackEdge(start,end)
|
||||||
|
print("End Edit mode")
|
||||||
|
|
||||||
|
|
||||||
|
# This method lets one whack out edges. So you can edit away edges after you generate the graph!
|
||||||
|
def whackEdge(self,node1,node2):
|
||||||
|
node1=node1.upper()
|
||||||
|
node2=node2.upper()
|
||||||
|
newEdges=[]
|
||||||
|
for edge in self.edges:
|
||||||
|
if edge.connects(node1,node2): # whack the edge
|
||||||
|
plt.plot([edge.x1, edge.x2], [edge.y1, edge.y2], 'b-', color='w', linewidth=0.5) # make the lines
|
||||||
|
plt.text(edge.midx, edge.midy, edge.label, size='x-small', color='w') # label the edge at its midpoint
|
||||||
|
plt.pause(0.05)
|
||||||
|
else: newEdges.append(edge)
|
||||||
|
self.edges=newEdges
|
||||||
|
|
||||||
|
def buildEdge(self,node1,node2):
|
||||||
|
node1 = node1.upper()
|
||||||
|
node2 = node2.upper()
|
||||||
|
for edge in self.edges: # avoid building redundant new edges!
|
||||||
|
if edge.connects(node1,node2):
|
||||||
|
print("already an edge from "+node1+" to "+node2)
|
||||||
|
return
|
||||||
|
p1=self.nodeLocation(node1)
|
||||||
|
p2=self.nodeLocation(node2)
|
||||||
|
if p1!=0 and p2!=0: # both nodes exist
|
||||||
|
print("building edge!")
|
||||||
|
edge=Edge(p1,p2, self.distDistortion) # make a new edge
|
||||||
|
edge.setLabels(node1,node2)
|
||||||
|
self.edges.append(edge)
|
||||||
|
plt.plot([edge.x1, edge.x2], [edge.y1, edge.y2], 'b-', color='k', linewidth=0.5) # make the lines
|
||||||
|
plt.text(edge.midx, edge.midy, edge.label, size='x-small',color='k') # label the edge at its midpoint
|
||||||
|
else: print("start or end node doesn't exist")
|
||||||
|
|
||||||
|
|
||||||
|
# Just kill and redraw the plot. Useful after you've edited the graph
|
||||||
|
def replot(self):
|
||||||
|
plt.close()
|
||||||
|
self.plot()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
# MAIN WORKER: Does the actual work of building a Roadmap.
|
||||||
|
# The biggest challenge here is linking the given points with a *non-intersecting* set of edges. In other words,
|
||||||
|
# we would like a "planar graph" imposed on these points. This is what Delauney function gives us: it basically
|
||||||
|
# finds non-overlapping triangles that cover all points; any one point is often a vertex in multiple triagles of
|
||||||
|
# the "puzzle" of triangles produced. We then need to extract unique edges and the nodes from this.
|
||||||
|
def buildGraph(self):
|
||||||
|
pointsArray = self.genPoints() # makes the array of (x,y) points to connect
|
||||||
|
tri=graph.Delaunay(pointsArray) # creates a planar graph on the given points. Creates a tri data structure.
|
||||||
|
triangles=pointsArray[tri.simplices] # list of triplets of three points. Each set of three points marks a
|
||||||
|
# triangle laid out by Delauney. Next need to extract the edges (point-to-point connections) from this.
|
||||||
|
# Now extract the unique edges from all the triangles to generate all edges in the graph
|
||||||
|
self.makeEdges(triangles) # populates self.edges with Edge objects
|
||||||
|
# Finally, turn the points array into actual Node objects.
|
||||||
|
Node.reset(Node) # Starts Node labeling at 'A'
|
||||||
|
self.nodes=list(map(Node,pointsArray[:,0], pointsArray[:,1])) #populates self.nodes with Node objects
|
||||||
|
self.labelEdges() # Just to make edge objects complete, go back and add endlabel (nodelabels) to those objects
|
||||||
|
|
||||||
|
|
||||||
|
# Makes all the edges in the new graph, based on the array of tris (basically triagles) produced by Delauney and
|
||||||
|
# pass in. Appends the new edges to the "edges" array.
|
||||||
|
def makeEdges(self,triArray):
|
||||||
|
edges = [] # temp array holding unique ((x,y),(x1,y1)) edge segments found so far
|
||||||
|
|
||||||
|
# an important helper. When we extract a new edges from a triagle, it might well be the duplicate
|
||||||
|
# of an edge from an adjacent triangle created by Delauney.
|
||||||
|
def checkAddAdj(adjs):
|
||||||
|
for adj in adjs:
|
||||||
|
if (adj in edges) or ((adj[1], adj[0]) in edges): # if edges already in there, skip it
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
edges.append((adj[0], adj[1]))
|
||||||
|
# go through the triangle array, extract each edge, check that we don't have it already, and add to edges.
|
||||||
|
for triplet in triArray:
|
||||||
|
s = list(map(list,triplet))
|
||||||
|
adjs = [(s[0], s[1]), (s[1], s[2]), (s[2], s[0])] # extracting the arcs between the points
|
||||||
|
checkAddAdj(adjs)
|
||||||
|
|
||||||
|
# okay, now edges array contains all unique edge segment. Now make edge objects out of them!
|
||||||
|
# use edgeLoss value to randomly drop some pct of Edges
|
||||||
|
for edge in edges:
|
||||||
|
if (np.random.rand() > self.edgeLoss):
|
||||||
|
self.edges.append(Edge(edge[0],edge[1],self.distDistortion)) #make Edge object
|
||||||
|
else: print("blip")
|
||||||
|
|
||||||
|
# For each edge, finds the label for its endpoints in the nodes, and adds to edge object.
|
||||||
|
def labelEdges(self):
|
||||||
|
for edge in self.edges:
|
||||||
|
edge.endlabel1=self.getNodeAtXY(edge.p1).label
|
||||||
|
edge.endlabel2=self.getNodeAtXY(edge.p2).label
|
||||||
|
|
||||||
|
# Function to ask the roadgraph to plot itself. If not interactive, need to call plot.show() to see it.
|
||||||
|
def plot(self):
|
||||||
|
plt.ion # turn on interactive mode. Not sure if it's critical
|
||||||
|
# first plot the nodes
|
||||||
|
plt.plot([x.x for x in self.nodes], [x.y for x in self.nodes], 'ko', color="#BBBCBD") # plot vertices circles first
|
||||||
|
for node in self.nodes: # now plot the labels
|
||||||
|
plt.text(node.x, node.y, node.label, color='b', size='large', weight='normal')
|
||||||
|
# Now plot in the edges
|
||||||
|
for edge in self.edges:
|
||||||
|
plt.plot([edge.x1,edge.x2],[edge.y1,edge.y2], 'b-', color='k', linewidth=0.5) # make the lines
|
||||||
|
plt.text(edge.midx,edge.midy,edge.label, size='x-small') # label the edge at its midpoint
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Function to return the roadgraph as simple list that can be fed to search program.
|
||||||
|
# prints it out, one line per edge. Format (nodeLabel1, nodeLabel2, edgeLabel, ((x1,y1),(x2,y2),(midx,midy)) )
|
||||||
|
# if you pass it a text string, it will print to that, else prints to console
|
||||||
|
def export(self,outfile=0):
|
||||||
|
if outfile != 0:
|
||||||
|
outfilename=outfile+".txt"
|
||||||
|
out=open(outfilename,'w')
|
||||||
|
for edge in self.edges:
|
||||||
|
theEdge=((self.getNodeAtXY(edge.p1).label,self.getNodeAtXY(edge.p2).label,edge.label))
|
||||||
|
locInfo=((edge.p1,edge.p2))
|
||||||
|
if outfile:
|
||||||
|
out.write(str(theEdge+locInfo)+"\n")
|
||||||
|
else:
|
||||||
|
print(theEdge+locInfo)
|
||||||
|
if outfile: out.close()
|
||||||
|
print("tada!")
|
||||||
|
|
||||||
|
|
||||||
|
# Returns a node that is located at a given point
|
||||||
|
def getNodeAtXY(self,aPoint): # gets the Node at location x-y of point
|
||||||
|
for node in self.nodes:
|
||||||
|
if (node.x==aPoint[0]) and (node.y==aPoint[1]): return node
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Just returns x-y location of a node, given its label
|
||||||
|
def nodeLocation(self,label): # finds and returns the location of node labeled 'label'
|
||||||
|
for node in self.nodes:
|
||||||
|
if node.label==label: return [node.x,node.y]
|
||||||
|
print("nodeLoc: No node with label "+label+" exists")
|
||||||
|
|
||||||
|
# a key function. Generates a random set of points that are some minimum distance apart.
|
||||||
|
def genPoints(self):
|
||||||
|
# While you still need points, generate random points, add to set if min dist from all currently found points.
|
||||||
|
points = [] # An array to put your points in as you find them.
|
||||||
|
found = 0 # Tracks points found so far
|
||||||
|
|
||||||
|
# Simple function that checks that candidate points are some min dist from all other points.
|
||||||
|
# keeps it from making cluttered graphs!
|
||||||
|
def checkpt(apoint, allpoints):
|
||||||
|
for point in allpoints:
|
||||||
|
dst = graph.distance.euclidean(point, apoint)
|
||||||
|
if (dst < self.mindist): return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
while found < self.numNodes:
|
||||||
|
p1 = self.expansion * np.random.randn(2)
|
||||||
|
if checkpt(p1, points):
|
||||||
|
points.append(p1)
|
||||||
|
found = found + 1
|
||||||
|
# print("found one:"+str(found))
|
||||||
|
if len(points) == 0: points.append(p1)
|
||||||
|
|
||||||
|
# Now just turn the points into a numpy array
|
||||||
|
pArray = np.array(points)
|
||||||
|
# print(points)
|
||||||
|
|
||||||
|
# AWESOME. Now shift all the points into the positive x-y quadrant
|
||||||
|
xmin = min(pArray[:, [0]])[0]
|
||||||
|
ymin = min(pArray[:, [1]])[0]
|
||||||
|
xshift = abs(xmin) + 1
|
||||||
|
yshift = abs(ymin) + 1
|
||||||
|
posArray = pArray + [xshift, yshift]
|
||||||
|
posArray = posArray.astype(int) # truncate the entire array of points into integer values. Easier!
|
||||||
|
return posArray
|
||||||
|
|
||||||
|
|
||||||
|
#mainly for debugging: Functions to have the graph display its edges and nodes
|
||||||
|
def show(self):
|
||||||
|
print("Here is the graph:")
|
||||||
|
print("Nodes: ")
|
||||||
|
print([x.get() for x in self.nodes])
|
||||||
|
print("Edges: ")
|
||||||
|
print([x.get() for x in self.edges])
|
||||||
|
|
||||||
|
def die(self): # just kills any open graph
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
### UNIT TESTING
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
x=GraphMaker(sys.argv[1])
|
||||||
|
x.export(sys.argv[2])
|
||||||
66
RouteFinding/node.py
Normal file
66
RouteFinding/node.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
|
||||||
|
'''Node.py: This class implements simple node object and associated methods.
|
||||||
|
Nodes are created by passing in an x,y cartesian location for the node. Node labels are created
|
||||||
|
by just assigning letters to nodes in ascending ASCII order; current index is kept as class variable
|
||||||
|
to ensure that each new node has a unique label.
|
||||||
|
The most interesting internal method here is makeLabel, which generates unique labels. We want easy to read
|
||||||
|
alpha labels...so it basically counts up in Base26, generating the corresponding ASCII labels. Tricky!
|
||||||
|
'''
|
||||||
|
|
||||||
|
__author__ = "Eck Doerry"
|
||||||
|
__copyright__ = "Copyright 2018, Northern Arizona University, Flagstaff AZ"
|
||||||
|
|
||||||
|
import scipy.spatial as graph
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
class Node:
|
||||||
|
|
||||||
|
# CLASS VAR: curIndex is used to make sure that all nodes have a unique label. Incremented every time a new
|
||||||
|
# node is generated.
|
||||||
|
curIndex=1
|
||||||
|
|
||||||
|
# CLASS METHOD. Allows for reseting the index counter for generating new nodes. Used only for debugging!
|
||||||
|
def reset(cls):
|
||||||
|
cls.curIndex=1
|
||||||
|
|
||||||
|
# Create a new node at point x-y. Optionally pass in a label for the node. If no explicit label is passed
|
||||||
|
# in (which is usually the case), it auto-generates the next label in a sequences starting with A-Z, then
|
||||||
|
# going to AA, AB, AC, etc.
|
||||||
|
def __init__(self,newX,newY, newLabel= '*'):
|
||||||
|
self.x=newX
|
||||||
|
self.y=newY
|
||||||
|
if newLabel=='*':
|
||||||
|
self.label=self.makeLabel()
|
||||||
|
else:
|
||||||
|
self.label=newLabel
|
||||||
|
|
||||||
|
# Makes an upper case label starting at 'A'. If >26 nodes, goes to 'AB', 'AC', etc.
|
||||||
|
# Works by basically counting in Base26: converts the index into strings in Base26!
|
||||||
|
def makeLabel(self):
|
||||||
|
index=self.curIndex
|
||||||
|
out = []
|
||||||
|
while index>25:
|
||||||
|
out.append(index % 26)
|
||||||
|
index = int(index / 26)
|
||||||
|
|
||||||
|
out.append(index-1)
|
||||||
|
out.reverse()
|
||||||
|
Node.curIndex +=1
|
||||||
|
return "".join([chr(65+x) for x in out])
|
||||||
|
|
||||||
|
|
||||||
|
# Distance: returns the cartesian distance between this node some other node that is passed in.
|
||||||
|
def distance(self,aNode):
|
||||||
|
return graph.distance.euclidean([self.x,self.y],[aNode.x,aNode.y])
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
return ([self.x,self.y],self.label)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Unit tests
|
||||||
|
# x=Node(4,5)
|
||||||
|
# y=Node(1,2)
|
||||||
|
# print(x.get())
|
||||||
|
# print(y.get())
|
||||||
|
|
||||||
128
RouteFinding/samplemap-1.txt
Normal file
128
RouteFinding/samplemap-1.txt
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
('AS', 'J', 99, [208, 215], [204, 116])
|
||||||
|
('J', 'T', 25, [204, 116], [227, 105])
|
||||||
|
('T', 'AS', 111, [227, 105], [208, 215])
|
||||||
|
('M', 'AS', 49, [159, 212], [208, 215])
|
||||||
|
('AS', 'AD', 79, [208, 215], [138, 252])
|
||||||
|
('AD', 'M', 45, [138, 252], [159, 212])
|
||||||
|
('AY', 'AI', 24, [33, 202], [56, 195])
|
||||||
|
('AI', 'AD', 99, [56, 195], [138, 252])
|
||||||
|
('AD', 'AY', 116, [138, 252], [33, 202])
|
||||||
|
('O', 'J', 34, [181, 142], [204, 116])
|
||||||
|
('AS', 'O', 77, [208, 215], [181, 142])
|
||||||
|
('AJ', 'AV', 18, [172, 57], [176, 75])
|
||||||
|
('AV', 'K', 32, [176, 75], [145, 84])
|
||||||
|
('K', 'AJ', 38, [145, 84], [172, 57])
|
||||||
|
('AA', 'AJ', 27, [145, 54], [172, 57])
|
||||||
|
('K', 'AA', 30, [145, 84], [145, 54])
|
||||||
|
('AK', 'M', 60, [110, 176], [159, 212])
|
||||||
|
('AI', 'AK', 57, [56, 195], [110, 176])
|
||||||
|
('M', 'AN', 47, [159, 212], [163, 165])
|
||||||
|
('AN', 'AS', 67, [163, 165], [208, 215])
|
||||||
|
('AN', 'O', 29, [163, 165], [181, 142])
|
||||||
|
('AL', 'N', 53, [77, 80], [67, 27])
|
||||||
|
('N', 'S', 17, [67, 27], [84, 31])
|
||||||
|
('S', 'AL', 49, [84, 31], [77, 80])
|
||||||
|
('AW', 'N', 47, [107, 1], [67, 27])
|
||||||
|
('N', 'AX', 66, [67, 27], [1, 19])
|
||||||
|
('AX', 'AW', 107, [1, 19], [107, 1])
|
||||||
|
('AW', 'S', 37, [107, 1], [84, 31])
|
||||||
|
('G', 'X', 28, [199, 81], [226, 72])
|
||||||
|
('AW', 'D', 26, [107, 1], [132, 9])
|
||||||
|
('D', 'S', 52, [132, 9], [84, 31])
|
||||||
|
('D', 'AA', 46, [132, 9], [145, 54])
|
||||||
|
('AA', 'S', 65, [145, 54], [84, 31])
|
||||||
|
('AW', 'H', 65, [107, 1], [172, 1])
|
||||||
|
('H', 'D', 40, [172, 1], [132, 9])
|
||||||
|
('Y', 'C', 26, [98, 75], [94, 101])
|
||||||
|
('C', 'AL', 27, [94, 101], [77, 80])
|
||||||
|
('AL', 'Y', 21, [77, 80], [98, 75])
|
||||||
|
('S', 'Y', 46, [84, 31], [98, 75])
|
||||||
|
('AA', 'Y', 51, [145, 54], [98, 75])
|
||||||
|
('A', 'B', 24, [92, 145], [95, 121])
|
||||||
|
('C', 'I', 32, [94, 101], [108, 130])
|
||||||
|
('B', 'C', 20, [95, 121], [94, 101])
|
||||||
|
('AK', 'U', 16, [110, 176], [122, 165])
|
||||||
|
('U', 'M', 59, [122, 165], [159, 212])
|
||||||
|
('AN', 'U', 41, [163, 165], [122, 165])
|
||||||
|
('U', 'AQ', 19, [122, 165], [140, 157])
|
||||||
|
('AQ', 'AN', 24, [140, 157], [163, 165])
|
||||||
|
('AU', 'AK', 26, [89, 160], [110, 176])
|
||||||
|
('AI', 'AU', 48, [56, 195], [89, 160])
|
||||||
|
('AV', 'AF', 19, [176, 75], [163, 90])
|
||||||
|
('AF', 'K', 18, [163, 90], [145, 84])
|
||||||
|
('AT', 'AP', 35, [30, 84], [58, 105])
|
||||||
|
('AP', 'AR', 51, [58, 105], [7, 103])
|
||||||
|
('AR', 'AT', 29, [7, 103], [30, 84])
|
||||||
|
('AT', 'AL', 47, [30, 84], [77, 80])
|
||||||
|
('AL', 'AP', 31, [77, 80], [58, 105])
|
||||||
|
('W', 'C', 19, [76, 108], [94, 101])
|
||||||
|
('B', 'W', 23, [95, 121], [76, 108])
|
||||||
|
('W', 'AP', 18, [76, 108], [58, 105])
|
||||||
|
('AL', 'W', 28, [77, 80], [76, 108])
|
||||||
|
('X', 'R', 36, [226, 72], [196, 52])
|
||||||
|
('R', 'AH', 20, [196, 52], [208, 36])
|
||||||
|
('AH', 'X', 40, [208, 36], [226, 72])
|
||||||
|
('G', 'R', 29, [199, 81], [196, 52])
|
||||||
|
('R', 'H', 56, [196, 52], [172, 1])
|
||||||
|
('H', 'AH', 50, [172, 1], [208, 36])
|
||||||
|
('R', 'AJ', 24, [196, 52], [172, 57])
|
||||||
|
('AJ', 'H', 56, [172, 57], [172, 1])
|
||||||
|
('R', 'AV', 30, [196, 52], [176, 75])
|
||||||
|
('G', 'AV', 23, [199, 81], [176, 75])
|
||||||
|
('AG', 'D', 22, [150, 23], [132, 9])
|
||||||
|
('H', 'AG', 31, [172, 1], [150, 23])
|
||||||
|
('AG', 'AA', 31, [150, 23], [145, 54])
|
||||||
|
('AJ', 'AG', 40, [172, 57], [150, 23])
|
||||||
|
('E', 'AA', 37, [123, 84], [145, 54])
|
||||||
|
('K', 'E', 22, [145, 84], [123, 84])
|
||||||
|
('E', 'Y', 26, [123, 84], [98, 75])
|
||||||
|
('E', 'C', 33, [123, 84], [94, 101])
|
||||||
|
('AB', 'AN', 36, [137, 140], [163, 165])
|
||||||
|
('U', 'AB', 29, [122, 165], [137, 140])
|
||||||
|
('AB', 'O', 44, [137, 140], [181, 142])
|
||||||
|
('AB', 'P', 29, [137, 140], [159, 121])
|
||||||
|
('P', 'O', 30, [159, 121], [181, 142])
|
||||||
|
('I', 'AE', 16, [108, 130], [108, 146])
|
||||||
|
('AE', 'A', 16, [108, 146], [92, 145])
|
||||||
|
('AE', 'AU', 23, [108, 146], [89, 160])
|
||||||
|
('AU', 'A', 15, [89, 160], [92, 145])
|
||||||
|
('AE', 'AB', 29, [108, 146], [137, 140])
|
||||||
|
('U', 'AE', 23, [122, 165], [108, 146])
|
||||||
|
('AK', 'AE', 30, [110, 176], [108, 146])
|
||||||
|
('AU', 'AC', 38, [89, 160], [55, 142])
|
||||||
|
('AC', 'A', 37, [55, 142], [92, 145])
|
||||||
|
('W', 'AC', 39, [76, 108], [55, 142])
|
||||||
|
('AC', 'AP', 37, [55, 142], [58, 105])
|
||||||
|
('AI', 'AC', 53, [56, 195], [55, 142])
|
||||||
|
('AC', 'B', 45, [55, 142], [95, 121])
|
||||||
|
('AC', 'AR', 61, [55, 142], [7, 103])
|
||||||
|
('AC', 'AY', 63, [55, 142], [33, 202])
|
||||||
|
('AY', 'AR', 102, [33, 202], [7, 103])
|
||||||
|
('O', 'AO', 36, [181, 142], [169, 108])
|
||||||
|
('AO', 'J', 35, [169, 108], [204, 116])
|
||||||
|
('P', 'AO', 16, [159, 121], [169, 108])
|
||||||
|
('F', 'N', 18, [49, 32], [67, 27])
|
||||||
|
('AL', 'F', 55, [77, 80], [49, 32])
|
||||||
|
('AT', 'F', 55, [30, 84], [49, 32])
|
||||||
|
('AM', 'AT', 27, [4, 76], [30, 84])
|
||||||
|
('AR', 'AM', 27, [7, 103], [4, 76])
|
||||||
|
('F', 'AM', 62, [49, 32], [4, 76])
|
||||||
|
('AM', 'AX', 57, [4, 76], [1, 19])
|
||||||
|
('Q', 'I', 19, [127, 124], [108, 130])
|
||||||
|
('C', 'Q', 40, [94, 101], [127, 124])
|
||||||
|
('E', 'Q', 40, [123, 84], [127, 124])
|
||||||
|
('Q', 'AE', 29, [127, 124], [108, 146])
|
||||||
|
('Q', 'AB', 18, [127, 124], [137, 140])
|
||||||
|
('Q', 'P', 32, [127, 124], [159, 121])
|
||||||
|
('AO', 'V', 25, [169, 108], [191, 96])
|
||||||
|
('V', 'J', 23, [191, 96], [204, 116])
|
||||||
|
('V', 'T', 37, [191, 96], [227, 105])
|
||||||
|
('V', 'G', 17, [191, 96], [199, 81])
|
||||||
|
('V', 'AV', 25, [191, 96], [176, 75])
|
||||||
|
('AO', 'AF', 18, [169, 108], [163, 90])
|
||||||
|
('L', 'E', 30, [150, 99], [123, 84])
|
||||||
|
('K', 'L', 15, [145, 84], [150, 99])
|
||||||
|
('L', 'Q', 33, [150, 99], [127, 124])
|
||||||
|
('L', 'P', 23, [150, 99], [159, 121])
|
||||||
|
('AF', 'L', 15, [163, 90], [150, 99])
|
||||||
|
('L', 'AO', 21, [150, 99], [169, 108])
|
||||||
Loading…
Add table
Add a link
Reference in a new issue