Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Halve runtime when simplifying large lines #4

Merged
merged 1 commit into from
May 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions lib/simplify.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,34 @@ defmodule Simplify do
simplify_dp_step(coordinates, tolerance * tolerance)
end

@spec simplify(%Geo.LineString{}, number) :: %Geo.LineString{}
@spec simplify(Geo.LineString.t(), number) :: Geo.LineString.t()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not perf related, but I couldn't help myself. (Cuts down on unnecessary recompiles by removing the compile-time dependency on the Geo library.)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

def simplify(%Geo.LineString{} = linestring, tolerance) do
%Geo.LineString{coordinates: simplify(linestring.coordinates, tolerance)}
end

defp simplify_dp_step(segment, _) when length(segment) < 3, do: segment
defp simplify_dp_step([], _), do: []
defp simplify_dp_step([_] = segment, _), do: segment
defp simplify_dp_step([_, _] = segment, _), do: segment

defp simplify_dp_step(segment, tolerance_squared) do
first = List.first(segment)
last = List.last(segment)
[first | tail] = segment
{last, middle} = List.pop_at(tail, -1)

{far_index, _, far_squared_dist} =
Enum.zip(0..(length(segment) - 1), segment)
|> Enum.drop(1)
|> Enum.drop(-1)
|> Enum.map(fn {i, p} -> {i, p, seg_dist(p, first, last)} end)
|> Enum.max_by(&elem(&1, 2))
{_, far_value, far_index, far_squared_dist} =
Enum.reduce(middle, {1, nil, 1, 0}, fn element, {idx, max_val, max_idx, max_dist} ->
dist = seg_dist(element, first, last)

if dist >= max_dist do
{idx + 1, element, idx, dist}
else
{idx + 1, max_val, max_idx, max_dist}
end
end)

if far_squared_dist > tolerance_squared do
front = simplify_dp_step(Enum.take(segment, far_index + 1), tolerance_squared)
[_ | back] = simplify_dp_step(Enum.drop(segment, far_index), tolerance_squared)
{pre_split, post_split} = Enum.split(segment, far_index + 1)
front = simplify_dp_step(pre_split, tolerance_squared)
[_ | back] = simplify_dp_step([far_value | post_split], tolerance_squared)

front ++ back
else
Expand Down
22 changes: 22 additions & 0 deletions test/simplify_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,26 @@ defmodule SimplifyTest do
assert length(simplified.coordinates) < length(ring.coordinates)
assert length(simplified.coordinates) == length(simplified_again.coordinates)
end

@tag bench: true
test "benchmark large ring simplification" do
ring =
Path.join(["test", "fixtures", "large_linestring.wkt"])
|> File.read!()
|> Geo.WKT.decode!()

runs = 10

{usec, _} =
:timer.tc(fn ->
for _ <- 1..runs do
_ = Simplify.simplify(ring, 1)
_ = Simplify.simplify(ring, 10)
_ = Simplify.simplify(ring, 100)
end
end)

avg_runtime_sec = Float.round(usec / 1_000_000 / runs, 4)
IO.puts("Avg runtime: #{avg_runtime_sec} seconds per run")
end
end
2 changes: 1 addition & 1 deletion test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ExUnit.start()
ExUnit.start(exclude: [:bench])