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