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¶meters=Temperature,PrecipitationAmount,Humidity,Pressure,DewPointTemperature,WindSpeedMS,WindDirection,WindGust,TotalCloudCover,HorizontalVisibility,WeatherSymbol3", *location) resp, err := http.Get(url) if err != nil { fmt.Fprintf(os.Stderr, "Error fetching data: %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) if err != nil { fmt.Fprintf(os.Stderr, "Error marshaling JSON: %v\n", err) os.Exit(1) } fmt.Println(string(jsonData)) }