From c2cf304b3d3c6ffab0a7e0686536809b27085ba8 Mon Sep 17 00:00:00 2001
From: XANTRONIX Industrial <xan@xantronix.com>
Date: Mon, 7 Apr 2025 12:26:10 -0400
Subject: [PATCH] Implement xmet-spc-render

---
 bin/xmet-spc-render | 97 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 97 insertions(+)
 create mode 100755 bin/xmet-spc-render

diff --git a/bin/xmet-spc-render b/bin/xmet-spc-render
new file mode 100755
index 0000000..036f678
--- /dev/null
+++ b/bin/xmet-spc-render
@@ -0,0 +1,97 @@
+#! /usr/bin/env python3
+
+import argparse
+import cairo
+
+from xmet.config import Config
+from xmet.db     import Database, DatabaseOrder
+
+from xmet.spc import SPCOutlookParser, \
+                     SPCOutlook, \
+                     SPCOutlookMap, \
+                     SPCOutlookType
+
+def render_categorical(db: Database,
+                       conus: SPCOutlookMap,
+                       outlook: SPCOutlook,
+                       args):
+    assets = {
+        'conus': config['map']['conus_dark' if args.dark else 'conus'],
+        'logo':  config['map']['logo_dark'  if args.dark else 'logo']
+    }
+
+    with cairo.SVGSurface(args.categorical, conus.width, conus.height) as surface:
+        cr = cairo.Context(surface)
+
+        conus.draw_base_map_from_file(cr, assets['conus'])
+        conus.draw_categories(cr, outlook)
+        conus.draw_cities(cr, db)
+        conus.draw_logo(cr, assets['logo'])
+        conus.draw_legend(cr, SPCOutlookType.CATEGORICAL)
+
+        if args.dark:
+            cr.set_source_rgb(1, 1, 1)
+
+        conus.draw_annotation(cr, outlook, SPCOutlookType.CATEGORICAL)
+
+def render_probabilistic(db: Database,
+                         conus: SPCOutlookMap,
+                         outlook: SPCOutlook,
+                         hazard: str,
+                         path: str,
+                         args):
+    assets = {
+        'conus': config['map']['conus_dark' if args.dark else 'conus'],
+        'logo':  config['map']['logo_dark'  if args.dark else 'logo']
+    }
+
+    with cairo.SVGSurface(path, conus.width, conus.height) as surface:
+        cr = cairo.Context(surface)
+
+        conus.draw_base_map_from_file(cr, assets['conus'])
+        conus.draw_probabilities(cr, outlook, hazard.upper())
+        conus.draw_cities(cr, db)
+        conus.draw_logo(cr, assets['logo'])
+        conus.draw_legend(cr, SPCOutlookType.PROBABILISTIC)
+
+        if args.dark:
+            cr.set_source_rgb(1, 1, 1)
+
+        hazard = ' '.join(map(lambda p: p.lower().capitalize(), hazard.split(' ')))
+
+        conus.draw_annotation(cr, outlook, SPCOutlookType.PROBABILISTIC, hazard)
+
+argparser = argparse.ArgumentParser(description='Render graphical SPC outlooks from text file')
+argparser.add_argument('--dark', action='store_true', help='Output dark mode graphics')
+argparser.add_argument('--valid',       type=str, help='Timestamp on or after most recent outlook')
+argparser.add_argument('--day',         type=int, help='Number of days from issuance outlook applies to')
+argparser.add_argument('--categorical', type=str, help='Output categorical risk graphic file')
+argparser.add_argument('--any-severe',  type=str, help='Output probabilistic severe risk graphic file')
+argparser.add_argument('--tornado',     type=str, help='Output probabilistic hail risk graphic file')
+argparser.add_argument('--hail',        type=str, help='Output probabilistic hail risk graphic file')
+argparser.add_argument('--wind',        type=str, help='Output probabilistic wind risk graphic file')
+
+args = argparser.parse_args()
+
+config = Config.load()
+db     = Database.from_config(config)
+
+parser = SPCOutlookParser()
+conus  = SPCOutlookMap()
+
+outlook = SPCOutlook.for_timestamp(db, args.valid, args.day)
+
+if args.categorical is not None:
+    render_categorical(db, conus, outlook, args)
+
+if args.any_severe is not None:
+    render_probabilistic(db, conus, outlook, 'any severe', args.any_severe, args)
+
+if args.tornado is not None:
+    render_probabilistic(db, conus, outlook, 'tornado', args.tornado, args)
+
+if args.hail is not None:
+    render_probabilistic(db, conus, outlook, 'hail', args.hail, args)
+
+if args.wind is not None:
+    render_probabilistic(db, conus, outlook, 'wind', args.wind, args)