How to search for objects within a specified radius of an XY coordinate (without gems)? - ruby

-- BACKGROUND --
First off, unfortunately as the software I'm working with has an in-built Ruby library, it doesn't allow for the installation of gems. I think Geocoder looked promising, alas...
I have X amount of links and nodes in 2D space and I wish to find the closest link to a given XY coordinate. Essentially looking to create my own method to do this, something like:
def manual_search_from_point(x, y, distance_from_point_to_search, collection_of_objects)
...
end
where "collection_of_objects" is normally an Array of the object group I am trying to retrieve (links).
Unsure if there is a way to continually radially search out further and further (increasing "distance_from_point_to_search") to find objects without already calculating the distance between objects. With that in mind I thought that gathering all links XY coordinates (endpoints etc.) and finding the distance from that point to the initial XY point may be worthwhile, using:
def self.distance(x1, y1, x2, y2)
Math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
end
...and going from there.
-- CURRENT STATUS --
I'm currently trying to add all links coordinates to a hash as values and the link object itself as a key, to then work through each link's XY values and work out the distance, and create a new hash with the link object again as Key, and distance to that object as value. I have made the first hash, however unsure how to cycle through the values as individual XY's. (One link objects can have many XY points)
points_hash = Hash[ array.map {|k| [k, k.bends]} ]
-- DESIRED OUTPUT --
From:
{#<Link:0x803a2e8>=>[394514.80, 421898.01, 394512.92, 421895.82]...} #(X1, Y1, X2, Y2)
to
{#<Link:0x803a2e8>=>[157], #(distance in metres (value not real distance))
\#<Link:0x803a2e8>=>[196]...} #(distance in metres (value not real distance))
Can give more information if its a little tricky to get what I'm after. Also happy to be told I'm going about this all wrong!
Many thanks.

Approach
Consider the link
[a, b]
where a and b are two-element arrays that correspond to x-y coordinates in Euclidean space. I will assume a != b.
In vector terminology, every point on the line that goes through the points a and b can be expressed as:
αa + (1-α)b
for some value of the scalar α. α satisfies 0 <= α <= 1 for points falling on the line segment between a and b. Those points are said to comprise a convex combination of a and b. I will compute the value of α (alpha) that corresponds to a point that is on both the line that passes through a and b and line that is perpendicular to that line and passes through a given point p.
First calculate the slope of the line that passes between a and b. Let
a = [ax,ay]
b = [bx,by]
Points c = [cx,cy] on the line that passes through a and b are expressed:
cx = alpha*ax + (1-alpha)*bx
cy = alpha*ay + (1-alpha)*by
which we can simplify to:
cx = alpha*(ax-bx) + bx
cy = alpha*(ay-by) + by
Note that cx and cy equal bx and by when alpha is zero and equal ax and ay when alpha equals 1.
Suppose now we are given a point:
p = [px,py]
and wish to find the point on the line (not necessarily the line segment) that passes through a and b that is the closest point to p. We can do that as follows.
First calculate the slope of the line that passes through a and b (assuming bx != ax, which would correspond to a vertical line):
slope = (by-ay)/(bx-ax)
Lines perpendicular to this line have a slope equal to:
pslope = -1/slope
Let's compute the intercept of such a line that passes through point p:
intercept + pslope*px = py
So
intercept = py - pslope*px
Now let's see where this line intersects the line that passes through a and b in terms of the value of alpha:
intercept + pslope(alpha*(ax-bx) + bx) = alpha*(ay-by) + by
Solving for alpha:
alpha = (by - pslope*bx - intercept)/(pslope*(ax-bx) - (ay-by))
Let's try this with an example. Consider just two links of a graph, as shown in the professionally-prepared drawing below. (Whoops! I see my graphic service neglected to label the point [2,3] as "A".) First consider the link, or line segment, A-B and the point P.
ax = 2
ay = 3
bx = 5
by = 7
px = 5
py = 2
slope = (by-ay).fdiv(bx-ax)
#=> (7-3).fdiv(5-2) => 1.333..
pslope = -1.0/slope
#=> -0.75
intercept = py - pslope*px
#=> 5.75
alpha = (by - pslope*bx - intercept)/(pslope*(ax-bx) - (ay-by))
#=> 0.8
cx = alpha*(ax-bx) + bx
#=> 2.6
cy = alpha*(ay-by) + by
#=> 3.8
Because 0 <= alpha (0.8) <= 1, (cx,cy) falls in the line segment A-B, so that point is the closest point on the line segment to P, not just the point on the line going through A and B that is closest to P.
The square of the distance from P to C is found to equal
(cx-px)**2 + (cy-py)**2
#=> (2.6-5.0)**2 + (3.8-2.0)**2 => 9
If we wished to include all links no farther than, say, 3.5 from P, that is equivalent to including all links whose squared distance to P is no more than:
max_dist_sqrd = 3.5**2
#=> 12.25
As 9.0 <= max_dist_sqrd (12.25), this link would be included.
Now consider the link D-E.
dx = 7
dy = 6
ex = 6
ey = 9
px = 5
py = 2
slope = (ey-dy).fdiv(ex-dx)
#=> (9-6).fdiv(6-7) => -3.0
pslope = -1.0/slope
#=> 1.0/3.0 => 0.333
intercept = py - pslope*px
#=> 2 - (0.333)*5 => 0.333..
alpha = (ey - pslope*ex - intercept)/(pslope*(dx-ex) - (dy-ey))
#=> (9 - 0.333*6 - 0.333)/(0.333*(7-6) - (6-9)) #=> 2.0
Because alpha > 1.0 we know that the point F is not on the line segment D-E. Let's take a brief excursion to see where the point is:
fx = alpha*(dx-ex) + ex
#=> 2.0(7-6) + 6 => 8.0
fy = alpha*(dy-ey) + ey
#=> 2.0(6-9) + 9 => 3.0
Because alpha > 1.0 we know that the closest point to P on the line segment D-E is D. Had alpha > 1.0, the closest point would have been E.
We therefore find that the closest distance squared from the point P to the line segment D-E equals:
(dx-px)**2 + (dy-py)**2
#=> (7-5)**2 + (6-2)**2 => 20
Since 20.0 > max_dist_sqrd (12.25), this link would not be included.
Code
def links_in_point_circle(links, point, max_dist)
max_dist_sqrd = max_dist**2
links.select { |link| sqrd_dist_to_link(link, point) <= max_dist_sqrd }
end
def sqrd_dist_to_link( ((xlow,ylow),(xhigh,yhigh)),(px,py) )
return vert_link_dist(xlow,ylow,yhigh,px,py) if xlow == xhigh
slope = -1.0/((yhigh-ylow).fdiv(xhigh-xlow))
intercept = py - slope*px
alpha = (yhigh - slope*xhigh - intercept)/(slope*(xlow-xhigh) - (ylow-yhigh))
closest_x, closest_y =
case alpha
when -Float::INFINITY...0
[xhigh, yhigh]
when 0..1.0
[alpha*(xlow-xhigh) + xhigh, alpha*(ylow-yhigh) + yhigh]
else
[xlow, ylow]
end
(closest_x-px)**2 + (closest_y-py)**2
end
def vert_link_dist(xlow, ylow, yhigh, px, py)
return Float::INFINITY if px != xlow
case
when py < ylow then (ylow-py)**2
when ylow..yhigh then 0
else (py-yhigh)**2
end
end
Example
links = [[[2,3],[5,7]], [[7,6], [6,9]]]
point = [5,2]
max_dist = 3.5
links_in_point_circle(links, point, max_dist)
#=> [[[2, 3], [5, 7]]]

Create an AABB-Tree: AABB-Tree is an spatial index i.e., it accelerate spatial queries. You can use other index datastructure like kd-tree, uniform grid, quad-tree, R-tree, etc. But I found AABB-tree simpler and near optimal for range queries (what you want to do is a range query e.g., like nearest neighbors)
1.1) First put each and every link into an AABB. Compute the centroid of each AABB.
1.2) Put all your AABBs into a big AABB that contains all of them. Make that big AABB the tree root node.
1.3) Find the largest side of the big AABB (it can be X or Y).
1.4) Sort all the AABBs by centroid coordinate (X or Y) corresponding to the largest side.
1.5) Find the middle of the sorted list and split it in two lists.
1.6) Create two child nodes attached to the root node and assign one list to each node.
1.7) Call step 1.2) recursively to build each child node into a subtree.
1.8) Stop the recursion when the list is of size <= N. Then assign all AABBs to that node.
Do a range query to find the closest link to a point (i.e., traverse the tree recursively).
2.1) Define a circle of infinite radius centered at the point you want to query. Pass the circle as parameter to the traverse function.
2.2) If the root node's AABB intersect with the circle, then call recursively the traverse function with the left and the right children. The order is important, so you should choose first the child closer to the circle center.
2.3) If the node is a leaf node (i.e., no children, it hold the list of AABBs of links) then check if circle intersect the AABBs of the links, if it does then compute the distance of point to each link and keep the shortest distance. While you compute the distance to each link compare it with the radius of the circle. If the distance is smaller than the radius update the radius if the circle to the shortest distance, and keep the ID of the link with shortest distance.
Once traverse function finish, you will get the shortest distance in logarithmic time.

