0001-opengis-add-reverse-geocoding-based-on-WFS-21558.patch
passerelle/apps/opengis/migrations/0005_auto_20180227_1531.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
from django.db import migrations, models |
|
5 |
import jsonfield.fields |
|
6 | ||
7 | ||
8 |
class Migration(migrations.Migration): |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('opengis', '0004_auto_20180219_1613'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.AddField( |
|
16 |
model_name='opengis', |
|
17 |
name='search_radius', |
|
18 |
field=models.IntegerField(default=5, verbose_name='Radius for point search'), |
|
19 |
), |
|
20 |
] |
passerelle/apps/opengis/models.py | ||
---|---|---|
58 | 58 |
query_layer = models.CharField(_('Query Layer'), max_length=256) |
59 | 59 |
projection = models.CharField(_('GIS projection'), choices=PROJECTIONS, |
60 | 60 |
default='EPSG:3857', max_length=16) |
61 |
search_radius = models.IntegerField(_('Radius for point search'), default=5) |
|
62 |
attributes_mapping = (('road', ('road', 'road_name', 'street', 'street_name', 'voie', 'nom_voie', 'rue')), |
|
63 |
('city', ('city', 'city_name', 'town', 'town_name', 'commune', 'nom_commune', 'ville', 'nom_ville')), |
|
64 |
('house_number', ('house_number', 'number', 'numero', 'numero_voie', 'numero_rue')), |
|
65 |
('postcode', ('postcode', 'postalCode', 'zipcode', 'codepostal', 'cp', 'code_postal', 'code_post')), |
|
66 |
('country', ('country', 'country_name', 'pays', 'nom_pays')) |
|
67 |
) |
|
61 | 68 | |
62 | 69 |
class Meta: |
63 | 70 |
verbose_name = _('OpenGIS') |
... | ... | |
152 | 159 |
raise APIError(u'OpenGIS Error: %s' % exception_code or 'unknown code', |
153 | 160 |
data={'text': content}) |
154 | 161 | |
162 |
def convert_coordinates(self, lon, lat, reverse=False): |
|
163 |
lon, lat = float(lon), float(lat) |
|
164 |
if self.projection != 'EPSG:4326': |
|
165 |
wgs84 = pyproj.Proj(init='EPSG:4326') |
|
166 |
target_projection = pyproj.Proj(init=self.projection) |
|
167 |
if reverse: |
|
168 |
lon, lat = pyproj.transform(target_projection, wgs84, lon, lat) |
|
169 |
else: |
|
170 |
lon, lat = pyproj.transform(wgs84, target_projection, lon, lat) |
|
171 |
return lon, lat |
|
172 | ||
155 | 173 |
@endpoint(perm='can_access', |
156 | 174 |
description=_('Get feature info'), |
157 | 175 |
parameters={ |
... | ... | |
237 | 255 |
return HttpResponse( |
238 | 256 |
self.requests.get(self.wms_service_url, params=params, cache_duration=300).content, |
239 | 257 |
content_type='image/png') |
258 | ||
259 |
@endpoint(perm='can_access', description=_('Get feature info')) |
|
260 |
def reverse(self, request, lat, lon, **kwargs): |
|
261 |
result = None |
|
262 | ||
263 |
lon, lat = self.convert_coordinates(lon, lat) |
|
264 | ||
265 |
cql_filter = 'DWITHIN(the_geom,Point(%s %s),%s,meters)' % (lon, lat, self.search_radius) |
|
266 |
params = { |
|
267 |
'VERSION': self.get_wfs_service_version(), |
|
268 |
'SERVICE': 'WFS', |
|
269 |
'REQUEST': 'GetFeature', |
|
270 |
'TYPENAMES': self.query_layer, |
|
271 |
'OUTPUTFORMAT': 'json', |
|
272 |
'CQL_FILTER': cql_filter |
|
273 |
} |
|
274 |
response = self.requests.get(self.wfs_service_url, params=params) |
|
275 |
closest_feature = {} |
|
276 |
min_delta = None |
|
277 |
for feature in response.json().get('features'): |
|
278 |
if not feature['geometry']['type'] == 'Point': |
|
279 |
continue # skip unknown |
|
280 |
lon_diff = abs(float(lon) - float(feature['geometry']['coordinates'][0])) |
|
281 |
lat_diff = abs(float(lat) - float(feature['geometry']['coordinates'][1])) |
|
282 |
delta = math.sqrt(lon_diff * lon_diff + lat_diff * lat_diff) |
|
283 |
if min_delta is None: |
|
284 |
min_delta = delta |
|
285 |
if delta <= min_delta: |
|
286 |
closest_feature = feature |
|
287 | ||
288 |
if closest_feature: |
|
289 |
result = {} |
|
290 |
point_lon = closest_feature['geometry']['coordinates'][0] |
|
291 |
point_lat = closest_feature['geometry']['coordinates'][1] |
|
292 |
point_lon, point_lat = self.convert_coordinates(point_lon, point_lat, reverse=True) |
|
293 |
result['lon'] = str(point_lon) |
|
294 |
result['lat'] = str(point_lat) |
|
295 |
result['address'] = {} |
|
296 | ||
297 |
for attribute, properties in self.attributes_mapping: |
|
298 |
for field in properties: |
|
299 |
if closest_feature['properties'].get(field): |
|
300 |
result['address'][attribute] = unicode(closest_feature['properties'][field]) |
|
301 |
break |
|
302 | ||
303 |
return result |
tests/test_opengis.py | ||
---|---|---|
101 | 101 | |
102 | 102 |
FAKE_ERROR = u'<ows:ExceptionReport xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0.0" xsi:schemaLocation="http://www.opengis.net/ows/1.1 https://sigmetropole.lametro.fr/geoserver/schemas/ows/1.1.0/owsAll.xsd">\n <ows:Exception exceptionCode="NoApplicableCode">\n <ows:ExceptionText>Could not parse CQL filter list.\nEncountered &quot;BIS&quot; at line 1, column 129.\nWas expecting one of:\n &lt;EOF&gt; \n &quot;and&quot; ...\n &quot;or&quot; ...\n &quot;;&quot; ...\n &quot;/&quot; ...\n &quot;*&quot; ...\n &quot;+&quot; ...\n &quot;-&quot; ...\n Parsing : strEqualsIgnoreCase(nom_commune, &apos;Grenoble&apos;) = true AND strEqualsIgnoreCase(nom_voie, &apos;rue albert recoura&apos;) = true AND numero=8 BIS.</ows:ExceptionText>\n </ows:Exception>\n</ows:ExceptionReport>\n' |
103 | 103 | |
104 |
FAKE_GEOLOCATED_FEATURE = '''{ |
|
105 |
"crs": { |
|
106 |
"properties": { |
|
107 |
"name": "urn:ogc:def:crs:EPSG::3945" |
|
108 |
}, |
|
109 |
"type": "name" |
|
110 |
}, |
|
111 |
"features": [ |
|
112 |
{ |
|
113 |
"geometry": { |
|
114 |
"coordinates": [ |
|
115 |
1914059.51, |
|
116 |
4224699.2 |
|
117 |
], |
|
118 |
"type": "Point" |
|
119 |
}, |
|
120 |
"geometry_name": "the_geom", |
|
121 |
"properties": { |
|
122 |
"code_insee": 38185, |
|
123 |
"code_post": 38000, |
|
124 |
"nom_afnor": "BOULEVARD EDOUARD REY", |
|
125 |
"nom_commune": "Grenoble", |
|
126 |
"nom_voie": "boulevard \u00e9douard rey", |
|
127 |
"numero": 17 |
|
128 |
}, |
|
129 |
"type": "Feature" |
|
130 |
}, |
|
131 |
{ |
|
132 |
"geometry": { |
|
133 |
"coordinates": [ |
|
134 |
1914042.47, |
|
135 |
4224665.2 |
|
136 |
], |
|
137 |
"type": "Point" |
|
138 |
}, |
|
139 |
"geometry_name": "the_geom", |
|
140 |
"properties": { |
|
141 |
"code_insee": 38185, |
|
142 |
"code_post": 38000, |
|
143 |
"nom_commune": "Grenoble", |
|
144 |
"nom_voie": "place victor hugo", |
|
145 |
"numero": 2 |
|
146 |
}, |
|
147 |
"type": "Feature" |
|
148 |
}, |
|
149 |
{ |
|
150 |
"geometry": { |
|
151 |
"coordinates": [ |
|
152 |
1914035.7, |
|
153 |
4224700.42 |
|
154 |
], |
|
155 |
"type": "Point" |
|
156 |
}, |
|
157 |
"geometry_name": "the_geom", |
|
158 |
"properties": { |
|
159 |
"code_insee": 38185, |
|
160 |
"code_post": 38000, |
|
161 |
"nom_commune": "Grenoble", |
|
162 |
"nom_voie": "boulevard \u00e9douard rey", |
|
163 |
"numero": 28 |
|
164 |
}, |
|
165 |
"type": "Feature" |
|
166 |
}, |
|
167 |
{ |
|
168 |
"geometry": { |
|
169 |
"coordinates": [ |
|
170 |
1914018.64, |
|
171 |
4224644.61 |
|
172 |
], |
|
173 |
"type": "Point" |
|
174 |
}, |
|
175 |
"geometry_name": "the_geom", |
|
176 |
"properties": { |
|
177 |
"code_insee": 38185, |
|
178 |
"code_post": 38000, |
|
179 |
"nom_commune": "Grenoble", |
|
180 |
"nom_voie": "place victor hugo", |
|
181 |
"numero": 4 |
|
182 |
}, |
|
183 |
"type": "Feature" |
|
184 |
} |
|
185 |
], |
|
186 |
"totalFeatures": 4, |
|
187 |
"type": "FeatureCollection" |
|
188 |
}''' |
|
189 | ||
104 | 190 | |
105 | 191 |
@pytest.fixture |
106 | 192 |
def connector(db): |
... | ... | |
250 | 336 |
assert result['err'] == 1 |
251 | 337 |
assert result['err_desc'] == 'OpenGIS Error: unparsable error' |
252 | 338 |
assert '<ows:' in result['data']['content'] |
339 | ||
340 |
@mock.patch('passerelle.utils.Request.get') |
|
341 |
def test_reverse_geocoding(mocked_get, app, connector): |
|
342 |
connector.search_radius = 45 |
|
343 |
connector.projection = 'EPSG:3945' |
|
344 |
connector.save() |
|
345 |
endpoint = utils.generic_endpoint_url('opengis', 'reverse', slug=connector.slug) |
|
346 |
assert endpoint == '/opengis/test/reverse' |
|
347 |
mocked_get.return_value = utils.FakedResponse(content=FAKE_GEOLOCATED_FEATURE, status_code=200) |
|
348 |
resp = app.get(endpoint, params={'lat':'45.1893469606986', 'lon': '5.72462060798'}) |
|
349 |
assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'DWITHIN(the_geom,Point(1914061.48604 4224640.45779),45,meters)' |
|
350 |
assert resp.json['lon'] == '5.72407744145' |
|
351 |
assert resp.json['lat'] == '45.1893972656' |
|
352 |
assert resp.json['address']['house_number'] == '4' |
|
353 |
assert resp.json['address']['road'] == 'place victor hugo' |
|
354 |
assert resp.json['address']['postcode'] == '38000' |
|
355 |
assert resp.json['address']['city'] == 'Grenoble' |
|
356 | ||
357 |
connector.projection = 'EPSG:4326' |
|
358 |
connector.search_radius = 10 |
|
359 |
connector.save() |
|
360 |
resp = app.get(endpoint, params={'lat':'45.183784', 'lon': '5.714885'}) |
|
361 |
assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'DWITHIN(the_geom,Point(5.714885 45.183784),10,meters)' |
|
253 |
- |