Compare commits

..

3 commits

Author SHA1 Message Date
5c27528247 Fix MIME type for image
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/vulnerability-scan Pipeline was successful
2024-11-23 22:13:46 -08:00
e6722876bd Log outage geometries to database (#68)
Some checks failed
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/vulnerability-scan Pipeline failed
To enable us to detect changes to to the geometry for future updating of the map (#67)

Reviewed-on: #68
2024-11-23 17:14:30 -08:00
fbdc85bc29 Merge pull request 'Update dependency numpy to v2.1.3' (#66) from renovate/numpy-2.x into main
Some checks failed
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/vulnerability-scan Pipeline failed
Reviewed-on: #66
2024-11-20 11:14:22 -08:00
2 changed files with 39 additions and 8 deletions

View file

@ -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: def convert_outage_geometry(event) -> MultiPolygon:
@ -9,3 +10,21 @@ def convert_outage_geometry(event) -> MultiPolygon:
for ring in event["polygons"]["rings"]: for ring in event["polygons"]["rings"]:
polygon_list.append(Polygon(ring)) polygon_list.append(Polygon(ring))
return MultiPolygon(polygon_list) 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

26
scl.py
View file

@ -5,15 +5,16 @@ from typing import Optional
import mastodon import mastodon
import requests import requests
import shapely import shapely
import staticmap
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 shapely import Geometry
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 geospatial import convert_outage_geometry from geospatial import DBGeometry, convert_outage_geometry
post_datetime_format = "%b %e %l:%M %p" 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): def __init__(self, *args, **kwargs):
self.attribution = "© Stadia Maps © OpenMapTiles © OpenStreetMap" self.attribution = "© Stadia Maps © OpenMapTiles © OpenStreetMap"
super(AttribStaticMap, self).__init__(*args, **kwargs) super(AttribStaticMap, self).__init__(*args, **kwargs)
@ -137,7 +138,7 @@ def do_initial_post(
) )
assert event["polygons"]["type"] == "polygon" assert event["polygons"]["type"] == "polygon"
for ring in event["polygons"]["rings"]: for ring in event["polygons"]["rings"]:
polygon = Polygon( polygon = staticmap.Polygon(
ring, ring,
# Appending 7F to the fill_color makes it 50% transparent # Appending 7F to the fill_color makes it 50% transparent
fill_color="{}7F".format(event_class["outage_color"]), fill_color="{}7F".format(event_class["outage_color"]),
@ -221,7 +222,7 @@ def do_initial_post(
map_image.save(map_image_file, format="WebP", method=6) map_image.save(map_image_file, format="WebP", method=6)
map_media_post = mastodon_client.media_post( map_media_post = mastodon_client.media_post(
map_image_file.getvalue(), map_image_file.getvalue(),
mime_type="image/png", mime_type="image/webp",
description=alt_text, description=alt_text,
) )
map_media_post_id = map_media_post["id"] map_media_post_id = map_media_post["id"]
@ -272,7 +273,9 @@ Cause: {}
class Base(DeclarativeBase): class Base(DeclarativeBase):
pass type_annotation_map = {
Geometry: DBGeometry,
}
class SclOutage(Base): class SclOutage(Base):
@ -292,9 +295,11 @@ class SclOutage(Base):
max_num_people: Mapped[int] = mapped_column() max_num_people: Mapped[int] = mapped_column()
neighborhood: Mapped[Optional[str]] = mapped_column() neighborhood: Mapped[Optional[str]] = mapped_column()
city: 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: 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") 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 # 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"] existing_record.max_num_people = event["numPeople"]
max_event_class = classify_event_size(existing_record.max_num_people) 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: if updated_properties:
updated_properties.sort() updated_properties.sort()
@ -426,6 +435,8 @@ with Session(engine) as session:
existing_record.map_media_post_id = initial_post_result[ existing_record.map_media_post_id = initial_post_result[
"map_media_post_id" "map_media_post_id"
] ]
else:
print("Existing record was found, and no properties were updated.")
session.commit() session.commit()
except NoResultFound: except NoResultFound:
@ -476,6 +487,7 @@ with Session(engine) as session:
max_num_people=event["numPeople"], max_num_people=event["numPeople"],
neighborhood=neighborhood, neighborhood=neighborhood,
city=city, city=city,
outage_geometries=outage_geometries,
) )
session.add(new_outage_record) session.add(new_outage_record)
session.commit() session.commit()