From 000acf19e380df1a4ddda9365a80ed811e838a39 Mon Sep 17 00:00:00 2001 From: Liam Steckler Date: Wed, 9 Jul 2025 19:02:17 -0700 Subject: [PATCH] Split map image generation into a new function --- scl.py | 209 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 109 insertions(+), 100 deletions(-) diff --git a/scl.py b/scl.py index a9c8520..7701d42 100644 --- a/scl.py +++ b/scl.py @@ -1,7 +1,7 @@ import io import re from datetime import datetime -from typing import Optional +from typing import Optional, Tuple import mastodon import requests @@ -9,6 +9,7 @@ import shapely import staticmap import yaml from mastodon import Mastodon +from mastodon.return_types import MediaAttachment from PIL import Image, ImageDraw, ImageFont from shapely import Geometry from sqlalchemy import create_engine, select @@ -83,7 +84,6 @@ class AttribStaticMap(staticmap.StaticMap, object): textSize = fnt.getbbox(self.attribution) textPosition = (image.size[0] - textSize[2], image.size[1] - textSize[3]) offset = 2 - options = {"fill": (255, 255, 255, 180)} d.rectangle( [ (textPosition[0] - (2 * offset), textPosition[1] - (2 * offset)), @@ -152,6 +152,108 @@ def get_hashtag_string(event) -> str: return hashtag_string +def generate_post_map_image( + event, event_class, outage_geometries: shapely.MultiPolygon +) -> Tuple[MediaAttachment, str]: + # Fallback location from the SCL API in case one couldn't be reverse geocoded + area_text = event["city"] + map = AttribStaticMap( + 1024, + 1024, + url_template=osm_url_template, + tile_size=512, + ) + assert event["polygons"]["type"] == "polygon" + for ring in event["polygons"]["rings"]: + polygon = staticmap.Polygon( + ring, + # Appending 7F to the fill_color makes it 50% transparent + fill_color="{}7F".format(event_class["outage_color"]), + outline_color=event_class["outage_color"], + simplify=True, + ) + map.add_polygon(polygon) + + try: + outage_center: shapely.Point = outage_geometries.centroid + + assert outage_center.geom_type == "Point" + # Check to make sure the calculated lat and lon are sane enough + # NW Corner + assert outage_center.y < 48 and outage_center.x > -122.6 + # SE Corner + assert outage_center.y > 47.2 and outage_center.x < -122 + + # 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=outage_center.y, + lon=outage_center.x, + ) + geocode_response = requests.get(geocode_url, headers=REQUESTS_HEADERS) + try: + geocode = geocode_response.json() + except requests.JSONDecodeError: + print("JSON could not be loaded from nominatim API") + raise + + city = geocode["features"][0]["properties"]["geocoding"]["city"] + street = geocode["features"][0]["properties"]["geocoding"]["name"] + event["geoloc_city"] = city + + if city != "Seattle": + city_not_seattle_text = " of {}".format(city) + else: + city_not_seattle_text = "" + + if ( + "locality" in geocode["features"][0]["properties"]["geocoding"] + and event_class["size"] != "Large" + ): + locality = geocode["features"][0]["properties"]["geocoding"]["locality"] + 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) + event["neighborhood"] = locality + elif "district" in geocode["features"][0]["properties"]["geocoding"]: + district = geocode["features"][0]["properties"]["geocoding"]["district"] + alt_text = "A map showing the location of the outage, centered around {} in the {} area{}.".format( + street, + district, + city_not_seattle_text, + ) + area_text = "the {} area{}".format( + district, + city_not_seattle_text, + ) + event["neighborhood"] = district + else: + alt_text = "A map showing the location of the outage, centered around {} in {}.".format( + street, + city, + ) + area_text = city + except Exception: + alt_text = "A map showing the location of the outage." + + map_image = map.render() + + with io.BytesIO() as map_image_file: + map_image.save(map_image_file, format="WebP", method=6) + map_media_post = mastodon_client.media_post( + map_image_file, + mime_type="image/webp", + description=alt_text, + ) + return (map_media_post, area_text) + + def do_initial_post( event, event_class, @@ -160,112 +262,19 @@ def do_initial_post( outage_geometries: shapely.MultiPolygon, ) -> dict[str, str | None]: post_id = None - map_media_post_id = None - # Fallback location from the SCL API in case one couldn't be reverse geocoded - area_text = event["city"] + map_media_post = None + area_text = str() + try: - map = AttribStaticMap( - 1024, - 1024, - url_template=osm_url_template, - tile_size=512, + map_media_post, area_text = generate_post_map_image( + event, event_class, outage_geometries ) - assert event["polygons"]["type"] == "polygon" - for ring in event["polygons"]["rings"]: - polygon = staticmap.Polygon( - ring, - # Appending 7F to the fill_color makes it 50% transparent - fill_color="{}7F".format(event_class["outage_color"]), - outline_color=event_class["outage_color"], - simplify=True, - ) - map.add_polygon(polygon) - - try: - outage_center: shapely.Point = outage_geometries.centroid - - assert outage_center.geom_type == "Point" - # Check to make sure the calculated lat and lon are sane enough - # NW Corner - assert outage_center.y < 48 and outage_center.x > -122.6 - # SE Corner - assert outage_center.y > 47.2 and outage_center.x < -122 - - # 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=outage_center.y, - lon=outage_center.x, - ) - geocode_response = requests.get(geocode_url, headers=REQUESTS_HEADERS) - try: - geocode = geocode_response.json() - except requests.JSONDecodeError: - print("JSON could not be loaded from nominatim API") - raise - - city = geocode["features"][0]["properties"]["geocoding"]["city"] - street = geocode["features"][0]["properties"]["geocoding"]["name"] - event["geoloc_city"] = city - - if city != "Seattle": - city_not_seattle_text = " of {}".format(city) - else: - city_not_seattle_text = "" - - if ( - "locality" in geocode["features"][0]["properties"]["geocoding"] - and event_class["size"] != "Large" - ): - locality = geocode["features"][0]["properties"]["geocoding"]["locality"] - 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) - event["neighborhood"] = locality - elif "district" in geocode["features"][0]["properties"]["geocoding"]: - district = geocode["features"][0]["properties"]["geocoding"]["district"] - alt_text = "A map showing the location of the outage, centered around {} in the {} area{}.".format( - street, - district, - city_not_seattle_text, - ) - area_text = "the {} area{}".format( - district, - city_not_seattle_text, - ) - event["neighborhood"] = district - else: - alt_text = "A map showing the location of the outage, centered around {} in {}.".format( - street, - city, - ) - area_text = city - except Exception: - alt_text = "A map showing the location of the outage." - - map_image = map.render() - - with io.BytesIO() as map_image_file: - map_image.save(map_image_file, format="WebP", method=6) - map_media_post = mastodon_client.media_post( - map_image_file.getvalue(), - mime_type="image/webp", - description=alt_text, - ) - map_media_post_id = map_media_post["id"] except Exception as e: print(e) print( "Ran into an issue with generating/uploading the map. Will post without it." ) - map_media_post = None hashtag_string = get_hashtag_string(event) est_restoration_post_text = str()