4.4 KiB · text History 4eee00d
#!/usr/bin/env python3
"""
Turn Font Awesome glyphs into Android vector drawables.

The web board draws its rows with Font Awesome -- a bus for a bus, a
broadcast tower for a realtime time, a clock for a scheduled one. Rather
than pick lookalikes by hand, this reads the same metadata the web uses
(vendored at clients/lilygo4.7/fontawesome-free-6.2.0-desktop) and emits
the same paths as drawables, so a route on the phone and a route on a
panel are the same shape.

Regenerate with:

    python3 tools/fa_to_vector.py

Font Awesome Free icons are CC BY 4.0; see the vendored LICENSE.txt.
"""

import json
import pathlib
import sys

HERE = pathlib.Path(__file__).resolve().parent
FA = HERE.parent.parent / "lilygo4.7" / "fontawesome-free-6.2.0-desktop" / "metadata" / "icons.json"
OUT = HERE.parent / "app" / "src" / "main" / "res" / "drawable"

# Every glyph the board can draw. Names are Font Awesome 6's own, and match
# what RoomSanctumWeb.IconHelpers and live_preview.ex ask for.
ICONS = [
    # Transit modes, as live_preview.ex maps them.
    "bus", "bus-simple", "train", "train-subway", "train-tram",
    "ferry", "cable-car", "mountain",
    # A time that came from the feed, and one that came from the timetable.
    "tower-broadcast", "clock",
    # Bikeshare.
    "bicycle", "bolt-lightning", "square-parking", "battery-half",
    # Which way to walk to it, on a Plani's cards.
    "location-arrow",
    # A train the source's fleet marks call out -- the MBTA's new ones.
    "star",
    # What the sky is doing. A detailed weather card says "Clouds" in words
    # and the word is the smallest part of the answer; these are the picture.
    "cloud", "cloud-sun", "cloud-rain", "cloud-showers-heavy", "cloud-bolt",
    "snowflake", "smog", "tornado",
    # A GBFS feed is not always bicycles: Getaround publishes cars, and other
    # operators mopeds. Only where the free set has a truthful glyph -- there
    # is no kick scooter in it, so those stay on the bicycle.
    "car", "motorcycle",
    # Alerts: the web uses a pin when the alert names the stop.
    "location-dot", "triangle-exclamation",
    # Tides: the web marks a high and a low with arrows to a line.
    "arrows-up-to-line", "arrows-down-to-line",
    # Sun and moon.
    "sun", "moon",
    # Weather's own measures.
    "temperature-half", "droplet", "wind", "gauge-high",
    # The rest of the source types, for card headers and rows.
    "cloud-sun", "water", "lungs", "house", "calendar-day",
    "seedling", "sun-plant-wilt", "code-branch", "envelopes-bulk",
    "circle-check", "circle-xmark", "circle-info", "list",
    # Brands, for the one source that is a company.
    "github",
]

# Font Awesome Free has some glyphs only as brands. Named here rather than
# guessed, so a solid glyph silently becoming a brand one is a diff.
BRANDS = {"github"}

TEMPLATE = """<!-- Generated by tools/fa_to_vector.py from Font Awesome Free 6.2.0 ({name}).
     Font Awesome Free icons are CC BY 4.0. Do not edit by hand. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="{vw}"
    android:viewportHeight="{vh}"
    android:tint="?android:attr/colorControlNormal">
    <path
        android:fillColor="@android:color/white"
        android:pathData="{path}" />
</vector>
"""


def resource_name(icon: str) -> str:
    return "fa_" + icon.replace("-", "_")


def main() -> int:
    if not FA.exists():
        print(f"font awesome metadata not found at {FA}", file=sys.stderr)
        return 1

    icons = json.loads(FA.read_text())
    OUT.mkdir(parents=True, exist_ok=True)

    for name in ICONS:
        icon = icons.get(name)
        style = "brands" if name in BRANDS else "solid"
        if icon is None or style not in icon.get("svg", {}):
            print(f"no free {style} glyph for {name}", file=sys.stderr)
            return 1

        svg = icon["svg"][style]
        _, _, vw, vh = svg["viewBox"]
        path = svg["path"]
        # Android's path parser is stricter than a browser's about quotes; the
        # data itself is plain path commands, so only the attribute needs care.
        assert '"' not in path, name

        target = OUT / f"{resource_name(name)}.xml"
        target.write_text(TEMPLATE.format(name=name, vw=vw, vh=vh, path=path))
        print(f"wrote {target.relative_to(HERE.parent)}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())