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') |
... | ... | |
231 | 238 |
return HttpResponse( |
232 | 239 |
self.requests.get(self.wms_service_url, params=params, cache_duration=300).content, |
233 | 240 |
content_type='image/png') |
241 | ||
242 |
@endpoint(perm='can_access', description=_('Get feature info')) |
|
243 |
def reverse(self, request, lat, lon): |
|
244 |
result = None |
|
245 | ||
246 |
if self.projection != 'epsg:4326': |
|
247 |
wgs84 = pyproj.Proj(init='epsg:4326') |
|
248 |
target_projection = pyproj.Proj(init=self.projection) |
|
249 |
lon, lat = pyproj.transform(wgs84, target_projection, lon, lat) |
|
250 | ||
251 |
cql_filter = 'DWITHIN(the_geom,Point(%s %s),%s,meters)' % (lon, lat, self.search_radius) |
|
252 |
params = { |
|
253 |
'VERSION': self.get_wfs_service_version(), |
|
254 |
'SERVICE': 'WFS', |
|
255 |
'REQUEST': 'GetFeature', |
|
256 |
'TYPENAMES': self.query_layer, |
|
257 |
'OUTPUTFORMAT': 'json', |
|
258 |
'CQL_FILTER': cql_filter |
|
259 |
} |
|
260 |
response = self.requests.get(self.wfs_service_url, params=params) |
|
261 |
min_lon_diff = None |
|
262 |
min_lat_diff = None |
|
263 |
closest_feature = {} |
|
264 |
for feature in response.json().get('features'): |
|
265 |
if not feature['geometry']['type'] == 'Point': |
|
266 |
continue # skip unknown |
|
267 |
lon_diff = abs(float(lon) - float(feature['geometry']['coordinates'][0])) |
|
268 |
lat_diff = abs(float(lat) - float(feature['geometry']['coordinates'][1])) |
|
269 |
if min_lon_diff is None and min_lat_diff is None: |
|
270 |
min_lon_diff = lon_diff |
|
271 |
min_lat_diff = lat_diff |
|
272 |
if lon_diff <= min_lon_diff and lat_diff <= min_lat_diff: |
|
273 |
closest_feature = feature |
|
274 | ||
275 |
if closest_feature: |
|
276 |
result = {} |
|
277 |
point_lon = closest_feature['geometry']['coordinates'][0] |
|
278 |
point_lat = closest_feature['geometry']['coordinates'][1] |
|
279 |
if self.projection != 'epsg:4326': |
|
280 |
point_lon, point_lat = pyproj.transform(target_projection, wgs84, |
|
281 |
point_lon, point_lat) |
|
282 |
result['lon'] = str(point_lon) |
|
283 |
result['lat'] = str(point_lat) |
|
284 |
result['address'] = {} |
|
285 | ||
286 |
for attribute, properties in self.attributes_mapping: |
|
287 |
if request.GET.get(attribute): |
|
288 |
result['address'][attribute] = request.GET[attribute] |
|
289 |
continue |
|
290 |
for field in properties: |
|
291 |
if closest_feature['properties'].get(field): |
|
292 |
result['address'][attribute] = unicode(closest_feature['properties'][field]) |
|
293 |
break |
|
294 | ||
295 | ||
296 |
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): |
... | ... | |
244 | 330 |
assert result['err'] == 1 |
245 | 331 |
assert result['err_desc'] == 'OpenGIS Error: unparsable error' |
246 | 332 |
assert '<ows:' in result['data']['content'] |
333 | ||
334 |
@mock.patch('passerelle.utils.Request.get') |
|
335 |
def test_reverse_geocoding(mocked_get, app, connector): |
|
336 |
connector.search_radius = 45 |
|
337 |
connector.projection = 'EPSG:3945' |
|
338 |
connector.save() |
|
339 |
endpoint = utils.generic_endpoint_url('opengis', 'reverse', slug=connector.slug) |
|
340 |
assert endpoint == '/opengis/test/reverse' |
|
341 |
mocked_get.return_value = utils.FakedResponse(content=FAKE_GEOLOCATED_FEATURE, status_code=200) |
|
342 |
resp = app.get(endpoint, params={'lat':'45.1895752129513', 'lon': '5.72438938399621'}) |
|
343 |
assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'DWITHIN(the_geom,Point(1914042.47393 4224665.19751),45,meters)' |
|
344 |
assert resp.json['lon'] == '5.72438933506' |
|
345 |
assert resp.json['lat'] == '45.1895752366' |
|
346 |
assert resp.json['address']['house_number'] == '2' |
|
347 |
assert resp.json['address']['road'] == 'place victor hugo' |
|
348 |
assert resp.json['address']['postcode'] == '38000' |
|
349 |
assert resp.json['address']['city'] == 'Grenoble' |
|
350 |
assert 'country' not in resp.json['address'] |
|
351 | ||
352 |
resp = app.get(endpoint, params={'lat':'45.1895752129513', 'lon': '5.72438938399621', 'country': 'France'}) |
|
353 |
assert resp.json['address']['country'] == 'France' |
|
354 | ||
355 |
connector.projection = 'EPSG:4326' |
|
356 |
connector.search_radius = 10 |
|
357 |
connector.save() |
|
358 |
resp = app.get(endpoint, params={'lat':'45.183784', 'lon': '5.714885'}) |
|
359 |
assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'DWITHIN(the_geom,Point(5.714885 45.183784),10,meters)' |
|
247 |
- |