import requests
import simdjson
from PIL import Image, ImageDraw
# https://geojson.io, draw a rectangle
MAP = [
[-123.04893493652342, 48.478838892384104],
[-122.65480041503905, 48.478838892384104],
[-122.65480041503905, 48.62610126300646],
[-123.04893493652342, 48.62610126300646],
]
VESSELS = "https://www.wsdot.com/ferries/vesselwatch/Vessels.ashx"
OVERPASS = "https://overpass-api.de/api/interpreter"
MIN_LON = min(MAP, key=lambda coord: coord[0])[0]
MIN_LAT = min(MAP, key=lambda coord: coord[1])[1]
MAX_LON = max(MAP, key=lambda coord: coord[0])[0]
MAX_LAT = max(MAP, key=lambda coord: coord[1])[1]
LON_RANGE = MAX_LON - MIN_LON
LAT_RANGE = MAX_LAT - MIN_LAT
WIDTH = 648
HEIGHT = 480
MAP_HEIGHT = int(1.5 * WIDTH * LAT_RANGE / LON_RANGE)
SCREEN = Image.new("1", (WIDTH, HEIGHT), color=1)
DRAW = ImageDraw.Draw(SCREEN)
MOVING = Image.open("moving.png")
STOPPED = Image.open("stopped.png")
OVERPASS_QUERY = f"""
[out:json];
(
way[place=island]({MIN_LAT}, {MIN_LON}, {MAX_LAT}, {MAX_LON});
rel[place=island]({MIN_LAT}, {MIN_LON}, {MAX_LAT}, {MAX_LON});
);
convert item ::=::,::geom=geom(),_osm_type=type(); // magic
out geom qt;
"""
def latlon_to_xy(lat: float, lon: float) -> (float, float):
x = WIDTH * (lon - MIN_LON) / LON_RANGE
y = min(MAP_HEIGHT, MAP_HEIGHT - (MAP_HEIGHT * (lat - MIN_LAT) / LAT_RANGE))
return x, y
def draw_coastlines(coastlines):
def linestring(item):
screen_coords = []
coords = item["coordinates"]
for lon, lat in coords:
screen_coords.append(latlon_to_xy(lat, lon))
DRAW.line(screen_coords, width=1, joint="curve")
for sequence in coastlines["elements"]:
item = sequence["geometry"]
t = item["type"]
if t == "LineString":
linestring(item)
elif t == "GeometryCollection":
for i in item["geometries"]:
linestring(i)
else:
raise
DRAW.rectangle((0, 0, WIDTH - 1, MAP_HEIGHT))
def paste(src: Image, dest_x, dest_y):
center_x = src.width / 2
center_y = src.height / 2
for x in range(src.width):
for y in range(src.height):
alpha = src.getpixel((x, y))[-1]
# PIL alpha compositing cannot be used directly since the images aren't both RGBA.
# this copies a pixel value to the screen only if the alpha channel in the original is nontransparent.
if alpha > 0:
DRAW.point((dest_x + x - center_x, dest_y + y - center_y))
def main():
filename = f"coastlines-{hash((MIN_LAT, MIN_LON, MAX_LAT, MAX_LON))}.json"
try:
with open(filename) as f:
coastlines = simdjson.load(f)
except FileNotFoundError:
print("Downloading coastlines")
coastlines = requests.get(OVERPASS, {"data": OVERPASS_QUERY}).json()
with open(filename, "w") as f:
simdjson.dump(coastlines, f)
draw_coastlines(coastlines)
boats = requests.get(VESSELS).json()["vessellist"]
boats = filter(lambda boat: boat["route"] == "ANA-SJ", boats)
boats = filter(lambda boat: boat["aterm_id"] != "", boats)
for boat in boats:
boat_lat = boat["lat"]
boat_lon = boat["lon"]
x, y = latlon_to_xy(boat_lat, boat_lon)
label = f"{boat['name'][:3].upper()} {boat['pos']}"
if boat["eta"] == "Calculating":
label += f" {boat['lastdock'][0]}->{boat['aterm'][0]} departing at {boat['nextdep']}"
# the frickin image is not centered lol
paste(STOPPED, x - 2, y)
else:
label += f" {boat['lastdock'][0]}->{boat['aterm'][0]} ETA {boat['eta']}"
paste(MOVING.rotate(-boat["head"]), x, y)
DRAW.point((x, y))
DRAW.text((x, y), label, anchor="mm")
SCREEN.save("map.bmp", "BMP")
if __name__ == "__main__":
main()