Index: bin/install
===================================================================
--- bin/install	(revision 93bbb89627095ae867fe5bc0aa92118f642d41d3)
+++ bin/install	(revision d6f8304b0bcb1c46bbfc6b8366865721828f2d77)
@@ -16,5 +16,4 @@
     chmod u+x ${current}/bin/js_deps
     chmod u+x ${current}/bin/start
-    chmod u+x ${current}/bin/screenshot_map
 }
 
Index: n/screenshot_map
===================================================================
--- bin/screenshot_map	(revision 93bbb89627095ae867fe5bc0aa92118f642d41d3)
+++ 	(revision )
@@ -1,29 +1,0 @@
-#!/bin/sh
-#
-# Make a screenshot of a workout map
-#
-
-uid=$1
-wid=$2
-dest=$3
-dummy=$4
-
-# make a dummy screenshot image first, that way if chrome fails, we still have an image
-cp ${dummy} screenshot.png
-
-# Make the screenshot with chrome headless
-`which chrome` --headless --disable-gpu --window-size="1300x436" --screenshot http://localhost:9999/${uid}/${wid}/map --virtual-time-budget=10000
-
-# crop it
-# `which convert` screenshot.png -chop 100x0 ${dest}
-
-# resize, 70% of the original image is enough.
-# we do this instead of setting a lower resolution/window size on the browser
-# because with smaller window size, the resulting images appear corrupted
-# randomly (like if the screenshot was taking before the map was finished
-# loading tiles)
-# `which convert` ${dest} -resize 70% ${dest}
-
-# remove leftover
-# rm screenshot.png
-mv screenshot.png ${dest}
Index: ow/models/workout.py
===================================================================
--- ow/models/workout.py	(revision 93bbb89627095ae867fe5bc0aa92118f642d41d3)
+++ ow/models/workout.py	(revision d6f8304b0bcb1c46bbfc6b8366865721828f2d77)
@@ -423,6 +423,5 @@
         return self.fit_file is not None
 
