Fix center of outage calculation #24
2 changed files with 50 additions and 19 deletions
11
geospatial.py
Normal file
11
geospatial.py
Normal 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)
|
58
scl.py
58
scl.py
|
@ -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"]
|
||||||
|
|
Loading…
Reference in a new issue