Search code examples
pythontkintercanvaslinetkinter-canvas

Python Tkinter Canvas get line cardinal direction


I have two points on a tkinter canvas. I need a function to determine which cardinal direction a line drawn between them would be the closest to (N, NW, W SW, S etc.) (Direction matters)? How would I go about doing this? Please note that in a canvas, the top-left is (0,0).

I have tried:

def dot_product(self, v, w):
    return v[0]*w[0]+v[1]*w[1]
def inner_angle(self, v, w):
    cosx=self.dot_product(v,w)/(sqrt(v[0]**2+v[1]**2)*sqrt(w[0]**2+w[1]**2))
    rad=acos(cosx)
    return rad*180/pi
def getAngle(self, A, B):
    inner=self.inner_angle(A,B)
    det = A[0]*B[1]-A[1]*B[0]
    if det<0:
        return inner
    else:
        return 360-inner

and:

def getBearing(self, pointA, pointB):

if (type(pointA) != tuple) or (type(pointB) != tuple):
    raise TypeError("Only tuples are supported as arguments")

lat1 = math.radians(pointA[0])
lat2 = math.radians(pointB[0])

diffLong = math.radians(pointB[1] - pointA[1])

x = math.sin(diffLong) * math.cos(lat2)
y = math.cos(lat1) * math.sin(lat2) - (math.sin(lat1) * math.cos(lat2) * math.cos(diffLong))

initial_bearing = math.atan2(x, y)

initial_bearing = math.degrees(initial_bearing)
compass_bearing = (initial_bearing + 360) % 360

return compass_bearing

