Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Add Tencent and Amap Maps as geopy's geocoder provider #593

Open
wants to merge 63 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
402f4f8
Add comments for environments
Zeroto521 Jun 26, 2022
f56d19f
Adjust the place of comment
Zeroto521 Jun 26, 2022
ddfec7f
Optionally required geopy
Zeroto521 Jun 26, 2022
c1daa86
EHN: New geoaccessor `to_geocode`
Zeroto521 Jun 26, 2022
40e7ea7
DOC: support single label
Zeroto521 Jun 26, 2022
450a9cf
EHN: New geoaccessor `to_geocode` for `DataFrame`
Zeroto521 Jun 26, 2022
491263b
lint codes
Zeroto521 Jun 26, 2022
8f79b75
Import geoaccessor.series all methods
Zeroto521 Jun 27, 2022
64fd41b
Finished raising error
Zeroto521 Jun 27, 2022
f63f4ae
Add kwargs for geocode
Zeroto521 Jun 27, 2022
32d1a00
Finished description
Zeroto521 Jun 27, 2022
1605ff1
Index methods
Zeroto521 Jun 27, 2022
0b0a195
Add see-also section
Zeroto521 Jun 27, 2022
ae0c131
Delete marks
Zeroto521 Jun 27, 2022
c83c904
adjust the sequences of condition
Zeroto521 Jun 27, 2022
5ee0b89
Add **kwargs for to_geocode
Zeroto521 Jun 27, 2022
c96a2ff
Finished basic description for to_geocode
Zeroto521 Jun 27, 2022
b378ba0
display data
Zeroto521 Jun 27, 2022
b57adcb
lint codes
Zeroto521 Jun 27, 2022
cc75378
Finished to_geocode examples section
Zeroto521 Jun 27, 2022
aae4adb
Test error case
Zeroto521 Jun 27, 2022
2a0f1dc
update the description of dataframe.to_geocdoe
Zeroto521 Jun 27, 2022
2b7fc1e
BOT: auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 27, 2022
1909fd4
Update the argument value of example
Zeroto521 Jun 27, 2022
82c0a49
rename `to_geocode` to `geocode`
Zeroto521 Jun 27, 2022
ee46f2d
Add raises section
Zeroto521 Jun 27, 2022
f4d7e89
rename `to_geocode` to `geocode`
Zeroto521 Jun 27, 2022
f97c46a
Merge branch 'main' into geoaccessor/dataframe/geocode
Zeroto521 Jul 1, 2022
b2f5dec
Merge branch 'main' into geoaccessor/dataframe/geocode
Zeroto521 Jul 1, 2022
be026f3
EHN: Add Tencent geocoder as geopy's geocoder provider
Zeroto521 Jul 2, 2022
b55a7bf
Index this geocoder
Zeroto521 Jul 2, 2022
fa92498
Merge branch 'main' into geoaccessor/dataframe/geocoder
Zeroto521 Jul 2, 2022
8062dee
BOT: auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 2, 2022
73a14c4
Remove default parameters
Zeroto521 Jul 2, 2022
b1b8417
suit with docstrings style
Zeroto521 Jul 13, 2022
c2755d5
use `self.domain` replace `domain`
Zeroto521 Jul 13, 2022
55e5cc1
Update checking status
Zeroto521 Jul 13, 2022
99840e5
use lower case
Zeroto521 Jul 13, 2022
fe6ea83
use upper case
Zeroto521 Jul 13, 2022
3729a7e
Add keyword `SmartGeocoder`
Zeroto521 Jul 13, 2022
4dde97e
lint codes
Zeroto521 Jul 13, 2022
656c9a9
BOT: auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 13, 2022
7f130fb
adjust sequences of codes
Zeroto521 Jul 15, 2022
4f89867
add `region` parameter description
Zeroto521 Jul 15, 2022
08a3fa8
add typing for api_key
Zeroto521 Jul 15, 2022
f76e855
Create amap.py
Zeroto521 Jul 15, 2022
ae07986
Merge branch 'main' into geoaccessor/dataframe/geocode
Zeroto521 Jul 16, 2022
15c67d0
Remove SmartGeocoder
Zeroto521 Aug 11, 2022
6dd12c9
Add related tencent api documentation link
Zeroto521 Aug 12, 2022
756c068
Merge branch 'main' into geoaccessor/dataframe/geocode
Zeroto521 Aug 16, 2022
b9a142f
correct `_check_status`
Zeroto521 Aug 17, 2022
bd6d2eb
update logic
Zeroto521 Aug 17, 2022
c808d67
make code more readable
Zeroto521 Aug 17, 2022
cb71baf
index Amap
Zeroto521 Aug 17, 2022
f36c1f3
complete basic logic
Zeroto521 Aug 17, 2022
3d3a3a3
BOT: auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 17, 2022
a5ba79c
remove sig
Zeroto521 Aug 17, 2022
548b019
Merge branch 'geoaccessor/dataframe/geocode' of https://github.com/Ze…
Zeroto521 Aug 17, 2022
a86532a
simplify a bit
Zeroto521 Aug 17, 2022
a755e56
rename variable
Zeroto521 Aug 17, 2022
832e03d
BOT: auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 17, 2022
011e2ae
Merge branch 'main' into geoaccessor/dataframe/geocode
Zeroto521 Dec 18, 2022
e299a78
Merge branch 'main' into geoaccessor/dataframe/geocode
Zeroto521 Feb 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dtoolkit/geoaccessor/geocoder/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from dtoolkit.geoaccessor.geocoder.amap import Amap # noqa
from dtoolkit.geoaccessor.geocoder.tencent import Tencent # noqa
280 changes: 280 additions & 0 deletions dtoolkit/geoaccessor/geocoder/amap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
from __future__ import annotations

