~phroa/boat-map

ref: 7aed69fa5b268603fa3939d83bef7ee26e344551 boat-map/make-map.py -rw-r--r-- 3.8 KiB
7aed69faJack Stratton Add pi requirements 1 year, 30 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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()