From 60d8b675093dbf97a066ad44256b4df2e3cbc584 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sat, 13 Apr 2019 16:49:49 +0200
Subject: [PATCH] misc: add distance template tag (#32262)

---
 debian/control                    |  1 +
 setup.py                          |  1 +
 tests/test_formdata.py            | 11 +++++++++++
 tests/test_templates.py           | 30 ++++++++++++++++++++++++++++++
 tox.ini                           |  1 +
 wcs/qommon/templatetags/qommon.py | 29 +++++++++++++++++++++++++++++
 6 files changed, 73 insertions(+)

diff --git a/debian/control b/debian/control
index aea6e1c6..06ab8fbc 100644
--- a/debian/control
+++ b/debian/control
@@ -17,6 +17,7 @@ Depends: ${misc:Depends}, ${python:Depends},
     python-django-ckeditor,
     python-feedparser,
     python-imaging,
+    python-pyproj,
     python-requests,
     python-vobject,
     python-xstatic-leaflet,
diff --git a/setup.py b/setup.py
index 539cc473..158787eb 100644
--- a/setup.py
+++ b/setup.py
@@ -110,6 +110,7 @@ setup(
             'gadjo>=0.53',
             'django-ckeditor<4.5.3',
             'XStatic-Leaflet',
+            'pyproj',
         ],
         package_dir = { 'wcs': 'wcs' },
         packages = find_packages(),
diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index eb13f2c5..6124b552 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -788,6 +788,15 @@ def test_lazy_map_variable(pub, variable_test_data):
             assert WorkflowStatusItem.compute('=form_var_map_lon', raises=True) == 4
             assert WorkflowStatusItem.compute('{{ form_var_map_lon }}', raises=True) == '4.0'
 
+            assert WorkflowStatusItem.compute('{{ form_var_map|distance:form_var_map|floatformat }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ form_var_map|distance:"2.1;4.1"|floatformat }}', raises=True) == '15685.4'
+            assert WorkflowStatusItem.compute('{{ "2.1;4.1"|distance:form_var_map|floatformat }}', raises=True) == '15685.4'
+            assert WorkflowStatusItem.compute('{{ form|distance:"1;2"|floatformat }}', raises=True) == '0'
+            assert WorkflowStatusItem.compute('{{ form|distance:"1.1;2.1"|floatformat }}', raises=True) == '15689.1'
+            assert WorkflowStatusItem.compute('{{ "1.1;2.1"|distance:form|floatformat }}', raises=True) == '15689.1'
+            assert WorkflowStatusItem.compute('{{ form|distance:form_var_map|floatformat }}', raises=True) == '248515.5'
+            assert WorkflowStatusItem.compute('{{ form_var_map|distance:form|floatformat }}', raises=True) == '248515.5'
+
     formdata.data['7'] = None
     formdata.store()
     pub.substitutions.reset()
@@ -798,6 +807,8 @@ def test_lazy_map_variable(pub, variable_test_data):
         pub.substitutions.feed(formdef)
         with pub.substitutions.temporary_feed(formdata, force_mode=mode):
             assert WorkflowStatusItem.compute('=form_var_map', raises=True) is None
+            assert WorkflowStatusItem.compute('{{ form_var_map|distance:"1;2"|floatformat }}', raises=True) == ''
+            assert WorkflowStatusItem.compute('{{ "1;2"|distance:form_var_map|floatformat }}', raises=True) == ''
 
 def test_lazy_conditions(pub, variable_test_data):
     condition = Condition({'type': 'django', 'value': 'form_var_foo_foo == "bar"'})
diff --git a/tests/test_templates.py b/tests/test_templates.py
index eac758c0..0978e321 100644
--- a/tests/test_templates.py
+++ b/tests/test_templates.py
@@ -7,6 +7,7 @@ import string
 from quixote import cleanup
 from qommon.template import Template, TemplateError
 from wcs.conditions import Condition
+from wcs.variables import LazyFormData
 
 from utilities import create_temporary_pub, clean_temporary_pub
 
@@ -459,3 +460,32 @@ def test_token_alphanum():
     assert not any(set(token) & set('01IiOo') for token in tokens)
     t = Template('{% if token1|token_check:token2 %}ok{% endif %}')
     assert t.render({'token1': tokens[0] + ' ', 'token2': tokens[0].lower()}) == 'ok'
+
+def test_distance():
+    t = Template('{{ "48;2"|distance:"48.1;2.1"|floatformat }}',)
+    assert t.render() == '13387.2'
+    t = Template('{{ coords|distance:"48.1;2.1"|floatformat }}',)
+    assert t.render({'coords': '48;2'}) == '13387.2'
+    t = Template('{{ "48;2"|distance:coords|floatformat }}',)
+    assert t.render({'coords': '48.1;2.1'}) == '13387.2'
+    t = Template('{{ c1|distance:c2|floatformat }}',)
+    assert t.render({'c1': '48;2', 'c2': '48.1;2.1'}) == '13387.2'
+
+    class MockFormData(object):
+        formdef = None
+        geolocations = {'base': {'lat': 48, 'lon': 2}}
+
+    lazy_formdata = LazyFormData(MockFormData())
+    for tpl in ('{{ formdata|distance:coords|floatformat }}',
+                '{{ coords|distance:formdata|floatformat }}'):
+        t = Template(tpl,)
+        assert t.render({'formdata': lazy_formdata, 'coords': '48.1;2.1'}) == '13387.2'
+        assert t.render({'formdata': lazy_formdata, 'coords': '49.1;3.1'}) == '146821.9'
+        assert t.render({'formdata': lazy_formdata, 'coords': 'abc;def'}) == ''
+        assert t.render({'formdata': lazy_formdata, 'coords': '42'}) == ''
+        assert t.render({'formdata': lazy_formdata, 'coords': ''}) == ''
+    MockFormData.geolocations = {}
+    for tpl in ('{{ formdata|distance:coords|floatformat }}',
+                '{{ coords|distance:formdata|floatformat }}'):
+        t = Template(tpl,)
+        assert t.render({'formdata': lazy_formdata, 'coords': '49.1;3.1'}) == ''
diff --git a/tox.ini b/tox.ini
index 1f4e794c..306deb34 100644
--- a/tox.ini
+++ b/tox.ini
@@ -30,6 +30,7 @@ deps =
     python-dateutil
     pillow
     feedparser
+    pyproj
 commands =
     ./getlasso.sh
     pickle:  py.test --without-postgresql-tests {env:COVERAGE:} {posargs:tests}
diff --git a/wcs/qommon/templatetags/qommon.py b/wcs/qommon/templatetags/qommon.py
index edc9f4e8..32403def 100644
--- a/wcs/qommon/templatetags/qommon.py
+++ b/wcs/qommon/templatetags/qommon.py
@@ -23,6 +23,8 @@ import math
 import string
 import random
 
+from pyproj import Geod
+
 from django import template
 from django.template import defaultfilters
 from django.utils import dateparse
@@ -300,3 +302,30 @@ def token_alphanum(length=4):
 @register.filter
 def token_check(token1, token2):
     return token1.strip().upper() == token2.strip().upper()
+
+
+def get_latlon(obj):
+    if getattr(obj, 'geoloc', None):
+        if 'base' in obj.geoloc:
+            return obj.geoloc['base']['lat'], obj.geoloc['base']['lon']
+        return None, None
+    if hasattr(obj, 'get_value'):
+        obj = obj.get_value()  # unlazy
+    if not obj or not isinstance(obj, basestring) or ';' not in obj:
+        return None, None
+    try:
+        return float(obj.split(';')[0]), float(obj.split(';')[1])
+    except ValueError:
+        return None, None
+
+@register.filter
+def distance(obj1, obj2):
+    lat1, lon1 = get_latlon(obj1)
+    if lat1 is None or lon1 is None:
+        return None
+    lat2, lon2 = get_latlon(obj2)
+    if lat2 is None or lon2 is None:
+        return None
+    geod = Geod(ellps='WGS84')
+    distance = geod.inv(lon1, lat1, lon2, lat2)[2]
+    return distance
-- 
2.20.1