from functools import partial
from typing import Callable

from geopy.exc import GeocoderAuthenticationFailure
from geopy.exc import GeocoderQueryError
from geopy.exc import GeocoderQuotaExceeded
from geopy.exc import GeocoderServiceError
from geopy.exc import GeocoderTimedOut
from geopy.geocoders.base import DEFAULT_SENTINEL
from geopy.geocoders.base import Geocoder
from geopy.location import Location
from geopy.util import logger


__all__ = ("Amap",)


class Amap(Geocoder):
"""
Geocoder using the Amap Maps API.

Documentation at:
https://lbs.amap.com/api/webservice/guide/api/georegeo
"""

api_path = "/v3/geocode/geo"
reverse_path = "/v3/geocode/regeo"

def __init__(
self,
api_key: str,
*,
scheme: str = None,
timeout: int = DEFAULT_SENTINEL,
proxies: dict = DEFAULT_SENTINEL,
user_agent: str = None,
ssl_context: ssl.SSLContext = DEFAULT_SENTINEL,
adapter_factory: Callable = None,
):
"""
:param str api_key: The API key required by Amap Map to perform
geocoding requests. API keys are managed through the Amap APIs
console (https://console.amap.com/dev/key/app).

: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`.
"""

super().__init__(
scheme=scheme,
timeout=timeout,
proxies=proxies,
user_agent=user_agent,
ssl_context=ssl_context,
adapter_factory=adapter_factory,
)
self.api_key = api_key
self.domin = "restapi.amap.com"
self.api = f"{self.scheme}://{self.domin}{self.api_path}"
self.reverse_api = f"{self.scheme}://{self.domin}{self.reverse_path}"

def geocode(
self,
query: str,
*,
city: str = None,
exactly_one: bool = True,
timeout: int = DEFAULT_SENTINEL,
) -> None | Location | list[Location]:
"""
Return a location point by address.

:param str query: The address or query you wish to geocode.

:param str city: The city of address.

: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``.
"""

params = {"address": query, "city": city, "key": self.api_key}
url = self._construct_url(self.api, params)

