Skip to content

Commit

Permalink
allow search for close point on line within a radius
Browse files Browse the repository at this point in the history
Mostly a performance improvement, since it's unlikely for a video
to return to the same place later in time. Effectively, this works
as an early return in many cases and prevents an exhaustive search
across the whole line/gpx track.
  • Loading branch information
breunigs committed Aug 11, 2024
1 parent 22688c5 commit 7dfc231
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 15 deletions.
3 changes: 2 additions & 1 deletion lib/article/decorators.ex
Original file line number Diff line number Diff line change
Expand Up @@ -312,14 +312,15 @@ defmodule Article.Decorators do
article's bbox.
"""
@spec start_image_path(Article.t()) :: binary() | nil
@search_radius_m 50
def start_image_path(art) do
with [track | _rest] <- article_with_tracks(art).tracks(),
bbox when is_map(bbox) <- bbox(art),
rendered <- Video.Generator.get(track) do
center = Geo.CheapRuler.center(bbox)

%{point: %{time_offset_ms: ms}} =
Geo.CheapRuler.closest_point_on_line(rendered.coords(), center)
Geo.CheapRuler.closest_point_on_line(rendered.coords(), center, @search_radius_m)

# VelorouteWeb.Router.Helpers.image_extract_path(
# VelorouteWeb.Endpoint,
Expand Down
74 changes: 62 additions & 12 deletions lib/geo/cheap_ruler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,16 @@ defmodule Geo.CheapRuler do
do: point2point_dist(from, to)

@doc ~S"""
It finds the closest point of a line to another given point. Optionally you
can pass the bearing and how strongly to consider it. Default is to ignore
the bearing.
It finds the closest point of a line to another given point. Distances are
expected and returned in meters. It will return the first point if there
are multiple with the same distance.
All points within `epsilon` meters are considered to be close. The default
value of 0.0 looks for a point exactly on the line and will do an exhaustive
search if needed. For larger values, only the first line segments within
`point` + `epsilon` are considered. If the line doesn't intersect that area
more than once, the returned point is still optimal. This is a performance
improvement if the closest point is near the beginning of the line.
## Examples
Expand All @@ -329,6 +336,25 @@ defmodule Geo.CheapRuler do
after: %Video.TimedPoint{lon: 10.04383358673, lat: 53.58986207956, time_offset_ms: 300}
}
Non-zero epsilon still finds good candidates (all points from the example
are within 100m from each other):
iex> Geo.CheapRuler.closest_point_on_line(
...> [
...> %{lat: 53.0001, lon: 9.9999},
...> %{lat: 53.0002, lon: 9.9999},
...> %{lat: 53.0004, lon: 9.9999}
...> ],
...> %{lat: 53.0003, lon: 9.9999},
...> 100.0
...> )
%{
dist: 0.0,
before: %{lat: 53.0002, lon: 9.9999},
point: %{lat: 53.0003, lon: 9.9999},
after: %{lat: 53.0004, lon: 9.9999}
}
It does not extend beyond line start/end points:
iex> Geo.CheapRuler.closest_point_on_line(
Expand All @@ -345,24 +371,45 @@ defmodule Geo.CheapRuler do
after: %{lat: 53.550572, lon: 9.994393}
}
Returns the first point given if there are multiple candidates:
iex> Geo.CheapRuler.closest_point_on_line(
...> [
...> %{lat: 53.550598, lon: 9.994402, time_offset_ms: 100},
...> %{lat: 53.550598, lon: 9.994402, time_offset_ms: 200}
...> ],
...> %{lat: 53.550598, lon: 9.994402}
...> )
%{
dist: 0.0,
before: %{lat: 53.550598, lon: 9.994402, time_offset_ms: 100},
point: %{lat: 53.550598, lon: 9.994402},
after: %{lat: 53.550598, lon: 9.994402, time_offset_ms: 100}
}
"""
@spec closest_point_on_line([Geo.Point.like()], Geo.Point.like()) :: %{
@spec closest_point_on_line(
line :: [Geo.Point.like()],
point :: Geo.Point.like(),
epsilon :: float() | non_neg_integer()
) :: %{
dist: float(),
before: Geo.Point.like(),
point: Geo.Point.like(),
after: Geo.Point.like()
}
def closest_point_on_line(line, point)
def closest_point_on_line(line, point, eps \\ 0.0)

def closest_point_on_line(line, %{lon: lon, lat: lat} = pt)
when is_list(line) and is_float(lon) and is_float(lat) do
def closest_point_on_line(line, %{lon: lon, lat: lat} = pt, epsilon)
when is_list(line) and is_float(lon) and is_float(lat) and epsilon >= 0.0 do
[head | tail] = line
# ensure it's a float to avoid conversions in the loop
eps2 = epsilon * epsilon * 1.0

dist = point2point_dist(head, pt)
acc = %{prev: head, dist: dist * dist, i: 0, before: head, after: head, t: 0.0}
acc = %{prev: head, dist: dist * dist, before: head, after: head, t: 0.0}

acc =
Enum.reduce(tail, acc, fn next, acc ->
Enum.reduce_while(tail, acc, fn next, acc ->
x = acc.prev.lon
y = acc.prev.lat
dx = (next.lon - x) * @kx
Expand All @@ -384,12 +431,15 @@ defmodule Geo.CheapRuler do
dy = (lat - y) * @ky
dist = dx * dx + dy * dy

next_acc = %{acc | i: acc.i + 1, prev: next}
next_acc = %{acc | prev: next}

if dist >= acc.dist do
next_acc
# Stop if we have a suitable candidate and the "next" point is again
# outside the epsilon range
break = if acc.dist <= eps2 && dist > eps2, do: :halt, else: :cont
{break, next_acc}
else
%{next_acc | dist: dist, t: t, before: acc.prev, after: next}
{:cont, %{next_acc | dist: dist, t: t, before: acc.prev, after: next}}
end
end)

Expand Down
6 changes: 5 additions & 1 deletion lib/veloroute_web/live/video_state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,10 @@ defmodule VelorouteWeb.Live.VideoState do
# info is available.
@first_group_bonus 5

# Consider points within this radius close enough and forgo searching the
# whole track for slightly better candidates
@search_radius_meters 5

# if we have a position, change the tracks default order by closeness to the position
defp update_from_tracks(state, tracks, near_position, accurate_position)
when is_map(near_position) do
Expand All @@ -409,7 +413,7 @@ defmodule VelorouteWeb.Live.VideoState do
else
dist =
rendered.coords()
|> Geo.CheapRuler.closest_point_on_line(near_position)
|> Geo.CheapRuler.closest_point_on_line(near_position, @search_radius_meters)
|> Map.fetch!(:dist)

dist =
Expand Down
3 changes: 2 additions & 1 deletion lib/video/rendered.ex
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,10 @@ defmodule Video.Rendered do
end || start_from(rendered, nil)
end

@search_radius_meters 10
def start_from(rendered, point) do
%{point: point, before: before, after: aft} =
Geo.CheapRuler.closest_point_on_line(rendered.coords(), point)
Geo.CheapRuler.closest_point_on_line(rendered.coords(), point, @search_radius_meters)

%{
lon: point.lon,
Expand Down

0 comments on commit 7dfc231

Please sign in to comment.