Files
cloudcover/main.go
2025-10-24 10:37:55 -04:00

151 lines
3.7 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sync"
"time"
"gocv.io/x/gocv"
)
type CloudResult struct {
URL string `json:"url"`
CloudCover float64 `json:"cloud_cover"`
LastUpdated string `json:"last_updated"`
Error string `json:"error,omitempty"`
}
var (
mu sync.Mutex
cache = make(map[string]CloudResult)
refreshFreq = 30 * time.Second // refresh interval
)
func estimateCloudCover(url string) (float64, error) {
// Fetch image
resp, err := http.Get(url)
if err != nil {
return 0, fmt.Errorf("failed to fetch image: %v", err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("failed to read image data: %v", err)
}
img, err := gocv.IMDecode(data, gocv.IMReadColor)
if err != nil {
return 0, fmt.Errorf("failed to decode image: %v", err)
}
defer img.Close()
// Convert to HSV
hsv := gocv.NewMat()
defer hsv.Close()
gocv.CvtColor(img, &hsv, gocv.ColorBGRToHSV)
channels := gocv.Split(hsv)
s, v := channels[1], channels[2]
defer func() {
for _, ch := range channels {
ch.Close()
}
}()
satMask := gocv.NewMat()
valMask := gocv.NewMat()
cloudMask := gocv.NewMat()
defer satMask.Close()
defer valMask.Close()
defer cloudMask.Close()
lowerS := gocv.NewMatFromScalar(gocv.NewScalar(0, 0, 0, 0), gocv.MatTypeCV8U)
upperS := gocv.NewMatFromScalar(gocv.NewScalar(60, 0, 0, 0), gocv.MatTypeCV8U)
lowerV := gocv.NewMatFromScalar(gocv.NewScalar(120, 0, 0, 0), gocv.MatTypeCV8U)
upperV := gocv.NewMatFromScalar(gocv.NewScalar(255, 0, 0, 0), gocv.MatTypeCV8U)
defer lowerS.Close()
defer upperS.Close()
defer lowerV.Close()
defer upperV.Close()
gocv.InRange(s, lowerS, upperS, &satMask)
gocv.InRange(v, lowerV, upperV, &valMask)
gocv.BitwiseAnd(satMask, valMask, &cloudMask)
totalPixels := cloudMask.Rows() * cloudMask.Cols()
if totalPixels == 0 {
return 0, fmt.Errorf("no pixels found in image")
}
cloudPixels := gocv.CountNonZero(cloudMask)
cloudCover := 100 * float64(cloudPixels) / float64(totalPixels)
return cloudCover, nil
}
func updateCache(url string) {
cover, err := estimateCloudCover(url)
mu.Lock()
defer mu.Unlock()
result := CloudResult{
URL: url,
CloudCover: cover,
LastUpdated: time.Now().Format(time.RFC3339),
}
if err != nil {
result.Error = err.Error()
}
cache[url] = result
}
func startAutoRefresh(url string) {
go func() {
for {
updateCache(url)
time.Sleep(refreshFreq)
}
}()
}
func handleCloudCover(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
if url == "" {
url = "http://clouds.service:8081/snapshot"
}
mu.Lock()
result, ok := cache[url]
mu.Unlock()
if !ok {
// first-time fetch triggers background updates
updateCache(url)
startAutoRefresh(url)
mu.Lock()
result = cache[url]
mu.Unlock()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
func main() {
http.HandleFunc("/cloudcover", handleCloudCover)
http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
fmt.Printf("☁️ Cloud cover API running on http://localhost:8080\n")
fmt.Printf(" Example: curl 'http://localhost:8080/cloudcover?url=http://clouds.local:8082/snapshot'\n")
log.Fatal(http.ListenAndServe(":8080", nil))
}