opengis: support namespaces in feature_info endpoint (#79982)
gitea/passerelle/pipeline/head This commit looks good Details

This commit is contained in:
Emmanuel Cazenave 2023-07-24 18:13:41 +02:00
parent 4f136ee898
commit 5b5008eb2f
3 changed files with 229 additions and 5 deletions

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.18 on 2023-07-25 09:45
from django.db import migrations, models
import passerelle.apps.opengis.models
class Migration(migrations.Migration):
dependencies = [
('opengis', '0015_computed_properties'),
]
operations = [
migrations.AddField(
model_name='opengis',
name='feature_info_namespaces',
field=models.TextField(
blank=True,
help_text='XML namespaces to consider for feature_info (one per line, format "{http://www.foo.bar/baz}")',
null=True,
validators=[passerelle.apps.opengis.models.validate_feature_info_namespaces],
verbose_name='Feature info namespaces',
),
),
]

View File

@ -20,6 +20,7 @@ import xml.etree.ElementTree as ET
import pyproj
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.db.models import JSONField, Q
from django.http import HttpResponse
@ -36,19 +37,41 @@ from passerelle.utils.jsonresponse import APIError
from passerelle.utils.templates import validate_template
def build_dict_from_xml(elem):
def build_dict_from_xml(elem, namespaces=None):
def match_namespaces(tag, namespaces):
for ns in namespaces or []:
if tag.startswith(ns):
return True
return False
d = {}
for child in elem.find('.'):
if child.tag.startswith('{'):
continue
attribute_name = slugify(child.tag).replace('-', '_')
if not match_namespaces(child.tag, namespaces):
d.update(build_dict_from_xml(child, namespaces))
continue
attribute_name = slugify(child.tag.split('}')[1]).replace('-', '_')
else:
attribute_name = slugify(child.tag).replace('-', '_')
if child.text and child.text.strip():
d[attribute_name] = child.text.strip()
else:
d[attribute_name] = build_dict_from_xml(child)
d[attribute_name] = build_dict_from_xml(child, namespaces)
return d
def validate_feature_info_namespaces(value):
if value:
for ns in value.splitlines():
ns = ns.strip()
if ns:
if ns.startswith('#'):
continue
if ns.startswith('{') and ns.endswith('}'):
continue
raise ValidationError(_('Accepetd format : {http://www.foo.bar/baz}'))
PROJECTIONS = (
('EPSG:2154', _('EPSG:2154 (Lambert-93)')),
('EPSG:3857', _('EPSG:3857 (WGS 84 / Pseudo-Mercator)')),
@ -71,6 +94,16 @@ class OpenGIS(BaseResource):
_('GIS projection'), choices=PROJECTIONS, default='EPSG:3857', max_length=16
)
search_radius = models.IntegerField(_('Radius for point search'), default=5)
feature_info_namespaces = models.TextField(
_('Feature info namespaces'),
blank=True,
null=True,
help_text=_(
'XML namespaces to consider for feature_info (one per line, format "{http://www.foo.bar/baz}")'
),
validators=[validate_feature_info_namespaces],
)
attributes_mapping = (
('road', ('road', 'road_name', 'street', 'street_name', 'voie', 'nom_voie', 'rue')),
('city', ('city', 'city_name', 'town', 'town_name', 'commune', 'nom_commune', 'ville', 'nom_ville')),
@ -82,6 +115,18 @@ class OpenGIS(BaseResource):
class Meta:
verbose_name = _('OpenGIS')
def get_feature_info_namespaces(self):
if self.feature_info_namespaces is None:
return []
namespaces = []
for ns in self.feature_info_namespaces.splitlines():
ns = ns.strip()
if not ns or ns.startswith('#'):
continue
if ns:
namespaces.append(ns)
return namespaces
def get_capabilities(self, service_type, service_url):
if not service_url:
raise APIError('no %s URL declared' % service_type)
@ -309,7 +354,7 @@ class OpenGIS(BaseResource):
}
response = self.requests.get(self.wms_service_url, params=params)
element = ET.fromstring(response.content)
return {'err': 0, 'data': build_dict_from_xml(element)}
return {'err': 0, 'data': build_dict_from_xml(element, self.get_feature_info_namespaces())}
# https://carton.entrouvert.org/hydda-tiles/16/33650/23378.pn
@endpoint(

View File

@ -37,6 +37,84 @@ FAKE_FEATURE_INFO = '''<?xml version="1.0" encoding="UTF-8"?>
</cad_cadastre.cadparcelle_layer>
</msGMLOutput>'''
FAKE_FEATURE_INFO_BIS = '''<?xml version="1.0" encoding="UTF-8"?>
<msGMLOutput
xmlns:gml="http://www.opengis.net/gml"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<eco_ecologie.zfe_layer>
<gml:name>Zone à faibles émissions de la Métropole de Lyon - ZFE</gml:name>
<eco_ecologie.zfe_feature>
<gml:boundedBy>
<gml:Box srsName="EPSG:3857">
<gml:coordinates>531238.257237,5735107.676599 546464.284851,5751147.206607</gml:coordinates>
</gml:Box>
</gml:boundedBy>
<gid>200046977-ZFE-001</gid>
<date_debut>2020-01-01</date_debut>
<date_fin></date_fin>
<vp_critair>V5</vp_critair>
<vp_horaires>24/7</vp_horaires>
<vul_critair>V3</vul_critair>
<vul_horaires>24/7</vul_horaires>
<pl_critair>V3</pl_critair>
<pl_horaires>24/7</pl_horaires>
<autobus_autocars_critair>V5</autobus_autocars_critair>
<autobus_autocars_horaires>24/7</autobus_autocars_horaires>
<deux_rm_critair>V5</deux_rm_critair>
<deux_rm_horaires>24/7</deux_rm_horaires>
<url_arrete>https://agora.grandlyon.com/webdelib/files/unzip//seance_264250/23_d1647427974276.pdf</url_arrete>
<url_site_information>https://www.grandlyon.com/actions/zfe.html#c20726</url_site_information>
<id>200046977-ZFE-001</id>
</eco_ecologie.zfe_feature>
</eco_ecologie.zfe_layer>
</msGMLOutput>'''
FAKE_FEATURE_INFO_WITH_NAMESPACE = '''<?xml version="1.0" encoding="UTF-8"?>
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs" xmlns="http://www.opengis.net/wfs" xmlns:gml="http://www.opengis.net/gml" xmlns:metropole-de-lyon="http://metropole-de-lyon" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs https://download.data.grandlyon.com/geoserver/schemas/wfs/1.0.0/WFS-basic.xsd http://metropole-de-lyon https://download.data.grandlyon.com/geoserver/wfs?service=WFS&amp;version=1.0.0&amp;request=DescribeFeatureType&amp;typeName=metropole-de-lyon%3Aeco_ecologie.zfe">
<gml:boundedBy>
<gml:Box srsName="http://www.opengis.net/gml/srs/epsg.xml#3857">
<gml:coordinates decimal="." cs="," ts=" ">531238.20604672,5735107.61605308 546464.26335844,5751147.23178471</gml:coordinates>
</gml:Box>
</gml:boundedBy>
<gml:featureMember>
<metropole-de-lyon:eco_ecologie.zfe fid="eco_ecologie.zfe.200046977-ZFE-001">
<gml:boundedBy>
<gml:Box srsName="http://www.opengis.net/gml/srs/epsg.xml#3857">
<gml:coordinates decimal="." cs="," ts=" ">531238.20604672,5735107.61605308 546464.26335844,5751147.23178471</gml:coordinates>
</gml:Box>
</gml:boundedBy>
<metropole-de-lyon:gid>200046977-ZFE-001</metropole-de-lyon:gid>
<metropole-de-lyon:date_debut>2020-01-01</metropole-de-lyon:date_debut>
<metropole-de-lyon:vp_critair>V5</metropole-de-lyon:vp_critair>
<metropole-de-lyon:vp_horaires>24/7</metropole-de-lyon:vp_horaires>
<metropole-de-lyon:vul_critair>V3</metropole-de-lyon:vul_critair>
<metropole-de-lyon:vul_horaires>24/7</metropole-de-lyon:vul_horaires>
<metropole-de-lyon:pl_critair>V3</metropole-de-lyon:pl_critair>
<metropole-de-lyon:pl_horaires>24/7</metropole-de-lyon:pl_horaires>
<metropole-de-lyon:autobus_autocars_critair>V5</metropole-de-lyon:autobus_autocars_critair>
<metropole-de-lyon:autobus_autocars_horaires>24/7</metropole-de-lyon:autobus_autocars_horaires>
<metropole-de-lyon:deux_rm_critair>V5</metropole-de-lyon:deux_rm_critair>
<metropole-de-lyon:deux_rm_horaires>24/7</metropole-de-lyon:deux_rm_horaires>
<metropole-de-lyon:url_arrete>https://agora.grandlyon.com/webdelib/files/unzip//seance_264250/23_d1647427974276.pdf</metropole-de-lyon:url_arrete>
<metropole-de-lyon:url_site_information>https://www.grandlyon.com/actions/zfe.html#c20726</metropole-de-lyon:url_site_information>
<metropole-de-lyon:the_geom>
<gml:Polygon srsName="http://www.opengis.net/gml/srs/epsg.xml#3857">
<gml:outerBoundaryIs>
<gml:LinearRing>
<gml:coordinates decimal="." cs="," ts=" ">pleinsde coordonnées</gml:coordinates>
</gml:LinearRing>
</gml:outerBoundaryIs>
</gml:Polygon>
</metropole-de-lyon:the_geom>
<metropole-de-lyon:id>200046977-ZFE-001</metropole-de-lyon:id>
</metropole-de-lyon:eco_ecologie.zfe>
</gml:featureMember>
</wfs:FeatureCollection>
'''
FAKE_SERVICE_CAPABILITIES = '''<?xml version="1.0" encoding="UTF-8"?>
<wfs:WFS_Capabilities version="2.0.0"
xmlns:wfs="http://www.opengis.net/wfs/2.0"
@ -440,6 +518,82 @@ def test_feature_info(mocked_get, app, connector):
assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:4326'
@mock.patch('passerelle.utils.Request.get')
def test_feature_info_bis(mocked_get, app, connector):
endpoint = tests.utils.generic_endpoint_url('opengis', 'feature_info', slug=connector.slug)
assert endpoint == '/opengis/test/feature_info'
mocked_get.return_value = tests.utils.FakedResponse(content=FAKE_FEATURE_INFO_BIS, status_code=200)
resp = app.get(endpoint, params={'lat': '45.796890', 'lon': '4.784140'})
assert (
mocked_get.call_args[1]['params']['bbox']
== '532556.896735,5747844.261214,532579.160633,5747876.194333'
)
assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:3857'
assert resp.json['data'] == {
'eco_ecologiezfe_layer': {
'eco_ecologiezfe_feature': {
'autobus_autocars_critair': 'V5',
'autobus_autocars_horaires': '24/7',
'date_debut': '2020-01-01',
'date_fin': {},
'deux_rm_critair': 'V5',
'deux_rm_horaires': '24/7',
'gid': '200046977-ZFE-001',
'id': '200046977-ZFE-001',
'pl_critair': 'V3',
'pl_horaires': '24/7',
'url_arrete': 'https://agora.grandlyon.com/webdelib/files/unzip//seance_264250/23_d1647427974276.pdf',
'url_site_information': 'https://www.grandlyon.com/actions/zfe.html#c20726',
'vp_critair': 'V5',
'vp_horaires': '24/7',
'vul_critair': 'V3',
'vul_horaires': '24/7',
}
}
}
@mock.patch('passerelle.utils.Request.get')
def test_feature_info_with_namespace(mocked_get, app, connector):
connector.feature_info_namespaces = '''
{http://metropole-de-lyon}
# a comment
'''
connector.save()
endpoint = tests.utils.generic_endpoint_url('opengis', 'feature_info', slug=connector.slug)
assert endpoint == '/opengis/test/feature_info'
mocked_get.return_value = tests.utils.FakedResponse(
content=FAKE_FEATURE_INFO_WITH_NAMESPACE, status_code=200
)
resp = app.get(endpoint, params={'lat': '45.796890', 'lon': '4.784140'})
assert (
mocked_get.call_args[1]['params']['bbox']
== '532556.896735,5747844.261214,532579.160633,5747876.194333'
)
assert mocked_get.call_args[1]['params']['crs'] == 'EPSG:3857'
assert resp.json['data'] == {
'eco_ecologiezfe': {
'autobus_autocars_critair': 'V5',
'autobus_autocars_horaires': '24/7',
'date_debut': '2020-01-01',
'deux_rm_critair': 'V5',
'deux_rm_horaires': '24/7',
'gid': '200046977-ZFE-001',
'id': '200046977-ZFE-001',
'pl_critair': 'V3',
'pl_horaires': '24/7',
'the_geom': {},
'url_arrete': 'https://agora.grandlyon.com/webdelib/files/unzip//seance_264250/23_d1647427974276.pdf',
'url_site_information': 'https://www.grandlyon.com/actions/zfe.html#c20726',
'vp_critair': 'V5',
'vp_horaires': '24/7',
'vul_critair': 'V3',
'vul_horaires': '24/7',
}
}
@mock.patch('passerelle.utils.Request.get')
@pytest.mark.parametrize(
'lat,lon',