From 4accc75c066926b93023b6efd3663ef6712bb3bc Mon Sep 17 00:00:00 2001 From: Liam Steckler Date: Sat, 20 Apr 2024 15:03:16 -0700 Subject: [PATCH] Fix center of outage calculation (#24) Solves #23 Reviewed-on: https://scm.gruezi.net/buckbanzai/seattlecitylight-mastodon-bot/pulls/24 --- .woodpecker/lint.yml | 2 +- geospatial.py | 11 +++++++++ requirements.txt | 3 +++ scl.py | 58 +++++++++++++++++++++++++++++--------------- 4 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 geospatial.py diff --git a/.woodpecker/lint.yml b/.woodpecker/lint.yml index 1bc5a73..1eead62 100644 --- a/.woodpecker/lint.yml +++ b/.woodpecker/lint.yml @@ -2,7 +2,7 @@ when: branch: main steps: - name: lint - image: python:3-alpine + image: python:3-slim commands: - python -m pip install --upgrade pip - python -m pip install -r requirements.txt diff --git a/geospatial.py b/geospatial.py new file mode 100644 index 0000000..36c8992 --- /dev/null +++ b/geospatial.py @@ -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) diff --git a/requirements.txt b/requirements.txt index 9e3fe3d..31cf953 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,12 +4,15 @@ charset-normalizer==3.3.2 decorator==5.1.1 greenlet==3.0.3 idna==3.6 +install==1.3.5 Mastodon.py==1.8.1 +numpy==1.26.4 pillow==10.2.0 python-dateutil==2.9.0.post0 python-magic==0.4.27 PyYAML==6.0.1 requests==2.31.0 +shapely==2.0.3 six==1.16.0 SQLAlchemy==2.0.28 staticmap==0.5.7 diff --git a/scl.py b/scl.py index fe1ed00..f2c31a3 100644 --- a/scl.py +++ b/scl.py @@ -1,17 +1,19 @@ import io -import math from datetime import datetime from typing import Optional import mastodon import requests +import shapely import yaml from mastodon import Mastodon from PIL import Image, ImageDraw, ImageFont from sqlalchemy import create_engine, select from sqlalchemy.exc import NoResultFound 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" @@ -104,7 +106,11 @@ def get_hashtag_string(event) -> str: 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]: post_id = None map_media_post_id = None @@ -127,30 +133,32 @@ def do_initial_post( simplify=True, ) map.add_polygon(polygon) - map_image = map.render() try: + outage_center: shapely.Point = outage_geometries.centroid - 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) - + assert outage_center.geom_type == "Point" # Check to make sure the calculated lat and lon are sane enough # 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 - 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 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], + lat=outage_center.y, + lon=outage_center.x, ) geocode_headers = {"User-Agent": "seattlecitylight-mastodon-bot"} geocode_response = requests.get(geocode_url, headers=geocode_headers) @@ -202,6 +210,8 @@ def do_initial_post( 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="PNG", optimize=True) map_media_post = mastodon_client.media_post( @@ -297,6 +307,8 @@ with Session(engine) as session: else: status = None + outage_geometries = convert_outage_geometry(event) + try: hashtag_string = get_hashtag_string(event) 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" ) 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.most_recent_post_id = initial_post_result["post_id"] @@ -382,7 +398,11 @@ with Session(engine) as session: ) else: 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"] map_media_post_id = initial_post_result["map_media_post_id"]