From 03f05fe44cefb8e0d4b1289dd47ffd921af7c449 Mon Sep 17 00:00:00 2001 From: Liam Steckler Date: Sun, 14 Jan 2024 12:27:02 -0800 Subject: [PATCH] Add reverse geocoding --- scl.py | 134 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 21 deletions(-) diff --git a/scl.py b/scl.py index 7bbc87a..3514a0a 100644 --- a/scl.py +++ b/scl.py @@ -1,8 +1,10 @@ import io +import math from datetime import datetime from typing import Optional import requests +import yaml from mastodon import Mastodon from PIL import Image, ImageDraw, ImageFont from sqlalchemy import create_engine, select @@ -20,11 +22,15 @@ except requests.JSONDecodeError: print("JSON could not be loaded from SCL API") raise -mastodon = Mastodon(access_token="scl_bot_mastodon.secret") - -with open("stadiamaps_api_key.secret", "r+") as stadiamaps_api_key_file: - # Reading from a file - stadiamaps_api_key = stadiamaps_api_key_file.read() +config = yaml.safe_load(open("config.yml")) +stadiamaps_api_key = config["stadiamaps"]["api_key"] +nominatim_url = config["nominatim"]["api_base_url"] +mastodon = Mastodon( + client_id=config["mastodon"]["client_id"], + client_secret=config["mastodon"]["client_secret"], + access_token=config["mastodon"]["access_token"], + api_base_url=config["mastodon"]["api_base_url"], +) class AttribStaticMap(StaticMap, object): @@ -181,22 +187,7 @@ with Session(engine) as session: # If the outage becomes medium/large, it'll then be posted as a new outage on the next run print("Outage is small, will not post") continue - print("Existing record not found") - post_text = """Seattle City Light is reporting a {} outage in {}. - -Start Date: {} -Est. Restoration: {} -Cause: {} - -{}""".format( - outage_size.lower(), - event["city"], - start_time.strftime(post_datetime_format), - estimated_restoration_time.strftime(post_datetime_format), - event["cause"], - hashtag_string, - ) try: map = AttribStaticMap( @@ -212,10 +203,92 @@ Cause: {} ) map.add_polygon(polygon) map_image = map.render() + + try: + + def num2deg(xtile, ytile, zoom): + n = 1 << zoom + lon_deg = xtile / n * 360.0 - 180.0 + lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) + lat_deg = math.degrees(lat_rad) + return lat_deg, lon_deg + + center_lat_lon = num2deg(map.x_center, map.y_center, map.zoom) + + # Zoom level 17 ensures that we won't get any building/POI names, just street names + geocode_url = "{nominatim_url}/reverse?lat={lat}&lon={lon}&format=geocodejson&zoom=17".format( + nominatim_url=nominatim_url, + lat=center_lat_lon[0], + lon=center_lat_lon[1], + ) + geocode_headers = {"User-Agent": "seattlecitylight-mastodon-bot"} + geocode_response = requests.get( + geocode_url, headers=geocode_headers + ) + try: + geocode = geocode_response.json() + except requests.JSONDecodeError: + print("JSON could not be loaded from nominatim API") + raise + + if ( + geocode["features"][0]["properties"]["geocoding"]["city"] + != "Seattle" + ): + city_not_seattle_text = " of {}".format( + geocode["features"][0]["properties"]["geocoding"]["city"] + ) + else: + city_not_seattle_text = "" + + street = geocode["features"][0]["properties"]["geocoding"]["name"] + + if "locality" in geocode["features"][0]["properties"]["geocoding"]: + locality = geocode["features"][0]["properties"]["geocoding"] + if locality == "Uptown": + locality = "Lower Queen Anne" + + alt_text = "A map showing the location of the outage, centered around {} in the {} area{}.".format( + street, + locality, + city_not_seattle_text, + ) + area_text = "the {} area{}".format( + locality, city_not_seattle_text + ) + elif ( + "district" in geocode["features"][0]["properties"]["geocoding"] + ): + alt_text = "A map showing the location of the outage, centered around {} in the {} area{}.".format( + street, + geocode["features"][0]["properties"]["geocoding"][ + "district" + ], + city_not_seattle_text, + ) + area_text = "the {} area{}".format( + geocode["features"][0]["properties"]["geocoding"][ + "district" + ], + city_not_seattle_text, + ) + else: + alt_text = "A map showing the location of the outage, centered around {} in {}.".format( + street, + geocode["features"][0]["properties"]["geocoding"]["city"], + ) + area_text = geocode["features"][0]["properties"]["geocoding"][ + "city" + ] + except Exception: + alt_text = "A map showing the location of the outage." + with io.BytesIO() as map_image_file: map_image.save(map_image_file, format="PNG", optimize=True) map_media_post = mastodon.media_post( - map_image_file.getvalue(), mime_type="image/png" + map_image_file.getvalue(), + mime_type="image/png", + description=alt_text, ) except Exception as e: @@ -225,6 +298,25 @@ Cause: {} ) map_media_post = None + # Fallback location from the SCL API in case one couldn't be reverse geocoded + if not area_text: + area_text = event["city"] + + post_text = """Seattle City Light is reporting a {} outage in {}. + +Start Date: {} +Est. Restoration: {} +Cause: {} + +{}""".format( + outage_size.lower(), + area_text, + start_time.strftime(post_datetime_format), + estimated_restoration_time.strftime(post_datetime_format), + event["cause"], + hashtag_string, + ) + mastodon_post_result = mastodon.status_post( status=post_text, media_ids=map_media_post,