|
| 1 | +""" demonstration of A* routing on a quadtree representation of a 2D map |
| 2 | +
|
| 3 | + (c) Volker Poplawski 2018 |
| 4 | +""" |
| 5 | +from tkinter import * |
| 6 | +from PIL import ImageTk, ImageDraw |
| 7 | +import mapgen |
| 8 | +import quadtree |
| 9 | +import astar |
| 10 | +import graph |
| 11 | + |
| 12 | + |
| 13 | +# square map height and width. power of 2. e.g 256, 512, 1024 |
| 14 | +MAPSIZE = 512 |
| 15 | + |
| 16 | + |
| 17 | +class MainObject: |
| 18 | + def run(self): |
| 19 | + self.mapimage = None |
| 20 | + self.quadtree = None |
| 21 | + self.startpoint = None |
| 22 | + self.drag_startp = False |
| 23 | + |
| 24 | + self._setupgui() |
| 25 | + |
| 26 | + self.root.mainloop() |
| 27 | + |
| 28 | + |
| 29 | + def _setupgui(self): |
| 30 | + self.root = Tk() |
| 31 | + self.root.title("QuadTree A*") |
| 32 | + |
| 33 | + self.canvas = Canvas(self.root, bg='gray', width=MAPSIZE, height=MAPSIZE) |
| 34 | + self.canvas.pack(side=LEFT) |
| 35 | + |
| 36 | + self.image_item = self.canvas.create_image((0, 0), anchor=NW) |
| 37 | + |
| 38 | + rightframe = Frame(self.root) |
| 39 | + rightframe.pack(side=LEFT, fill=Y) |
| 40 | + |
| 41 | + mapframe = Frame(rightframe, relief=SUNKEN, borderwidth=2) |
| 42 | + mapframe.pack(padx=5, pady=5) |
| 43 | + |
| 44 | + label = Label(mapframe, text="Map", font=("Helvetica", 13)) |
| 45 | + label.pack() |
| 46 | + |
| 47 | + frame1 = Frame(mapframe) |
| 48 | + frame1.pack(fill=X, padx=4) |
| 49 | + |
| 50 | + kernellbl = Label(frame1, text="Kernel Size") |
| 51 | + kernellbl.pack(side=LEFT, pady=4) |
| 52 | + |
| 53 | + self.kernelsizevar = StringVar(self.root) |
| 54 | + self.kernelsizevar.set("7*7") |
| 55 | + kernelmenu = OptionMenu(frame1, self.kernelsizevar, "13*13", "11*11", "9*9", "7*7", "5*5", "3*3") |
| 56 | + kernelmenu.pack(fill=X, expand=True) |
| 57 | + |
| 58 | + frame2 = Frame(mapframe) |
| 59 | + frame2.pack(fill=X, padx=4) |
| 60 | + |
| 61 | + iterslbl = Label(frame2, text="Num Iterations") |
| 62 | + iterslbl.pack(side=LEFT, pady=4) |
| 63 | + |
| 64 | + var = StringVar(self.root) |
| 65 | + var.set("40") |
| 66 | + self.iterspin = Spinbox(frame2, from_=0, to=100, textvariable=var) |
| 67 | + self.iterspin.pack(expand=True) |
| 68 | + |
| 69 | + genbtn = Button(mapframe, text="Generate Map", command=self.onButtonGeneratePress) |
| 70 | + genbtn.pack(pady=2) |
| 71 | + |
| 72 | + qtframe = Frame(rightframe, relief=SUNKEN, borderwidth=2) |
| 73 | + qtframe.pack(fill=X, padx=5, pady=5) |
| 74 | + |
| 75 | + label = Label(qtframe, text="QuadTree", font=("Helvetica", 13)) |
| 76 | + label.pack() |
| 77 | + |
| 78 | + frame1 = Frame(qtframe) |
| 79 | + frame1.pack(fill=X, padx=4) |
| 80 | + |
| 81 | + label = Label(frame1, text="Depth Limit") |
| 82 | + label.pack(side=LEFT, pady=4) |
| 83 | + |
| 84 | + var = StringVar(self.root) |
| 85 | + var.set("100") |
| 86 | + self.limitspin = Spinbox(frame1, from_=2, to=100, textvariable=var) |
| 87 | + self.limitspin.pack(expand=True) |
| 88 | + |
| 89 | + self.qtlabelvar = StringVar() |
| 90 | + label = Label(qtframe, fg='#FF8080', textvariable=self.qtlabelvar) |
| 91 | + label.pack() |
| 92 | + |
| 93 | + quadtreebtn = Button(qtframe, text="Generate QuadTree", command=self.onButtonQuadTreePress) |
| 94 | + quadtreebtn.pack(pady=2) |
| 95 | + |
| 96 | + astarframe = Frame(rightframe, relief=SUNKEN, borderwidth=2) |
| 97 | + astarframe.pack(fill=X, padx=5, pady=5) |
| 98 | + |
| 99 | + label = Label(astarframe, text="Path", font=("Helvetica", 13)) |
| 100 | + label.pack() |
| 101 | + |
| 102 | + self.pathlabelvar = StringVar() |
| 103 | + label = Label(astarframe, fg='#0000FF', textvariable=self.pathlabelvar) |
| 104 | + label.pack() |
| 105 | + |
| 106 | + self.astarlabelvar = StringVar() |
| 107 | + label = Label(astarframe, fg='#8080FF', textvariable=self.astarlabelvar) |
| 108 | + label.pack() |
| 109 | + |
| 110 | + label = Label(rightframe, text="Instructions", font=("Helvetica", 13)) |
| 111 | + label.pack() |
| 112 | + label = Label(rightframe, justify=LEFT, text= |
| 113 | + "Generate a random map.\n" |
| 114 | + "Black regions are impassable.\n" |
| 115 | + "Generate QuadTree on map.\n" |
| 116 | + "Set start position by dragging blue circle.\n" |
| 117 | + "Click anywhere on map to find a path.") |
| 118 | + label.pack(padx=14) |
| 119 | + |
| 120 | + self.canvas.bind('<ButtonPress-1>', self.onMouseButton1Press) |
| 121 | + self.canvas.bind('<ButtonRelease-1>', self.onMouseButton1Release) |
| 122 | + self.canvas.bind('<B1-Motion>', self.onMouseMove) |
| 123 | + |
| 124 | + |
| 125 | + def onMouseButton1Press(self, event): |
| 126 | + if not self.quadtree: |
| 127 | + return |
| 128 | + |
| 129 | + if self.startpoint in self.canvas.find_overlapping(event.x, event.y, event.x, event.y): |
| 130 | + self.drag_startp = True |
| 131 | + return |
| 132 | + |
| 133 | + startx, starty, _, _ = self.canvas.coords(self.startpoint) |
| 134 | + start = self.quadtree.get(startx + 6, starty + 6) |
| 135 | + goal = self.quadtree.get(event.x, event.y) |
| 136 | + |
| 137 | + adjacent = graph.make_adjacent_function(self.quadtree) |
| 138 | + path, distances, considered = astar.astar(adjacent, graph.euclidian, graph.euclidian, start, goal) |
| 139 | + |
| 140 | + im = self.qtmapimage.copy() |
| 141 | + draw = ImageDraw.Draw(im) |
| 142 | + |
| 143 | + self.astarlabelvar.set("Nodes visited: {} considered: {}".format(len(distances), considered)) |
| 144 | + for tile in distances: |
| 145 | + fill_tile(draw, tile, color=(0xC0, 0xC0, 0xFF)) |
| 146 | + |
| 147 | + if path: |
| 148 | + self.pathlabelvar.set("Path Cost: {} Nodes: {}".format(round(distances[goal], 1), len(path))) |
| 149 | + for tile in path: |
| 150 | + fill_tile(draw, tile, color=(0, 0, 255)) |
| 151 | + else: |
| 152 | + self.pathlabelvar.set("No Path found.") |
| 153 | + |
| 154 | + self._updateimage(im) |
| 155 | + |
| 156 | + |
| 157 | + def onMouseButton1Release(self, event): |
| 158 | + self.drag_startp = False |
| 159 | + |
| 160 | + |
| 161 | + def onMouseMove(self, event): |
| 162 | + if self.drag_startp: |
| 163 | + self.canvas.coords(self.startpoint, event.x-6, event.y-6, event.x+6, event.y+6) |
| 164 | + |
| 165 | + |
| 166 | + def onButtonGeneratePress(self): |
| 167 | + ksize = int(self.kernelsizevar.get().split('*')[0]) |
| 168 | + numiter = int(self.iterspin.get()) |
| 169 | + |
| 170 | + self.root.config(cursor="watch") |
| 171 | + self.root.update() |
| 172 | + self.mapimage = mapgen.generate_map(MAPSIZE, kernelsize=ksize, numiterations=numiter) |
| 173 | + self._updateimage(self.mapimage) |
| 174 | + self.quadtree = None |
| 175 | + self.qtlabelvar.set("") |
| 176 | + self.canvas.delete(self.startpoint) |
| 177 | + self.startpoint = None |
| 178 | + self.astarlabelvar.set("") |
| 179 | + self.pathlabelvar.set("") |
| 180 | + self.root.config(cursor="") |
| 181 | + |
| 182 | + |
| 183 | + def onButtonQuadTreePress(self): |
| 184 | + if not self.mapimage: |
| 185 | + return |
| 186 | + |
| 187 | + depthlimit = int(self.limitspin.get()) |
| 188 | + self.quadtree = quadtree.Tile(self.mapimage, limit=depthlimit) |
| 189 | + self.qtmapimage = self.mapimage.copy() |
| 190 | + draw = ImageDraw.Draw(self.qtmapimage) |
| 191 | + draw_quadtree(draw, self.quadtree, 8) |
| 192 | + self._updateimage(self.qtmapimage) |
| 193 | + |
| 194 | + self.qtlabelvar.set("Depth: {} Nodes: {}".format(self.quadtree.depth(), self.quadtree.count())) |
| 195 | + self.astarlabelvar.set("") |
| 196 | + self.pathlabelvar.set("") |
| 197 | + |
| 198 | + if not self.startpoint: |
| 199 | + pos = MAPSIZE//2 |
| 200 | + self.startpoint = self.canvas.create_oval(pos-6, pos-6, pos+6, pos+6, fill='#2028FF', width=2) |
| 201 | + |
| 202 | + |
| 203 | + def _updateimage(self, image): |
| 204 | + self.imagetk = ImageTk.PhotoImage(image) |
| 205 | + self.canvas.itemconfig(self.image_item, image=self.imagetk) |
| 206 | + |
| 207 | + |
| 208 | + |
| 209 | + |
| 210 | +def draw_quadtree(draw, tile, maxdepth): |
| 211 | + if tile.level == maxdepth: |
| 212 | + draw_tile(draw, tile, color=(255, 110, 110)) |
| 213 | + return |
| 214 | + |
| 215 | + if tile.childs: |
| 216 | + for child in tile.childs: |
| 217 | + draw_quadtree(draw, child, maxdepth) |
| 218 | + else: |
| 219 | + draw_tile(draw, tile, color=(255, 110, 110)) |
| 220 | + |
| 221 | + |
| 222 | +def draw_tile(draw, tile, color): |
| 223 | + draw.rectangle([tile.bb.x, tile.bb.y, tile.bb.x+tile.bb.w, tile.bb.y+tile.bb.h], outline=color) |
| 224 | + |
| 225 | + |
| 226 | +def fill_tile(draw, tile, color): |
| 227 | + draw.rectangle([tile.bb.x+1, tile.bb.y+1, tile.bb.x+tile.bb.w-1, tile.bb.y+tile.bb.h-1], outline=None, fill=color) |
| 228 | + |
| 229 | + |
| 230 | + |
| 231 | +if __name__ == '__main__': |
| 232 | + o = MainObject() |
| 233 | + o.run() |
0 commit comments