mirror of
https://github.com/Sosokker/openweathermap-dashboard.git
synced 2025-12-18 13:44:04 +01:00
184 lines
4.2 KiB
Go
184 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
type Coordinate struct {
|
|
Place string `json:"place"`
|
|
Lon float32 `json:"lon"`
|
|
Lat float32 `json:"lat"`
|
|
}
|
|
|
|
type Weather struct {
|
|
Id int `json:"id"`
|
|
Main string `json:"main"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type DataEntry struct {
|
|
Name string `json:"name"`
|
|
Coord Coordinate `json:"coord"`
|
|
Weather []Weather `json:"weather"`
|
|
Rain struct {
|
|
PerHour float32 `json:"1h"`
|
|
} `json:"rain"`
|
|
State string `json:"state,omitempty"`
|
|
}
|
|
|
|
func (c Coordinate) String() string {
|
|
return fmt.Sprintf("(%f, %f, %s)", c.Lat, c.Lon, c.Place)
|
|
}
|
|
|
|
func (w Weather) String() string {
|
|
return fmt.Sprintf("(%d, %s, %s)", w.Id, w.Main, w.Description)
|
|
}
|
|
|
|
func (d DataEntry) String() string {
|
|
return fmt.Sprintf("Place Name: %s\nCoordinate: %s\nWeather: %s\nRain per Hour: %f\n", d.Name, d.Coord, d.Weather, d.Rain.PerHour)
|
|
}
|
|
|
|
func (d *DataEntry) normalizeRainPerHour(min, max float32) {
|
|
if max == 0 {
|
|
return
|
|
}
|
|
d.Rain.PerHour = (float32(d.Rain.PerHour) - min) / (max - min) * 100
|
|
}
|
|
|
|
func loadEnv() string {
|
|
apiKeys, err := godotenv.Read()
|
|
if err != nil {
|
|
log.Fatalln("Error: can't load environment or .env file")
|
|
}
|
|
|
|
apiKey, exist := apiKeys["OPENWEATHERMAP_API_KEY"]
|
|
if !exist {
|
|
log.Fatalln("Error: OPENWEATHERMAP_API_KEY does not existed!")
|
|
}
|
|
if strings.TrimSpace(apiKey) == "" {
|
|
log.Fatalln("Error: OPENWEATHERMAP_API_KEY can't be empty!")
|
|
}
|
|
return apiKey
|
|
}
|
|
|
|
// Fetch weather data with specific latitude and longitude and decode into struct
|
|
func fetchWeatherData(lat, lon float32, apiKey string) DataEntry {
|
|
var url = fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%s", lat, lon, strings.TrimSpace(apiKey))
|
|
fmt.Println(url)
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
log.Fatalln("Error:", err)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
log.Fatalln("Error: Status not okay: ", resp.StatusCode)
|
|
}
|
|
|
|
var entry DataEntry
|
|
err = json.NewDecoder(resp.Body).Decode(&entry)
|
|
if err != nil {
|
|
log.Fatalln("Error: error decode json to struct", err)
|
|
}
|
|
|
|
return entry
|
|
}
|
|
|
|
func readCoordData(filepath string, coordCh chan<- Coordinate) {
|
|
file, err := os.Open(filepath)
|
|
if err != nil {
|
|
log.Fatalln("Error: unable to read file", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
r := csv.NewReader(file)
|
|
for {
|
|
line, err := r.Read()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
log.Fatalln("Error: error while reading file", err)
|
|
}
|
|
|
|
place, lat, lon := line[0], line[1], line[2]
|
|
place = strings.TrimSpace(place)
|
|
latf, err := strconv.ParseFloat(strings.TrimSpace(lat), 32)
|
|
if err != nil {
|
|
// log.Fatalln("Error: unable to convert string to float", err)
|
|
continue
|
|
}
|
|
|
|
lonf, err := strconv.ParseFloat(strings.TrimSpace(lon), 32)
|
|
if err != nil {
|
|
// log.Fatalln("Error: unable to convert string to float", err)
|
|
continue
|
|
}
|
|
|
|
coordCh <- Coordinate{place, float32(lonf), float32(latf)}
|
|
}
|
|
close(coordCh)
|
|
}
|
|
|
|
// https://www.stackhawk.com/blog/golang-cors-guide-what-it-is-and-how-to-enable-it/
|
|
func enableCors(w *http.ResponseWriter) {
|
|
(*w).Header().Set("Access-Control-Allow-Origin", "*")
|
|
}
|
|
|
|
// lat, lon, rain+place
|
|
// state-id, rainfall (scale 100)
|
|
|
|
func rawDataHandler(w http.ResponseWriter, r *http.Request) {
|
|
enableCors(&w)
|
|
apiKey := loadEnv()
|
|
|
|
coordCh := make(chan Coordinate)
|
|
go readCoordData("data/data.csv", coordCh)
|
|
|
|
var max float32 = 0.0
|
|
var min float32 = 10000000.0
|
|
|
|
item := strings.TrimSpace(r.URL.Query().Get("scale"))
|
|
scale, _ := strconv.ParseBool(item)
|
|
|
|
var entries []DataEntry
|
|
for coord := range coordCh {
|
|
|
|
entry := fetchWeatherData(coord.Lat, coord.Lon, apiKey)
|
|
entry.Coord.Place = coord.Place // fetched data not have place field
|
|
if entry.Rain.PerHour > max {
|
|
max = entry.Rain.PerHour
|
|
}
|
|
if entry.Rain.PerHour < min {
|
|
min = entry.Rain.PerHour
|
|
}
|
|
|
|
if scale {
|
|
entry.normalizeRainPerHour(min, max)
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
if err := json.NewEncoder(w).Encode(&entries); err != nil {
|
|
http.Error(w, "Failed to encode JSON", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
http.HandleFunc("/api/data", rawDataHandler) // /api/data?scale=100
|
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
|
}
|