diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2026-02-01 13:24:19 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2026-02-01 13:24:19 +0200 |
| commit | c9cc45a8c97496a3996df968360f096cf7172407 (patch) | |
| tree | 7fb09bd4f2354f357009b9d20a4c45f568c30ff0 | |
| download | weather-c9cc45a8c97496a3996df968360f096cf7172407.tar.zst | |
Initial
| -rw-r--r-- | main.go | 417 |
1 files changed, 417 insertions, 0 deletions
@@ -0,0 +1,417 @@ +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)) +} |
