From 75fa0f1ccc555601dc3b5a4ad60734d4eae5cae1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 23 Mar 2020 15:36:32 +0100
Subject: [PATCH] arcgis: tile endpoint (#17762)

---
 passerelle/apps/arcgis/models.py | 37 ++++++++++++++++++++++++++++
 tests/test_arcgis.py             | 41 ++++++++++++++++++++++++++++++++
 2 files changed, 78 insertions(+)

diff --git a/passerelle/apps/arcgis/models.py b/passerelle/apps/arcgis/models.py
index ae0404df..973e9c80 100644
--- a/passerelle/apps/arcgis/models.py
+++ b/passerelle/apps/arcgis/models.py
@@ -14,11 +14,13 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import math
 import string
 
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.db import models
+from django.http import HttpResponse
 from django.shortcuts import get_object_or_404
 from django.utils.six.moves.urllib import parse as urlparse
 from django.utils import six
@@ -191,6 +193,41 @@ class ArcGIS(BaseResource, HTTPResource):
             return {'data': features[0]}
         return {'data': features}
 
+    @endpoint(
+        name='tile',
+        description=_('Tiles layer'),
+        pattern=r'^(?P<layer>[\w/]+)/(?P<zoom>\d+)/(?P<tile_x>\d+)/(?P<tile_y>\d+)\.png$')
+    def tile(self, request, layer, zoom, tile_x, tile_y):
+        zoom = int(zoom)
+        tile_x = int(tile_x)
+        tile_y = int(tile_y)
+
+        def num2deg(xtile, ytile, zoom):
+            # http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Python
+            n = 2.0 ** zoom
+            lon_deg = xtile / n * 360.0 - 180.0
+            lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
+            lat_deg = math.degrees(lat_rad)
+            return (lon_deg, lat_deg)
+
+        bbox = '%s,%s,%s,%s' % (
+            num2deg(tile_x, tile_y, zoom) +
+            num2deg(tile_x+1, tile_y+1, zoom))
+
+        # imageSR=3857: default projection for leaflet
+        base_url = self.base_url
+        if not base_url.endswith('/'):
+            base_url += '/'
+        return HttpResponse(
+            self.requests.get(
+                base_url +
+                '%s/MapServer/export' % layer +
+                '?dpi=96&format=png24&bboxSR=4326&imageSR=3857&' +
+                'transparent=true&size=256,256&f=image&' +
+                'bbox=%s' % bbox
+            ).content,
+            content_type='image/png')
+
     @endpoint(name='q',
               description=_('Query'),
               pattern=r'^(?P<query_slug>[\w:_-]+)/$',
diff --git a/tests/test_arcgis.py b/tests/test_arcgis.py
index 2445d735..281324b9 100644
--- a/tests/test_arcgis.py
+++ b/tests/test_arcgis.py
@@ -338,6 +338,47 @@ def test_q_endpoint(arcgis, query, app):
         }
 
 
+def test_tile_endpoint(arcgis, app):
+    assert arcgis.base_url == 'https://arcgis.example.net/'
+
+    with mock.patch('passerelle.utils.Request.get') as requests_get:
+        requests_get.return_value = utils.FakedResponse(content='', status_code=200)
+
+        resp = app.get('/arcgis/test/tile/layer1/13/4258/2991.png')
+        assert requests_get.call_count == 1
+        assert requests_get.call_args[0][0] == (
+            'https://arcgis.example.net/layer1/MapServer/export'
+            '?dpi=96&format=png24&bboxSR=4326&imageSR=3857&transparent=true&size=256,256&f=image'
+            '&bbox=7.119140625,43.6122167682,7.1630859375,43.5803908556')
+        assert resp.content_type == 'image/png'
+
+    # test layer and folders
+    with mock.patch('passerelle.utils.Request.get') as requests_get:
+        requests_get.return_value = utils.FakedResponse(content='', status_code=200)
+
+        resp = app.get('/arcgis/test/tile/layer1/foo/bar/13/4258/2991.png')
+        assert requests_get.call_count == 1
+        assert requests_get.call_args[0][0] == (
+            'https://arcgis.example.net/layer1/foo/bar/MapServer/export'
+            '?dpi=96&format=png24&bboxSR=4326&imageSR=3857&transparent=true&size=256,256&f=image'
+            '&bbox=7.119140625,43.6122167682,7.1630859375,43.5803908556')
+        assert resp.content_type == 'image/png'
+
+    # test missing trailing slash
+    arcgis.base_url = 'https://arcgis.example.net'
+    arcgis.save()
+    with mock.patch('passerelle.utils.Request.get') as requests_get:
+        requests_get.return_value = utils.FakedResponse(content='', status_code=200)
+
+        resp = app.get('/arcgis/test/tile/layer1/13/4258/2991.png')
+        assert requests_get.call_count == 1
+        assert requests_get.call_args[0][0] == (
+            'https://arcgis.example.net/layer1/MapServer/export'
+            '?dpi=96&format=png24&bboxSR=4326&imageSR=3857&transparent=true&size=256,256&f=image'
+            '&bbox=7.119140625,43.6122167682,7.1630859375,43.5803908556')
+        assert resp.content_type == 'image/png'
+
+
 def test_export_import(query):
     assert ArcGIS.objects.count() == 1
     assert Query.objects.count() == 1
-- 
2.20.1

