From a9e26c89165418d16ec58772232ef08e7a6425f3 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Wed, 6 Sep 2023 17:39:53 +0000 Subject: [PATCH] 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 --- playground.ipynb | 323 ++++++++++++++++++++++++----------- surplus/__init__.py | 1 + surplus/surplus.py | 400 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 536 insertions(+), 188 deletions(-) diff --git a/playground.ipynb b/playground.ipynb index 79e6cd2..fa9a3f5 100644 --- a/playground.ipynb +++ b/playground.ipynb @@ -42,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -356,166 +356,289 @@ "cell_type": "markdown", "metadata": {}, "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", "metadata": {}, "source": [ - "### testing rate-limited default geocoding functions" + "### testing rate-limited and cached default geocoding functions" ] }, { "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": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "1.33318835, 103.77461234638255\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" + "1\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": [ - "from surplus import SurplusGeocoderProtocol, SurplusReverserProtocol\n", + "from timeit import timeit\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", - "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", - "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", "metadata": {}, "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", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "# TODO\n", - "\n", - "test1 = LocalCodeQuery(\"9R3J+R9\", \"Singapore\")\n", - "test2 = LocalCodeQuery(\"G227+XF\", \"St Lucia, Queensland, Australia\")\n", - "\n", "level = 13" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "{'suburb': 'Bishan',\n", - " 'city': 'Singapore',\n", - " 'county': 'Central',\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" + "name": "stdout", + "output_type": "stream", + "text": [ + "St Lucia, St Lucia, Queensland, Australia\n", + "Austin, Travis County, Texas, United States\n" + ] } ], "source": [ "(\n", - " response := geocoding.reverser(\n", - " test1.to_lat_long_coord(geocoding.geocoder).get(), level=level\n", + " au_response := geocoding.reverser(\n", + " (\n", + " au_target := (\n", + " LocalCodeQuery(\n", + " \"G227+XF\", \"St Lucia, Queensland, Australia\"\n", + " ).to_lat_long_coord(geocoding.geocoder)\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", - "execution_count": null, + "execution_count": 13, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "{'suburb': 'St Lucia',\n", - " 'city_district': 'St Lucia',\n", - " 'city': 'Brisbane City',\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", - " '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", - " 'address': {'suburb': 'St Lucia',\n", - " 'city_district': 'St Lucia',\n", - " 'city': 'Brisbane City',\n", - " 'state': 'Queensland',\n", - " 'ISO3166-2-lvl4': 'AU-QLD',\n", - " 'postcode': '4072',\n", - " 'country': 'Australia',\n", - " 'country_code': 'au'},\n", - " 'boundingbox': ['-27.5187362', '-27.4787362', '152.9881642', '153.0281642']},\n", - " 'latitude': -27.4987362,\n", - " 'longitude': 153.0081642}" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{'addresstype': 'suburb',\n", + " 'boundingbox': ['-27.5187362', '-27.4787362', '152.9881642', '153.0281642'],\n", + " 'class': 'place',\n", + " 'display_name': 'St Lucia, Brisbane City, Queensland, 4072, Australia',\n", + " 'importance': 0.27501,\n", + " 'lat': '-27.4987362',\n", + " 'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. '\n", + " 'http://osm.org/copyright',\n", + " 'lon': '153.0081642',\n", + " 'name': 'St Lucia',\n", + " 'osm_id': 88800268,\n", + " 'osm_type': 'node',\n", + " 'place_id': 54477898,\n", + " 'place_rank': 19,\n", + " 'type': 'suburb'}\n", + "\n", + "Latlong(latitude=-27.4987362, longitude=153.0081642, bounding_box=[-27.5187362, -27.4787362, 152.9881642, 153.0281642])\n" + ] } ], "source": [ - "(\n", - " response := geocoding.reverser(\n", - " test2.to_lat_long_coord(geocoding.geocoder).get(), level=level\n", + "from geopy.geocoders import Nominatim\n", + "from pprint import pprint\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 \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", - ")" + " print(plus_code)" ] }, { diff --git a/surplus/__init__.py b/surplus/__init__.py index 6b26542..18ae3e2 100644 --- a/surplus/__init__.py +++ b/surplus/__init__.py @@ -48,6 +48,7 @@ from .surplus import ( SHAREABLE_TEXT_LINE_4_KEYS, SHAREABLE_TEXT_LINE_5_KEYS, SHAREABLE_TEXT_LINE_6_KEYS, + SHAREABLE_TEXT_LOCALITY, SHAREABLE_TEXT_NAMES, VERSION, VERSION_SUFFIX, diff --git a/surplus/surplus.py b/surplus/surplus.py index 4d57c62..eff63e4 100644 --- a/surplus/surplus.py +++ b/surplus/surplus.py @@ -136,6 +136,13 @@ SHAREABLE_TEXT_NAMES: Final[tuple[str, ...]] = ( + SHAREABLE_TEXT_LINE_2_KEYS + ("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 @@ -169,6 +176,19 @@ class EmptyQueryError(SurplusException): # 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): """ enum representing what the result type of conversion should be @@ -273,11 +293,16 @@ class Result(NamedTuple, Generic[ResultType]): class Latlong(NamedTuple): """ - typing.NamedTuple representing a latitude-longitude coordinate pair + typing.NamedTuple representing a latitude-longitude coordinate pair and any extra + information arguments latitude: 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 def __str__(self) -> str: ... @@ -285,6 +310,7 @@ class Latlong(NamedTuple): latitude: float longitude: float + bounding_box: tuple[float, float, float, float] | None = None 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. - function can be functools.lru_cache()-wrapped if the geocoding service asks for - caching + **the function returned MUST supply a `bounding_box` attribute to the to-be-returned + [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 """ @@ -346,8 +375,8 @@ class SurplusReverserProtocol(Protocol): 'raw': {...}, } - function can be functools.lru_cache()-wrapped if the geocoding service asks for - caching + function can and should be at minimum functools.lru_cache()-wrapped if the geocoding + service asks for caching exceptions are handled by the caller, 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}'" ) + 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( latitude=location.latitude, longitude=location.longitude, + bounding_box=bounding_box, ) 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 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 match split_query: @@ -921,28 +970,26 @@ def parse_query(behaviour: Behaviour) -> Result[Query]: else: # has comma, possibly a latlong coord comma_split_single: list[str] = single.split(",") - if len(comma_split_single) > 2: - return Result[Query]( - LatlongQuery(EMPTY_LATLONG), - error=LatlongParseError("unable to parse latlong coord"), - ) + if len(comma_split_single) == 2: + try: # try to type cast query + latitude = float(comma_split_single[0].strip(",")) + longitude = float(comma_split_single[-1].strip(",")) - try: # try to type cast query - latitude = float(comma_split_single[0].strip(",")) - longitude = float(comma_split_single[-1].strip(",")) + except ValueError: # not a latlong coord, fallback + return Result[Query](StringQuery(single)) - except ValueError: # not a latlong coord, fallback - return Result[Query](StringQuery(single)) - - else: # are floats, so is a latlong coord - return Result[Query]( - LatlongQuery( - Latlong( - latitude=latitude, - longitude=longitude, + else: # are floats, so is a latlong coord + return Result[Query]( + LatlongQuery( + Latlong( + latitude=latitude, + longitude=longitude, + ) ) ) - ) + + # not a latlong coord, fallback + return Result[Query](StringQuery(original_query)) case [left_single, right_single]: # possibly a: @@ -1065,9 +1112,27 @@ def _unique(l: Sequence[str]) -> list[str]: 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: - """(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( line_number: int, @@ -1083,6 +1148,8 @@ def _generate_text( line number to prefix with line_keys: Sequence[str] list of keys to .get() from location dict + seperator: str = ", " + seperator to join elements with filter: Callable[[str], list[bool]] = lambda e: True 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 @@ -1135,6 +1202,14 @@ def _generate_text( if key.lower().startswith("iso3166"): 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 st_line0_keys = SHAREABLE_TEXT_LINE_0_KEYS st_line1_keys = SHAREABLE_TEXT_LINE_1_KEYS @@ -1144,48 +1219,110 @@ def _generate_text( st_line5_keys = SHAREABLE_TEXT_LINE_5_KEYS st_line6_keys = SHAREABLE_TEXT_LINE_6_KEYS st_names = SHAREABLE_TEXT_NAMES + st_locality: tuple[str, ...] = () - match iso3166_2.split("-"): - case _: - pass + match split_iso3166_2: + case ["SG", *_]: # Singapore + 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 - text: list[str] = [] + match mode: + case TextGenerationEnum.SHAREABLE_TEXT: + text: list[str] = [] - seen_names: list[str] = [ - detail - for detail in _unique( - [str(location.get(location_key, "")) for location_key in st_names] - ) - if detail != "" - ] + seen_names: list[str] = [ + detail + for detail in _unique( + [str(location.get(location_key, "")) for location_key in st_names] + ) + if detail != "" + ] - if debug: - print(f"debug: _generate_text: {seen_names=}", file=behaviour.stderr) + if debug: + print(f"debug: _generate_text: {seen_names=}", file=behaviour.stderr) - general_global_info: list[str] = [ - str(location.get(detail, "")) for detail in st_line6_keys - ] + general_global_info: list[str] = [ + 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( - _generate_text_line( - 4, - st_line4_keys, - filter=lambda ak: [ - # everything here should be True if the element is to be kept - ak not in general_global_info, - not any(True if (ak in sn) else False for sn in seen_names), - ], - ) - ) - text.append(_generate_text_line(5, st_line5_keys)) - text.append(_generate_text_line(6, st_line6_keys)) + text.append( + _generate_text_line( + line_number=0, + 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: [ + # everything here should be True if the element is to be kept + ak not in general_global_info, + not any(True if (ak in sn) else False for sn in seen_names), + ], + ) + ) + text.append( + _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]: @@ -1233,17 +1370,17 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: return Result[str]("", error=latlong_result.error) 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 try: - location: dict[str, Any] = behaviour.reverser(latlong_result.get()) + location = behaviour.reverser(latlong_result.get()) except Exception as exc: return Result[str]("", error=exc) if behaviour.debug: - print(f"debug: cli: {location=}", file=behaviour.stderr) + print(f"debug: {location=}", file=behaviour.stderr) # generate text if behaviour.debug: @@ -1275,7 +1412,7 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: return Result[str]("", error=latlong_query.error) if behaviour.debug: - print(f"debug: cli: {latlong_query.get()=}", file=behaviour.stderr) + print(f"debug: {latlong_query.get()=}", file=behaviour.stderr) # perform operation try: @@ -1293,19 +1430,6 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: if isinstance(query, LocalCodeQuery): 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 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) 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 - # TODO: https://github.com/markjoshwel/surplus/issues/18 - # https://github.com/google/open-location-code/wiki/Guidance-for-shortening-codes + # reverse location and handle result + try: + 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: # return the latlong if already given a latlong if isinstance(query, LatlongQuery): @@ -1340,7 +1564,7 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: return Result[str]("", error=latlong_result.error) if behaviour.debug: - print(f"debug: cli: {latlong_result.get()=}", file=behaviour.stderr) + print(f"debug: {latlong_result.get()=}", file=behaviour.stderr) # perform operation return Result[str](str(latlong_result.get()))