From e6722876bd4dbaf45d59dc30c25ab6921e9ed406 Mon Sep 17 00:00:00 2001 From: Liam Steckler Date: Sat, 23 Nov 2024 17:14:30 -0800 Subject: [PATCH] Log outage geometries to database (#68) To enable us to detect changes to to the geometry for future updating of the map (#67) Reviewed-on: https://scm.gruezi.net/buckbanzai/seattlecitylight-mastodon-bot/pulls/68 --- geospatial.py | 21 ++++++++++++++++++++- scl.py | 24 ++++++++++++++++++------ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/geospatial.py b/geospatial.py index 36c8992..003370b 100644 --- a/geospatial.py +++ b/geospatial.py @@ -1,4 +1,5 @@ -from shapely import MultiPolygon, Polygon +from shapely import MultiPolygon, Polygon, Geometry, to_wkb, from_wkb +from sqlalchemy.types import TypeDecorator, LargeBinary def convert_outage_geometry(event) -> MultiPolygon: @@ -9,3 +10,21 @@ def convert_outage_geometry(event) -> MultiPolygon: for ring in event["polygons"]["rings"]: polygon_list.append(Polygon(ring)) return MultiPolygon(polygon_list) + + +class DBGeometry(TypeDecorator): + impl = LargeBinary + cache_ok = True + + def process_bind_param(self, value, dialect): + if isinstance(value, Geometry): + value = to_wkb(value) + return value + + def process_result_value(self, value, dialect): + if value is None: + return value + else: + if not isinstance(value, Geometry): + value = from_wkb(value) + return value diff --git a/scl.py b/scl.py index b768461..4f1a4db 100644 --- a/scl.py +++ b/scl.py @@ -5,15 +5,16 @@ from typing import Optional import mastodon import requests import shapely +import staticmap import yaml from mastodon import Mastodon from PIL import Image, ImageDraw, ImageFont +from shapely import Geometry 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 geospatial import convert_outage_geometry +from geospatial import DBGeometry, convert_outage_geometry post_datetime_format = "%b %e %l:%M %p" @@ -36,7 +37,7 @@ mastodon_client = Mastodon( ) -class AttribStaticMap(StaticMap, object): +class AttribStaticMap(staticmap.StaticMap, object): def __init__(self, *args, **kwargs): self.attribution = "© Stadia Maps © OpenMapTiles © OpenStreetMap" super(AttribStaticMap, self).__init__(*args, **kwargs) @@ -137,7 +138,7 @@ def do_initial_post( ) assert event["polygons"]["type"] == "polygon" for ring in event["polygons"]["rings"]: - polygon = Polygon( + polygon = staticmap.Polygon( ring, # Appending 7F to the fill_color makes it 50% transparent fill_color="{}7F".format(event_class["outage_color"]), @@ -272,7 +273,9 @@ Cause: {} class Base(DeclarativeBase): - pass + type_annotation_map = { + Geometry: DBGeometry, + } class SclOutage(Base): @@ -292,9 +295,11 @@ class SclOutage(Base): max_num_people: Mapped[int] = mapped_column() neighborhood: Mapped[Optional[str]] = mapped_column() city: Mapped[Optional[str]] = mapped_column() + outage_geometries: Mapped[Optional[Geometry]] = mapped_column() + geometries_modified: Mapped[Optional[bool]] = mapped_column() def __repr__(self) -> str: - return f"SclOutage(scl_outage_id={self.scl_outage_id!r}, most_recent_post_id={self.most_recent_post_id!r}, initial_post_id={self.initial_post_id!r}, map_media_post_id={self.map_media_post_id!r}, last_updated_time={self.last_updated_time!r}, no_longer_in_response_time={self.no_longer_in_response_time!r}, start_time={self.start_time!r}, num_people={self.num_people!r}, max_num_people={self.max_num_people!r}, neighborhood={self.neighborhood!r}, city={self.city!r})" + return f"SclOutage(scl_outage_id={self.scl_outage_id!r}, most_recent_post_id={self.most_recent_post_id!r}, initial_post_id={self.initial_post_id!r}, map_media_post_id={self.map_media_post_id!r}, last_updated_time={self.last_updated_time!r}, no_longer_in_response_time={self.no_longer_in_response_time!r}, start_time={self.start_time!r}, num_people={self.num_people!r}, max_num_people={self.max_num_people!r}, neighborhood={self.neighborhood!r}, city={self.city!r}, outage_geometries={self.outage_geometries!r}, geometries_modified={self.geometries_modified!r})" engine = create_engine("sqlite:///scl.db") @@ -366,6 +371,10 @@ with Session(engine) as session: # Used to determine the maximum number of people affected by this outage, to determine if it's worth posting about existing_record.max_num_people = event["numPeople"] max_event_class = classify_event_size(existing_record.max_num_people) + if existing_record.outage_geometries != outage_geometries: + print("Geometries modified") + existing_record.outage_geometries = outage_geometries + existing_record.geometries_modified = True if updated_properties: updated_properties.sort() @@ -426,6 +435,8 @@ with Session(engine) as session: existing_record.map_media_post_id = initial_post_result[ "map_media_post_id" ] + else: + print("Existing record was found, and no properties were updated.") session.commit() except NoResultFound: @@ -476,6 +487,7 @@ with Session(engine) as session: max_num_people=event["numPeople"], neighborhood=neighborhood, city=city, + outage_geometries=outage_geometries, ) session.add(new_outage_record) session.commit()