Skip to content

Commit

Permalink
Fix polyline plot errors (#36)
Browse files Browse the repository at this point in the history
There are two errors for polylines that have popped up on Sentry lately.
It seems that people have activities with either a single point, or
multiple points directly on top of eachother, which results in div/0
errors. This fixes those, and adds tests for the polyline visualisations
while I'm at it.
  • Loading branch information
JohnScolaro authored Nov 7, 2023
1 parent 58ec230 commit e34aac1
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 6 deletions.
21 changes: 16 additions & 5 deletions backend/active_statistics/statistics/images/polyline_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def create_images(activity_iterator: Iterable[Activity], path: str) -> None:
)
for activity in activity_iterator
if activity.start_date_local is not None
and activity.map.summary_polyline is not None
]
compact_activities.sort(key=lambda x: x.start_date_local)

Expand Down Expand Up @@ -118,13 +119,18 @@ def create_image(
for encoded_polyline in encoded_polylines:
polyline: Polyline = pl.decode(encoded_polyline)

# If the polyline has nothing in it, just return.
if not polyline:
if len(polyline) <= 1:
# I think we have ran into instances where people have ONLY activities with a single
# lat-long pair here, which means that the max cheby distance is 0, and then we do a
# divide by zero. So lets just continue if there is a single point, because lets be
# real, a single point is silly to plot.
continue

polyline = apply_equirectangular_approximation(polyline)
polyline = custom_scale_polyline(polyline)
polylines.append(polyline)
optional_polyline = custom_scale_polyline(polyline)

if optional_polyline is not None:
polylines.append(optional_polyline)

if not polylines:
return None
Expand Down Expand Up @@ -188,7 +194,7 @@ def equirectangular_approximation(lat, lon, reference_lat):
return polyline


def custom_scale_polyline(polyline: Polyline) -> Polyline:
def custom_scale_polyline(polyline: Polyline) -> None | Polyline:
"""
The goal here is to scale some arbitrary polyline into the center of a unit
square. This will make it easier to place them all in the future.
Expand All @@ -206,6 +212,11 @@ def custom_scale_polyline(polyline: Polyline) -> Polyline:
width = max_x - min_x
height = max_y - min_y

# Handle the case where all points are in the same spot. To avoid a div/0
# error here, just return None because we can't scale this.
if max([height, width]) == 0:
return None

# Calculate the scaling factors for x and y to fit the polyline into a unit square
if width > height:
scaling_factor = 1 / width
Expand Down
13 changes: 12 additions & 1 deletion backend/active_statistics/statistics/images/polyline_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ def create_image(
# If the polyline has nothing in it, just return.
polyline = decode_polyline(encoded_polyline)

if len(polyline) == 0:
if len(polyline) <= 1:
# I think we have ran into instances where people have ONLY activities with a single
# lat-long pair here, which means that the max cheby distance is 0, and then we do a
# divide by zero. So lets just continue if there is a single point, because lets be
# real, a single point is silly to plot.
continue

polyline = apply_equirectangular_approximation(polyline)
Expand All @@ -117,6 +121,13 @@ def create_image(
max_chebychev_distance(polyline) for polyline in polylines
)

if max_all_max_chebychev_distances == 0:
# This can occur if a polyline is any number of points all at the exact
# same location, and that polyline if the only polyline that that
# activity (or possible all polylines for that activity have identical
# points.) We just want to return if that's the case.
return None

scaled_polylines = [
aesthetically_scale_polyline(
polyline, max_all_max_chebychev_distances, min([height, width])
Expand Down
82 changes: 82 additions & 0 deletions backend/tests/test_visualisations/test_polyline_grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import shutil

import pytest
from active_statistics.statistics.images.polyline_grid import create_images
from stravalib.model import Activity, PolylineMap
from tests.factories.activity_factories import ActivityFactory


@pytest.fixture(scope="function")
def temp_directory(request, tmp_path_factory):
# Create a temporary directory
temp_dir = tmp_path_factory.mktemp("temp_directory")

# Define a finalizer to clean up the directory after the test
def cleanup_temp_directory():
# Delete the directory and its contents
shutil.rmtree(temp_dir)

# Add the finalizer to the request to ensure cleanup
request.addfinalizer(cleanup_temp_directory)

return temp_dir


class TestPolylineGrid:
def test_no_activities(self, temp_directory) -> None:
activities: list[Activity] = []
create_images((_ for _ in activities), temp_directory)

def test_activities_with_no_polylines(self, temp_directory) -> None:
activities = [
ActivityFactory(
type="Run",
map=PolylineMap(id="123", polyline=None, summary_polyline=None),
),
]
create_images(activities, temp_directory)

def test_activities_with_blank_polylines(self, temp_directory) -> None:
activities: list[Activity] = [
ActivityFactory(
type="Run",
map=PolylineMap(id="123", polyline=None, summary_polyline=""),
),
]
create_images((_ for _ in activities), temp_directory)

def test_activities_with_single_point_polylines(self, temp_directory) -> None:
activities: list[Activity] = [
ActivityFactory(
type="Run",
map=PolylineMap(id="123", polyline=None, summary_polyline="??"),
),
]
create_images((_ for _ in activities), temp_directory)

def test_activities_with_multi_point_identical_polylines(
self, temp_directory
) -> None:
# Yes, this polyline is from real data.
activities: list[Activity] = [
ActivityFactory(
type="Run",
map=PolylineMap(
id="123", polyline=None, summary_polyline="fszfDwsbe\\??"
),
),
]
create_images((_ for _ in activities), temp_directory)

def test_activities_with_normal_polylines(self, temp_directory) -> None:
activities: list[Activity] = [
ActivityFactory(
type="Run",
map=PolylineMap(
id="123",
polyline=None,
summary_polyline="tdufD}|}d\C?e@Nm@\eAn@u@j@_Aj@i@b@q@\EN]HcAZyBdAWVcAt@g@Vs@n@{@j@c@r@mAnAW`@Mb@MVJR@Tw@jAGp@LtALl@RVTf@Vr@b@bBP`@FX?JCp@Jt@Vh@P^PTf@`@l@pABJ`@fA\t@~@hAd@`ARl@z@dBV^rA~AV\hBjDh@lA~BlC`@j@rCxERXdBbBbArAVf@jAjAXRb@LXPzA|ARJVD^TdAz@v@x@Rb@^l@x@`Az@t@dAl@vBvAjBtA`@^hBvAvA`Ap@j@dB`AlDlC~@n@hAh@hA\^Rd@\jAp@RFf@Df@Cf@BbB~@fAp@f@`@`@b@^Tf@Fr@Ah@OX[|@sBvAsDLc@d@eAHe@Hq@N_@FIFUAc@D{@DiEF}B?gBBsAD~CAfCErAEdEG~AJj@@h@GjAc@tD_@zAQf@Sd@{@rAIFa@RWDe@Ga@AWQM[[YWIkAk@s@g@{@g@k@[[Mi@[]IkAFSCWGiAi@{@c@eAs@cAa@mCgB}B}AeCoB{@g@mAcA_Am@uAkAeBgAiBoAc@_@a@c@e@_@}@_Aw@{A[a@u@o@a@WaA_@]Yy@}@w@_@]I_@Wk@i@eDeEWg@aBoB{@mAe@{@aBwBWg@y@gAqA{BqAgB{AeCi@u@sAmC[w@gAkBi@_BUe@w@mA{@iBKgA_@yAMq@Qi@q@_BKsAGc@Ak@Fe@JW`@m@Ie@Ng@PQZe@`@a@p@_AVi@bByAVYhCwAnAaAZO\Kb@Wd@O",
),
),
]
create_images((_ for _ in activities), temp_directory)
82 changes: 82 additions & 0 deletions backend/tests/test_visualisations/test_polyline_overlay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import shutil

import pytest
from active_statistics.statistics.images.polyline_overlay import create_images
from stravalib.model import Activity, PolylineMap
from tests.factories.activity_factories import ActivityFactory


@pytest.fixture(scope="function")
def temp_directory(request, tmp_path_factory):
# Create a temporary directory
temp_dir = tmp_path_factory.mktemp("temp_directory")

# Define a finalizer to clean up the directory after the test
def cleanup_temp_directory():
# Delete the directory and its contents
shutil.rmtree(temp_dir)

# Add the finalizer to the request to ensure cleanup
request.addfinalizer(cleanup_temp_directory)

return temp_dir


class TestPolylineOverlay:
def test_no_activities(self, temp_directory) -> None:
activities: list[Activity] = []
create_images((_ for _ in activities), temp_directory)

def test_activities_with_no_polylines(self, temp_directory) -> None:
activities = [
ActivityFactory(
type="Run",
map=PolylineMap(id="123", polyline=None, summary_polyline=None),
),
]
create_images((_ for _ in activities), temp_directory)

def test_activities_with_blank_polylines(self, temp_directory) -> None:
activities: list[Activity] = [
ActivityFactory(
type="Run",
map=PolylineMap(id="123", polyline=None, summary_polyline=""),
),
]
create_images((_ for _ in activities), temp_directory)

def test_activities_with_single_point_polylines(self, temp_directory) -> None:
activities: list[Activity] = [
ActivityFactory(
type="Run",
map=PolylineMap(id="123", polyline=None, summary_polyline="??"),
),
]
create_images((_ for _ in activities), temp_directory)

def test_activities_with_multi_point_identical_polylines(
self, temp_directory
) -> None:
# Yes, this polyline is from real data.
activities: list[Activity] = [
ActivityFactory(
type="Run",
map=PolylineMap(
id="123", polyline=None, summary_polyline="fszfDwsbe\\??"
),
),
]
create_images((_ for _ in activities), temp_directory)

def test_activities_with_normal_polylines(self, temp_directory) -> None:
activities: list[Activity] = [
ActivityFactory(
type="Run",
map=PolylineMap(
id="123",
polyline=None,
summary_polyline="tdufD}|}d\C?e@Nm@\eAn@u@j@_Aj@i@b@q@\EN]HcAZyBdAWVcAt@g@Vs@n@{@j@c@r@mAnAW`@Mb@MVJR@Tw@jAGp@LtALl@RVTf@Vr@b@bBP`@FX?JCp@Jt@Vh@P^PTf@`@l@pABJ`@fA\t@~@hAd@`ARl@z@dBV^rA~AV\hBjDh@lA~BlC`@j@rCxERXdBbBbArAVf@jAjAXRb@LXPzA|ARJVD^TdAz@v@x@Rb@^l@x@`Az@t@dAl@vBvAjBtA`@^hBvAvA`Ap@j@dB`AlDlC~@n@hAh@hA\^Rd@\jAp@RFf@Df@Cf@BbB~@fAp@f@`@`@b@^Tf@Fr@Ah@OX[|@sBvAsDLc@d@eAHe@Hq@N_@FIFUAc@D{@DiEF}B?gBBsAD~CAfCErAEdEG~AJj@@h@GjAc@tD_@zAQf@Sd@{@rAIFa@RWDe@Ga@AWQM[[YWIkAk@s@g@{@g@k@[[Mi@[]IkAFSCWGiAi@{@c@eAs@cAa@mCgB}B}AeCoB{@g@mAcA_Am@uAkAeBgAiBoAc@_@a@c@e@_@}@_Aw@{A[a@u@o@a@WaA_@]Yy@}@w@_@]I_@Wk@i@eDeEWg@aBoB{@mAe@{@aBwBWg@y@gAqA{BqAgB{AeCi@u@sAmC[w@gAkBi@_BUe@w@mA{@iBKgA_@yAMq@Qi@q@_BKsAGc@Ak@Fe@JW`@m@Ie@Ng@PQZe@`@a@p@_AVi@bByAVYhCwAnAaAZO\Kb@Wd@O",
),
),
]
create_images((_ for _ in activities), temp_directory)
1 change: 1 addition & 0 deletions captions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Run.png": "All Run activities overlaid as if they have the same starting point.", "Run_animation.gif": "An animation of overlaid Run activities."}

0 comments on commit e34aac1

Please sign in to comment.