From 322157e7e1b4caf3b7d009537648da1592dc80cc Mon Sep 17 00:00:00 2001 From: jlKronos01 <joelleungyancheuk@gmail.com> Date: Thu, 7 Nov 2024 00:09:51 +0000 Subject: [PATCH] Create BeaconPositioningTest.py --- BeaconPositioningTest.py | 460 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 BeaconPositioningTest.py diff --git a/BeaconPositioningTest.py b/BeaconPositioningTest.py new file mode 100644 index 00000000..e0df28c3 --- /dev/null +++ b/BeaconPositioningTest.py @@ -0,0 +1,460 @@ +import tkinter as tk, random +from tkinter import ttk + +class Blip: + def __init__(self, app, pos, id, color, outlineColorSelected, outlineColorDeselected): + self.app = app + canvas = app.canvas + self.pos = x, y = list(pos) + self.id = id + self.color = color + self.radius = 10 + self.outlineColorSelected = outlineColorSelected + self.outlineColorDeselected = outlineColorDeselected + self.circle = canvas.create_oval(x - self.radius, y - self.radius, x + self.radius, y + self.radius, fill = color, outline = "") + self.text = canvas.create_text(x, y - self.radius, text = ",".join(map(str, self.app.worldCoords(pos))), anchor = "s") + self.selected = False + + def isInside(self, pos): + return bool(self.app.distance(pos, self.pos) <= self.radius) + + def select(self): + self.app.canvas.itemconfig(self.circle, outline = self.outlineColorSelected, width = 2) + + def deselect(self): + self.app.canvas.itemconfig(self.circle, outline = self.outlineColorDeselected) + + def __del__(self): + self.app.canvas.delete(self.circle) + self.app.canvas.delete(self.text) + +class SetupBlip(Blip): + def __init__(self, app, pos, mode, id = 0, movable = True): + super().__init__(app, pos, id, "purple" if mode == "beacon" else "green", "red", "") + self.mode = mode + self.movable = movable + self.isShowingCircle = True + canvas = app.canvas + x, y = pos + if mode == "beacon": + self.isShowingCircle = True + self.idText = canvas.create_text(x, y, text = str(id), fill = "white") + distance = app.distance(self.pos, app.tag.pos) + self.distanceCircle = canvas.create_oval(self.pos[0] - distance, self.pos[1] - distance, self.pos[0] + distance, self.pos[1] + distance, outline = "grey", dash = 3) + self.distanceLine = canvas.create_line(*self.pos, *app.tag.pos, fill = "grey", dash = 3) + canvas.tag_lower(self.distanceCircle) + canvas.tag_lower(self.distanceLine) + + def disableDragging(self): + self.movable = False + self.outlineColorDeselected = "grey" + self.app.canvas.itemconfigure(self.circle, fill = "", outline = self.outlineColorDeselected if self == self.app.selectedBlip else self.outlineColorDeselected, width = 2) + if self.mode == "beacon": + self.app.canvas.itemconfigure(self.idText, fill = "black") + + def enableDragging(self): + self.movable = True + self.outlineColorDeselected = "" + self.app.canvas.itemconfigure(self.circle, fill = self.color, outline = self.outlineColorDeselected if self == self.app.selectedBlip else self.outlineColorDeselected, width = 2) + if self.mode == "beacon": + self.app.canvas.itemconfigure(self.idText, fill = "white") + + def showCircle(self): + self.isShowingCircle = True + self.app.canvas.itemconfigure(self.distanceLine, state = "normal") + self.app.canvas.itemconfigure(self.distanceCircle, state = "normal") + + def hideCircle(self): + self.isShowingCircle = False + self.app.canvas.itemconfigure(self.distanceLine, state = "hidden") + self.app.canvas.itemconfigure(self.distanceCircle, state = "hidden") + + def move(self, delta): + self.pos = [self.pos[0] + delta[0], self.pos[1] + delta[1]] + self.update() + if self.mode == "tag": + self.app.updateBeacons() + + def update(self): + x, y = self.pos + self.app.canvas.coords(self.circle, x - self.radius, y - self.radius, x + self.radius, y + self.radius) + self.app.canvas.coords(self.text, x, y - self.radius) + self.app.canvas.itemconfig(self.text, text = ",".join(map(str, self.app.worldCoords(self.pos)))) + if self.mode == "beacon": + self.app.canvas.coords(self.idText, x, y) + distance = self.app.distance(self.pos, app.tag.pos) + self.app.canvas.coords(self.distanceCircle, self.pos[0] - distance, self.pos[1] - distance, self.pos[0] + distance, self.pos[1] + distance) + self.app.canvas.coords(self.distanceLine, *self.pos, *self.app.tag.pos) + +class RecordedBlip(Blip): + def __init__(self, app, pos, id, distances): + super().__init__(app, pos, id, "grey", "red", "") + app.canvas.tag_lower(self.circle) + app.canvas.tag_lower(self.text) + self.distances = distances + x, y = pos + + self.distanceCircles = [app.canvas.create_oval(x - dist, y - dist, x + dist, y + dist, fill = "", outline = "grey", dash = 2, state = "hidden") for dist in distances] + self.distanceLines = [app.canvas.create_line(x, y, *app.beacons[i].pos, fill = "grey", dash = 2, state = "hidden") for i in range(len(distances))] + for circle, line in zip(self.distanceCircles, self.distanceLines): + app.canvas.tag_lower(circle) + app.canvas.tag_lower(line) + + def showDistanceMarkers(self, distID = "all"): + if distID == "all": + for circle, line in zip(self.distanceCircles, self.distanceLines): + self.app.canvas.itemconfig(circle, state = "normal") + self.app.canvas.itemconfig(line, state = "normal") + else: + self.app.canvas.itemconfig(self.distanceCircles[distID], state = "normal") + self.app.canvas.itemconfig(self.distanceLines[distID], state = "normal") + + def hideDistanceMarkers(self, distID = "all"): + if distID == "all": + for circle, line in zip(self.distanceCircles, self.distanceLines): + self.app.canvas.itemconfig(circle, state = "hidden") + self.app.canvas.itemconfig(line, state = "hidden") + else: + self.app.canvas.itemconfig(self.distanceCircles[distID], state = "hidden") + self.app.canvas.itemconfig(self.distanceLines[distID], state = "hidden") + + def __del__(self): + self.app.canvas.delete(self.circle) + self.app.canvas.delete(self.text) + for circle, line in zip(self.distanceCircles, self.distanceLines): + self.app.canvas.delete(circle) + self.app.canvas.delete(line) + +class App(tk.Tk): + def __init__(self, *args, **kwargs): + self.width, self.height = width, height = kwargs.pop("width", 800), kwargs.pop("height", 600) + self.scale = kwargs.pop("scale", 30) + super().__init__(*args, **kwargs) + self.title("Beacon positioning") + self.geometry("{}x{}+{}+{}".format(width, height, (self.winfo_screenwidth() - width) // 2, (self.winfo_screenheight() - height) // 2)) + self.resizable(False, False) + + self.options = options = ["Beacon 0", "Beacon 1", "Beacon 2", "Beacon 3", "Tag"] + self.selectStringvar = tk.StringVar(self, value = options) + + self.grid_columnconfigure((0, 1), weight = 1) + self.grid_rowconfigure((3), weight = 1) + + #Canvas + self.canvas = tk.Canvas(self, highlightthickness = 1, width = height, height = height, highlightbackground = "black") + self.canvas.grid(row = 0, column = 0, sticky = "nsw", rowspan = 6) + self.canvas.create_line(0, height / 2, height, height / 2, fill = "grey") + self.canvas.create_line(height / 2, 0, height / 2, height, fill = "grey") + self.canvas.bind("<ButtonPress-1>", self.click) + self.canvas.bind("<B1-Motion>", self.drag) + self.canvas.bind("<B1-ButtonRelease>", self.release) + + # ttk.Label(self, text = """Instructions: + # Set up beacon positions by selecting and dragging them around. + # Move tag to desired position, and record a position. Repeat until 3 points have been recorded. + # Once the recording process begins, beacons can no longer be moved to reflect reality.""").grid(row = 4, column = 0, sticky = "nsew", rowspan = 2) + + #Radio button selectors + self.radioButtonFrame = ttk.Labelframe(self, text = "Select blip") + self.radioButtonFrame.grid(row = 0, column = 1, sticky = "nsew", padx = 5, pady = 5) + self.radioButtons = {name: ttk.Radiobutton(self.radioButtonFrame, text = name, variable = self.selectStringvar, value = name, command = self.radioSelect) for i, name in enumerate(options)} + for i, radioButton in enumerate(self.radioButtons.values()): + radioButton.grid(row = i, column = 0, sticky = "new") + + #Properties frame + self.propertiesFrame = ttk.Labelframe(self, text = "Properties") + self.propertiesFrame.grid(row = 1, column = 1, sticky = "nsew", padx = 5, pady = 5) + self.propertiesFrame.grid_columnconfigure(1, weight = 1, uniform = "column") + + #Property frame labels + ttk.Label(self.propertiesFrame, text = "\tx: ", justify = "right").grid(row = 0, column = 0, sticky = "nse") + ttk.Label(self.propertiesFrame, text = "\ty: ", justify = "right").grid(row = 1, column = 0, sticky = "nse") + # ttk.Label(self.propertiesFrame, text = "Show circle: ", justify = "right").grid(row = 2, column = 0, sticky = "nse") + + #Entry box X + self.xStringVar = tk.StringVar(self) + self.xStringVar.trace_add("write", self.writeX) + self.xEntry = ttk.Entry(self.propertiesFrame, textvariable = self.xStringVar) + self.xEntry.grid(row = 0, column = 1, sticky = "nsew", padx = 5) + + #Entry box Y + self.yStringVar = tk.StringVar(self) + self.yStringVar.trace_add("write", self.writeY) + self.yEntry = ttk.Entry(self.propertiesFrame, textvariable = self.yStringVar) + self.yEntry.grid(row = 1, column = 1, sticky = "nsew", padx = 5) + + #Checkbox + self.showCircleVar = tk.BooleanVar(self) + self.showCirclesCheckbox = ttk.Checkbutton(self.propertiesFrame, text = "Show distance radius", state = "disabled", variable = self.showCircleVar, command = self.checkboxCallback) + self.showCirclesCheckbox.grid(row = 2, column = 0, columnspan = 2, sticky = "nsw") + + #Live distances frame + self.distancesFrame = ttk.Labelframe(self, text = "Distance to beacons") + self.distancesFrame.grid(row = 2, column = 1, sticky = "nsew", padx = 5, pady = 5) + self.distancesFrame.grid_columnconfigure(0, weight = 1) + #Distance labels + self.distanceLabels = {"Beacon {}".format(i): ttk.Label(self.distancesFrame, text = "Dist{}: -".format(i)) for i in range(4)} + for i, label in enumerate(self.distanceLabels.values()): + label.grid(row = i, column = 0, sticky = "nsew") + #Record button + self.recordButton = ttk.Button(self.distancesFrame, text = "Record", command = self.recordButtonCallback, state = "disabled") + self.recordButton.grid(row = len(self.distanceLabels), column = 0, sticky = "nsew") + + #Recorded blip's properties frame + self.recordedBlipFrame = ttk.Labelframe(self, text = "Recorded positions properties") + self.recordedBlipFrame.grid(row = 3, column = 1, sticky = "nsew", padx = 5, pady = 5) + self.recordedBlipFrame.grid_columnconfigure((0, 1), weight = 1) + #Recorded blip's properties checkboxes + self.recordedBlipCheckboxVars = [tk.BooleanVar(self) for i in range(4)] + self.recordedBlipsCheckboxes = [ttk.Checkbutton(self.recordedBlipFrame, text = "Beacon {}".format(i), variable = self.recordedBlipCheckboxVars[i], state = "disabled", command = self.recordedBlipCheckboxCallback) for i in range(4)] + for i, checkbox in enumerate(self.recordedBlipsCheckboxes): + self.recordedBlipCheckboxVars[i].set(False) + checkbox.selection_clear() + checkbox.grid(row = i, column = 0, sticky = "nsew", columnspan = 2) + + #Show all and hide all buttons + self.showAllButton = ttk.Button(self.recordedBlipFrame, text = "Show all", state = "disabled", command = self.showAllCallback) + self.showAllButton.grid(row = len(self.recordedBlipsCheckboxes), column = 0, sticky = "nsew") + self.hideAllButton = ttk.Button(self.recordedBlipFrame, text = "Hide all", state = "disabled", command = self.hideAllCallback) + self.hideAllButton.grid(row = len(self.recordedBlipsCheckboxes), column = 1, sticky = "nsew") + + #Toggle all circles button + self.toggleAllCirclesButton = ttk.Button(self, text = "Hide all beacon circles", command = self.toggleAllCircles) + self.toggleAllCirclesButton.grid(row = 4, column = 1, sticky = "nsew") + + #Reset button + ttk.Button(self, text = "Reset", command = self.reset).grid(row = 5, column = 1, sticky = "nsew") + + self.selectedBlip = None + self.lastClickPos = None + self.dragging = False + + self.tag = SetupBlip(self, [height // 2, height // 2], "tag") + self.beacons = [SetupBlip(self, pos, "beacon", i) for i, pos in enumerate([(height // 4, height // 4), (height - height // 4, height // 4), (height // 4, height - height // 4), (height - height // 4, height - height // 4)])] + self.recordedBlips = [] + + def distance(self, pos1, pos2): + x1, y1 = pos1 + x2, y2 = pos2 + return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5 + + def worldCoords(self, pixelCoords): + px, py = pixelCoords + x = ((px / self.height) - 0.5) * self.scale + y = -((py / self.height) - 0.5) * self.scale + return round(x, 3), round(y, 3) + + def pixelCoords(self, worldCoords, mode): + if mode == "x": + return int(((worldCoords / self.scale) + 0.5) * self.height) + elif mode == "y": + return int(((-worldCoords / self.scale) + 0.5) * self.height) + + def updateBeacons(self): + for beacon in self.beacons: + beacon.update() + + def selectBlip(self, blip): + if self.selectedBlip: + self.selectedBlip.deselect() + self.selectedBlip = blip + blip.select() + self.selectStringvar.set(self.getSelectionName(self.selectedBlip)) + worldCoords = self.worldCoords(self.selectedBlip.pos) + self.xStringVar.set(str(worldCoords[0])) + self.yStringVar.set(str(worldCoords[1])) + + if self.selectedBlip in self.beacons: + self.showCirclesCheckbox.config(state = "normal") + self.recordButton.config(state = "disabled") + if self.selectedBlip.isShowingCircle: + self.showCircleVar.set(True) + else: + self.showCircleVar.set(False) + else: + self.showCircleVar.set(False) + self.showCirclesCheckbox.config(state = "disabled") + if len(self.recordedBlips) < 3: + self.recordButton.config(state = "normal") + self.updateDistanceLabels() + + def deselectBlip(self): + if self.selectBlip: + self.selectedBlip.deselect() + self.selectedBlip = None + self.selectStringvar.set("") + self.xStringVar.set("") + self.yStringVar.set("") + self.showCircleVar.set(False) + self.showCirclesCheckbox.config(state = "disabled") + self.recordButton.config(state = "disabled") + self.updateDistanceLabels() + + def click(self, event): + x, y = event.x, event.y + selectedBlips = list(filter(lambda blip: blip.isInside((x, y)), self.beacons + [self.tag] + self.recordedBlips)) + + if len(selectedBlips): + self.selectBlip(selectedBlips[0]) + if self.selectedBlip not in self.recordedBlips: + if self.selectedBlip.movable: + self.lastClickPos = x, y + self.dragging = self.selectedBlip.movable + + elif self.selectedBlip: + self.deselectBlip() + + def drag(self, event): + if self.selectedBlip and self.selectedBlip not in self.recordedBlips: + if self.selectedBlip.movable: + pos = [event.x, event.y] + delta = pos[0] - self.lastClickPos[0], pos[1] - self.lastClickPos[1] + + self.selectedBlip.move(delta) + worldCoords = self.worldCoords(self.selectedBlip.pos) + self.xStringVar.set(str(worldCoords[0])) + self.yStringVar.set(str(worldCoords[1])) + if self.selectedBlip == self.tag: + self.updateDistanceLabels() + self.lastClickPos = pos + + def release(self, event): + self.dragging = False + + def updateDistanceLabels(self): + if self.selectedBlip == self.tag: + for beacon in self.beacons: + dist = round(self.distance(self.worldCoords(beacon.pos), self.worldCoords(self.tag.pos)), 3) + self.distanceLabels[self.getSelectionName(beacon)].config(text = "Dist{}: {}".format(self.getSelectionName(beacon)[-1], dist)) + elif self.selectedBlip in self.recordedBlips: + for i, dist in enumerate(self.selectedBlip.distances): + self.distanceLabels["Beacon {}".format(i)].config(text = "Dist{}: {}".format(i, dist)) + else: + for name, label in self.distanceLabels.items(): + label.config(text = "Dist{}: -".format(name[-1])) + + def radioSelect(self): #on radio button selection callback + self.selectBlip(self.beacons[int(self.selectStringvar.get()[-1])] if self.selectStringvar.get() != "Tag" else self.tag) + + def checkboxCallback(self): + if self.selectedBlip: + if self.showCircleVar.get(): + self.selectedBlip.showCircle() + else: + self.selectedBlip.hideCircle() + + def recordButtonCallback(self): + for beacon in self.beacons: + beacon.disableDragging() + + self.xEntry.config(state = "disabled") + self.yEntry.config(state = "disabled") + + if len(self.recordedBlips) < 3: + if len(self.recordedBlips) == 0: + self.showAllButton.config(state = "normal") + self.hideAllButton.config(state = "normal") + for checkbox in self.recordedBlipsCheckboxes: + checkbox.config(state = "normal") + self.recordedBlips.append(RecordedBlip(self, self.tag.pos, len(self.recordedBlips), [round(self.distance(self.tag.pos, beacon.pos), 3) for beacon in self.beacons])) + + if len(self.recordedBlips) == 3: + self.recordButton.config(state = "disabled") + + def recordedBlipCheckboxCallback(self): + values = [var.get() for var in self.recordedBlipCheckboxVars] + + for i, value in enumerate(values): + for blip in self.recordedBlips: + if value: + blip.showDistanceMarkers(i) + else: + blip.hideDistanceMarkers(i) + + def showAllCallback(self): + for var in self.recordedBlipCheckboxVars: + var.set(True) + for blip in self.recordedBlips: + blip.showDistanceMarkers() + + def hideAllCallback(self): + for var in self.recordedBlipCheckboxVars: + var.set(False) + for blip in self.recordedBlips: + blip.hideDistanceMarkers() + + def toggleAllCircles(self): + text = self.toggleAllCirclesButton.cget("text") + if text == "Hide all beacon circles": + self.toggleAllCirclesButton.config(text = "Show all beacon circles") + if self.selectedBlip != self.tag and self.selectedBlip != None: + self.showCircleVar.set(False) + for beacon in self.beacons: + beacon.hideCircle() + elif text == "Show all beacon circles": + self.toggleAllCirclesButton.config(text = "Hide all beacon circles") + if self.selectedBlip != self.tag and self.selectedBlip != None: + self.showCircleVar.set(True) + for beacon in self.beacons: + beacon.showCircle() + + def writeX(self, var, index, mode): + strVal = self.xStringVar.get() + if self.selectedBlip and not self.dragging and self.selectedBlip not in self.recordedBlips: + if self.selectedBlip.movable: + try: + self.selectedBlip.pos[0] = self.pixelCoords(float(strVal), "x") + except ValueError: + pass + self.selectedBlip.update() + + def writeY(self, var, index, mode): + strVal = self.yStringVar.get() + if self.selectedBlip and not self.dragging and self.selectedBlip not in self.recordedBlips: + if self.selectedBlip.movable: + try: + self.selectedBlip.pos[1] = self.pixelCoords(float(strVal), "y") + except ValueError: + pass + self.selectedBlip.update() + + def getSelectionName(self, blip): + # return self.beacons.index(blip) if blip in self.beacons else len(self.beacons) + if blip not in self.recordedBlips: + return "Beacon {}".format(blip.id) if blip.mode == "beacon" else "Tag" + else: + return + + def reset(self): #TBD: add reset of recorded blips + positions = [(self.height // 4, self.height // 4), (self.height - self.height // 4, self.height // 4), (self.height // 4, self.height - self.height // 4), (self.height - self.height // 4, self.height - self.height // 4)] + for beacon, pos in zip(self.beacons, positions): + beacon.pos = list(pos) + beacon.enableDragging() + beacon.showCircle() + + self.tag.pos = [self.height // 2, self.height // 2] + self.tag.update() + self.updateBeacons() + + self.recordButton.config(state = "normal") + self.xEntry.config(state = "normal") + self.yEntry.config(state = "normal") + self.showCircleVar.set(True) + + if self.selectedBlip: + worldCoords = self.worldCoords(self.selectedBlip.pos) + self.xStringVar.set(str(worldCoords[0])) + self.yStringVar.set(str(worldCoords[1])) + + for i in range(len(self.recordedBlips))[::-1]: + del self.recordedBlips[i] + + self.showAllButton.config(state = "disabled") + self.hideAllButton.config(state = "disabled") + for var in self.recordedBlipCheckboxVars: + var.set(False) + for checkbox in self.recordedBlipsCheckboxes: + checkbox.config(state = "disabled") + +app = App() +app.mainloop() \ No newline at end of file -- GitLab