usse/funda-scraper/venv/lib/python3.10/site-packages/geopy/geocoders/ignfrance.py

485 lines
17 KiB
Python

import warnings
import xml.etree.ElementTree as ET
from functools import partial
from urllib.parse import urlencode
from geopy.exc import GeocoderQueryError
from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder
from geopy.location import Location
from geopy.util import logger
__all__ = ("IGNFrance", )
class IGNFrance(Geocoder):
"""Geocoder using the IGN France GeoCoder OpenLS API.
Documentation at:
https://geoservices.ign.fr/services-web-essentiels
"""
xml_request = """<?xml version="1.0" encoding="UTF-8"?>
<XLS version="1.2"
xmlns="http://www.opengis.net/xls"
xmlns:gml="http://www.opengis.net/gml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/xls
http://schemas.opengis.net/ols/1.2/olsAll.xsd">
<RequestHeader srsName="epsg:4326"/>
<Request methodName="{method_name}"
maximumResponses="{maximum_responses}"
requestID=""
version="1.2">
{sub_request}
</Request>
</XLS>"""
api_path = '/essentiels/geoportail/ols'
def __init__(
self,
api_key=None,
*,
username=None,
password=None,
referer=None,
domain='wxs.ign.fr',
scheme=None,
timeout=DEFAULT_SENTINEL,
proxies=DEFAULT_SENTINEL,
user_agent=None,
ssl_context=DEFAULT_SENTINEL,
adapter_factory=None
):
"""
:param str api_key: Not used.
.. deprecated:: 2.3
IGNFrance geocoding methods no longer accept or require
authentication, see `<https://geoservices.ign.fr/actualites/2021-10-04-evolution-des-modalites-dacces-aux-services-web>`_.
This parameter is scheduled for removal in geopy 3.0.
:param str username: Not used.
.. deprecated:: 2.3
See the `api_key` deprecation note.
:param str password: Not used.
.. deprecated:: 2.3
See the `api_key` deprecation note.
:param str referer: Not used.
.. deprecated:: 2.3
See the `api_key` deprecation note.
:param str domain: Currently it is ``'wxs.ign.fr'``, can
be changed for testing purposes for developer API
e.g ``'gpp3-wxs.ign.fr'`` at the moment.
:param str scheme:
See :attr:`geopy.geocoders.options.default_scheme`.
:param int timeout:
See :attr:`geopy.geocoders.options.default_timeout`.
:param dict proxies:
See :attr:`geopy.geocoders.options.default_proxies`.
:param str user_agent:
See :attr:`geopy.geocoders.options.default_user_agent`.
:type ssl_context: :class:`ssl.SSLContext`
:param ssl_context:
See :attr:`geopy.geocoders.options.default_ssl_context`.
:param callable adapter_factory:
See :attr:`geopy.geocoders.options.default_adapter_factory`.
.. versionadded:: 2.0
""" # noqa
super().__init__(
scheme=scheme,
timeout=timeout,
proxies=proxies,
user_agent=user_agent,
ssl_context=ssl_context,
adapter_factory=adapter_factory,
)
if api_key or username or password or referer:
warnings.warn(
"IGNFrance no longer accepts or requires authentication, "
"so api_key, username, password and referer are not used "
"anymore. These arguments should be removed. "
"In geopy 3 these options will be removed, causing "
"an error instead of this warning.",
DeprecationWarning,
stacklevel=2,
)
self.domain = domain.strip('/')
api_path = self.api_path
self.api = '%s://%s%s' % (self.scheme, self.domain, api_path)
def geocode(
self,
query,
*,
query_type='StreetAddress',
maximum_responses=25,
is_freeform=False,
filtering=None,
exactly_one=True,
timeout=DEFAULT_SENTINEL
):
"""
Return a location point by address.
:param str query: The query string to be geocoded.
:param str query_type: The type to provide for geocoding. It can be
`PositionOfInterest`, `StreetAddress` or `CadastralParcel`.
`StreetAddress` is the default choice if none provided.
:param int maximum_responses: The maximum number of responses
to ask to the API in the query body.
:param str is_freeform: Set if return is structured with
freeform structure or a more structured returned.
By default, value is False.
:param str filtering: Provide string that help setting geocoder
filter. It contains an XML string. See examples in documentation
and ignfrance.py file in directory tests.
:param bool exactly_one: Return one result or a list of results, if
available.
:param int timeout: Time, in seconds, to wait for the geocoding service
to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
exception. Set this only if you wish to override, on this call
only, the value set during the geocoder's initialization.
:rtype: ``None``, :class:`geopy.location.Location` or a list of them, if
``exactly_one=False``.
"""
# Check if acceptable query type
if query_type not in ['PositionOfInterest',
'StreetAddress',
'CadastralParcel']:
raise GeocoderQueryError("""You did not provided a query_type the
webservice can consume. It should be PositionOfInterest,
'StreetAddress or CadastralParcel""")
# Check query validity for CadastralParcel
if query_type == 'CadastralParcel' and len(query.strip()) != 14:
raise GeocoderQueryError("""You must send a string of fourteen
characters long to match the cadastre required code""")
sub_request = """
<GeocodeRequest returnFreeForm="{is_freeform}">
<Address countryCode="{query_type}">
<freeFormAddress>{query}</freeFormAddress>
{filtering}
</Address>
</GeocodeRequest>
"""
xml_request = self.xml_request.format(
method_name='LocationUtilityService',
sub_request=sub_request,
maximum_responses=maximum_responses
)
# Manage type change for xml case sensitive
if is_freeform:
is_freeform = 'true'
else:
is_freeform = 'false'
# Manage filtering value
if filtering is None:
filtering = ''
# Create query using parameters
request_string = xml_request.format(
is_freeform=is_freeform,
query=query,
query_type=query_type,
filtering=filtering
)
params = {
'xls': request_string
}
url = "?".join((self.api, urlencode(params)))
logger.debug("%s.geocode: %s", self.__class__.__name__, url)
callback = partial(
self._parse_xml, is_freeform=is_freeform, exactly_one=exactly_one
)
return self._request_raw_content(url, callback, timeout=timeout)
def reverse(
self,
query,
*,
reverse_geocode_preference=('StreetAddress', ),
maximum_responses=25,
filtering='',
exactly_one=True,
timeout=DEFAULT_SENTINEL
):
"""
Return an address by location point.
:param query: The coordinates for which you wish to obtain the
closest human-readable addresses.
:type query: :class:`geopy.point.Point`, list or tuple of ``(latitude,
longitude)``, or string as ``"%(latitude)s, %(longitude)s"``.
:param list reverse_geocode_preference: Enable to set expected results
type. It can be `StreetAddress` or `PositionOfInterest`.
Default is set to `StreetAddress`.
:param int maximum_responses: The maximum number of responses
to ask to the API in the query body.
:param str filtering: Provide string that help setting geocoder
filter. It contains an XML string. See examples in documentation
and ignfrance.py file in directory tests.
:param bool exactly_one: Return one result or a list of results, if
available.
:param int timeout: Time, in seconds, to wait for the geocoding service
to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
exception. Set this only if you wish to override, on this call
only, the value set during the geocoder's initialization.
:rtype: ``None``, :class:`geopy.location.Location` or a list of them, if
``exactly_one=False``.
"""
sub_request = """
<ReverseGeocodeRequest>
{reverse_geocode_preference}
<Position>
<gml:Point>
<gml:pos>{query}</gml:pos>
</gml:Point>
{filtering}
</Position>
</ReverseGeocodeRequest>
"""
xml_request = self.xml_request.format(
method_name='ReverseGeocodeRequest',
sub_request=sub_request,
maximum_responses=maximum_responses
)
for pref in reverse_geocode_preference:
if pref not in ('StreetAddress', 'PositionOfInterest'):
raise GeocoderQueryError(
'`reverse_geocode_preference` must contain '
'one or more of: StreetAddress, PositionOfInterest'
)
point = self._coerce_point_to_string(query, "%(lat)s %(lon)s")
reverse_geocode_preference = '\n'.join(
'<ReverseGeocodePreference>%s</ReverseGeocodePreference>' % pref
for pref
in reverse_geocode_preference
)
request_string = xml_request.format(
maximum_responses=maximum_responses,
query=point,
reverse_geocode_preference=reverse_geocode_preference,
filtering=filtering
)
url = "?".join((self.api, urlencode({'xls': request_string})))
logger.debug("%s.reverse: %s", self.__class__.__name__, url)
callback = partial(
self._parse_xml,
exactly_one=exactly_one,
is_reverse=True,
is_freeform='false'
)
return self._request_raw_content(url, callback, timeout=timeout)
def _parse_xml(self,
page,
is_reverse=False,
is_freeform=False,
exactly_one=True):
"""
Returns location, (latitude, longitude) from XML feed
and transform to json
"""
# Parse the page
tree = ET.fromstring(page.encode('utf-8'))
# Clean tree from namespace to facilitate XML manipulation
def remove_namespace(doc, namespace):
"""Remove namespace in the document in place."""
ns = '{%s}' % namespace
nsl = len(ns)
for elem in doc.iter():
if elem.tag.startswith(ns):
elem.tag = elem.tag[nsl:]
remove_namespace(tree, 'http://www.opengis.net/gml')
remove_namespace(tree, 'http://www.opengis.net/xls')
remove_namespace(tree, 'http://www.opengis.net/xlsext')
# Return places as json instead of XML
places = self._xml_to_json_places(tree, is_reverse=is_reverse)
if not places:
return None
if exactly_one:
return self._parse_place(places[0], is_freeform=is_freeform)
else:
return [
self._parse_place(
place,
is_freeform=is_freeform
) for place in places
]
def _xml_to_json_places(self, tree, is_reverse=False):
"""
Transform the xml ElementTree due to XML webservice return to json
"""
select_multi = (
'GeocodedAddress'
if not is_reverse
else 'ReverseGeocodedLocation'
)
adresses = tree.findall('.//' + select_multi)
places = []
sel_pl = './/Address/Place[@type="{}"]'
for adr in adresses:
el = {}
el['pos'] = adr.find('./Point/pos')
el['street'] = adr.find('.//Address/StreetAddress/Street')
el['freeformaddress'] = adr.find('.//Address/freeFormAddress')
el['municipality'] = adr.find(sel_pl.format('Municipality'))
el['numero'] = adr.find(sel_pl.format('Numero'))
el['feuille'] = adr.find(sel_pl.format('Feuille'))
el['section'] = adr.find(sel_pl.format('Section'))
el['departement'] = adr.find(sel_pl.format('Departement'))
el['commune_absorbee'] = adr.find(sel_pl.format('CommuneAbsorbee'))
el['commune'] = adr.find(sel_pl.format('Commune'))
el['insee'] = adr.find(sel_pl.format('INSEE'))
el['qualite'] = adr.find(sel_pl.format('Qualite'))
el['territoire'] = adr.find(sel_pl.format('Territoire'))
el['id'] = adr.find(sel_pl.format('ID'))
el['id_tr'] = adr.find(sel_pl.format('ID_TR'))
el['bbox'] = adr.find(sel_pl.format('Bbox'))
el['nature'] = adr.find(sel_pl.format('Nature'))
el['postal_code'] = adr.find('.//Address/PostalCode')
el['extended_geocode_match_code'] = adr.find(
'.//ExtendedGeocodeMatchCode'
)
place = {}
def testContentAttrib(selector, key):
"""
Helper to select by attribute and if not attribute,
value set to empty string
"""
return selector.attrib.get(
key,
None
) if selector is not None else None
place['accuracy'] = testContentAttrib(
adr.find('.//GeocodeMatchCode'), 'accuracy')
place['match_type'] = testContentAttrib(
adr.find('.//GeocodeMatchCode'), 'matchType')
place['building'] = testContentAttrib(
adr.find('.//Address/StreetAddress/Building'), 'number')
place['search_centre_distance'] = testContentAttrib(
adr.find('.//SearchCentreDistance'), 'value')
for key, value in iter(el.items()):
if value is not None:
place[key] = value.text
else:
place[key] = None
# We check if lat lng is not empty and unpack accordingly
if place['pos']:
lat, lng = place['pos'].split(' ')
place['lat'] = lat.strip()
place['lng'] = lng.strip()
else:
place['lat'] = place['lng'] = None
# We removed the unused key
place.pop("pos", None)
places.append(place)
return places
def _request_raw_content(self, url, callback, *, timeout):
"""
Send the request to get raw content.
"""
return self._call_geocoder(
url,
callback,
timeout=timeout,
is_json=False,
)
def _parse_place(self, place, is_freeform=None):
"""
Get the location, lat, lng and place from a single json place.
"""
# When freeform already so full address
if is_freeform == 'true':
location = place.get('freeformaddress')
else:
# For parcelle
if place.get('numero'):
location = place.get('street')
else:
# When classic geocoding
# or when reverse geocoding
location = "%s %s" % (
place.get('postal_code', ''),
place.get('commune', ''),
)
if place.get('street'):
location = "%s, %s" % (
place.get('street', ''),
location,
)
if place.get('building'):
location = "%s %s" % (
place.get('building', ''),
location,
)
return Location(location, (place.get('lat'), place.get('lng')), place)