151 lines
3.7 KiB
Go
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))
|
|
}
|