#!/bin/bash
# Supervolcano Monitor - Live Volcanic Surveillance Grid
# Fetches seismograms, webcams, and earthquake data for Earth's supervolcanoes
# Assembles a monitoring dashboard grid via ImageMagick montage
# Alias: svm
#
# Tracked: Yellowstone | Long Valley | Campi Flegrei | Taupo | Toba | Aira
# Sources: USGS Earthquake Hazards, USGS HANS, GeoNet NZ
# Requires: curl, jq, imagemagick, mpv
export DISPLAY=:0
# ═══════════════════════════════════════════════════════════════
# CONFIGURATION
# ═══════════════════════════════════════════════════════════════
cache="$HOME/.cache/maritime/supervolcano"
grid_file="$cache/grid.png"
mkdir -p "$cache"
PW=640 PH=480
# Fonts
FONT_BOLD=$(fc-match -f '%{file}' 'DejaVu Sans:Bold' 2>/dev/null)
FONT_REG=$(fc-match -f '%{file}' 'DejaVu Sans' 2>/dev/null)
FB=() FR=()
[ -n "$FONT_BOLD" ] && FB=(-font "$FONT_BOLD")
[ -n "$FONT_REG" ] && FR=(-font "$FONT_REG")
# Grid layout order (3 columns x 2 rows)
ORDER=(yellowstone longvalley campi_flegrei taupo toba aira)
# ═══════════════════════════════════════════════════════════════
# VOLCANO DEFINITIONS
# ═══════════════════════════════════════════════════════════════
declare -A VNAME VLOC VBBOX VVNUM VIMG
VNAME=( [yellowstone]="YELLOWSTONE" [longvalley]="LONG VALLEY"
[campi_flegrei]="CAMPI FLEGREI" [taupo]="TAUPO"
[toba]="TOBA" [aira]="AIRA" )
VLOC=( [yellowstone]="Wyoming, USA" [longvalley]="California, USA"
[campi_flegrei]="Naples, Italy" [taupo]="New Zealand"
[toba]="Sumatra, Indonesia" [aira]="Kagoshima, Japan" )
# Bounding boxes: minlat|maxlat|minlon|maxlon
VBBOX=( [yellowstone]="44.0|45.0|-111.5|-109.5"
[longvalley]="37.4|38.0|-119.3|-118.5"
[campi_flegrei]="40.6|40.95|13.9|14.25"
[taupo]="-39.0|-38.0|175.5|176.5"
[toba]="2.0|3.0|98.0|99.5"
[aira]="31.3|31.8|130.4|131.0" )
# USGS HANS volcano numbers (US volcanoes only)
VVNUM=( [yellowstone]="325010" [longvalley]="323822" )
# Live image sources (seismograms / webcams)
VIMG=( [yellowstone]="https://earthquake.usgs.gov/static/earthquake-network-operations/Seismic_Data/telemetry_data/LKWY_24hr.png"
[taupo]="https://images.geonet.org.nz/volcano/cameras/latest/ruapehunorth.jpg" )
# Alternate images for slideshow mode
declare -A VIMG_ALT
VIMG_ALT=(
[yellowstone_flwy]="https://earthquake.usgs.gov/static/earthquake-network-operations/Seismic_Data/telemetry_data/FLWY_24hr.png"
[yellowstone_snow]="https://earthquake.usgs.gov/static/earthquake-network-operations/Seismic_Data/telemetry_data/SNOW_24hr.png"
[taupo_tongariro]="https://images.geonet.org.nz/volcano/cameras/latest/tongariro.jpg"
[taupo_ngauruhoe]="https://images.geonet.org.nz/volcano/cameras/latest/ngauruhoe.jpg"
)
# ═══════════════════════════════════════════════════════════════
# DATA FETCHING
# ═══════════════════════════════════════════════════════════════
# Earthquake count for a volcano's bounding box (last 7 days)
fetch_quake_count() {
local key="$1"
IFS='|' read -r minlat maxlat minlon maxlon <<< "${VBBOX[$key]}"
local start
start=$(date -u -d '7 days ago' +%Y-%m-%d)
local count
count=$(curl -sf "https://earthquake.usgs.gov/fdsnws/event/1/count?format=text&starttime=${start}&minlatitude=${minlat}&maxlatitude=${maxlat}&minlongitude=${minlon}&maxlongitude=${maxlon}")
echo "${count:-0}" > "$cache/count_${key}.txt"
}
# USGS HANS alert level
fetch_usgs_alert() {
local key="$1"
local vnum="${VVNUM[$key]}"
[ -z "$vnum" ] && return
local json
json=$(curl -sf "https://volcanoes.usgs.gov/hans-public/api/volcano/getVolcano/${vnum}")
local alert
alert=$(echo "$json" | jq -r '.alertLevel // "NORMAL"' 2>/dev/null)
echo "${alert:-NORMAL}" > "$cache/alert_${key}.txt"
}
# GeoNet NZ alert levels
fetch_geonet_alerts() {
local json
json=$(curl -sf "https://api.geonet.org.nz/volcano/val")
local level
level=$(echo "$json" | jq -r '[.features[] | select(.properties.volcanoID | test("taupo";"i"))][0].properties.level // 0' 2>/dev/null)
case "$level" in
0) echo "NORMAL" ;;
1) echo "MINOR UNREST" ;;
2) echo "MODERATE UNREST" ;;
3) echo "MINOR ERUPTION" ;;
4) echo "MODERATE ERUPTION" ;;
5) echo "MAJOR ERUPTION" ;;
*) echo "NORMAL" ;;
esac > "$cache/alert_taupo.txt"
}
# Fetch live image for a volcano
fetch_live_image() {
local key="$1"
local url="${VIMG[$key]}"
[ -z "$url" ] && return 1
curl -sLo "$cache/raw_${key}.img" "$url"
}
# ═══════════════════════════════════════════════════════════════
# PANEL GENERATION
# ═══════════════════════════════════════════════════════════════
# Alert level -> color
alert_color() {
case "$1" in
NORMAL) echo "#3fb950" ;;
ADVISORY|"MINOR UNREST") echo "#d29922" ;;
WATCH|"MODERATE UNREST") echo "#db6d28" ;;
WARNING|*ERUPTION*) echo "#f85149" ;;
*) echo "#8b949e" ;;
esac
}
# Generate status card (no live image available)
make_card() {
local key="$1"
local name="${VNAME[$key]}"
local loc="${VLOC[$key]}"
local quakes alert ac
quakes=$(cat "$cache/count_${key}.txt" 2>/dev/null || echo "?")
alert=$(cat "$cache/alert_${key}.txt" 2>/dev/null || echo "—")
ac=$(alert_color "$alert")
magick -size ${PW}x${PH} xc:'#0d1117' \
"${FB[@]}" \
-fill '#58a6ff' -pointsize 36 -gravity North -annotate +0+30 "$name" \
"${FR[@]}" \
-fill '#8b949e' -pointsize 20 -annotate +0+75 "$loc" \
-fill white -pointsize 80 -gravity Center -annotate +0-20 "$quakes" \
-fill '#8b949e' -pointsize 16 -annotate +0+45 "earthquakes (7 days)" \
"${FB[@]}" \
-fill "$ac" -pointsize 22 -gravity South -annotate +0+30 "$alert" \
"$cache/panel_${key}.png"
}
# Label a live image with overlay
make_labeled() {
local key="$1"
local name="${VNAME[$key]}"
local loc="${VLOC[$key]}"
local quakes
quakes=$(cat "$cache/count_${key}.txt" 2>/dev/null || echo "?")
local input="$cache/raw_${key}.img"
local output="$cache/panel_${key}.png"
[ ! -f "$input" ] || [ ! -s "$input" ] && make_card "$key" && return
magick "$input" \
-resize ${PW}x${PH}! \
-fill 'rgba(0,0,0,0.7)' -draw "rectangle 0,0 ${PW},60" \
-fill 'rgba(0,0,0,0.7)' -draw "rectangle 0,$((PH-40)) ${PW},${PH}" \
"${FB[@]}" \
-fill '#58a6ff' -pointsize 26 -gravity NorthWest -annotate +10+16 "$name" \
"${FR[@]}" \
-fill '#8b949e' -pointsize 16 -gravity NorthEast -annotate +10+22 "$loc" \
-fill white -pointsize 16 -gravity SouthWest -annotate +10+12 "Quakes (7d): $quakes" \
"${FB[@]}" \
-fill '#3fb950' -pointsize 16 -gravity SouthEast -annotate +10+12 "LIVE" \
"$output"
}
# Build panel for a volcano (picks labeled or card)
make_panel() {
local key="$1"
if [ -n "${VIMG[$key]}" ] && [ -f "$cache/raw_${key}.img" ] && [ -s "$cache/raw_${key}.img" ]; then
make_labeled "$key"
else
make_card "$key"
fi
}
# ═══════════════════════════════════════════════════════════════
# GRID ASSEMBLY
# ═══════════════════════════════════════════════════════════════
generate_grid() {
local panels=()
for key in "${ORDER[@]}"; do
make_panel "$key"
panels+=("$cache/panel_${key}.png")
done
montage "${panels[@]}" \
-tile 3x2 -geometry ${PW}x${PH}+4+4 \
-background '#010409' \
"${FB[@]}" -fill '#58a6ff' -pointsize 22 \
-title "SUPERVOLCANO MONITOR · $(date -u '+%Y-%m-%d %H:%M UTC')" \
"$grid_file"
echo "Grid saved: $grid_file"
}
# ═══════════════════════════════════════════════════════════════
# COMMANDS
# ═══════════════════════════════════════════════════════════════
update() {
echo "Fetching supervolcano data..."
# Earthquake counts (parallel)
for key in "${ORDER[@]}"; do
fetch_quake_count "$key" &
done
# Alert levels (parallel)
fetch_usgs_alert yellowstone &
fetch_usgs_alert longvalley &
fetch_geonet_alerts &
# Live images (parallel)
for key in "${ORDER[@]}"; do
[ -n "${VIMG[$key]}" ] && fetch_live_image "$key" &
done
# Alternate images (parallel)
for alt_key in "${!VIMG_ALT[@]}"; do
curl -sLo "$cache/raw_${alt_key}.img" "${VIMG_ALT[$alt_key]}" &
done
wait
echo "Data fetched. Generating grid..."
generate_grid
notify-send -t 3000 -i "$grid_file" "Supervolcano Monitor" "Dashboard updated"
}
# Resolve shorthand volcano name
resolve() {
case "${1,,}" in
y*) echo "yellowstone" ;;
l*|lv) echo "longvalley" ;;
c*|cf) echo "campi_flegrei" ;;
ta*|nz) echo "taupo" ;;
to*) echo "toba" ;;
a*) echo "aira" ;;
*) echo "" ;;
esac
}
# Display grid in mpv
show_grid() {
[ ! -f "$grid_file" ] && echo "No grid cached. Run: svm update" && return 1
setsid -f mpv --title="Supervolcano Monitor" --autofit=90% \
--loop-file=inf "$grid_file"
}
# View specific volcano panel
show_volcano() {
local key
key=$(resolve "$1")
[ -z "$key" ] && echo "Unknown volcano: $1" && return 1
local panel="$cache/panel_${key}.png"
[ ! -f "$panel" ] && echo "No data for $key. Run: svm update" && return 1
setsid -f mpv --title="${VNAME[$key]}" --autofit=60% \
--loop-file=inf "$panel"
}
# Slideshow of all panels + alternates
show_all() {
local imgs=()
for key in "${ORDER[@]}"; do
[ -f "$cache/panel_${key}.png" ] && imgs+=("$cache/panel_${key}.png")
done
for alt_key in "${!VIMG_ALT[@]}"; do
[ -f "$cache/raw_${alt_key}.img" ] && imgs+=("$cache/raw_${alt_key}.img")
done
setsid -f mpv --title="Supervolcano Monitor - All Data" --idle --autofit=70% \
--loop-playlist --image-display-duration=4 "${imgs[@]}"
}
# Send notification
notify() {
if [ -n "$1" ]; then
local key
key=$(resolve "$1")
[ -z "$key" ] && return
local panel="$cache/panel_${key}.png"
[ -f "$panel" ] && notify-send -t 60000 -i "$panel" "${VNAME[$key]}" "${VLOC[$key]}"
else
[ -f "$grid_file" ] && notify-send -t 60000 -i "$grid_file" "Supervolcano Monitor" ""
fi
}
# Text status report
status() {
echo "SUPERVOLCANO STATUS — $(date -u '+%Y-%m-%d %H:%M UTC')"
echo "═══════════════════════════════════════════════════════"
for key in "${ORDER[@]}"; do
local quakes alert
quakes=$(cat "$cache/count_${key}.txt" 2>/dev/null || echo "?")
alert=$(cat "$cache/alert_${key}.txt" 2>/dev/null || echo "—")
printf " %-16s %-22s %4s quakes %s\n" "${VNAME[$key]}" "${VLOC[$key]}" "$quakes" "$alert"
done
echo "═══════════════════════════════════════════════════════"
}
# Help
help() {
cat << 'EOF'
Supervolcano Monitor - Live Volcanic Surveillance Grid
══════════════════════════════════════════════════════════
Usage: svm <command> [volcano]
Commands:
update, u Fetch all data and generate grid
grid, g Display cached dashboard grid
notify, n [volcano] Send grid/volcano to dunst notification
images, i [volcano] View specific volcano panel in mpv
all Slideshow of all panels and alternates
status, s Text status report
help, h Show this help
Volcanoes:
yellowstone, y Yellowstone Caldera (Wyoming, USA)
longvalley, l Long Valley Caldera (California, USA)
campi_flegrei, c Campi Flegrei (Naples, Italy)
taupo, t Taupo Caldera (New Zealand)
toba, to Toba Caldera (Sumatra, Indonesia)
aira, a Aira Caldera (Kagoshima, Japan)
Examples:
svm update Refresh all data and build grid
svm grid Open dashboard grid in mpv
svm images yellowstone View Yellowstone seismogram
svm notify taupo Send Taupo webcam to dunst
svm status Print text status report
Live Data Sources:
Yellowstone USGS 24hr seismogram (LKWY station)
Taupo GeoNet Ruapehu North webcam
All USGS earthquake API (7-day count)
US volcanoes USGS HANS alert levels
NZ volcanoes GeoNet volcanic alert levels
Grid Layout (3x2):
┌──────────────┬──────────────┬──────────────┐
│ Yellowstone │ Long Valley │ Campi Flegrei │
│ (seismogram) │ (status) │ (status) │
├──────────────┼──────────────┼──────────────┤
│ Taupo │ Toba │ Aira │
│ (webcam) │ (status) │ (status) │
└──────────────┴──────────────┴──────────────┘
Requires: curl, jq, imagemagick, mpv
══════════════════════════════════════════════════════════
EOF
}
# Status bar output
tobar() {
local elevated=false
for key in "${ORDER[@]}"; do
local alert
alert=$(cat "$cache/alert_${key}.txt" 2>/dev/null || echo "NORMAL")
case "$alert" in
NORMAL|—) ;;
*) elevated=true ;;
esac
done
if $elevated; then
echo " SVM"
else
echo " SVM"
fi
}
# ═══════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════
case "$1" in
u*) update ;;
g*) show_grid ;;
n*)
shift
notify "$1"
;;
i*)
shift
if [ -n "$1" ]; then
show_volcano "$1"
else
show_grid
fi
;;
all) show_all ;;
s*) status ;;
h*|--help) help ;;
"") tobar ;;
*) help ;;
esac
# Status bar click handlers (dwmblocks)
case $BLOCK_BUTTON in
1) notify-send -t 60000 -i "$grid_file" "Supervolcano Monitor" "$(status)" ;;
2) update ;;
3) show_grid ;;
4) notify-send -t 60000 -i "$cache/panel_yellowstone.png" "Yellowstone" "24hr Seismogram" ;;
5) notify-send -t 60000 -i "$cache/panel_taupo.png" "Taupo" "Ruapehu North Webcam" ;;
6) setsid -f "$TERMINAL" -e "$EDITOR" "$0" ;;
esac