Fix center of outage calculation #24

Merged
buckbanzai merged 3 commits from fix-map-extent into main 2024-04-20 15:03:16 -07:00
4 changed files with 54 additions and 20 deletions

View file

@ -2,7 +2,7 @@ when:
branch: main branch: main
steps: steps:
- name: lint - name: lint
image: python:3-alpine image: python:3-slim
commands: commands:
- python -m pip install --upgrade pip - python -m pip install --upgrade pip
- python -m pip install -r requirements.txt - python -m pip install -r requirements.txt

11
geospatial.py Normal file
View file

@ -0,0 +1,11 @@
from shapely import MultiPolygon, Polygon
def convert_outage_geometry(event) -> MultiPolygon:
assert event["polygons"]["type"] == "polygon"
assert event["polygons"]["hasZ"] is False
assert event["polygons"]["hasM"] is False
polygon_list = []
for ring in event["polygons"]["rings"]:
polygon_list.append(Polygon(ring))
return MultiPolygon(polygon_list)

View file

@ -4,12 +4,15 @@ charset-normalizer==3.3.2
decorator==5.1.1 decorator==5.1.1
greenlet==3.0.3 greenlet==3.0.3
idna==3.6 idna==3.6
install==1.3.5
Mastodon.py==1.8.1 Mastodon.py==1.8.1
numpy==1.26.4
pillow==10.2.0 pillow==10.2.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-magic==0.4.27 python-magic==0.4.27
PyYAML==6.0.1 PyYAML==6.0.1
requests==2.31.0 requests==2.31.0
shapely==2.0.3
six==1.16.0 six==1.16.0
SQLAlchemy==2.0.28 SQLAlchemy==2.0.28
staticmap==0.5.7 staticmap==0.5.7

58
scl.py
View file

@ -1,17 +1,19 @@
import io import io
import math
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
import mastodon import mastodon
import requests import requests
import shapely
import yaml 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
from sqlalchemy.exc import NoResultFound from sqlalchemy.exc import NoResultFound
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
from staticmap import Polygon, StaticMap from staticmap import CircleMarker, Polygon, StaticMap
from geospatial import convert_outage_geometry
post_datetime_format = "%b %e %l:%M %p" post_datetime_format = "%b %e %l:%M %p"
@ -104,7 +106,11 @@ def get_hashtag_string(event) -> str:
def do_initial_post( def do_initial_post(
event, event_class, start_time: datetime, estimated_restoration_time: datetime event,
event_class,
start_time: datetime,
estimated_restoration_time: datetime,
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_id = None
@ -127,30 +133,32 @@ def do_initial_post(
simplify=True, simplify=True,
) )
map.add_polygon(polygon) map.add_polygon(polygon)
map_image = map.render()
try: try:
outage_center: shapely.Point = outage_geometries.centroid
def num2deg(xtile, ytile, zoom): assert outage_center.geom_type == "Point"
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)
# Check to make sure the calculated lat and lon are sane enough # Check to make sure the calculated lat and lon are sane enough
# NW Corner # NW Corner
assert center_lat_lon[0] < 48 and center_lat_lon[1] > -122.6 assert outage_center.y < 48 and outage_center.x > -122.6
# SE Corner # SE Corner
assert center_lat_lon[0] > 47.2 and center_lat_lon[1] < -122 assert outage_center.y > 47.2 and outage_center.x < -122
marker_outline = CircleMarker(
(outage_center.x, outage_center.y), "white", 18
)
marker = CircleMarker(
(outage_center.x, outage_center.y), event_class["outage_color"], 12
)
map.add_marker(marker_outline)
map.add_marker(marker)
# Zoom level 17 ensures that we won't get any building/POI names, just street names # 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( geocode_url = "{nominatim_url}/reverse?lat={lat}&lon={lon}&format=geocodejson&zoom=17".format(
nominatim_url=nominatim_url, nominatim_url=nominatim_url,
lat=center_lat_lon[0], lat=outage_center.y,
lon=center_lat_lon[1], lon=outage_center.x,
) )
geocode_headers = {"User-Agent": "seattlecitylight-mastodon-bot"} geocode_headers = {"User-Agent": "seattlecitylight-mastodon-bot"}
geocode_response = requests.get(geocode_url, headers=geocode_headers) geocode_response = requests.get(geocode_url, headers=geocode_headers)
@ -202,6 +210,8 @@ def do_initial_post(
except Exception: except Exception:
alt_text = "A map showing the location of the outage." alt_text = "A map showing the location of the outage."
map_image = map.render()
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_client.media_post( map_media_post = mastodon_client.media_post(
@ -297,6 +307,8 @@ with Session(engine) as session:
else: else:
status = None status = None
outage_geometries = convert_outage_geometry(event)
try: try:
hashtag_string = get_hashtag_string(event) hashtag_string = get_hashtag_string(event)
existing_record = lookup_result.one() existing_record = lookup_result.one()
@ -361,7 +373,11 @@ with Session(engine) as session:
"Posting an event that grew above the threshold required to post" "Posting an event that grew above the threshold required to post"
) )
initial_post_result = do_initial_post( initial_post_result = do_initial_post(
event, event_class, start_time, estimated_restoration_time event,
event_class,
start_time,
estimated_restoration_time,
outage_geometries,
) )
existing_record.initial_post_id = initial_post_result["post_id"] existing_record.initial_post_id = initial_post_result["post_id"]
existing_record.most_recent_post_id = initial_post_result["post_id"] existing_record.most_recent_post_id = initial_post_result["post_id"]
@ -382,7 +398,11 @@ with Session(engine) as session:
) )
else: else:
initial_post_result = do_initial_post( initial_post_result = do_initial_post(
event, event_class, start_time, estimated_restoration_time event,
event_class,
start_time,
estimated_restoration_time,
outage_geometries,
) )
post_id = initial_post_result["post_id"] post_id = initial_post_result["post_id"]
map_media_post_id = initial_post_result["map_media_post_id"] map_media_post_id = initial_post_result["map_media_post_id"]