Using methods provided by Cary Swoveland above, the answer is almost there. Using an example below:
class Link
attr_accessor :bends
def initialize(bends)
#bends = bends
end
end
link_1 = Link.new([1, 1, 2, 2, 3, 3, 4, 4])
link_2 = Link.new([5, 5, 6, 6, 7, 7, 8, 8])
link_3 = Link.new([11, 11, 14, 14, 17, 17, 22, 22, 25, 25])
valid_links = Array.new
links = [link_1, link_2, link_3]
links.each do |link|
# Conditional goes here, to populate valid_links
valid_links << link
end
point = [1.1, 1.1]
def link_coordinates(link)
# To get the link XYs in desired format:
# [[[x1, y1], [x2, y2]], [[x2, y2], [x3, y3]], [[x3, y3], [x4, y4]]] (1)
link = link.bends.each_slice(2).each_cons(2).to_a
#=> [[[1, 1], [2, 2]], [[2, 2], [3, 3]], [[3, 3], [4, 4]]] (for first link)
end
closest_segment = valid_links.min_by { |link| sqrd_dist_to_link(link_coordinates(link), point)}
The above yields an error within sqrd_dist_to_link. Where before when "links" was only using values and in the format:
links = [[[x1, y1], [x2, y2]], [[x2, y2], [x3, y3]], [[x3, y3], [x4, y4]]] (2)
#closest_segment = links.min_by { |link| sqrd_dist_to_link(link, point)}
succeeded in returning the nearest XY coordinates of the segment in the format: [[x1, y1], [x2, y2]].
Considering the "links" labelled "(1)" and the links labelled "(2)" output the same format, I'm uncertain how to tweak things so that valid_links.min_by { |link| sqrd_dist_to_link(link_coordinates(link), point)} will retrieve the link object of the nearest XY segment as desired.
Note:
I will update this when I have it working correctly so that it qualifies correctly as an answer.

Related

Maximum possible number of rectangles that can be crossed with a single straight line

