weather/afd.go

208 lines
5.0 KiB
Go

package main
import (
"context"
"fmt"
"strings"
"time"
)
func runDiscussion(ctx context.Context, cache *Cache, loc Location, searchQuery, userAgent string) (string, error) {
client := NewNWSClient(userAgent)
if loc.OfficeID == "" {
office, err := client.FetchOfficeID(ctx, loc)
if err != nil {
return "", err
}
loc.OfficeID = office
if err := cache.UpdateLocation(loc); err != nil {
if searchQuery == "" {
return "", err
}
if err := cache.SaveLocation(searchQuery, loc); err != nil {
return "", err
}
}
}
cached, ok, err := cache.LoadAFD()
if err != nil {
return "", err
}
productID, productURL, err := client.FetchLatestAFDMeta(ctx, loc.OfficeID)
if err != nil {
return "", err
}
if ok && cached.OfficeID == loc.OfficeID && cached.ProductID == productID {
cached.CheckedAt = time.Now().UTC()
if err := cache.SaveAFD(cached); err != nil {
return "", err
}
return cached.Text, nil
}
text, issuanceTime, err := client.FetchAFDText(ctx, productURL)
if err != nil {
return "", err
}
payload := AFDCache{
OfficeID: loc.OfficeID,
ProductID: productID,
Text: text,
IssuanceTime: issuanceTime,
CheckedAt: time.Now().UTC(),
}
if err := cache.SaveAFD(payload); err != nil {
return "", err
}
return text, nil
}
func (c *NWSClient) FetchLatestAFDMeta(ctx context.Context, officeID string) (string, string, error) {
officeID = strings.TrimSpace(strings.ToUpper(officeID))
if officeID == "" {
return "", "", fmt.Errorf("office id is empty")
}
url := fmt.Sprintf("https://api.weather.gov/products/types/AFD/locations/%s", officeID)
var resp struct {
Graph []struct {
ID string `json:"id"`
Ref string `json:"@id"`
} `json:"@graph"`
}
if err := c.getJSON(ctx, url, &resp); err != nil {
return "", "", err
}
if len(resp.Graph) == 0 {
return "", "", fmt.Errorf("no AFD products for %s", officeID)
}
productID := strings.TrimSpace(resp.Graph[0].ID)
productURL := strings.TrimSpace(resp.Graph[0].Ref)
if productID == "" || productURL == "" {
return "", "", fmt.Errorf("invalid AFD metadata")
}
return productID, productURL, nil
}
func (c *NWSClient) FetchAFDText(ctx context.Context, url string) (string, string, error) {
var resp struct {
ProductText string `json:"productText"`
IssuanceTime string `json:"issuanceTime"`
}
if err := c.getJSON(ctx, url, &resp); err != nil {
return "", "", err
}
text := sanitizeAFDText(resp.ProductText)
if text == "" {
return "", "", fmt.Errorf("AFD product text missing")
}
return text, strings.TrimSpace(resp.IssuanceTime), nil
}
func sanitizeAFDText(text string) string {
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" {
lines = lines[1:]
}
if len(lines) == 0 {
return ""
}
for i, line := range lines {
if strings.Contains(strings.ToLower(line), "area forecast discussion") {
lines = lines[i:]
break
}
}
for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" {
lines = lines[1:]
}
return strings.TrimSpace(strings.Join(lines, "\n"))
}
func formatAFDMarkdown(text string) string {
lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
out := make([]string, 0, len(lines))
titleDone := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "&&" {
continue
}
if !titleDone && strings.EqualFold(trimmed, "Area Forecast Discussion") {
out = append(out, "# Area Forecast Discussion")
titleDone = true
continue
}
if header, rest := splitKeyMessage(trimmed); header != "" {
out = append(out, "## "+header)
if rest != "" {
out = append(out, rest)
}
continue
}
if isAFDHeaderLine(trimmed) {
title := afdHeaderToTitle(trimmed)
out = append(out, "# "+title)
continue
}
out = append(out, line)
}
return strings.TrimSpace(strings.Join(out, "\n"))
}
func isAFDHeaderLine(line string) bool {
if !strings.HasPrefix(line, ".") {
return false
}
return strings.HasSuffix(line, "...") || strings.HasSuffix(line, ".")
}
func afdHeaderToTitle(line string) string {
trimmed := strings.Trim(line, ". ")
parts := strings.Fields(trimmed)
for i, part := range parts {
subparts := strings.Split(part, "/")
for j, sub := range subparts {
subparts[j] = titleWord(sub)
}
parts[i] = strings.Join(subparts, "/")
}
return strings.Join(parts, " ")
}
func titleWord(word string) string {
if word == "" {
return ""
}
lower := strings.ToLower(word)
r := []rune(lower)
r[0] = []rune(strings.ToUpper(string(r[0])))[0]
return string(r)
}
func splitKeyMessage(line string) (string, string) {
lower := strings.ToLower(line)
if !strings.HasPrefix(lower, "key message") {
return "", ""
}
parts := strings.SplitN(line, "...", 2)
header := strings.TrimSpace(parts[0])
rest := ""
if len(parts) > 1 {
rest = strings.TrimSpace(parts[1])
}
fields := strings.Fields(header)
if len(fields) < 2 {
return "", ""
}
fields[0] = "Key"
fields[1] = "Message"
return strings.Join(fields, " "), rest
}