diff options
Diffstat (limited to 'main.go')
| -rw-r--r-- | main.go | 401 |
1 files changed, 7 insertions, 394 deletions
@@ -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¶meters=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)) } |