I found this challenge problem which states the following :
Suppose that there are n rectangles on the XY plane. Write a program to calculate the maximum possible number of rectangles that can be crossed with a single straight line drawn on this plane.
I have been brainstorming for quite a time but couldn't find any solution.
Maybe at some stage, we use dynamic programming steps but couldn't figure out how to start.
Here is a sketch of an O(n^2 log n) solution.
First, the preliminaries shared with other answers.
When we have a line passing through some rectangles, we can translate it to any of the two sides until it passes through a corner of some rectangle.
After that, we fix that corner as the center of rotation and rotate the line to any of the two sides until it passes through another corner.
During the whole process, all points of intersection between our line and rectangle sides stayed on these sides, so the number of intersections stayed the same, as did the number of rectangles crossed by the line.
As a result, we can consider only lines which pass through two rectangle corners, which is capped by O(n^2), and is a welcome improvement compared to the infinite space of arbitrary lines.
So, how do we efficiently check all these lines?
First, let us have an outer loop which fixes one point A and then considers all lines passing through A.
There are O(n) choices of A.
Now, we have one point A fixed, and want to consider all lines AB passing through all other corners B.
In order to do that, first sort all other corners B according to the polar angle of AB, or, in other words, angle between axis Ox and vector AB.
Angles are measured from -PI to +PI or from 0 to 2 PI or otherwise, the point in which we cut the circle to sort angles can be arbitrary.
The sorting is done in O(n log n).
Now, we have points B1, B2, ..., Bk sorted by the polar angle around point A (their number k is something like 4n-4, all corners of all rectangles except the one where point A is a corner).
First, look at the line AB1 and count the number of rectangles crossed by that line in O(n).
After that, consider rotating AB1 to AB2, then AB2 to AB3, all the way to ABk.
The events which happen during the rotation are as follows:
When we rotate to ABi, and Bi is the first corner of some rectangle in our order, the number of rectangles crossed increases by 1 as soon as the rotating line hits Bi.
When we rotate to ABj, and Bj is the last corner of some rectangle in our order, the number of rectangles crossed decreases by 1 as soon as the line rotates past Bj.
Which corners are first and last can be established with some O(n) preprocessing, after the sort, but before considering the ordered events.
In short, we can rotate to the next such event and update the number of rectangles crossed in O(1).
And there are k = O(n) events in total.
What's left to do is to track the global maximum of this quantity throughout the whole algorithm.
The answer is just this maximum.
The whole algorithm runs in O(n * (n log n + n + n)), which is O(n^2 log n), just as advertised.
Solution
In the space of all lines in the graph, the lines which pass by a corner are exactly the ones where the number or intersections is about to decrease. In other words, they each form a local maximum.
And for every line which passes by at least one corner, there exist an associated line that passes by two corners that has the same number of intersections.
The conclusion is that we only need to check the lines formed by two rectangle corners as they form a set that fully represents the local maxima of our problem. From those we pick the one which has the most intersections.
Time complexity
This solution first needs to recovers all lines that pass by two corners. The number of such line is O(n^2).
We then need to count the number of intersections between a given line and a rectangle. This can obviously be done in O(n) by comparing to each rectangles.
There might be a more efficient way to proceed, but we know that this algorithm is then at most O(n^3).
Python3 implementation
Here is a Python implementation of this algorithm. I oriented it more toward readability than efficiency, but it does exactly what the above defines.
def get_best_line(rectangles):
"""
Given a set of rectangles, return a line which intersects the most rectangles.
"""
# Recover all corners from all rectangles
corners = set()
for rectangle in rectangles:
corners |= set(rectangle.corners)
corners = list(corners)
# Recover all lines passing by two corners
lines = get_all_lines(corners)
# Return the one which has the highest number of intersections with rectangles
return max(
((line, count_intersections(rectangles, line)) for line in lines),
key=lambda x: x[1])
This implementation uses the following helpers.
def get_all_lines(points):
"""
Return a generator providing all lines generated
by a combination of two points out of 'points'
"""
for i in range(len(points)):
for j in range(i, len(points)):
yield Line(points[i], points[j])
def count_intersections(rectangles, line):
"""
Return the number of intersections with rectangles
"""
count = 0
for rectangle in rectangles:
if line in rectangle:
count += 1
return count
And here are the class definition that serve as data structure for rectangles and lines.
import itertools
from decimal import Decimal
class Rectangle:
def __init__(self, x_range, y_range):
"""
a rectangle is defined as a range in x and a range in y.
By example, the rectangle (0, 0), (0, 1), (1, 0), (1, 1) is given by
Rectangle((0, 1), (0, 1))
"""
self.x_range = sorted(x_range)
self.y_range = sorted(y_range)
def __contains__(self, line):
"""
Return whether 'line' intersects the rectangle.
To do so we check if the line intersects one of the diagonals of the rectangle
"""
c1, c2, c3, c4 = self.corners
x1 = line.intersect(Line(c1, c4))
x2 = line.intersect(Line(c2, c3))
if x1 is True or x2 is True \
or x1 is not None and self.x_range[0] <= x1 <= self.x_range[1] \
or x2 is not None and self.x_range[0] <= x2 <= self.x_range[1]:
return True
else:
return False
#property
def corners(self):
"""Return the corners of the rectangle sorted in dictionary order"""
return sorted(itertools.product(self.x_range, self.y_range))
class Line:
def __init__(self, point1, point2):
"""A line is defined by two points in the graph"""
x1, y1 = Decimal(point1[0]), Decimal(point1[1])
x2, y2 = Decimal(point2[0]), Decimal(point2[1])
self.point1 = (x1, y1)
self.point2 = (x2, y2)
def __str__(self):
"""Allows to print the equation of the line"""
if self.slope == float('inf'):
return "y = {}".format(self.point1[0])
else:
return "y = {} * x + {}".format(round(self.slope, 2), round(self.origin, 2))
#property
def slope(self):
"""Return the slope of the line, returning inf if it is a vertical line"""
x1, y1, x2, y2 = *self.point1, *self.point2
return (y2 - y1) / (x2 - x1) if x1 != x2 else float('inf')
#property
def origin(self):
"""Return the origin of the line, returning None if it is a vertical line"""
x, y = self.point1
return y - x * self.slope if self.slope != float('inf') else None
def intersect(self, other):
"""
Checks if two lines intersect.
Case where they intersect: return the x coordinate of the intersection
Case where they do not intersect: return None
Case where they are superposed: return True
"""
if self.slope == other.slope:
if self.origin != other.origin:
return None
else:
return True
elif self.slope == float('inf'):
return self.point1[0]
elif other.slope == float('inf'):
return other.point1[0]
elif self.slope == 0:
return other.slope * self.origin + other.origin
elif other.slope == 0:
return self.slope * other.origin + self.origin
else:
return (other.origin - self.origin) / (self.slope - other.slope)
Example
Here is a working example of the above code.
rectangles = [
Rectangle([0.5, 1], [0, 1]),
Rectangle([0, 1], [1, 2]),
Rectangle([0, 1], [2, 3]),
Rectangle([2, 4], [2, 3]),
]
# Which represents the following rectangles (not quite to scale)
#
# *
# *
#
# ** **
# ** **
#
# **
# **
We can clearly see that an optimal solution should find a line that passes by three rectangles and that is indeed what it outputs.
print('{} with {} intersections'.format(*get_best_line(rectangles)))
# prints: y = 0.50 * x + -5.00 with 3 intersections
(Edit of my earlier answer that considered rotating the plane.)
Here's sketch of the O(n^2) algorithm, which combines Gassa's idea with Evgeny Kluev's reference to dual line arrangements as sorted angular sequences.
We start out with a doubly connected edge list or similar structure, allowing us to split an edge in O(1) time, and a method to traverse the faces we create as we populate a 2-dimensional plane. For simplicity, let's use just three of the twelve corners on the rectangles below:
9| (5,9)___(7,9)
8| | |
7| (4,6)| |
6| ___C | |
5| | | | |
4| |___| | |
3| ___ |___|(7,3)
2| | | B (5,3)
1|A|___|(1,1)
|_ _ _ _ _ _ _ _
1 2 3 4 5 6 7
We insert the three points (corners) in the dual plane according to the following transformation:
point p => line p* as a*p_x - p_y
line l as ax + b => point l* as (a, -b)
Let's enter the points in order A, B, C. We first enter A => y = x - 1. Since there is only one edge so far, we insert B => y = 5x - 3, which creates the vertex, (1/2, -1/2) and splits our edge. (One elegant aspect of this solution is that each vertex (point) in the dual plane is actually the dual point of the line passing through the rectangles' corners. Observe 1 = 1/2*1 + 1/2 and 3 = 1/2*5 + 1/2, points (1,1) and (5,3).)
Entering the last point, C => y = 4x - 6, we now look for the leftmost face (could be an incomplete face) where it will intersect. This search is O(n) time since we have to try each face. We find and create the vertex (-3, -18), splitting the lower edge of 5x - 3 and traverse up the edges to split the right half of x - 1 at vertex (5/3, 2/3). Each insertion has O(n) time since we must first find the leftmost face, then traverse each face to split edges and mark the vertices (intersection points for the line).
In the dual plane we now have:
After constructing the line arrangement, we begin our iteration on our three example points (rectangle corners). Part of the magic in reconstructing a sorted angular sequence in relation to one point is partitioning the angles (each corresponding with an ordered line intersection in the dual plane) into those corresponding with a point on the right (with a greater x-coordinate) and those on the left and concatenating the two sequences to get an ordered sequence from -90 deg to -270 degrees. (The points on the right transform to lines with positive slopes in relation to the fixed point; the ones on left, with negative slopes. Rotate your sevice/screen clockwise until the line for (C*) 4x - 6 becomes horizontal and you'll see that B* now has a positive slope and A* negative.)
Why does it work? If a point p in the original plane is transformed into a line p* in the dual plane, then traversing that dual line from left to right corresponds with rotating a line around p in the original plane that also passes through p. The dual line marks all the slopes of this rotating line by the x-coordinate from negative infinity (vertical) to zero (horizontal) to infinity (vertical again).
(Let's summarize the rectangle-count-logic, updating the count_array for the current rectangle while iterating through the angular sequence: if it's 1, increment the current intersection count; if it's 4 and the line is not directly on a corner, set it to 0 and decrement the current intersection count.)
Pick A, lookup A*
=> x - 1.
Obtain the concatenated sequence by traversing the edges in O(n)
=> [(B*) 5x - 3, (C*) 4x - 6] ++ [No points left of A]
Initialise an empty counter array, count_array of length n-1
Initialise a pointer, ptr, to track rectangle corners passed in
the opposite direction of the current vector.
Iterate:
vertex (1/2, -1/2)
=> line y = 1/2x + 1/2 (AB)
perform rectangle-count-logic
if the slope is positive (1/2 is positive):
while the point at ptr is higher than the line:
perform rectangle-count-logic
else if the slope is negative:
while the point at ptr is lower than the line:
perform rectangle-count-logic
=> ptr passes through the rest of the points up to the corner
across from C, so intersection count is unchanged
vertex (5/3, 2/3)
=> line y = 5/3x - 2/3 (AC)
We can see that (5,9) is above the line through AC (y = 5/3x - 2/3), which means at this point we would have counted the intersection with the rightmost rectangle and not yet reset the count for it, totaling 3 rectangles for this line.
We can also see in the graph of the dual plane, the other angular sequences:
for point B => B* => 5x - 3: [No points right of B] ++ [(C*) 4x - 6, (A*) x - 1]
for point C => C* => 4x - 6: [(B*) 5x - 3] ++ [(A*) x - 1]
(note that we start at -90 deg up to -270 deg)
How about the following algorithm:
RES = 0 // maximum number of intersections
CORNERS[] // all rectangles corners listed as (x, y) points
for A in CORNERS
for B in CORNERS // optimization: starting from corner next to A
RES = max(RES, CountIntersectionsWithLine(A.x, A.y, B.x, B.y))
return RES
In other words, start drawing lines from each rectangle corner to each other rectangle corner and find the maximum number of intersections. As suggested by #weston, we can avoid calculating same line twice by starting inner loop from the corner next to A.
If you consider a rotating line at angle Θ and if you project all rectangles onto this line, you obtain N line segments. The maximum number of rectangles crossed by a perpendicular to this line is easily obtained by sorting the endpoints by increasing abscissa and keeping a count of the intervals met from left to right (keep a trace of whether an endpoint is a start or an end). This is shown in green.
Now two rectangles are intersected by all the lines at an angle comprised between the two internal tangents [example in red], so that all "event" angles to be considered (i.e. all angles for which a change of count can be observed) are these N(N-1) angles.
Then the brute force resolution scheme is
for all limit angles (O(N²) of them),
project the rectangles on the rotating line (O(N) operations),
count the overlaps and keep the largest (O(N Log N) to sort, then O(N) to count).
This takes in total O(N³Log N) operations.
Assuming that the sorts needn't be re-done in full for every angle if we can do them incrementally, we can hope for a complexity lowered to O(N³). This needs to be checked.
Note:
The solutions that restrict the lines to pass through the corner of one rectangle are wrong. If you draw wedges from the four corners of a rectangle to the whole extent of another, there will remain empty space in which can lie a whole rectangle that won't be touched, even though there exists a line through the three of them.
We can have an O(n^2 (log n + m)) dynamic-programming method by adapting Andriy Berestovskyy's idea of iterating over the corners slightly to insert the relationship of the current corner vis a vis all the other rectangles into an interval tree for each of our 4n iteration cycles.
A new tree will be created for the corner we are trying. For each rectangle's four corners we'll iterate over each of the other rectangles. What we'll insert will be the angles marking the arc the paired-rectangle's farthest corners create in relation to the current fixed corner.
In the example directly below, for the fixed lower rectangle's corner R when inserting the record for the middle rectangle, we would insert the angles marking the arc from p2 to p1 in relation to R (about (37 deg, 58 deg)). Then when we check the high rectangle in relation to R, we'll insert the interval of angles marking the arc from p4 to p3 in relation to R (about (50 deg, 62 deg)).
When we insert the next arc record, we'll check it against all intersecting intervals and keep a record of the most intersections.
(Note that because any arc on a 360 degree circle for our purpose has a counterpart rotated 180 degrees, we may need to make an arbitrary cutoff (any alternative insights would be welcome). For example, this means that an arc from 45 degrees to 315 degrees would split into two: [0, 45] and [135, 180]. Any non-split arc could only intersect with one or the other but either way, we may need an extra hash to make sure rectangles are not double-counted.)

Rectangular room with reflecting walls with a hero and enemy puzzle

Current situation
Am stuck with this Google foobar question from past 9 days with 8/10 test cases passing. Could someone please help me as to what is wrong with my code?
Problem statement
Uh-oh - you've been cornered by one of Commander Lambdas elite guards! Fortunately, you grabbed a beam weapon from an abandoned guardpost while you were running through the station, so you have a chance to fight your way out. But the beam weapon is potentially dangerous to you as well as to the elite guard: its beams reflect off walls, meaning you'll have to be very careful where you shoot to avoid bouncing a shot toward yourself!
Luckily, the beams can only travel a certain maximum distance before becoming too weak to cause damage. You also know that if a beam hits a corner, it will bounce back in exactly the same direction. And of course, if the beam hits either you or the guard, it will stop immediately (albeit painfully).
Write a function answer(dimensions, your_position, guard_position, distance) that gives an array of 2 integers of the width and height of the room, an array of 2 integers of your x and y coordinates in the room, an array of 2 integers of the guard's x and y coordinates in the room, and returns an integer of the number of distinct directions that you can fire to hit the elite guard, given the maximum distance that the beam can travel.
The room has integer dimensions [1 < x_dim <= 1000, 1 < y_dim <= 1000]. You and the elite guard are both positioned on the integer lattice at different distinct positions (x, y) inside the room such that [0 < x < x_dim, 0 < y < y_dim]. Finally, the maximum distance that the beam can travel before becoming harmless will be given as an integer 1 < distance <= 10000.
For example, if you and the elite guard were positioned in a room with dimensions [3, 2], you_position [1, 1], guard_position [2, 1], and a maximum shot distance of 4, you could shoot in seven different directions to hit the elite guard (given as vector bearings from your location): [1, 0], [1, 2], [1, -2], [3, 2], [3, -2], [-3, 2], and [-3, -2]. As specific examples, the shot at bearing [1, 0] is the straight line horizontal shot of distance 1, the shot at bearing [-3, -2] bounces off the left wall and then the bottom wall before hitting the elite guard with a total shot distance of sqrt(13), and the shot at bearing [1, 2] bounces off just the top wall before hitting the elite guard with a total shot distance of sqrt(5).
Links referred
This post gives a test case and it is passing in my solution
This post gives a GUI to understand the problem
My approach
I reflect the enemy position in the mirrored planes of the rectangular room on all directions as long as the reflected point is within the given distance from the source point. This will give the enemy positions in reflected planes as a straight line from source. This distance is less than the given distance. Once these points are generated, I check if any of the points from that list makes the beam passes through hero before hitting the enemy.
Code in Python
from math import *
BOTTOM_LEFT = (0,0)
TOP_RIGHT = (0,0)
SOURCE = (0,0)
TARGET = (0,0)
DISTANCE = 0
BOTTOM_RIGHT = (0,0)
TOP_LEFT = (0,0)
def answer(tr, src, bp, dist):
global BOTTOM_LEFT
BOTTOM_LEFT = (0,0)
global TOP_RIGHT
TOP_RIGHT = tr
global SOURCE
SOURCE = src
global TARGET
TARGET = bp
global DISTANCE
DISTANCE = dist
global BOTTOM_RIGHT
BOTTOM_RIGHT = (TOP_RIGHT[0], BOTTOM_LEFT[1])
global TOP_LEFT
TOP_LEFT = (BOTTOM_LEFT[0], TOP_RIGHT[1])
all_targets = get_targets(start_point = TARGET, distance = DISTANCE)
CORNERS = [BOTTOM_LEFT, BOTTOM_RIGHT, TOP_LEFT, TOP_RIGHT]
corner_angles = [4]
i = 0
while i < 4:
ca = degrees(atan2(CORNERS[i][1] - SOURCE[1], CORNERS[i][0] - SOURCE[0]))
corner_angles.append(ca)
i = i+1
valid_angles = set()
for tgt in all_targets:
pa = degrees(atan2(tgt[1] - SOURCE[1], tgt[0] - SOURCE[0]))
if(tgt[0] - SOURCE[0] > 0 ):
valid_angles.add(pa)
elif(tgt[0] - SOURCE[0] < 0):
if pa % 45 != 0 and pa not in corner_angles:
valid_angles.add(pa)
else:
valid_angles.add(pa)
if(src == bp):
return 0
else:
return len(valid_angles)
def get_mirrored(point):
ret = []
x = point[0]
y = point[1] - 2*(point[1] - TOP_RIGHT[1])
ret.append((x,y))
ret.append((point[0], point[1] - 2*(point[1] - BOTTOM_LEFT[1])))
ret.append((point[0] - 2*(point[0] - BOTTOM_LEFT[0]), point[1]))
ret.append((point[0] - 2*(point[0] - TOP_RIGHT[0]), point[1]))
return ret
def get_targets(start_point, distance):
targets = []
targets.append(start_point)
all_targets = set()
all_targets.add((start_point[0],start_point[1]))
last_targets = all_targets
while True:
new_level_targets = set()
for tgt in last_targets:
new_targets = get_mirrored(tgt)
new_targets = set(t for t in new_targets if hypot(SOURCE[0] - t[0], SOURCE[1] - t[1]) <= DISTANCE)
new_targets -= all_targets
new_level_targets |= new_targets
if not new_level_targets:
break
all_targets |= new_level_targets
last_targets = new_level_targets
return all_targets
print(answer((3,2), (1,1), (2,1), 4)) #Answer 7
print(answer((300,275), (150,150), (185,100), 500) #Answer 9

Find indices of polygon vertices nearest to a point

Heading
I need to find the indices of the polygon nearest to a point
So in this case the ouput would be 4 and 0. Such that if the red point is added I know to where to place the vertex in the array. Does anyone know where to start?
(Sorry if the title is misleading, I wasnt sure how to phrase it properly)
In this case the ouput would be 0 and 1, rather than the closest 4.
Point P lies on the segment AB, if two simple conditions are met together:
AP x PB = 0 //cross product, vectors are collinear or anticollinear, P lies on AB line
AP . PB > 0 //scalar product, exclude anticollinear case to ensure that P is inside the segment
So you can check all sequential vertice pairs (pseudocode):
if (P.X-V[i].X)*(V[i+1].Y-P.Y)-(P.Y-V[i].Y)*(V[i+1].X-P.X)=0 then
//with some tolerance if point coordinates are float
if (P.X-V[i].X)*(V[i+1].X-P.X)+(P.Y-V[i].Y)*(V[i+1].Y-P.Y)>0
then P belongs to (i,i+1) segment
This is fast direct (brute-force) method.
Special data structures exist in computer geometry to quickly select candidate segments - for example, r-tree. But these complicated methods will gain for long (many-point) polylines and for case where the same polygon is used many times (so pre-treatment is negligible)
I'll assume that the new point is to be added to an edge. So you are given the coordinates of a point a = (x, y) and you want to find the indices of the edge on which it lies. Let's call the vertices of that edge b, c. Observe that the area of the triangle abc is zero.
So iterate over all edges and choose the one that minimizes area of triangle abc where a is your point and bc is current edge.
a = input point
min_area = +infinity
closest_edge = none
n = number of vertices in polygon
for(int i = 1; i <= n; i++)
{ b = poly[ i - 1 ];
c = poly[ i % n ];
if(area(a, b, c) < min_area)
{ min_area = area(a, b, c);
closest_edge = bc
}
}
You can calculate area using:
/* Computes area x 2 */
int area(a, b, c)
{ int ans = 0;
ans = (a.x*b.y + b.x*x.y + c.x*a.y) - (a.y*b.x + b.y*c.x + c.y*a.x);
return ABS(ans);
}
I think you would be better off trying to compare the distance from the actual point to a comparable point on the line. The closest comparable point would be the one that forms a perpendicular line like this. a is your point in question and b is the comparable point on the line line between the two vertices that you will check distance to.
However there's another method which I think might be more optimal for this case (as it seems most of your test points lie pretty close to the desired line already). Instead of find the perpendicular line point we can simply check the point on the line that has the same X value like this. b in this case is a lot easier to calculate:
X = a.X - 0.X;
Slope = (1.Y - 0.Y) / (1.X - 0.X);
b.X = 0.X + X;
b.Y = 0.Y + (X * Slope);
And the distance is simply the difference in Y values between a and b:
distance = abs(a.Y - b.Y);
One thing to keep in mind is that this method will become more inaccurate as the slope increases as well as become infinite when the slope is undefined. I would suggest flipping it when the slope > 1 and checking for a b that lies at the same y rather than x. That would look like this:
Y = a.Y - 0.Y;
Inverse_Slope = (1.X - 0.X) / (1.Y - 0.Y);
b.Y = 0.Y + Y;
b.X = 0.Y + (Y * Inverse_Slope);
distance = abs(a.X - b.X);
Note: You should also check whether b.X is between 0.X and 1.X and b.Y is between 0.Y and 1.Y in the second case. That way we are not checking against points that dont lie on the line segment.
I admit I don't know the perfect terminology when it comes to this kind of thing so it might be a little confusing, but hope this helps!
Rather than checking if the point is close to an edge with a prescribed tolerance, as MBo suggested, you can fin the edge with the shortest distance to the point. The distance must be computed with respect to the line segment, not the whole line.
How do you compute this distance ? Let P be the point and Q, R two edge endpoints.
Let t be in range [0,1], you need to minimize
D²(P, QR) = D²(P, Q + t QR) = (PQ + t QR)² = PQ² + 2 t PQ.QR + t² QR².
The minimum is achieved when the derivative cancels, i.e. t = - PQ.QR / QR². If this quantity exceeds the range [0,1], just clamp it to 0 or 1.
To summarize,
if t <= 0, D² = PQ²
if t >= 1, D² = PR²
otherwise, D² = PQ² - t² QR²
Loop through all the vertices, calculate the distance of that vertex to the point, find the minimum.
double min_dist = Double.MAX_VALUE;
int min_index=-1;
for(int i=0;i<num_vertices;++i) {
double d = dist(vertices[i],point);
if(d<min_dist) {
min_dist = d;
min_index = i;
}
}

Calculate bounding polygon of alpha shape from the Delaunay triangulation

Given a set of points in the plane, a notion of alpha-shape, for a given positive number alpha, is defined by finding the Delaunay triangulation and deleting any triangles for which at least one edge exceeds alpha in length. Here's an example using d3:
http://bl.ocks.org/gka/1552725
The problem is that, when there are thousands of points, simply drawing all the interior triangles is too slow for an interactive visualization, so I'd like to just find the bounding polygons. This isn't so simple, because as you can see from that example sometimes there might be two such polygons.
As a simplification, suppose some clustering has been performed so that there's guaranteed to be a unique bounding polygon for each triangulation. What's the best way to find this bounding polygon? In particular, the edges have to be ordered consistently and it must support the possibility of "holes" (think a torus or donut shape--this is expressible in GeoJSON).
Here is some Python code that does what you need. I modified the alpha-shape (concave hull) computation from here so that it doesn't insert inner edges (the only_outer parameter). I also made it self-contained so it doesn't depend on an outside library.
from scipy.spatial import Delaunay
import numpy as np
def alpha_shape(points, alpha, only_outer=True):
"""
Compute the alpha shape (concave hull) of a set of points.
:param points: np.array of shape (n,2) points.
:param alpha: alpha value.
:param only_outer: boolean value to specify if we keep only the outer border
or also inner edges.
:return: set of (i,j) pairs representing edges of the alpha-shape. (i,j) are
the indices in the points array.
"""
assert points.shape[0] > 3, "Need at least four points"
def add_edge(edges, i, j):
"""
Add a line between the i-th and j-th points,
if not in the list already
"""
if (i, j) in edges or (j, i) in edges:
# already added
assert (j, i) in edges, "Can't go twice over same directed edge right?"
if only_outer:
# if both neighboring triangles are in shape, it is not a boundary edge
edges.remove((j, i))
return
edges.add((i, j))
tri = Delaunay(points)
edges = set()
# Loop over triangles:
# ia, ib, ic = indices of corner points of the triangle
for ia, ib, ic in tri.simplices:
pa = points[ia]
pb = points[ib]
pc = points[ic]
# Computing radius of triangle circumcircle
# www.mathalino.com/reviewer/derivation-of-formulas/derivation-of-formula-for-radius-of-circumcircle
a = np.sqrt((pa[0] - pb[0]) ** 2 + (pa[1] - pb[1]) ** 2)
b = np.sqrt((pb[0] - pc[0]) ** 2 + (pb[1] - pc[1]) ** 2)
c = np.sqrt((pc[0] - pa[0]) ** 2 + (pc[1] - pa[1]) ** 2)
s = (a + b + c) / 2.0
area = np.sqrt(s * (s - a) * (s - b) * (s - c))
circum_r = a * b * c / (4.0 * area)
if circum_r < alpha:
add_edge(edges, ia, ib)
add_edge(edges, ib, ic)
add_edge(edges, ic, ia)
return edges
If you run it with the following test code you will get
this figure:
from matplotlib.pyplot import *
# Constructing the input point data
np.random.seed(0)
x = 3.0 * np.random.rand(2000)
y = 2.0 * np.random.rand(2000) - 1.0
inside = ((x ** 2 + y ** 2 > 1.0) & ((x - 3) ** 2 + y ** 2 > 1.0) & ((x - 1.5) ** 2 + y ** 2 > 0.09))
points = np.vstack([x[inside], y[inside]]).T
# Computing the alpha shape
edges = alpha_shape(points, alpha=0.25, only_outer=True)
# Plotting the output
figure()
axis('equal')
plot(points[:, 0], points[:, 1], '.')
for i, j in edges:
plot(points[[i, j], 0], points[[i, j], 1])
show()
Create a graph in which the nodes correspond to Delaunay triangles and in which there is a graph edge between two triangles if and only if they share two vertices.
Compute the connected components of the graph.
For each connected component, find all of the nodes that have less than three adjacent nodes (that is, those that have degree 0, 1, or 2). These correspond to the boundary triangles. We define the edges of a boundary triangle that are not shared with another triangle to be the boundary edges of that boundary triangle.
As an example, I have highlighted these boundary triangles in your example "question mark" Delaunay triangulation:
By definition, every boundary triangle is adjacent to at most two other boundary triangles. The boundary edges of boundary triangles form cycles. You can simply traverse those cycles to determine polygon shapes for the boundaries. This will also work for polygons with holes if you keep them in mind in your implementation.
I know it is a delayed answer, but the methods posted here did not work for me for various reasons.
The package Alphashape that is mentioned is generally good but its downside is that it uses Shapely's cascade_union and triangulation methods to give you the final concave hull which is super slow for large datasets and low alpha values (high precision). In this case the code posted by Iddo Hanniel (and the revision by Harold) will keep a great number of edges on the interior and the only way to dissolve them is to use the aforementioned cascade_union and triangulation from Shapely.
I generally work with complex forms and the code below works fine and it is fast (2 seconds for 100K 2D points):
import numpy as np
from shapely.geometry import MultiLineString
from shapely.ops import unary_union, polygonize
from scipy.spatial import Delaunay
from collections import Counter
import itertools
def concave_hull(coords, alpha): # coords is a 2D numpy array
# i removed the Qbb option from the scipy defaults.
# it is much faster and equally precise without it.
# unless your coords are integers.
# see http://www.qhull.org/html/qh-optq.htm
tri = Delaunay(coords, qhull_options="Qc Qz Q12").vertices
ia, ib, ic = (
tri[:, 0],
tri[:, 1],
tri[:, 2],
) # indices of each of the triangles' points
pa, pb, pc = (
coords[ia],
coords[ib],
coords[ic],
) # coordinates of each of the triangles' points
a = np.sqrt((pa[:, 0] - pb[:, 0]) ** 2 + (pa[:, 1] - pb[:, 1]) ** 2)
b = np.sqrt((pb[:, 0] - pc[:, 0]) ** 2 + (pb[:, 1] - pc[:, 1]) ** 2)
c = np.sqrt((pc[:, 0] - pa[:, 0]) ** 2 + (pc[:, 1] - pa[:, 1]) ** 2)
s = (a + b + c) * 0.5 # Semi-perimeter of triangle
area = np.sqrt(
s * (s - a) * (s - b) * (s - c)
) # Area of triangle by Heron's formula
filter = (
a * b * c / (4.0 * area) < 1.0 / alpha
) # Radius Filter based on alpha value
# Filter the edges
edges = tri[filter]
# now a main difference with the aforementioned approaches is that we dont
# use a Set() because this eliminates duplicate edges. in the list below
# both (i, j) and (j, i) pairs are counted. The reasoning is that boundary
# edges appear only once while interior edges twice
edges = [
tuple(sorted(combo)) for e in edges for combo in itertools.combinations(e, 2)
]
count = Counter(edges) # count occurrences of each edge
# keep only edges that appear one time (concave hull edges)
edges = [e for e, c in count.items() if c == 1]
# these are the coordinates of the edges that comprise the concave hull
edges = [(coords[e[0]], coords[e[1]]) for e in edges]
# use this only if you need to return your hull points in "order" (i think
# its CCW)
ml = MultiLineString(edges)
poly = polygonize(ml)
hull = unary_union(list(poly))
hull_vertices = hull.exterior.coords.xy
return edges, hull_vertices
There now exists a python package alphashape which is extremely easy to use, and can be installed by pip or conda.
The main function has similar inputs to the answer given by #Iddo Hanniel, adjusting the second positional argument would give you the desired outline.
Alternatively, you could leave the seconda positional argument blank and the function would optimize that parameter for you to give you the best concave hull. Beware, the computational time is increased greatly if you let the function optimize the value.
Slightly revise Hanniel's answer for 3d point case (tetrahedron).
def alpha_shape(points, alpha, only_outer=True):
"""
Compute the alpha shape (concave hull) of a set of points.
:param points: np.array of shape (n, 3) points.
:param alpha: alpha value.
:param only_outer: boolean value to specify if we keep only the outer border
or also inner edges.
:return: set of (i,j) pairs representing edges of the alpha-shape. (i,j) are
the indices in the points array.
"""
assert points.shape[0] > 3, "Need at least four points"
def add_edge(edges, i, j):
"""
Add a line between the i-th and j-th points,
if not in the set already
"""
if (i, j) in edges or (j, i) in edges:
# already added
if only_outer:
# if both neighboring triangles are in shape, it's not a boundary edge
if (j, i) in edges:
edges.remove((j, i))
return
edges.add((i, j))
tri = Delaunay(points)
edges = set()
# Loop over triangles:
# ia, ib, ic, id = indices of corner points of the tetrahedron
print(tri.vertices.shape)
for ia, ib, ic, id in tri.vertices:
pa = points[ia]
pb = points[ib]
pc = points[ic]
pd = points[id]
# Computing radius of tetrahedron Circumsphere
# http://mathworld.wolfram.com/Circumsphere.html
pa2 = np.dot(pa, pa)
pb2 = np.dot(pb, pb)
pc2 = np.dot(pc, pc)
pd2 = np.dot(pd, pd)
a = np.linalg.det(np.array([np.append(pa, 1), np.append(pb, 1), np.append(pc, 1), np.append(pd, 1)]))
Dx = np.linalg.det(np.array([np.array([pa2, pa[1], pa[2], 1]),
np.array([pb2, pb[1], pb[2], 1]),
np.array([pc2, pc[1], pc[2], 1]),
np.array([pd2, pd[1], pd[2], 1])]))
Dy = - np.linalg.det(np.array([np.array([pa2, pa[0], pa[2], 1]),
np.array([pb2, pb[0], pb[2], 1]),
np.array([pc2, pc[0], pc[2], 1]),
np.array([pd2, pd[0], pd[2], 1])]))
Dz = np.linalg.det(np.array([np.array([pa2, pa[0], pa[1], 1]),
np.array([pb2, pb[0], pb[1], 1]),
np.array([pc2, pc[0], pc[1], 1]),
np.array([pd2, pd[0], pd[1], 1])]))
c = np.linalg.det(np.array([np.array([pa2, pa[0], pa[1], pa[2]]),
np.array([pb2, pb[0], pb[1], pb[2]]),
np.array([pc2, pc[0], pc[1], pc[2]]),
np.array([pd2, pd[0], pd[1], pd[2]])]))
circum_r = math.sqrt(math.pow(Dx, 2) + math.pow(Dy, 2) + math.pow(Dz, 2) - 4 * a * c) / (2 * abs(a))
if circum_r < alpha:
add_edge(edges, ia, ib)
add_edge(edges, ib, ic)
add_edge(edges, ic, id)
add_edge(edges, id, ia)
add_edge(edges, ia, ic)
add_edge(edges, ib, id)
return edges
It turns out TopoJSON has a merge algorithm which performs just this task: https://github.com/mbostock/topojson/wiki/API-Reference#merge
There's even an example showing it in action: http://bl.ocks.org/mbostock/9927735
In my case, it was easy for me to generate TopoJSON data, and this library function accomplished the task perfectly for me.
Building up on #Timothy's answer, I used the following algorithm to calculate the boundary rings of a Delaunay triangulation.
from matplotlib.tri import Triangulation
import numpy as np
def get_boundary_rings(x, y, elements):
mpl_tri = Triangulation(x, y, elements)
idxs = np.vstack(list(np.where(mpl_tri.neighbors == -1))).T
unique_edges = list()
for i, j in idxs:
unique_edges.append((mpl_tri.triangles[i, j],
mpl_tri.triangles[i, (j+1) % 3]))
unique_edges = np.asarray(unique_edges)
ring_collection = list()
initial_idx = 0
for i in range(1, len(unique_edges)-1):
if unique_edges[i-1, 1] != unique_edges[i, 0]:
try:
idx = np.where(
unique_edges[i-1, 1] == unique_edges[i:, 0])[0][0]
unique_edges[[i, idx+i]] = unique_edges[[idx+i, i]]
except IndexError:
ring_collection.append(unique_edges[initial_idx:i, :])
initial_idx = i
continue
# if there is just one ring, the exception is never reached,
# so populate ring_collection before returning.
if len(ring_collection) == 0:
ring_collection.append(np.asarray(unique_edges))
return ring_collection
Alpha shapes is defined as a delaunay triangulation without edges exceeding alpha. First of remove all interior triangles and then all edges exceeding alpha.

How to find out if a ray intersects a rectangle?

We have a ray that starts at point A(X, Y) and goes on forever through given point B(X, Y) != A. We have a rectangle defined by points K,L,M,N each with its (X, Y).
I wonder how to detect if our ray intersects with any point of our rectangle (get a bool, not precice coordinates)? What is algorithm for calculating such value?
Let me get this straight. You have a vector v headed off in direction (b_x - a_x, b_y - a_y) and starting at (a_x, a_y).
Consider the vector w = (b_y - a_y, a_x - b_x). It is at right angles to the first. (Verify with a dot product.) Therefore for any point (p_x, p_y) you can easily tell which side of the vector it is on by taking a dot product of (p_x - a_x, p_y - a_y) with w and looking at the sign.
So take that dot product with all 4 corners of your rectangle. If any give a 0 dot product, they are on the vector, if the signs change there is an intersection, if the sign is always the same there is no intersection.
You can use the sweep line algorithm to do so.
http://en.wikipedia.org/wiki/Sweep_line_algorithm
A less clever, but conceptually simpler approach: the ray intersects the rectangle if and only if it intersects at least one of the sides. So for each side of the rectangle, find the intersection (if any) of the line passing through the endpoints with the ray AB; then it's simply a range check to determine if that intersection lies is part of the line segment on the boundary of the rectangle, or if it is outside.
You probably want to compute the segment (if any) of the ray AB that intersects the rectangle. If your rectangle is axis-aligned, this will be easier to compute in a numerical sense, but the logic should be similar.
You can represent a directed line L as [a, b, c] such that, if point P is (X, Y):
let L(P) = a*X + b*Y + c
then, if L(P) == 0, point P is on L
if L(P) > 0, point P is to the left of L
if L(P) < 0, point P is to the right of L
Note that this is redundant in the sense that, given any k > 0, [k*a, k*b, k*c] represents the same line (this property makes it a homogeneous coordinate system). We can also represent points with homogeneous coordinates by augmenting them with a third coordinate:
2D point P = (X, Y)
-> homogeneous coordinates [x, y, w] for P are [X, Y, 1]
L(P) = L.a*P.x + L.b*P.y + L.c*P.w == a*X + b*Y + c*1
In any case, given two corners of your rectangle (say, P and Q), you can compute the homogeneous coordinates of the line through P and Q using a 3-D cross-product of their homogeneous coordinates:
homogeneous coordinates for line PQ are: [P.X, P.Y, 1] cross [Q.X, Q.Y, 1]
-> PQ.a = P.Y - Q.Y
PQ.b = Q.X - P.X
PQ.c = P.X*Q.Y - Q.X*P.Y
You can verify mathematically that points P and Q are both on the above-described line PQ.
To represent the segment of line AB that intersects the rectangle, first compute vector V = B - A, as in #btilly's answer. For homogeneous coordinates, this works as follows:
A = [A.X, A.Y, 1]
B = [B.X, B.Y, 1]
-> V = B - A = [B.X-A.X, B.Y-A.Y, 0]
for any point C on AB: homogeneous coordinates for C = u*A + v*V
(where u and v are not both zero)
Point C will be on the ray part of the line only if u and v are both non-negative. (This representation may seem obscure, compared to the usual formulation of C = A + lambda * V, but doing it this way avoids unnecessary divide-by-zero cases...)
Now, we can compute the ray intersection: we represent a segment of the line AB by the parametric [u,v] coordinates of each endpoint: { start = [start.u, start.v]; end = [end.u, end.v] }.
We compute the edges of the rectangle in the counterclockwise direction, so that points inside the rectangle are on the left/positive side (L(P)>0) of every edge.
Starting segment is entire ray:
start.u = 1; start.v = 0
end.u = 0; end.v = 1
for each counterclockwise-directed edge L of the rectangle:
compute:
L(A) = L.a*A.X + L.b*A.Y + L.c
L(V) = L.a*V.X + L.b*V.Y
L(start) = start.u * L(A) + start.v * L(V)
L(end) = end.u * L(A) + end.v * L(V)
if L(start) and L(end) are both less than zero:
exit early: return "no intersection found"
if L(start) and L(end) are both greater or equal to zero:
do not update the segment; continue with the next line
else, if L(start) < 0:
update start coordinates:
start.u := L(V)
start.v := -L(A)
else, if L(end) < 0:
update end coordinates:
end.u := -L(V)
end.v := L(A)
on normal loop exit, the ray does intersect the rectangle;
the part of the ray inside the rectangle is the segment between points:
homog_start = start.u * A + start.v * V
homog_end = end.u * A + end.v * V
return "intersection found":
intersection_start.X = homog_start.x/homog_start.w
intersection_start.Y = homog_start.y/homog_start.w
intersection_end.X = homog_end.x/homog_end.w
intersection_end.Y = homog_end.y/homog_end.w
Note that this will work for arbitrary convex polygons, not just rectangles; the above is actually a general ray/convex polygon intersection algorithm. For a rectangle, you can unroll the for-loop; and, if the rectangle is axis-aligned, you can drastically simplify the arithmetic. However, the 4-case decision in the inner loop should remain the same for each edge.

Resources