(I used this function to get direction (The code is incomplete, it's more of an example))

def findDirection(self, p1, p2):
    bearing = self.getBearing(p1, p2) # OR getAngle()
    print(bearing)
    index = [180, 0]
    closest = min(index, key=lambda x:abs(x-bearing))
    if closest == 10:
        print(str(bearing) + " : UP")
    elif closest == 360:
        print(str(bearing) + " : DOWN")
    elif closest == 0:
        print(str(bearing) + " : RIGHT")
    elif closest == 180:
        print(str(bearing) + " : LEFT")

None of those work. The result doesn't seem to be consistent enough to use. Is there a better way to do it?


Solution

  • Here is my proposed approach to determine the closest compass direction to the one pointed at by a line segment [A, B] defined by its end points point_a and point_b:

    1. All calculations are done in standard cartesian coordinates, the change to screen coordinates is done at the end. that simplifies the approach, and makes the code re-usable elsewhere.
    2. First change the origin to point_a
    3. second calculate the angle of the line segment with the x_axis
    4. determine the closest bearing (in standard cartesian coordinates)
    5. convert the standard bearing to screen coordinates bearings (a horizontal flip)

    with points defined in screen coordinates (Y axis down), call get_bearings(point_a, point_b)
    If the points defined in standard cartesian coordinates (Y axis up), call assign_bearing_to_compass(point_a, point_b)
    (The tests underneath the code show the results of using points in standard coordinates, and screen coordinates.)


    import math
    
    
    def _change_origin_of_point_b_to_point_a(point_a, point_b):
        # uses standard Y axis orientation, not screen orientation
        return (point_b[0] - point_a[0], point_b[1] - point_a[1])
    
    def _calc_angle_segment_a_b_with_x_axis(point_a, point_b):
        # uses standard Y axis orientation, not screen orientation
        xa, ya = point_a
        xb, yb = _change_origin_of_point_b_to_point_a(point_a, point_b)
        return math.atan2(yb, xb)
    
    def determine_bearing_in_degrees(point_a, point_b):
        """returns the angle in degrees that line segment [point_a, point_b)]
           makes with the horizontal X axis 
        """
        # uses standard Y axis orientation, not screen orientation
        return _calc_angle_segment_a_b_with_x_axis(point_a, point_b) * 180 / math.pi
    
    def assign_bearing_to_compass(point_a, point_b):
        """returns the standard bearing of line segment [point_a, point_b)
        """
        # uses standard Y axis orientation, not screen orientation    
        compass = {'W' : [157.5, -157.5], 
                   'SW': [-157.5, -112.5], 
                   'S' : [-112.5, -67.5], 
                   'SE': [-67.5, -22.5], 
                   'E' : [-22.5, 22.5], 
                   "NE": [22.5, 67.5], 
                   'N' : [67.5, 112.5], 
                   'NW': [112.5, 157.5]}
    
        bear = determine_bearing_in_degrees(point_a, point_b)
        for direction, interval in compass.items():
            low, high = interval
            if bear >= low and bear < high:
                return direction
        return 'W'
    
    def _convert_to_negative_Y_axis(compass_direction):
        """flips the compass_direction horizontally
        """
        compass_conversion = {'E' : 'E', 
                              'SE': 'NE', 
                              'S' : 'N', 
                              'SW': 'NW', 
                              'W' : 'W', 
                              "NW": 'SW', 
                              'N' : 'S', 
                              'NE': 'SE'}
        return compass_conversion[compass_direction]
    
    def get_bearings(point_a, point_b):
        return _convert_to_negative_Y_axis(assign_bearing_to_compass(point_a, point_b))
    

    Tests:

    (using the standard trigonometric circle quadrants)

    Quadrant I:

    point_a = (0, 0)
    points_b = [(1, 0), (1, 3), (1, 2), (1, 1), (2, 1), (3, 1), (0, 1)]
    print("point_a, point_b     Y_up     Y_down (in screen coordinates)")
    for point_b in points_b:
        print(point_a, ' ', point_b, '      ', assign_bearing_to_compass(point_a, point_b), '        ', get_bearings(point_a, point_b))
    

    results:

    point_a, point_b     Y_up     Y_down (in screen coordinates)
    (0, 0)   (1, 0)        E          E
    (0, 0)   (1, 3)        N          S
    (0, 0)   (1, 2)        NE         SE
    (0, 0)   (1, 1)        NE         SE
    (0, 0)   (2, 1)        NE         SE
    (0, 0)   (3, 1)        E          E
    (0, 0)   (0, 1)        N          S
    

    Quadrant II:

    point_a = (0, 0)
    points_b = [(-1, 0), (-1, 3), (-1, 2), (-1, 1), (-2, 1), (-3, 1), (0, 1)]
    print("point_a, point_b     Y_up     Y_down (in screen coordinates)")
    for point_b in points_b:
        print(point_a, ' ', point_b, '      ', assign_bearing_to_compass(point_a, point_b), '        ', get_bearings(point_a, point_b))
    

    results:

    point_a, point_b     Y_up     Y_down (in screen coordinates)
    (0, 0)   (-1, 0)       W          W
    (0, 0)   (-1, 3)       N          S
    (0, 0)   (-1, 2)       NW         SW
    (0, 0)   (-1, 1)       NW         SW
    (0, 0)   (-2, 1)       NW         SW
    (0, 0)   (-3, 1)       W          W
    (0, 0)   (0, 1)        N          S
    

    Quadrant III:

    point_a = (0, 0)
    points_b = [(-1, 0), (-1, -3), (-1, -2), (-1, -1), (-2, -1), (-3, -1), (0, -1)]
    print("point_a, point_b     Y_up     Y_down (in screen coordinates)")
    for point_b in points_b:
        print(point_a, ' ', point_b, '      ', assign_bearing_to_compass(point_a, point_b), '        ', get_bearings(point_a, point_b))
    

    results:

    point_a, point_b     Y_up     Y_down (in screen coordinates)
    (0, 0)   (-1, 0)        W          W
    (0, 0)   (-1, -3)       S          N
    (0, 0)   (-1, -2)       SW         NW
    (0, 0)   (-1, -1)       SW         NW
    (0, 0)   (-2, -1)       SW         NW
    (0, 0)   (-3, -1)       W          W
    (0, 0)   (0, -1)        S          N
    

    Quadrant IV:

    point_a = (0, 0)
    points_b = [(1, 0), (1, -3), (1, -2), (1, -1), (2, -1), (3, -1), (0, -1)]
    print("point_a, point_b     Y_up     Y_down (in screen coordinates)")
    for point_b in points_b:
        print(point_a, ' ', point_b, '      ', assign_bearing_to_compass(point_a, point_b), '        ', get_bearings(point_a, point_b))
    

    results:

    point_a, point_b     Y_up     Y_down (in screen coordinates)
    (0, 0)   (1, 0)        E          E
    (0, 0)   (1, -3)       S          N
    (0, 0)   (1, -2)       SE         NE
    (0, 0)   (1, -1)       SE         NE
    (0, 0)   (2, -1)       SE         NE
    (0, 0)   (3, -1)       E          E
    (0, 0)   (0, -1)       S          N