class RoutePlanGUI: '''The GUI for the Route Planning System All the GUI features are created and run though this class. All expected user input will come though this GUI, along with all output to the user Attributes: root The Root from which the application is running #max_height Maximium height for the window #max_width transport_mode Transport mode setting locations List of all the current locations location_view Index of what can currently be viewed VIEW_SIZE A limit of how many items can be viewed at one time to ensure they fit on screen latitude The current latitude of the device running the system longitude The cyrrent longitude of the device running the system zoom The zoom level of the tiles map_width The width of the displayed map map_height The height of the displayed map goompy A goompy object to act as the API to download the map tiles Methods: _draw_UI Creates the UI features and places them in the correct location _redraw_map Redraws the map _add_location Adds a location to the list of locations _remove_location Removes a location from the list of locations _move_boxes Moves the displayed list of locations _search Runs the search algorithm after fetching and passing it data ''' def __init__(self, root): self.root = root self.max_height = self.root.winfo_screenheight() self.max_width = self.root.winfo_screenwidth() self.root.geometry((str(int(self.max_width/2))+"x"+str(int(self.max_height/2)))) ## self.radiogroup = Frame() ## self.root.bind("<Key>", self.user_input) ## self.root.bind("<Button>", self.user_input) self.root.bind("<Escape>", lambda e: self.root.destroy()) # The user is able to select differnt modes of transportation # They are diffined here, as well as the mechanism for storing them self.transport_mode = tk.StringVar() self.transport_mode.set("Walking") self.transport_modes = ["Walking", "Bicycling", "Driving", "Transit"] # All the locations the user is using, stored in a list self.locations = [] # The index used to calualate which locations are currently visable self.location_view = 0 # Used to limit the system from attempting to display more locations # than possible self.VIEW_SIZE = 10 # The coordinates of the current location of the user self.latitude = float( location.CURRENT_LOCATION[:location.CURRENT_LOCATION.find(",")] ) self.longitude = float( location.CURRENT_LOCATION[location.CURRENT_LOCATION.find(",")+1:] ) # The zoom level on the map self.zoom = 15 # The dimentions of the map self.map_width = 500 self.map_height = 500 # GooMPy object to act as an API self.goompy = GooMPy(self.map_width, self.map_height, self.latitude, self.longitude, self.zoom, "roadmap") self.live = False # Starts the system self._draw_UI() self.root.mainloop() def _draw_UI(self): '''Draw the UI ''' # Backbone of the GUI layout self.frame = tk.Frame(self.root) # Placement of Radiobuttons with correct control assignment for index, mode in enumerate(self.transport_modes): tk.Radiobutton( self.frame, text=mode, variable=self.transport_mode, value=mode ).grid(row=0, column=index) # Movement buttons self.up_button = tk.Button( self.frame, text=" Up ", bg="yellow", command=lambda: self._move_boxes(1) ) self.down_button = tk.Button( self.frame, text="Down", bg="yellow", command=lambda: self._move_boxes(-1) ) self.showing_movement_buttons = False # Start button self.go_button = tk.Button( self.frame, text="Start Search", bg="yellow", command=lambda: self._search() ) # Entry box for user input, along with an associated button self.entry_box = tk.Entry(self.frame) self.entry_box.insert("end", "Add Location") self.entry_box.bind("<Return>", self._add_location) self.entry_button = tk.Button( self.frame, text="+", bg="green", command=self._add_location ) # Configureation of the widgets just defined self.entry_box.grid(row=2, column=0, columnspan=4, sticky="ew") self.entry_button.grid(row=2, column=4) self.go_button.grid(row=1, column=3, columnspan=2, sticky="e") # Canvas to hold the map self.canvas = tk.Canvas( self.root, width=self.map_width, height=self.map_height, bg="black" ) # Underlay to configure the tile image self.label = tk.Label(self.canvas) self.zoom_in_button = tk.Button(self.canvas, text="+", width=1, command=lambda:self._map_zoom(+1)) self.zoom_out_button = tk.Button(self.canvas, text="-", width=1, command=lambda:self._map_zoom(-1)) # Packing of the two layout features self.frame.pack(side="left", fill="y") self.canvas.pack(side="right", expand=True, fill="both") ## x = int(self.canvas['width']) - 50 ## y = int(self.canvas['height']) - 80 ## ## self.zoom_in_button.place(x=x, y=y) ## self.zoom_out_button.place(x=x, y=y+30) # Load a tile self._reload() def _redraw_map(self): '''Fetch a new tile and place that on the map ''' ## print("redrawing") # Get the tile that goompy has been told to fetch self.goompy._fetch_and_update() self.image = self.goompy.getImage() # Load the image tile onto the map self.image_tk = ImageTk.PhotoImage(self.image) self.label['image'] = self.image_tk self.label.place( x=0, y=0, width=self.map_width, height=self.map_height ) def _reload(self): self.coords = None if self.live: self._redraw_map() def _add_location(self, event=None): '''Make the user's input a Location and add to the list of locations ''' # The details of the new location user_input = self.entry_box.get() new_location = location.Location(user_input) self.locations.append(new_location) precise = new_location.location # goompy loading a new tile as per the location self.goompy.lat = float(precise[:precise.find(",")]) self.goompy.lon = float(precise[precise.find(",")+1:]) self._reload() # Differnt actions depending on how many locations currently exist if len(self.locations) > self.VIEW_SIZE: # Configure the movement buttons is not configured if not self.showing_movement_buttons: self.up_button.grid(row=1, column=0, columnspan=2, sticky="w") self.down_button.grid( row=self.VIEW_SIZE+10, column=0, columnspan=2, sticky="w" ) # Ensures the latest location is displayed at the bottom while len(self.locations)-self.location_view > self.VIEW_SIZE: self._move_boxes(1) else: # Move the entry box, and its button, down on space self.entry_box.grid_configure( row=self.entry_box.grid_info()["row"]+1 ) self.entry_button.grid_configure( row=self.entry_box.grid_info()["row"] ) # The row the the entry box moved from row = self.entry_box.grid_info()["row"]-1 # Create a Label and a Button for the new location tk.Label( self.frame, text=user_input, bg="white", anchor="w" ).grid(row=row, column=0, sticky="ew", columnspan=4) tk.Button( self.frame, text="X", bg="red", command=lambda: self._remove_location(len(self.locations)) ).grid(row=row, column=4, sticky="ew") # Reset the text in the entry box self.entry_box.delete(0, "end") self.entry_box.insert("end", "Add Location") def _remove_location(self, row): '''Remove the location selected by the user by visualy and from list ''' # Remove the location from the location list self.locations.pop(row+self.location_view-1) # Marker to indicate if the locations below should move up move = False # List of all the locations, as per what is on the Labels remaining_locations = [x.user_input for x in self.locations] # Index to keep track of where is being investigated index = 0 # Looping though all the slaves and adjusting them as needed for slave in self.frame.grid_slaves()[::-1]: # Reversed for simplicity # Anaylse and configure the Labels if type(slave) == tk.Label: if self.location_view+index == len(self.locations): slave.grid_forget() else: if slave.cget("text") not in remaining_locations: move = True if move: slave.config( text=self.locations[ self.location_view+index ].user_input ) index += 1 # Ensure that the final button is removed if needed if (type(slave) == tk.Button and self.location_view+index-1 == len(self.locations)): slave.grid_forget() self.location_view -= 1 def _move_boxes(self, direction): '''Move the visual list of locations in required direction ''' for i in self.locations: print(i) # Ensure that the given command is valid in the current configuration if ((self.location_view == 0 and direction == -1) or (self.location_view+self.VIEW_SIZE == len(self.locations) and direction == 1)): return None else: self.location_view += direction # Iterate though the Labels and change their values for index, slave in enumerate(self.frame.grid_slaves()[::-1]): if type(slave) == tk.Label: slave.config( text=self.locations[ self.location_view+index ].user_input ) ## def user_input(self, event): ## if event.char == "a": ## print(self.transport_mode.get()) def _search(self): '''Calculate and return the most efficent route ''' # Using the Latitude and Longitude, calculate the distance matrix precise_locations = [l.location for l in self.locations] distance_matrix_info = location.GM.distance_matrix( precise_locations, precise_locations, mode=self.transport_mode.get().lower() ) self.distance_matrix = [] for origin in distance_matrix_info["rows"]: self.distance_matrix.append( [dest["duration"]["value"] for dest in origin["elements"]] ) ######################################################################################################################## t = time() _time, _route = search.nearest_neighbour(self.distance_matrix) print("nn time") print(time()-t) t2 = time() _time2, _route2 = search.brute_force(self.distance_matrix) print("bf time") print(time()-t2) print(_time) print(_route) ######################################################################################################################## # Write message to the user about the best route msg = "The best route to visit every location in the minimun amount of time is " for loc in _route[:-1]: msg += self.locations[loc].user_input msg += ", then " msg += "and then finally " msg += self.locations[_route[-1]].user_input # Set up the message to tell the user which route is best self.route_box = tk.Toplevel(master=self.root) self.route_box.title("Best Route") # Configure widgets in message box tk.Message(self.route_box, text=msg).pack() tk.Button( self.route_box, text="OK", command=lambda:self.route_box.destroy() ).pack()