Index: ow/models/workout.py
===================================================================
--- ow/models/workout.py	(revision 02aee97ce8bc24334a15d021de5480d2e311736b)
+++ ow/models/workout.py	(revision b3374f67ac41b8f9c34dc73d894cc89c757ea3df)
@@ -14,5 +14,4 @@
     create_blob,
     mps_to_kmph,
-    save_map_screenshot,
     timedelta_to_hms
 )
@@ -423,5 +422,17 @@
         return self.fit_file is not None
 
-    def map_screenshot(self, request):
+    @property
+    def map_screenshot_name(self):
+        return os.path.join(
+            str(self.owner.uid), str(self.workout_id) + '.png')
+
+    @property
+    def map_screenshot_path(self):
+        current_path = os.path.abspath(os.path.dirname(__file__))
+        return os.path.join(
+            current_path, '../static/maps', self.map_screenshot_name)
+
+    @property
+    def map_screenshot(self):
         """
         Return the static path to the screenshot image of the map for
@@ -431,14 +442,8 @@
             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', screenshot_name)
-
-        if not os.path.exists(screenshot_path):
-            # screenshot does not exist, generate it
-            save_map_screenshot(self, request)
-
-        static_path = os.path.join('static/maps', screenshot_name)
-        return request.static_url('ow:' + static_path)
+        if not os.path.exists(self.map_screenshot_path):
+            return None
+
+        static_path = os.path.join('static/maps', self.map_screenshot_name)
+        # return a string we can use with request.static_url
+        return 'ow:' + static_path
Index: ow/static/js/ow.js
===================================================================
--- ow/static/js/ow.js	(revision 02aee97ce8bc24334a15d021de5480d2e311736b)
+++ ow/static/js/ow.js	(revision b3374f67ac41b8f9c34dc73d894cc89c757ea3df)
@@ -452,2 +452,30 @@
 
 };
+
+
+owjs.map_shots = function(spec) {
+
+    "use strict";
+
+    var img_selector = spec.img_selector;
+
+    var run = function run(){
+        $(img_selector).each(function(){
+            var img = $(this);
+            var a = $(this).parent();
+            var url = a.attr('href') + 'map-shot';
+            var jqxhr = $.getJSON(url, function(info) {
+                img.fadeOut('fast', function () {
+                    img.attr('src', info['url']);
+                    img.fadeIn('fast');
+                });
+                img.removeClass('js-needs-map');
+            });
+        });
+    };
+
+    var that = {}
+    that.run = run;
+    return that
+
+};
Index: ow/templates/dashboard.pt
===================================================================
--- ow/templates/dashboard.pt	(revision 02aee97ce8bc24334a15d021de5480d2e311736b)
+++ ow/templates/dashboard.pt	(revision b3374f67ac41b8f9c34dc73d894cc89c757ea3df)
@@ -79,8 +79,14 @@
 
             <div class="workout-map" tal:condition="workout.has_gpx">
-                <a href="" tal:attributes="href request.resource_url(workout)">
-                    <img src="" tal:attributes="src workout.map_screenshot(request);
-                              alt workout.title; title workout.title">
-                </a>
+              <a href="" tal:attributes="href request.resource_url(workout)">
+                <tal:has-screenshot tal:condition="workout.map_screenshot is not None">
+                  <img src="" tal:attributes="src request.static_url(workout.map_screenshot);
+                            alt workout.title; title workout.title">
+                </tal:has-screenshot>
+                <tal:has-not-screenshot tal:condition="workout.map_screenshot is None">
+                  <img src="" tal:attributes="src request.static_url('ow:static/media/img/no_map.png');
+                            alt workout.title; title workout.title; class 'js-needs-map'">
+                </tal:has-not-screenshot>
+              </a>
             </div>
 
@@ -193,4 +199,10 @@
 
     <script type="text/javascript">
+
+     var map_shots = owjs.map_shots({
+         img_selector: 'img.js-needs-map',
+     })
+     map_shots.run();
+
      var week_chart = owjs.week_chart({
          chart_selector: 'div.js-week-stats svg',
Index: ow/templates/profile.pt
===================================================================
--- ow/templates/profile.pt	(revision 02aee97ce8bc24334a15d021de5480d2e311736b)
+++ ow/templates/profile.pt	(revision b3374f67ac41b8f9c34dc73d894cc89c757ea3df)
@@ -144,6 +144,12 @@
               <div class="workout-map" tal:condition="workout.has_gpx">
                 <a href="" tal:attributes="href request.resource_url(workout)">
-                  <img src="" tal:attributes="src workout.map_screenshot(request);
-                            alt workout.title; title workout.title">
+                  <tal:has-screenshot tal:condition="workout.map_screenshot is not None">
+                    <img src="" tal:attributes="src request.static_url(workout.map_screenshot);
+                              alt workout.title; title workout.title">
+                  </tal:has-screenshot>
+                  <tal:has-not-screenshot tal:condition="workout.map_screenshot is None">
+                    <img src="" tal:attributes="src request.static_url('ow:static/media/img/no_map.png');
+                              alt workout.title; title workout.title; class 'js-needs-map'">
+                  </tal:has-not-screenshot>
                 </a>
               </div>
@@ -187,4 +193,9 @@
 
     <script type="text/javascript">
+     var map_shots = owjs.map_shots({
+         img_selector: 'img.js-needs-map',
+     })
+     map_shots.run();
+
      var y_axis_labels = {
          "distance": "Kilometers",
Index: ow/tests/helpers.py
===================================================================
--- ow/tests/helpers.py	(revision 02aee97ce8bc24334a15d021de5480d2e311736b)
+++ ow/tests/helpers.py	(revision b3374f67ac41b8f9c34dc73d894cc89c757ea3df)
@@ -1,3 +1,6 @@
+
 def join(*args, **kwargs):
-    """ Faked join method, for mocking purposes """
-    return '/' + '/'.join([arg for arg in args])
+    """
+    Faked join method, for mocking purposes
+    """
+    return '/' + '/'.join([arg.strip('/') for arg in args])
Index: ow/tests/models/test_workout.py
===================================================================
--- ow/tests/models/test_workout.py	(revision 02aee97ce8bc24334a15d021de5480d2e311736b)
+++ ow/tests/models/test_workout.py	(revision b3374f67ac41b8f9c34dc73d894cc89c757ea3df)
@@ -7,5 +7,4 @@
 import pytest
 from pyramid.security import Allow, Everyone, Deny, ALL_PERMISSIONS
-from pyramid.testing import DummyRequest
 
 from ow.models.workout import Workout
@@ -573,28 +572,27 @@
         assert workout.has_fit
 
+    def test_map_screenshot_name(self, root):
+        workout = root['john']['1']
+        assert workout.map_screenshot_name == (
+            str(root['john'].uid) + '/' + str(workout.workout_id) + '.png')
+
+    def test_map_screenshot_path(self, root):
+        workout = root['john']['1']
+        assert workout.map_screenshot_path.endswith(
+            'static/maps/' + workout.map_screenshot_name)
+
     @patch('ow.models.workout.os')
-    @patch('ow.models.workout.save_map_screenshot')
-    def test_map_screenshot_no_gpx(self, sms, os, root):
-        workout = root['john']['1']
-        request = DummyRequest()
-        assert workout.map_screenshot(request) is None
-        assert not os.path.abspath.called
-        assert not os.path.dirname.called
+    def test_map_screenshot_no_gpx(self, os, root):
+        workout = root['john']['1']
+        assert workout.map_screenshot is None
         assert not os.path.join.called
         assert not os.path.exists.called
-        assert not sms.called
 
     @patch('ow.models.workout.os')
-    @patch('ow.models.workout.save_map_screenshot')
-    def test_map_screenshot_save(self, sms, os, root):
-        """
-        A workout with a tracking file has no map screenshot, one is
-        saved to the filesystem.
-        This test simply asserts the calls to the separate methods that
-        look for existing screenshots and save a new one
-        """
-        os.path.abspath.return_value = 'current_dir'
-        os.path.join.side_effect = join
-        # This forces the "save screenshot" code to be run
+    def test_map_screenshot_no_shot(self, os, root):
+        """
+        A workout with a tracking file has no map screenshot
+        """
+        # This says "no screenshot found"
         os.path.exists.return_value = False
 
@@ -603,45 +601,24 @@
         workout.tracking_filetype = 'gpx'
 
-        uid = str(root['john'].uid)
-        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 == 3
-        assert os.path.exists.called
-        sms.assert_called_once_with(workout, request)
+        assert workout.map_screenshot is None
+        assert os.path.join.called
+        os.path.exists.assert_called_once_with(workout.map_screenshot_path)
 
     @patch('ow.models.workout.os')
-    @patch('ow.models.workout.save_map_screenshot')
-    def test_map_screenshot_do_not_save(self, sms, os, root):
+    def test_map_screenshot_has_shot(self, os, root):
         """
         A workout with a tracking file has a map screenshot, the path to that
         is returned without doing anything else
         """
-        os.path.abspath.return_value = 'current_dir'
+        # This says "yeah, we have a screenshot"
+        os.path.exists.return_value = True
+        os.path.abspath.return_value = '/'
         os.path.join.side_effect = join
-        # This forces the "save screenshot" code NOT to be run
-        os.path.eisxts.return_value = True
 
         workout = root['john']['1']
         workout.tracking_file = 'faked gpx file'
         workout.tracking_filetype = 'gpx'
-
         uid = str(root['john'].uid)
-        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 == 3
-        assert os.path.exists.called
-        assert not sms.called
+        assert workout.map_screenshot == 'ow:/static/maps/' + uid + '/1.png'
+        os.path.exists.assert_called_once_with(workout.map_screenshot_path)
+        assert os.path.join.called
Index: ow/views/workout.py
===================================================================
--- ow/views/workout.py	(revision 02aee97ce8bc24334a15d021de5480d2e311736b)
+++ ow/views/workout.py	(revision b3374f67ac41b8f9c34dc73d894cc89c757ea3df)
@@ -1,4 +1,5 @@
 from decimal import Decimal
 from datetime import datetime, timedelta, time, timezone
+import json
 
 import gpxpy
@@ -17,5 +18,5 @@
 from ..models.workout import Workout
 from ..models.user import User
-from ..utilities import slugify
+from ..utilities import slugify, save_map_screenshot
 from ..catalog import get_catalog, reindex_object, remove_from_catalog
 
@@ -255,2 +256,20 @@
                                'elevation': center_point.elevation}
     return {'start_point': start_point}
+
+
+@view_config(
+    context=Workout,
+    permission='edit',
+    name='map-shot')
+def workout_map_shot(context, request):
+    """
+    Ask for the screenshot of a map, creating one if it does not exist.
+    A json object is returned, containing the info for the needed screenshot
+    """
+    if context.map_screenshot is None:
+        save_map_screenshot(context, request)
+
+    info = {'url': request.static_url(context.map_screenshot)}
+    return Response(content_type='application/json',
+                    charset='utf-8',
+                    body=json.dumps(info))