logger.debug(f"{self.__class__.__name__}.geocode: {url}")
callback = partial(self._parse_geocode_json, exactly_one=exactly_one)

return self._call_geocoder(url, callback, timeout=timeout)

def reverse(
self,
query: str,
*,
exactly_one: bool = True,
timeout: int = DEFAULT_SENTINEL,
) -> None | Location | list[Location]:
"""
Return an address by location point.

:type query: :class:`geopy.point.Point`, list or tuple of ``(latitude,
longitude)``, or string as ``"%(latitude)s, %(longitude)s"``.
:param query: The coordinates for which you wish to obtain the
closest human-readable addresses.

:param bool exactly_one: Return one result or a list of results, if
available. Tencent's API always return at most one result.

: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``.
"""

params = {
"key": self.api_key,
"location": self._coerce_point_to_string(
query,
output_format="%(lon)s,%(lat)s",
),
}
url = self._construct_url(self.reverse_api, params)

logger.debug(f"{self.__class__.__name__}.reverse: {url}")
callback = partial(self._parse_reverse_json, exactly_one=exactly_one)

return self._call_geocoder(url, callback, timeout=timeout)

def _construct_url(self, base_api: str, params: dict) -> str:
"""
Construct geocoding request url.

:param str base_api: Geocoding function base address - self.api
or self.reverse_api.

:param dict params: Geocoding params.

:return: string URL.
"""
from urllib.parse import urlencode

# Remove empty value item
params = {k: v for k, v in params.items() if v}
query_string = urlencode(params)
return f"{base_api}?{query_string}"

def _parse_geocode_json(
self,
response: dict,
exactly_one: bool = True,
) -> None | Location | list[Location]:
"""
Returns location, (latitude, longitude) from JSON feed.
"""

def _parse_place(place: dict) -> None | Location:
"""
Returns location, (latitude, longitude) from JSON feed.
"""

if not isinstance(place, dict):
return

address = place.get("formatted_address")
point = self._parse_coordinate(place.get("location"))
return Location(address, point, place)

if not isinstance(response, dict):
return
self._check_status(response.get("infocode"), response.get("info"))

places = response.get("geocodes")
if exactly_one:
return _parse_place(places[0])
return [_parse_place(place) for place in places]

def _parse_reverse_json(self, response: dict, exactly_one: bool = True):
"""
Returns location, (latitude, longitude) from JSON feed.
"""

if not isinstance(response, dict):
return
self._check_status(response.get("infocode"), response.get("info"))

place = response.get("regeocode", {})
address = place.get("formatted_address")
point = self._parse_coordinate(
place.get("addressComponent", {})
.get("streetNumber", {})
.get("location", None),
)
location = Location(address, point, place)
return location if exactly_one else [location]

def _parse_coordinate(self, location: str) -> tuple(float | None, float | None):
"""
Get the lat and lng from a string ("lat,lng").
"""

if not isinstance(location, str):
return (None, None)

return tuple(reversed(tuple(map(float, location.split(",")))))

def _check_status(self, code: str, info: str):
"""
Validates error statuses.

Documentation at:
https://lbs.amap.com/api/webservice/guide/tools/info
"""

code = int(code)

if code == 10000:
return
elif code in {
10001,
10002,
10005,
10006,
10007,
10009,
10010,
10012,
10026,
10041,
}:
raise GeocoderAuthenticationFailure(f"{info}.")
elif (
code
in {
10003,
10004,
10014,
10015,
10019,
10020,
10021,
10029,
10044,
10045,
}
or 40000 <= code <= 50000
):
raise GeocoderQuotaExceeded(f"{info}.")
elif code in {10013, 10017} or 20000 <= code < 30000:
raise GeocoderQueryError(f"{info}.")
elif code == 10011 or 30000 <= code < 40000:
raise GeocoderServiceError(f"{info}.")
else:
raise GeocoderQueryError(f"{info}.")
Loading