s+: complete conversion to local code

i hate GIS

- geocoders now have to return a bounding box
- new SHAREABLE_TEXT_LOCALITY constant, also exposed
- _generate_text now does double duty for locality and sharetext generation
This commit is contained in:
Mark Joshwel 2023-09-06 17:39:53 +00:00
parent d3ada0b386
commit a9e26c8916
3 changed files with 536 additions and 188 deletions

View file

@ -42,7 +42,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 2, "execution_count": 1,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -356,166 +356,289 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## 2.1.0: the adventure of shortening global/full Plus Codes" "## 2.1.0: adventures in of shortening global/full Plus Codes"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"### testing rate-limited default geocoding functions" "### testing rate-limited and cached default geocoding functions"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 5, "execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"test_geocoding = SurplusDefaultGeocoding(user_agent=\"surplus/playground\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"1.33318835, 103.77461234638255\n", "1\n",
"{'amenity': 'Ngee Ann Polytechnic', 'house_number': '535', 'road': 'Clementi Road', 'neighbourhood': 'Ewart Park', 'suburb': 'Bukit Timah', 'city': 'Singapore', 'county': 'Northwest', 'ISO3166-2-lvl6': 'SG-03', 'postcode': '599489', 'country': 'Singapore', 'country_code': 'sg', 'raw': {'place_id': 250910125, 'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright', 'osm_type': 'relation', 'osm_id': 2535118, 'lat': '1.33318835', 'lon': '103.77461234638255', 'class': 'amenity', 'type': 'university', 'place_rank': 30, 'importance': 0.34662169301918117, 'addresstype': 'amenity', 'name': 'Ngee Ann Polytechnic', 'display_name': 'Ngee Ann Polytechnic, 535, Clementi Road, Ewart Park, Bukit Timah, Singapore, Northwest, 599489, Singapore', 'address': {'amenity': 'Ngee Ann Polytechnic', 'house_number': '535', 'road': 'Clementi Road', 'neighbourhood': 'Ewart Park', 'suburb': 'Bukit Timah', 'city': 'Singapore', 'county': 'Northwest', 'ISO3166-2-lvl6': 'SG-03', 'postcode': '599489', 'country': 'Singapore', 'country_code': 'sg'}, 'boundingbox': ['1.3289692', '1.3372184', '103.7701481', '103.7783945']}, 'latitude': 1.33318835, 'longitude': 103.77461234638255}\n" "2\n",
"3\n",
"4\n",
"5\n",
"\n",
"1\n",
"2\n",
"3\n",
"4\n",
"5\n",
"\n",
"3.1107698050s\t->\t0.0000886890s\t\t(-3.1106811160002508s)\n"
] ]
} }
], ],
"source": [ "source": [
"from surplus import SurplusGeocoderProtocol, SurplusReverserProtocol\n", "from timeit import timeit\n",
"\n", "\n",
"\n", "\n",
"test_geocoding = SurplusDefaultGeocoding(user_agent=\"surplus/playground\")\n", "test_stmt = \"\"\"\\\n",
"print(1)\n",
"test_geocoding.geocoder(\"Wisma Atria\") # instant\n",
"print(2)\n",
"test_geocoding.geocoder(\"Temasek Polytechnic\") # after 1 second\n",
"print(3)\n",
"location = test_geocoding.geocoder(\"Ngee Ann Polytechnic\") # after 1 second\n",
"print(4)\n",
"test_geocoding.reverser(f\"{location.latitude}, {location.longitude}\") # instant\n",
"print(5)\n",
"test_geocoding.reverser(f\"{location.latitude}, {location.longitude}\") # instant (cached)\n",
"print()\n",
"\"\"\"\n",
"\n", "\n",
"print(location := test_geocoding.geocoder(\"Ngee Ann Polytechnic\"))\n", "time_cold_call = timeit(test_stmt, globals=globals(), number=1) # expecting 3-4 seconds\n",
"time_2nd_call = timeit(test_stmt, globals=globals(), number=1) # should be instant\n",
"\n", "\n",
"print(reversed := test_geocoding.reverser(f\"{location.latitude}, {location.longitude}\"))" "print(\n",
" f\"{time_cold_call:.10f}s\\t->\\t{time_2nd_call:.10f}s\\t\\t({time_2nd_call - time_cold_call}s)\"\n",
")"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"### loop for less information until a local code is made" "### reversing the query latlong and using the address information to form a locality"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 3,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# TODO\n",
"\n",
"test1 = LocalCodeQuery(\"9R3J+R9\", \"Singapore\")\n",
"test2 = LocalCodeQuery(\"G227+XF\", \"St Lucia, Queensland, Australia\")\n",
"\n",
"level = 13" "level = 13"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 4,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "name": "stdout",
"text/plain": [ "output_type": "stream",
"{'suburb': 'Bishan',\n", "text": [
" 'city': 'Singapore',\n", "St Lucia, St Lucia, Queensland, Australia\n",
" 'county': 'Central',\n", "Austin, Travis County, Texas, United States\n"
" 'ISO3166-2-lvl6': 'SG-01',\n",
" 'country': 'Singapore',\n",
" 'country_code': 'sg',\n",
" 'raw': {'place_id': 251115282,\n",
" 'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright',\n",
" 'osm_type': 'way',\n",
" 'osm_id': 795946716,\n",
" 'lat': '1.3519117',\n",
" 'lon': '103.8489708',\n",
" 'class': 'place',\n",
" 'type': 'suburb',\n",
" 'place_rank': 19,\n",
" 'importance': 0.39184907371668787,\n",
" 'addresstype': 'suburb',\n",
" 'name': 'Bishan',\n",
" 'display_name': 'Bishan, Singapore, Central, Singapore',\n",
" 'address': {'suburb': 'Bishan',\n",
" 'city': 'Singapore',\n",
" 'county': 'Central',\n",
" 'ISO3166-2-lvl6': 'SG-01',\n",
" 'country': 'Singapore',\n",
" 'country_code': 'sg'},\n",
" 'boundingbox': ['1.3416846', '1.3679829', '103.8184512', '103.8604083']},\n",
" 'latitude': 1.3519117,\n",
" 'longitude': 103.8489708}"
] ]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
} }
], ],
"source": [ "source": [
"(\n", "(\n",
" response := geocoding.reverser(\n", " au_response := geocoding.reverser(\n",
" test1.to_lat_long_coord(geocoding.geocoder).get(), level=level\n", " (\n",
" au_target := (\n",
" LocalCodeQuery(\n",
" \"G227+XF\", \"St Lucia, Queensland, Australia\"\n",
" ).to_lat_long_coord(geocoding.geocoder)\n",
" )\n", " )\n",
")" " ).get(),\n",
" level=level,\n",
" )\n",
")\n",
"\n",
"au_locality = f\"{au_response['suburb']}, {au_response['city_district']}, {au_response['state']}, {au_response['country']}\"\n",
"print(au_locality)\n",
"\n",
"(\n",
" us_response := geocoding.reverser(\n",
" (\n",
" us_target := (\n",
" LocalCodeQuery(\"77Q4+7X\", \"Austin, Texas, USA\").to_lat_long_coord(\n",
" geocoding.geocoder\n",
" )\n",
" )\n",
" ).get(),\n",
" level=level,\n",
" )\n",
")\n",
"\n",
"us_locality = f\"{us_response['city']}, {us_response['county']}, {us_response['state']}, {us_response['country']}\"\n",
"print(us_locality)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### getting boundary boxes"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 13,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "name": "stdout",
"text/plain": [ "output_type": "stream",
"{'suburb': 'St Lucia',\n", "text": [
" 'city_district': 'St Lucia',\n", "{'addresstype': 'suburb',\n",
" 'city': 'Brisbane City',\n", " 'boundingbox': ['-27.5187362', '-27.4787362', '152.9881642', '153.0281642'],\n",
" 'state': 'Queensland',\n",
" 'ISO3166-2-lvl4': 'AU-QLD',\n",
" 'postcode': '4072',\n",
" 'country': 'Australia',\n",
" 'country_code': 'au',\n",
" 'raw': {'place_id': 54477898,\n",
" 'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright',\n",
" 'osm_type': 'node',\n",
" 'osm_id': 88800268,\n",
" 'lat': '-27.4987362',\n",
" 'lon': '153.0081642',\n",
" 'class': 'place',\n", " 'class': 'place',\n",
" 'type': 'suburb',\n",
" 'place_rank': 19,\n",
" 'importance': 0.27501,\n",
" 'addresstype': 'suburb',\n",
" 'name': 'St Lucia',\n",
" 'display_name': 'St Lucia, Brisbane City, Queensland, 4072, Australia',\n", " 'display_name': 'St Lucia, Brisbane City, Queensland, 4072, Australia',\n",
" 'address': {'suburb': 'St Lucia',\n", " 'importance': 0.27501,\n",
" 'city_district': 'St Lucia',\n", " 'lat': '-27.4987362',\n",
" 'city': 'Brisbane City',\n", " 'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. '\n",
" 'state': 'Queensland',\n", " 'http://osm.org/copyright',\n",
" 'ISO3166-2-lvl4': 'AU-QLD',\n", " 'lon': '153.0081642',\n",
" 'postcode': '4072',\n", " 'name': 'St Lucia',\n",
" 'country': 'Australia',\n", " 'osm_id': 88800268,\n",
" 'country_code': 'au'},\n", " 'osm_type': 'node',\n",
" 'boundingbox': ['-27.5187362', '-27.4787362', '152.9881642', '153.0281642']},\n", " 'place_id': 54477898,\n",
" 'latitude': -27.4987362,\n", " 'place_rank': 19,\n",
" 'longitude': 153.0081642}" " 'type': 'suburb'}\n",
"\n",
"Latlong(latitude=-27.4987362, longitude=153.0081642, bounding_box=[-27.5187362, -27.4787362, 152.9881642, 153.0281642])\n"
] ]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
} }
], ],
"source": [ "source": [
"(\n", "from geopy.geocoders import Nominatim\n",
" response := geocoding.reverser(\n", "from pprint import pprint\n",
" test2.to_lat_long_coord(geocoding.geocoder).get(), level=level\n", "\n",
"target_query: Result[Latlong] = au_target\n",
"target_locality: str = au_locality\n",
"\n",
"raw_geocoding = Nominatim(user_agent=\"surplus/playground\")\n",
"latlong = raw_geocoding.geocode(target_locality)\n",
"pprint(latlong.raw)\n",
"print()\n",
"\n",
"# done: now implmented in surplus as surplus.Latlong.bounding_box\n",
"locality_latlong = geocoding.geocoder(target_locality)\n",
"pprint(locality_latlong)"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[-27.5187362, -27.4787362, 152.9881642, 153.0281642]\n",
"(True, True, True, True)\n",
"(True, True, True, True)\n"
]
}
],
"source": [
"# based on <https://github.com/google/open-location-code/wiki/Guidance-for-shortening-codes>\n",
"\n",
"target_latlong = target_query.get()\n",
"if locality_latlong.bounding_box is None:\n",
" ... # raise some error\n",
"\n",
"print(locality_latlong.bounding_box)\n",
"check1 = (\n",
" # The center point of the feature is within 0.4 degrees latitude and 0.4 degrees longitude\n",
" (\n",
" (target_latlong.latitude - 0.4)\n",
" <= locality_latlong.latitude\n",
" <= (target_latlong.latitude + 0.4)\n",
" ),\n",
" (\n",
" (target_latlong.longitude - 0.4)\n",
" <= locality_latlong.longitude\n",
" <= (target_latlong.longitude + 0.4)\n",
" ),\n",
" # The bounding box of the feature is less than 0.8 degrees high and wide.\n",
" abs(locality_latlong.bounding_box[0] - locality_latlong.bounding_box[1]) < 0.8,\n",
" abs(locality_latlong.bounding_box[2] - locality_latlong.bounding_box[3]) < 0.8,\n",
")\n",
"\n",
"\n",
"check2 = (\n",
" # The center point of the feature is within 0.4 degrees latitude and 0.4 degrees longitude\n",
" (\n",
" (target_latlong.latitude - 8)\n",
" <= locality_latlong.latitude\n",
" <= (target_latlong.latitude + 8)\n",
" ),\n",
" (\n",
" (target_latlong.longitude - 8)\n",
" <= locality_latlong.longitude\n",
" <= (target_latlong.longitude + 8)\n",
" ),\n",
" # The bounding box of the feature is less than 0.8 degrees high and wide.\n",
" abs(locality_latlong.bounding_box[0] - locality_latlong.bounding_box[1]) < 16,\n",
" abs(locality_latlong.bounding_box[2] - locality_latlong.bounding_box[3]) < 16,\n",
")\n",
"\n",
"print(check1)\n",
"print(check2)"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"G227+XF St Lucia, St Lucia, Queensland, Australia\n"
]
}
],
"source": [
"from pluscodes import encode\n",
"\n",
"target_plus_code = encode(\n",
" lat=target_latlong.latitude, lon=target_latlong.longitude, code_length=10\n",
")\n",
"portion_plus_code = \"\"\n",
"\n",
"if check1:\n",
" portion_plus_code = target_plus_code[4:]\n",
" print(portion_plus_code, target_locality)\n",
"\n",
"elif check2:\n",
" portion_plus_code = target_plus_code[2:]\n",
" print(portion_plus_code, target_locality)\n",
"\n",
"else:\n",
" print(\n",
" \"info: could not determine a suitable geographical feature to use as locality for shortening.\"\n",
" )\n", " )\n",
")" " print(plus_code)"
] ]
}, },
{ {

View file

@ -48,6 +48,7 @@ from .surplus import (
SHAREABLE_TEXT_LINE_4_KEYS, SHAREABLE_TEXT_LINE_4_KEYS,
SHAREABLE_TEXT_LINE_5_KEYS, SHAREABLE_TEXT_LINE_5_KEYS,
SHAREABLE_TEXT_LINE_6_KEYS, SHAREABLE_TEXT_LINE_6_KEYS,
SHAREABLE_TEXT_LOCALITY,
SHAREABLE_TEXT_NAMES, SHAREABLE_TEXT_NAMES,
VERSION, VERSION,
VERSION_SUFFIX, VERSION_SUFFIX,

View file

@ -136,6 +136,13 @@ SHAREABLE_TEXT_NAMES: Final[tuple[str, ...]] = (
+ SHAREABLE_TEXT_LINE_2_KEYS + SHAREABLE_TEXT_LINE_2_KEYS
+ ("house_name", "road") + ("house_name", "road")
) )
SHAREABLE_TEXT_LOCALITY: dict[str, tuple[str, ...]] = {
"default": ("city_district", "district", "city", *SHAREABLE_TEXT_LINE_6_KEYS),
"SG": ("country",),
}
# adjusts geocoder zoom level when geocoding latlong into an address
LOCALITY_GEOCODER_LEVEL: int = 13
# exceptions # exceptions
@ -169,6 +176,19 @@ class EmptyQueryError(SurplusException):
# data structures # data structures
class TextGenerationEnum(Enum):
"""
(internal use) enum representing what type of text to generate for _generate_text()
values
SHAREABLE_TEXT: str = "sharetext"
LOCAL_CODE: str = "localcode"
"""
SHAREABLE_TEXT: str = "sharetext"
LOCALITY_TEXT: str = "locality_text"
class ConversionResultTypeEnum(Enum): class ConversionResultTypeEnum(Enum):
""" """
enum representing what the result type of conversion should be enum representing what the result type of conversion should be
@ -273,11 +293,16 @@ class Result(NamedTuple, Generic[ResultType]):
class Latlong(NamedTuple): class Latlong(NamedTuple):
""" """
typing.NamedTuple representing a latitude-longitude coordinate pair typing.NamedTuple representing a latitude-longitude coordinate pair and any extra
information
arguments arguments
latitude: float latitude: float
longitude: float longitude: float
bounding_box: tuple[float, float, float, float] | None = None
a four-tuple representing a bounding box, (lat1, lat2, lon1, lon2) or None
the user does not need to enter this. this attribute is only used for
shortening plus codes, and will be supplied by the geocoding service.
methods methods
def __str__(self) -> str: ... def __str__(self) -> str: ...
@ -285,6 +310,7 @@ class Latlong(NamedTuple):
latitude: float latitude: float
longitude: float longitude: float
bounding_box: tuple[float, float, float, float] | None = None
def __str__(self) -> str: def __str__(self) -> str:
""" """
@ -306,8 +332,11 @@ class SurplusGeocoderProtocol(Protocol):
name string to location function. must take in a string and return a Latlong. name string to location function. must take in a string and return a Latlong.
function can be functools.lru_cache()-wrapped if the geocoding service asks for **the function returned MUST supply a `bounding_box` attribute to the to-be-returned
caching [Latlong](#class-latlong).** the bounding box is used when surplus shortens Plus Codes.
function can and should be at minimum functools.lru_cache()-wrapped if the geocoding
service asks for caching
exceptions are handled by the caller exceptions are handled by the caller
""" """
@ -346,8 +375,8 @@ class SurplusReverserProtocol(Protocol):
'raw': {...}, 'raw': {...},
} }
function can be functools.lru_cache()-wrapped if the geocoding service asks for function can and should be at minimum functools.lru_cache()-wrapped if the geocoding
caching service asks for caching
exceptions are handled by the caller, exceptions are handled by the caller,
see the playground notebook in repository root for sample output see the playground notebook in repository root for sample output
@ -673,9 +702,24 @@ class SurplusDefaultGeocoding:
f"No suitable location could be geolocated from '{place}'" f"No suitable location could be geolocated from '{place}'"
) )
bounding_box: tuple[float, float, float, float] | None = location.raw.get(
"boundingbox", None
)
if location.raw.get("boundingbox", None) is not None:
_bounding_box = [float(c) for c in location.raw.get("boundingbox", [])]
if len(_bounding_box) == 4:
bounding_box = (
_bounding_box[0],
_bounding_box[1],
_bounding_box[2],
_bounding_box[3],
)
return Latlong( return Latlong(
latitude=location.latitude, latitude=location.latitude,
longitude=location.longitude, longitude=location.longitude,
bounding_box=bounding_box,
) )
def reverser(self, latlong: Latlong, level: int = 18) -> dict[str, Any]: def reverser(self, latlong: Latlong, level: int = 18) -> dict[str, Any]:
@ -906,7 +950,12 @@ def parse_query(behaviour: Behaviour) -> Result[Query]:
split_query = behaviour.query split_query = behaviour.query
if behaviour.debug: if behaviour.debug:
print(f"debug: {split_query=}\ndebug: {original_query=}", file=behaviour.stderr) print(
f"debug: parse_query: {split_query=}\n",
f"debug: parse_query: {original_query=}",
sep="",
file=behaviour.stderr,
)
# not a plus/local code, try to match for latlong or string query # not a plus/local code, try to match for latlong or string query
match split_query: match split_query:
@ -921,12 +970,7 @@ def parse_query(behaviour: Behaviour) -> Result[Query]:
else: # has comma, possibly a latlong coord else: # has comma, possibly a latlong coord
comma_split_single: list[str] = single.split(",") comma_split_single: list[str] = single.split(",")
if len(comma_split_single) > 2: if len(comma_split_single) == 2:
return Result[Query](
LatlongQuery(EMPTY_LATLONG),
error=LatlongParseError("unable to parse latlong coord"),
)
try: # try to type cast query try: # try to type cast query
latitude = float(comma_split_single[0].strip(",")) latitude = float(comma_split_single[0].strip(","))
longitude = float(comma_split_single[-1].strip(",")) longitude = float(comma_split_single[-1].strip(","))
@ -944,6 +988,9 @@ def parse_query(behaviour: Behaviour) -> Result[Query]:
) )
) )
# not a latlong coord, fallback
return Result[Query](StringQuery(original_query))
case [left_single, right_single]: case [left_single, right_single]:
# possibly a: # possibly a:
# space-seperated latlong coord # space-seperated latlong coord
@ -1065,9 +1112,27 @@ def _unique(l: Sequence[str]) -> list[str]:
def _generate_text( def _generate_text(
location: dict[str, Any], behaviour: Behaviour, debug: bool = False location: dict[str, Any],
behaviour: Behaviour,
mode: TextGenerationEnum = TextGenerationEnum.SHAREABLE_TEXT,
debug: bool = False,
) -> str: ) -> str:
"""(internal function) generate shareable text from location dict""" """
(internal function) generate shareable text from location dict
arguments
location: dict[str, Any]
dictionary from geocoding reverser function
behaviour: Behaviour
surplus behaviour
mode: GenerationModeEnum = GenerationModeEnum.SHAREABLE_TEXT
generation mode, defaults to shareable text generation
debug: bool = False
behaviour-seperate debug flag because this function is called twice by
surplus in debug mode, one for debug and one for non-debug output
returns str
"""
def _generate_text_line( def _generate_text_line(
line_number: int, line_number: int,
@ -1083,6 +1148,8 @@ def _generate_text(
line number to prefix with line number to prefix with
line_keys: Sequence[str] line_keys: Sequence[str]
list of keys to .get() from location dict list of keys to .get() from location dict
seperator: str = ", "
seperator to join elements with
filter: Callable[[str], list[bool]] = lambda e: True filter: Callable[[str], list[bool]] = lambda e: True
function that takes in a string and returns a list of bools, used to function that takes in a string and returns a list of bools, used to
filter elements from line_keys. list will be passed to all(). if all filter elements from line_keys. list will be passed to all(). if all
@ -1135,6 +1202,14 @@ def _generate_text(
if key.lower().startswith("iso3166"): if key.lower().startswith("iso3166"):
iso3166_2 = location.get(key, "") iso3166_2 = location.get(key, "")
split_iso3166_2 = [part.upper() for part in iso3166_2.split("-")]
if debug:
print(
f"debug: _generate_text: {split_iso3166_2=}",
file=behaviour.stderr,
)
# skeleton code to allow for changing keys based on iso3166-2 code # skeleton code to allow for changing keys based on iso3166-2 code
st_line0_keys = SHAREABLE_TEXT_LINE_0_KEYS st_line0_keys = SHAREABLE_TEXT_LINE_0_KEYS
st_line1_keys = SHAREABLE_TEXT_LINE_1_KEYS st_line1_keys = SHAREABLE_TEXT_LINE_1_KEYS
@ -1144,12 +1219,32 @@ def _generate_text(
st_line5_keys = SHAREABLE_TEXT_LINE_5_KEYS st_line5_keys = SHAREABLE_TEXT_LINE_5_KEYS
st_line6_keys = SHAREABLE_TEXT_LINE_6_KEYS st_line6_keys = SHAREABLE_TEXT_LINE_6_KEYS
st_names = SHAREABLE_TEXT_NAMES st_names = SHAREABLE_TEXT_NAMES
st_locality: tuple[str, ...] = ()
match iso3166_2.split("-"): match split_iso3166_2:
case _: case ["SG", *_]: # Singapore
pass if debug:
print(
"debug: _generate_text: "
f"using special key arrangements for '{iso3166_2}' (Singapore)",
file=behaviour.stderr,
)
st_locality = SHAREABLE_TEXT_LOCALITY["SG"]
case _: # default
if debug:
print(
"debug: _generate_text: "
f"using default key arrangements for '{iso3166_2}'",
file=behaviour.stderr,
)
st_locality = SHAREABLE_TEXT_LOCALITY["default"]
# start generating text # start generating text
match mode:
case TextGenerationEnum.SHAREABLE_TEXT:
text: list[str] = [] text: list[str] = []
seen_names: list[str] = [ seen_names: list[str] = [
@ -1167,14 +1262,35 @@ def _generate_text(
str(location.get(detail, "")) for detail in st_line6_keys str(location.get(detail, "")) for detail in st_line6_keys
] ]
text.append(_generate_text_line(0, st_line0_keys))
text.append(_generate_text_line(1, st_line1_keys))
text.append(_generate_text_line(2, st_line2_keys))
text.append(_generate_text_line(3, st_line3_keys, seperator=" "))
text.append( text.append(
_generate_text_line( _generate_text_line(
4, line_number=0,
st_line4_keys, line_keys=st_line0_keys,
)
)
text.append(
_generate_text_line(
line_number=1,
line_keys=st_line1_keys,
)
)
text.append(
_generate_text_line(
line_number=2,
line_keys=st_line2_keys,
)
)
text.append(
_generate_text_line(
line_number=3,
line_keys=st_line3_keys,
seperator=" ",
)
)
text.append(
_generate_text_line(
line_number=4,
line_keys=st_line4_keys,
filter=lambda ak: [ filter=lambda ak: [
# everything here should be True if the element is to be kept # everything here should be True if the element is to be kept
ak not in general_global_info, ak not in general_global_info,
@ -1182,11 +1298,32 @@ def _generate_text(
], ],
) )
) )
text.append(_generate_text_line(5, st_line5_keys)) text.append(
text.append(_generate_text_line(6, st_line6_keys)) _generate_text_line(
line_number=5,
line_keys=st_line5_keys,
)
)
text.append(
_generate_text_line(
line_number=6,
line_keys=st_line6_keys,
)
)
return "".join(_unique(text)).rstrip() return "".join(_unique(text)).rstrip()
case TextGenerationEnum.LOCALITY_TEXT:
return _generate_text_line(
line_number=0,
line_keys=st_locality,
)
case _:
raise NotImplementedError(
f"unknown mode '{mode}' (expected a TextGenerationEnum)"
)
def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
""" """
@ -1233,17 +1370,17 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
return Result[str]("", error=latlong_result.error) return Result[str]("", error=latlong_result.error)
if behaviour.debug: if behaviour.debug:
print(f"debug: cli: {latlong_result.get()=}", file=behaviour.stderr) print(f"debug: {latlong_result.get()=}", file=behaviour.stderr)
# reverse location and handle result # reverse location and handle result
try: try:
location: dict[str, Any] = behaviour.reverser(latlong_result.get()) location = behaviour.reverser(latlong_result.get())
except Exception as exc: except Exception as exc:
return Result[str]("", error=exc) return Result[str]("", error=exc)
if behaviour.debug: if behaviour.debug:
print(f"debug: cli: {location=}", file=behaviour.stderr) print(f"debug: {location=}", file=behaviour.stderr)
# generate text # generate text
if behaviour.debug: if behaviour.debug:
@ -1275,7 +1412,7 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
return Result[str]("", error=latlong_query.error) return Result[str]("", error=latlong_query.error)
if behaviour.debug: if behaviour.debug:
print(f"debug: cli: {latlong_query.get()=}", file=behaviour.stderr) print(f"debug: {latlong_query.get()=}", file=behaviour.stderr)
# perform operation # perform operation
try: try:
@ -1293,19 +1430,6 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
if isinstance(query, LocalCodeQuery): if isinstance(query, LocalCodeQuery):
return Result[str](str(query)) return Result[str](str(query))
latlong: Latlong = EMPTY_LATLONG
# if its a plus code, convert to latlong first
if isinstance(query, PlusCodeQuery):
pluscode_latlong_result = PlusCodeQuery.to_lat_long_coord(
query, geocoder=behaviour.geocoder
)
if not pluscode_latlong_result:
return Result[str]("", error=pluscode_latlong_result.error)
latlong = pluscode_latlong_result.get()
# get latlong and handle result # get latlong and handle result
latlong_result = query.to_lat_long_coord(geocoder=behaviour.geocoder) latlong_result = query.to_lat_long_coord(geocoder=behaviour.geocoder)
@ -1313,21 +1437,121 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
return Result[str]("", error=latlong_result.error) return Result[str]("", error=latlong_result.error)
if behaviour.debug: if behaviour.debug:
print(f"debug: cli: {latlong_result.get()=}", file=behaviour.stderr) print(f"debug: {latlong_result.get()=}", file=behaviour.stderr)
latlong = latlong_result.get() query_latlong = latlong_result.get()
# perform operation # reverse location and handle result
# TODO: https://github.com/markjoshwel/surplus/issues/18 try:
# https://github.com/google/open-location-code/wiki/Guidance-for-shortening-codes location = behaviour.reverser(
query_latlong, level=LOCALITY_GEOCODER_LEVEL
return Result[str](
text,
error=NotImplementedError(
"converting to Plus Code is not implemented yet"
),
) )
except Exception as exc:
return Result[str]("", error=exc)
if behaviour.debug:
print(f"debug: {location=}", file=behaviour.stderr)
# generate locality portion of local code
if behaviour.debug:
print(
_generate_text(
location=location,
behaviour=behaviour,
mode=TextGenerationEnum.LOCALITY_TEXT,
debug=behaviour.debug,
).strip()
)
portion_locality: str = _generate_text(
location=location,
behaviour=behaviour,
mode=TextGenerationEnum.LOCALITY_TEXT,
).strip()
# reverse locality portion
try:
locality_latlong: Latlong = behaviour.geocoder(portion_locality)
# check now if bounding_box is set and valid
assert locality_latlong.bounding_box is not None, (
"(shortening) geocoder-returned latlong has .bounding_box=None"
f" - {locality_latlong.bounding_box}"
)
assert len(locality_latlong.bounding_box) == 4, (
"(shortening) geocoder-returned latlong has len(.bounding_box) < 4"
f" - {locality_latlong.bounding_box}"
)
assert all([type(c) == float for c in locality_latlong.bounding_box]), (
"(shortening) geocoder-returned latlong has non-float in .bounding_box"
f" - {locality_latlong.bounding_box}"
)
except Exception as exc:
return Result[str]("", error=exc)
plus_code = _PlusCode_encode(
lat=query_latlong.latitude,
lon=query_latlong.longitude,
)
# https://github.com/google/open-location-code/wiki/Guidance-for-shortening-codes
check1 = (
# The center point of the feature is within 0.4 degrees latitude and 0.4
# degrees longitude
(
(query_latlong.latitude - 0.4)
<= locality_latlong.latitude
<= (query_latlong.latitude + 0.4)
),
(
(query_latlong.longitude - 0.4)
<= locality_latlong.longitude
<= (query_latlong.longitude + 0.4)
),
# The bounding box of the feature is less than 0.8 degrees high and wide.
abs(locality_latlong.bounding_box[0] - locality_latlong.bounding_box[1])
< 0.8,
abs(locality_latlong.bounding_box[2] - locality_latlong.bounding_box[3])
< 0.8,
)
check2 = (
# The center point of the feature is within 0.4 degrees latitude and 0.4
# degrees longitude"
(
(query_latlong.latitude - 8)
<= locality_latlong.latitude
<= (query_latlong.latitude + 8)
),
(
(query_latlong.longitude - 8)
<= locality_latlong.longitude
<= (query_latlong.longitude + 8)
),
# The bounding box of the feature is less than 0.8 degrees high and wide.
abs(locality_latlong.bounding_box[0] - locality_latlong.bounding_box[1])
< 16,
abs(locality_latlong.bounding_box[2] - locality_latlong.bounding_box[3])
< 16,
)
if check1:
return Result[str](f"{plus_code[4:]} {portion_locality}")
elif check2:
return Result[str](f"{plus_code[2:]} {portion_locality}")
print(
"info: could not determine a suitable geographical feature to use as "
"locality for shortening. full plus code is returned.",
file=behaviour.stderr,
)
return Result[str](plus_code)
case ConversionResultTypeEnum.LATLONG: case ConversionResultTypeEnum.LATLONG:
# return the latlong if already given a latlong # return the latlong if already given a latlong
if isinstance(query, LatlongQuery): if isinstance(query, LatlongQuery):
@ -1340,7 +1564,7 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
return Result[str]("", error=latlong_result.error) return Result[str]("", error=latlong_result.error)
if behaviour.debug: if behaviour.debug:
print(f"debug: cli: {latlong_result.get()=}", file=behaviour.stderr) print(f"debug: {latlong_result.get()=}", file=behaviour.stderr)
# perform operation # perform operation
return Result[str](str(latlong_result.get())) return Result[str](str(latlong_result.get()))