Split map image generation into a new function

This commit is contained in:
Liam Steckler 2025-07-09 19:02:17 -07:00
parent 3c40e695a7
commit 000acf19e3

209
scl.py
View file

@ -1,7 +1,7 @@
import io import io
import re import re
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, Tuple
import mastodon import mastodon
import requests import requests
@ -9,6 +9,7 @@ import shapely
import staticmap import staticmap
import yaml import yaml
from mastodon import Mastodon from mastodon import Mastodon
from mastodon.return_types import MediaAttachment
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from shapely import Geometry from shapely import Geometry
from sqlalchemy import create_engine, select from sqlalchemy import create_engine, select
@ -83,7 +84,6 @@ class AttribStaticMap(staticmap.StaticMap, object):
textSize = fnt.getbbox(self.attribution) textSize = fnt.getbbox(self.attribution)
textPosition = (image.size[0] - textSize[2], image.size[1] - textSize[3]) textPosition = (image.size[0] - textSize[2], image.size[1] - textSize[3])
offset = 2 offset = 2
options = {"fill": (255, 255, 255, 180)}
d.rectangle( d.rectangle(
[ [
(textPosition[0] - (2 * offset), textPosition[1] - (2 * offset)), (textPosition[0] - (2 * offset), textPosition[1] - (2 * offset)),
@ -152,6 +152,108 @@ def get_hashtag_string(event) -> str:
return hashtag_string 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( def do_initial_post(
event, event,
event_class, event_class,
@ -160,112 +262,19 @@ def do_initial_post(
outage_geometries: shapely.MultiPolygon, outage_geometries: shapely.MultiPolygon,
) -> dict[str, str | None]: ) -> dict[str, str | None]:
post_id = None post_id = None
map_media_post_id = None map_media_post = None
# Fallback location from the SCL API in case one couldn't be reverse geocoded area_text = str()
area_text = event["city"]
try: try:
map = AttribStaticMap( map_media_post, area_text = generate_post_map_image(
1024, event, event_class, outage_geometries
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.getvalue(),
mime_type="image/webp",
description=alt_text,
)
map_media_post_id = map_media_post["id"]
except Exception as e: except Exception as e:
print(e) print(e)
print( print(
"Ran into an issue with generating/uploading the map. Will post without it." "Ran into an issue with generating/uploading the map. Will post without it."
) )
map_media_post = None
hashtag_string = get_hashtag_string(event) hashtag_string = get_hashtag_string(event)
est_restoration_post_text = str() est_restoration_post_text = str()