Add reverse geocoding

This commit is contained in:
Liam Steckler 2024-01-14 12:27:02 -08:00
parent d4a4d5a18c
commit 03f05fe44c

134
scl.py
View file

@ -1,8 +1,10 @@
import io import io
import math
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
import requests import requests
import yaml
from mastodon import Mastodon from mastodon import Mastodon
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from sqlalchemy import create_engine, select from sqlalchemy import create_engine, select
@ -20,11 +22,15 @@ except requests.JSONDecodeError:
print("JSON could not be loaded from SCL API") print("JSON could not be loaded from SCL API")
raise raise
mastodon = Mastodon(access_token="scl_bot_mastodon.secret") config = yaml.safe_load(open("config.yml"))
stadiamaps_api_key = config["stadiamaps"]["api_key"]
with open("stadiamaps_api_key.secret", "r+") as stadiamaps_api_key_file: nominatim_url = config["nominatim"]["api_base_url"]
# Reading from a file mastodon = Mastodon(
stadiamaps_api_key = stadiamaps_api_key_file.read() 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): 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 # 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") print("Outage is small, will not post")
continue continue
print("Existing record not found") 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: try:
map = AttribStaticMap( map = AttribStaticMap(
@ -212,10 +203,92 @@ Cause: {}
) )
map.add_polygon(polygon) map.add_polygon(polygon)
map_image = map.render() 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: with io.BytesIO() as map_image_file:
map_image.save(map_image_file, format="PNG", optimize=True) map_image.save(map_image_file, format="PNG", optimize=True)
map_media_post = mastodon.media_post( 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: except Exception as e:
@ -225,6 +298,25 @@ Cause: {}
) )
map_media_post = None 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( mastodon_post_result = mastodon.status_post(
status=post_text, status=post_text,
media_ids=map_media_post, media_ids=map_media_post,