-    @property
-    def map_screenshot(self):
+    def map_screenshot(self, request):
         """
         Return the static path to the screenshot image of the map for
@@ -432,16 +431,14 @@
             return None
 
+        screenshot_name = os.path.join(
+            str(self.owner.uid), str(self.workout_id) + '.png')
         current_path = os.path.abspath(os.path.dirname(__file__))
         screenshot_path = os.path.join(
-            current_path, '../static/maps',
-            str(self.owner.uid), str(self.workout_id)) + '.png'
+            current_path, '../static/maps', screenshot_name)
 
         if not os.path.exists(screenshot_path):
             # screenshot does not exist, generate it
-            save_map_screenshot(self)
-
-        # the value returned is relative to the static files served
-        # by the app, so we can use request.static_url() with it
-        static_path = os.path.join('static/maps', str(self.owner.uid),
-                                   str(self.workout_id))
-        return 'ow:' + static_path + '.png'
+            save_map_screenshot(self, request)
+
+        static_path = os.path.join('static/maps', screenshot_name)
+        return request.static_url('ow:' + static_path)
Index: ow/templates/dashboard.pt
===================================================================
--- ow/templates/dashboard.pt	(revision 93bbb89627095ae867fe5bc0aa92118f642d41d3)
+++ ow/templates/dashboard.pt	(revision d6f8304b0bcb1c46bbfc6b8366865721828f2d77)
@@ -80,5 +80,5 @@
             <div class="workout-map" tal:condition="workout.has_gpx">
                 <a href="" tal:attributes="href request.resource_url(workout)">
-                    <img src="" tal:attributes="src request.static_url(workout.map_screenshot);
+                    <img src="" tal:attributes="src workout.map_screenshot(request);
                               alt workout.title; title workout.title">
                 </a>
Index: ow/templates/profile.pt
===================================================================
--- ow/templates/profile.pt	(revision 93bbb89627095ae867fe5bc0aa92118f642d41d3)
+++ ow/templates/profile.pt	(revision d6f8304b0bcb1c46bbfc6b8366865721828f2d77)
@@ -144,5 +144,5 @@
               <div class="workout-map" tal:condition="workout.has_gpx">
                 <a href="" tal:attributes="href request.resource_url(workout)">
-                  <img src="" tal:attributes="src request.static_url(workout.map_screenshot);
+                  <img src="" tal:attributes="src workout.map_screenshot(request);
                             alt workout.title; title workout.title">
                 </a>
Index: ow/tests/models/test_workout.py
===================================================================
--- ow/tests/models/test_workout.py	(revision 93bbb89627095ae867fe5bc0aa92118f642d41d3)
+++ ow/tests/models/test_workout.py	(revision d6f8304b0bcb1c46bbfc6b8366865721828f2d77)
@@ -7,4 +7,5 @@
 import pytest
 from pyramid.security import Allow, Everyone, Deny, ALL_PERMISSIONS
+from pyramid.testing import DummyRequest
 
 from ow.models.workout import Workout
@@ -576,5 +577,6 @@
     def test_map_screenshot_no_gpx(self, sms, os, root):
         workout = root['john']['1']
-        assert workout.map_screenshot is None
+        request = DummyRequest()
+        assert workout.map_screenshot(request) is None
         assert not os.path.abspath.called
         assert not os.path.dirname.called
@@ -602,10 +604,16 @@
 
         uid = str(root['john'].uid)
-        assert workout.map_screenshot == 'ow:/static/maps/' + uid + '/1.png'
+        request = DummyRequest()
+        # dummyrequest can't resolve static assets without adding a lot
+        # of boilerplate, no need for that here
+        request.static_url = Mock()
+        request.static_url.return_value = 'ow:/static/maps/' + uid + '/1.png'
+        res = workout.map_screenshot(request)
+        assert res == 'ow:/static/maps/' + uid + '/1.png'
         assert os.path.abspath.called
         assert os.path.dirname.called
-        assert os.path.join.call_count == 2
+        assert os.path.join.call_count == 3
         assert os.path.exists.called
-        sms.assert_called_once_with(workout)
+        sms.assert_called_once_with(workout, request)
 
     @patch('ow.models.workout.os')
@@ -619,5 +627,5 @@
         os.path.join.side_effect = join
         # This forces the "save screenshot" code NOT to be run
-        os.path.exists.return_value = True
+        os.path.eisxts.return_value = True
 
         workout = root['john']['1']
@@ -626,8 +634,14 @@
 
         uid = str(root['john'].uid)
-        assert workout.map_screenshot == 'ow:/static/maps/' + uid + '/1.png'
+        request = DummyRequest()
+        # dummyrequest can't resolve static assets without adding a lot
+        # of boilerplate, no need for that here
+        request.static_url = Mock()
+        request.static_url.return_value = 'ow:/static/maps/' + uid + '/1.png'
+        res = workout.map_screenshot(request)
+        assert res == 'ow:/static/maps/' + uid + '/1.png'
         assert os.path.abspath.called
         assert os.path.dirname.called
-        assert os.path.join.call_count == 2
+        assert os.path.join.call_count == 3
         assert os.path.exists.called
         assert not sms.called
Index: ow/tests/test_utilities.py
===================================================================
--- ow/tests/test_utilities.py	(revision 93bbb89627095ae867fe5bc0aa92118f642d41d3)
+++ ow/tests/test_utilities.py	(revision d6f8304b0bcb1c46bbfc6b8366865721828f2d77)
@@ -1,9 +1,10 @@
 import os
 from datetime import timedelta, datetime
-from unittest.mock import patch
+from unittest.mock import patch, Mock
 from pyexpat import ExpatError
 from xml.dom.minidom import Element
 
 import pytest
+from pyramid.testing import DummyRequest
 
 from ow.models.root import OpenWorkouts
@@ -83,9 +84,13 @@
         assert kmph_to_mps(30) == 30 * 0.277778
 
+    @patch('ow.utilities.shutil')
     @patch('ow.utilities.os')
-    @patch('ow.utilities.subprocess')
-    def test_save_map_screenshot_no_gpx(self, subprocess, os, root, john):
-        saved = save_map_screenshot(john['1'])
+    @patch('ow.utilities.Browser')
+    def test_save_map_screenshot_no_gpx(
+            self, Browser, os, shutil, root, john):
+        request = DummyRequest()
+        saved = save_map_screenshot(john['1'], request)
         assert not saved
+        assert not Browser.called
         assert not os.path.abspath.called
         assert not os.path.dirname.called
@@ -93,10 +98,11 @@
         assert not os.path.exists.called
         assert not os.makedirs.called
-        assert not subprocess.run.called
+        assert not shutil.move.called
         # even having a fit tracking file, nothing is done
         john['1'].tracking_file = 'faked fit file'
         john['1'].tracking_filetype = 'fit'
-        saved = save_map_screenshot(john['1'])
+        saved = save_map_screenshot(john['1'], request)
         assert not saved
+        assert not Browser.called
         assert not os.path.abspath.called
         assert not os.path.dirname.called
@@ -104,9 +110,14 @@
         assert not os.path.exists.called
         assert not os.makedirs.called
-        assert not subprocess.run.called
-
+        assert not shutil.move.called
+
+    @patch('ow.utilities.shutil')
     @patch('ow.utilities.os')
-    @patch('ow.utilities.subprocess')
-    def test_save_map_screenshot_with_gpx(self, subprocess, os, root, john):
+    @patch('ow.utilities.Browser')
+    def test_save_map_screenshot_with_gpx(
+            self, Browser, os, shutil, root, john):
+        request = DummyRequest()
+        browser = Mock()
+        Browser.return_value = browser
         os.path.abspath.return_value = 'current_dir'
         os.path.join.side_effect = join
@@ -116,19 +127,29 @@
         os.path.exists.return_value = False
 
+        map_url = request.resource_url(john['1'], 'map')
+
         john['1'].tracking_file = 'faked gpx content'
         john['1'].tracking_filetype = 'gpx'
-        saved = save_map_screenshot(john['1'])
+        saved = save_map_screenshot(john['1'], request)
         assert saved
+        Browser.assert_called_once_with('chrome', headless=True)
+        browser.driver.set_window_size.assert_called_once_with(1300, 436)
+        browser.visit.assert_called_once_with(map_url)
+        browser.screenshot.assert_called_once
         os.path.abspath.assert_called_once
         assert os.path.dirname.called
-        assert os.path.join.call_count == 3
+        assert os.path.join.call_count == 2
         assert os.path.exists.called
         assert os.makedirs.called
-        subprocess.run.assert_called_once
-
+        os.shutil.move.assert_called_once
+
+    @patch('ow.utilities.shutil')
     @patch('ow.utilities.os')
-    @patch('ow.utilities.subprocess')
+    @patch('ow.utilities.Browser')
     def test_save_map_screenshot_with_gpx_makedirs(
-            self, subprocess, os, root, john):
+            self, Browser, os, shutil, root, john):
+        request = DummyRequest()
+        browser = Mock()
+        Browser.return_value = browser
         os.path.abspath.return_value = 'current_dir'
         os.path.join.side_effect = join
@@ -136,14 +157,20 @@
         os.path.exists.return_value = True
 
+        map_url = request.resource_url(john['1'], 'map')
+
         john['1'].tracking_file = 'faked gpx content'
         john['1'].tracking_filetype = 'gpx'
-        saved = save_map_screenshot(john['1'])
+        saved = save_map_screenshot(john['1'], request)
         assert saved
+        Browser.assert_called_once_with('chrome', headless=True)
+        browser.driver.set_window_size.assert_called_once_with(1300, 436)
+        browser.visit.assert_called_once_with(map_url)
+        browser.screenshot.assert_called_once
         os.path.abspath.assert_called_once
         assert os.path.dirname.called
-        assert os.path.join.call_count == 3
+        assert os.path.join.call_count == 2
         assert os.path.exists.called
         assert not os.makedirs.called
-        subprocess.run.assert_called_once
+        os.shutil.move.assert_called_once
 
     def test_timedelta_to_hms(self):
Index: ow/utilities.py
===================================================================
--- ow/utilities.py	(revision 93bbb89627095ae867fe5bc0aa92118f642d41d3)
+++ ow/utilities.py	(revision d6f8304b0bcb1c46bbfc6b8366865721828f2d77)
@@ -3,5 +3,6 @@
 import logging
 import calendar
-import subprocess
+import shutil
+import time
 from datetime import datetime, timedelta
 from decimal import Decimal
@@ -11,4 +12,6 @@
 from xml.dom import minidom
 from ZODB.blob import Blob
+from splinter import Browser
+
 
 log = logging.getLogger(__name__)
@@ -192,9 +195,24 @@
 
 
-def save_map_screenshot(workout):
+def save_map_screenshot(workout, request):
+
     if workout.has_gpx:
+
+        map_url = request.resource_url(workout, 'map')
+
+        browser = Browser('chrome', headless=True)
+        browser.driver.set_window_size(1300, 436)
+
+        browser.visit(map_url)
+        # we need to wait a moment before taking the screenshot, to ensure
+        # all tiles are loaded in the map.
+        time.sleep(5)
+
+        # splinter saves the screenshot with a random name (even if we do
+        # provide a name) so we get the path to that file and later on we
+        # move it to the proper place
+        splinter_screenshot_path = browser.screenshot()
+
         current_path = os.path.abspath(os.path.dirname(__file__))
-        tool_path = os.path.join(current_path, '../bin/screenshot_map')
-
         screenshots_path = os.path.join(
             current_path, 'static/maps', str(workout.owner.uid))
@@ -206,7 +224,5 @@
         screenshot_path += '.png'
 
-        subprocess.run(
-            [tool_path, str(workout.owner.uid), str(workout.workout_id),
-             screenshot_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        shutil.move(splinter_screenshot_path, screenshot_path)
 
         return True
Index: setup.py
===================================================================
--- setup.py	(revision 93bbb89627095ae867fe5bc0aa92118f642d41d3)
+++ setup.py	(revision d6f8304b0bcb1c46bbfc6b8366865721828f2d77)
@@ -30,5 +30,6 @@
     'lxml',
     'pytz',
-    'fitparse'
+    'fitparse',
+    'splinter'
 ]
 
