summaryrefslogtreecommitdiffstats
path: root/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go401
1 files changed, 7 insertions, 394 deletions
diff --git a/main.go b/main.go
index b761844..c5008be 100644
--- a/main.go
+++ b/main.go
@@ -1,417 +1,30 @@
+// main.go - Main application entry point
package main
import (
"encoding/json"
- "encoding/xml"
"flag"
"fmt"
- "io"
- "math"
- "net/http"
"os"
- "strconv"
- "strings"
- "time"
)
-type FeatureCollection struct {
- XMLName xml.Name `xml:"http://www.opengis.net/wfs/2.0 FeatureCollection"`
- Member []Member `xml:"http://www.opengis.net/wfs/2.0 member"`
-}
-
-type Member struct {
- Observation Observation `xml:"http://www.opengis.net/om/2.0 OM_Observation"`
-}
-
-type Observation struct {
- Result Result `xml:"http://www.opengis.net/om/2.0 result"`
-}
-
-type Result struct {
- MultiPointCoverage MultiPointCoverage `xml:"http://www.opengis.net/gmlcov/1.0 MultiPointCoverage"`
-}
-
-type MultiPointCoverage struct {
- DomainSet DomainSet `xml:"http://www.opengis.net/gml/3.2 domainSet"`
- RangeSet RangeSet `xml:"http://www.opengis.net/gml/3.2 rangeSet"`
- RangeType RangeType `xml:"http://www.opengis.net/gmlcov/1.0 rangeType"`
-}
-
-type DomainSet struct {
- MultiPoint MultiPoint `xml:"http://www.opengis.net/gml/3.2 MultiPoint"`
-}
-
-type MultiPoint struct {
- PosList string `xml:"http://www.opengis.net/gml/3.2 posList"`
-}
-
-type RangeSet struct {
- DataBlock DataBlock `xml:"http://www.opengis.net/gml/3.2 DataBlock"`
-}
-
-type DataBlock struct {
- TupleList string `xml:"http://www.opengis.net/gml/3.2 doubleOrNilReasonTupleList"`
-}
-
-type RangeType struct {
- DataRecord DataRecord `xml:"http://www.opengis.net/swe/2.0 DataRecord"`
-}
-
-type DataRecord struct {
- Field []Field `xml:"http://www.opengis.net/swe/2.0 field"`
-}
-
-type Field struct {
- Name string `xml:"name,attr"`
-}
-
-type OWMResponse struct {
- Lat float64 `json:"lat"`
- Lon float64 `json:"lon"`
- Timezone string `json:"timezone"`
- TimezoneOffset int `json:"timezone_offset"`
- Current Current `json:"current"`
- Hourly []Current `json:"hourly"`
-}
-
-type Current struct {
- Dt int64 `json:"dt"`
- Sunrise int64 `json:"sunrise,omitempty"`
- Sunset int64 `json:"sunset,omitempty"`
- Temp float64 `json:"temp"`
- FeelsLike float64 `json:"feels_like"`
- Pressure int `json:"pressure"`
- Humidity int `json:"humidity"`
- DewPoint float64 `json:"dew_point"`
- Uvi float64 `json:"uvi"`
- Clouds int `json:"clouds"`
- Visibility int `json:"visibility"`
- WindSpeed float64 `json:"wind_speed"`
- WindDeg int `json:"wind_deg"`
- WindGust float64 `json:"wind_gust"`
- Weather []Weather `json:"weather"`
- Rain *Rain `json:"rain,omitempty"`
-}
-
-type Rain struct {
- OneH float64 `json:"1h"`
-}
-
-type Weather struct {
- ID int `json:"id"`
- Main string `json:"main"`
- Description string `json:"description"`
- Icon string `json:"icon"`
-}
-
-// Parameters for the sunrise and sunset calculation
-type Parameters struct {
- Latitude float64
- Longitude float64
- UtcOffset float64
- Date time.Time
-}
-
-// GetSunriseSunset calculates the sunrise and sunset times
-func (p Parameters) GetSunriseSunset() (time.Time, time.Time, error) {
- // Convert the date to julian day
- julianDay := p.DateToJulianDay()
-
- // Calculate the julian century
- julianCentury := (julianDay - 2451545) / 36525.0
-
- // Geom mean long sun (deg)
- geomMeanLongSun := math.Mod(280.46646+julianCentury*(36000.76983+julianCentury*0.0003032), 360)
-
- // Geom mean anom sun (deg)
- geomMeanAnomSun := 357.52911 + julianCentury*(35999.05029-0.0001537*julianCentury)
-
- // Eccent earth orbit
- eccentEarthOrbit := 0.016708634 - julianCentury*(0.000042037+0.0000001267*julianCentury)
-
- // Sun eq of ctr
- sunEqOfCtr := math.Sin(degToRad(geomMeanAnomSun))*(1.914602-julianCentury*(0.004817+0.000014*julianCentury)) +
- math.Sin(degToRad(2*geomMeanAnomSun))*(0.019993-0.000101*julianCentury) +
- math.Sin(degToRad(3*geomMeanAnomSun))*0.000289
-
- // Sun true long (deg)
- sunTrueLong := geomMeanLongSun + sunEqOfCtr
-
- // Sun app long (deg)
- sunAppLong := sunTrueLong - 0.00569 - 0.00478*math.Sin(degToRad(125.04-1934.136*julianCentury))
-
- // Mean obliq ecliptic (deg)
- meanObliqEcliptic := 23 + (26+(21.448-julianCentury*(46.815+julianCentury*(0.00059-julianCentury*0.001813)))/60)/60
-
- // Obliq corr (deg)
- obliqCorr := meanObliqEcliptic + 0.00256*math.Cos(degToRad(125.04-1934.136*julianCentury))
-
- // Sun declin (deg)
- sunDeclin := radToDeg(math.Asin(math.Sin(degToRad(obliqCorr)) * math.Sin(degToRad(sunAppLong))))
-
- // Var y
- varY := math.Tan(degToRad(obliqCorr/2)) * math.Tan(degToRad(obliqCorr/2))
-
- // Eq of time (minutes)
- eqOfTime := 4 * radToDeg(varY*math.Sin(2*degToRad(geomMeanLongSun))-
- 2*eccentEarthOrbit*math.Sin(degToRad(geomMeanAnomSun))+
- 4*eccentEarthOrbit*varY*math.Sin(degToRad(geomMeanAnomSun))*math.Cos(2*degToRad(geomMeanLongSun))-
- 0.5*varY*varY*math.Sin(4*degToRad(geomMeanLongSun))-
- 1.25*eccentEarthOrbit*eccentEarthOrbit*math.Sin(2*degToRad(geomMeanAnomSun)))
-
- // HA sunrise (deg)
- haSunrise := radToDeg(math.Acos(math.Cos(degToRad(90.833))/(math.Cos(degToRad(p.Latitude))*math.Cos(degToRad(sunDeclin))) - math.Tan(degToRad(p.Latitude))*math.Tan(degToRad(sunDeclin))))
-
- // Solar noon (LST)
- solarNoon := (720 - 4*p.Longitude - eqOfTime + p.UtcOffset*60) / 1440
-
- // Sunrise time (LST)
- sunriseTime := solarNoon - haSunrise*4/1440
-
- // Sunset time (LST)
- sunsetTime := solarNoon + haSunrise*4/1440
-
- // Convert the sunrise and sunset to time.Time
- sunrise := p.julianDayToDate(julianDay + sunriseTime)
- sunset := p.julianDayToDate(julianDay + sunsetTime)
-
- return sunrise, sunset, nil
-}
-
-// DateToJulianDay converts a date to julian day
-func (p Parameters) DateToJulianDay() float64 {
- year := float64(p.Date.Year())
- month := float64(p.Date.Month())
- day := float64(p.Date.Day())
-
- if month <= 2 {
- year -= 1
- month += 12
- }
-
- a := math.Floor(year / 100)
- b := 2 - a + math.Floor(a/4)
-
- return math.Floor(365.25*(year+4716)) + math.Floor(30.6001*(month+1)) + day + b - 1524.5
-}
-
-// julianDayToDate converts a julian day to date
-func (p Parameters) julianDayToDate(julianDay float64) time.Time {
- julianDay += 0.5
- z := math.Floor(julianDay)
- f := julianDay - z
-
- var a float64
- if z >= 2299161 {
- alpha := math.Floor((z - 1867216.25) / 36524.25)
- a = z + 1 + alpha - math.Floor(alpha/4)
- } else {
- a = z
- }
-
- b := a + 1524
- c := math.Floor((b - 122.1) / 365.25)
- d := math.Floor(365.25 * c)
- e := math.Floor((b - d) / 30.6001)
-
- day := b - d - math.Floor(30.6001*e) + f
- month := e - 1
- if month > 12 {
- month -= 12
- }
- year := c - 4715
- if month > 2 {
- year -= 1
- }
-
- hour := int(math.Floor((day - math.Floor(day)) * 24))
- minute := int(math.Floor(((day-math.Floor(day))*24 - float64(hour)) * 60))
- second := int(math.Floor((((day-math.Floor(day))*24-float64(hour))*60 - float64(minute)) * 60))
-
- return time.Date(int(year), time.Month(month), int(math.Floor(day)), hour, minute, second, 0, time.UTC).Add(time.Hour * time.Duration(p.UtcOffset))
-}
-
-func degToRad(deg float64) float64 {
- return deg * math.Pi / 180
-}
-
-func radToDeg(rad float64) float64 {
- return rad * 180 / math.Pi
-}
-
-// Map FMI WeatherSymbol3 to OWM weather
-var fmiToOwm = map[int]Weather{
- 1: {800, "Clear", "clear sky", "01"},
- 2: {801, "Clouds", "few clouds", "02"},
- 3: {802, "Clouds", "scattered clouds", "03"},
- 4: {803, "Clouds", "broken clouds", "04"},
- 5: {804, "Clouds", "overcast clouds", "04"},
- // Add more mappings as needed, e.g.
- 21: {520, "Rain", "light intensity shower rain", "09"},
- 22: {521, "Rain", "shower rain", "09"},
- 23: {522, "Rain", "heavy intensity shower rain", "09"},
- 30: {500, "Rain", "light rain", "10"},
- 31: {501, "Rain", "moderate rain", "10"},
- 32: {502, "Rain", "heavy intensity rain", "10"},
- // Snow
- 41: {600, "Snow", "light snow", "13"},
- // Thunder
- 91: {200, "Thunderstorm", "thunderstorm with light rain", "11"},
- // etc. Expand based on full list
-}
-
-// CalculateFeelsLike simple apparent temperature
-func calculateFeelsLike(temp, humidity float64, windSpeed float64) float64 {
- e := humidity / 100 * 6.105 * math.Exp(17.27*temp/(237.7+temp))
- return temp + 0.33*e - 0.70*windSpeed - 4.00
-}
-
func main() {
location := flag.String("place", "Helsinki", "Location for the forecast")
flag.Parse()
- url := fmt.Sprintf("https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::harmonie::surface::point::multipointcoverage&place=%s&parameters=Temperature,PrecipitationAmount,Humidity,Pressure,DewPointTemperature,WindSpeedMS,WindDirection,WindGust,TotalCloudCover,HorizontalVisibility,WeatherSymbol3", *location)
-
- resp, err := http.Get(url)
+ // Create client and get forecast
+ client := NewFMIClient()
+ response, err := client.GetForecast(*location)
if err != nil {
- fmt.Fprintf(os.Stderr, "Error fetching data: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
- defer resp.Body.Close()
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error reading response: %v\n", err)
- os.Exit(1)
- }
-
- var fc FeatureCollection
- err = xml.Unmarshal(body, &fc)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error parsing XML: %v\n", err)
- os.Exit(1)
- }
-
- if len(fc.Member) == 0 {
- fmt.Fprintf(os.Stderr, "No data found\n")
- os.Exit(1)
- }
-
- mpc := fc.Member[0].Observation.Result.MultiPointCoverage
-
- pos := strings.Fields(mpc.DomainSet.MultiPoint.PosList)
- if len(pos) < 3 {
- fmt.Fprintf(os.Stderr, "Invalid posList\n")
- os.Exit(1)
- }
-
- lat, _ := strconv.ParseFloat(pos[0], 64)
- lon, _ := strconv.ParseFloat(pos[1], 64)
-
- var times []int64
- for i := 2; i < len(pos); i += 3 {
- t, _ := strconv.ParseInt(pos[i], 10, 64)
- times = append(times, t)
- }
-
- params := []string{}
- for _, f := range mpc.RangeType.DataRecord.Field {
- params = append(params, f.Name)
- }
-
- paramIndex := make(map[string]int)
- for i, p := range params {
- paramIndex[p] = i
- }
-
- valsStr := strings.Fields(strings.TrimSpace(mpc.RangeSet.DataBlock.TupleList))
- numPoints := len(times)
- numParams := len(params)
- if len(valsStr) != numPoints*numParams {
- fmt.Fprintf(os.Stderr, "Data mismatch\n")
- os.Exit(1)
- }
-
- var values [][]float64
- for i := 0; i < numPoints; i++ {
- row := make([]float64, numParams)
- for j := 0; j < numParams; j++ {
- row[j], _ = strconv.ParseFloat(valsStr[i*numParams+j], 64)
- }
- values = append(values, row)
- }
-
- loc, _ := time.LoadLocation("Europe/Helsinki")
- firstDt := time.Unix(times[0], 0).UTC()
- _, offset := firstDt.In(loc).Zone()
-
- var response OWMResponse
- response.Lat = lat
- response.Lon = lon
- response.Timezone = "Europe/Helsinki"
- response.TimezoneOffset = offset
-
- // Calculate sunrise/sunset for the day of first forecast
- p := Parameters{
- Latitude: lat,
- Longitude: lon,
- UtcOffset: float64(offset) / 3600,
- Date: firstDt,
- }
- sunrise, sunset, _ := p.GetSunriseSunset()
- response.Current.Sunrise = sunrise.Unix()
- response.Current.Sunset = sunset.Unix()
-
- var hourly []Current
- for i := 0; i < numPoints; i++ {
- row := values[i]
- dt := times[i]
- var curr Current
- curr.Dt = dt
- curr.Temp = row[paramIndex["Temperature"]]
- curr.Humidity = int(row[paramIndex["Humidity"]])
- curr.Pressure = int(row[paramIndex["Pressure"]])
- curr.DewPoint = row[paramIndex["DewPointTemperature"]]
- curr.WindSpeed = row[paramIndex["WindSpeedMS"]]
- curr.WindDeg = int(row[paramIndex["WindDirection"]])
- curr.WindGust = row[paramIndex["WindGust"]]
- curr.Clouds = int(row[paramIndex["TotalCloudCover"]])
- curr.Visibility = int(row[paramIndex["HorizontalVisibility"]])
- curr.Uvi = 0 // Not available
- curr.FeelsLike = calculateFeelsLike(curr.Temp, float64(curr.Humidity), curr.WindSpeed)
-
- precip := row[paramIndex["PrecipitationAmount"]]
- if precip > 0 {
- curr.Rain = &Rain{OneH: precip}
- }
-
- symbol := int(row[paramIndex["WeatherSymbol3"]])
- w, ok := fmiToOwm[symbol]
- if !ok {
- w = Weather{741, "Fog", "unknown", "50"} // Default
- }
-
- // Day or night
- t := time.Unix(dt, 0).UTC()
- isDay := t.After(sunrise) && t.Before(sunset)
- if isDay {
- w.Icon += "d"
- } else {
- w.Icon += "n"
- }
- curr.Weather = []Weather{w}
-
- hourly = append(hourly, curr)
- }
-
- response.Current = hourly[0]
- response.Hourly = hourly
-
- jsonData, err := json.Marshal(response)
+ // Output JSON
+ jsonData, err := json.MarshalIndent(response, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marshaling JSON: %v\n", err)
os.Exit(1)
}
-
fmt.Println(string(jsonData))
}