summaryrefslogtreecommitdiffstats
path: root/parser.go
blob: 6cb67e80213618a6abea40cdbb942506350ecc66 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// parser.go - XML parsing and data extraction
package main

import (
	"encoding/xml"
	"fmt"
	"math"
	"strconv"
	"strings"
)

// ParseForecastXML parses XML response into ForecastData
func ParseForecastXML(xmlData []byte) (*ForecastData, error) {
	var fc FeatureCollection
	if err := xml.Unmarshal(xmlData, &fc); err != nil {
		return nil, fmt.Errorf("unmarshaling XML: %w", err)
	}

	if len(fc.Members) == 0 {
		return nil, fmt.Errorf("no data found in response")
	}

	mpc := fc.Members[0].GridSeriesObservation.Result.MultiPointCoverage

	// Parse positions
	lat, lon, times, err := parsePositions(mpc.DomainSet.SimpleMultiPoint.Positions)
	if err != nil {
		return nil, fmt.Errorf("parsing positions: %w", err)
	}

	// Parse parameters
	params := make([]string, 0, len(mpc.RangeType.DataRecord.Fields))
	paramIndex := make(map[string]int)
	for i, f := range mpc.RangeType.DataRecord.Fields {
		params = append(params, f.Name)
		paramIndex[f.Name] = i
	}

	// Parse values
	values, err := parseTupleList(mpc.RangeSet.DataBlock.TupleList, len(times), len(params))
	if err != nil {
		return nil, fmt.Errorf("parsing values: %w", err)
	}

	return &ForecastData{
		Latitude:   lat,
		Longitude:  lon,
		Timezone:   "Europe/Helsinki", // Default, could be parsed from XML
		Timestamps: times,
		Parameters: params,
		Values:     values,
		ParamIndex: paramIndex,
	}, nil
}

func parsePositions(positionsStr string) (lat, lon float64, times []int64, err error) {
	lines := strings.Split(strings.TrimSpace(positionsStr), "\n")

	for i, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		parts := strings.Fields(line)
		if len(parts) != 3 {
			return 0, 0, nil, fmt.Errorf("invalid position format at line %d: %s", i, line)
		}

		parsedLat, err := strconv.ParseFloat(parts[0], 64)
		if err != nil {
			return 0, 0, nil, fmt.Errorf("parsing latitude: %w", err)
		}

		parsedLon, err := strconv.ParseFloat(parts[1], 64)
		if err != nil {
			return 0, 0, nil, fmt.Errorf("parsing longitude: %w", err)
		}

		timestamp, err := strconv.ParseInt(parts[2], 10, 64)
		if err != nil {
			return 0, 0, nil, fmt.Errorf("parsing timestamp: %w", err)
		}

		if i == 0 {
			lat, lon = parsedLat, parsedLon
		} else if math.Abs(parsedLat-lat) > 0.001 || math.Abs(parsedLon-lon) > 0.001 {
			return 0, 0, nil, fmt.Errorf("inconsistent location at line %d", i)
		}

		times = append(times, timestamp)
	}

	if len(times) == 0 {
		return 0, 0, nil, fmt.Errorf("no positions found")
	}

	return lat, lon, times, nil
}

func parseTupleList(tupleStr string, numPoints, numParams int) ([][]float64, error) {
	tupleStr = strings.TrimSpace(tupleStr)
	valuesStr := strings.Fields(tupleStr)

	if len(valuesStr) != numPoints*numParams {
		return nil, fmt.Errorf("data mismatch: expected %d values, got %d",
			numPoints*numParams, len(valuesStr))
	}

	values := make([][]float64, numPoints)
	for i := 0; i < numPoints; i++ {
		row := make([]float64, numParams)
		for j := 0; j < numParams; j++ {
			valStr := valuesStr[i*numParams+j]
			if valStr == "NaN" {
				row[j] = math.NaN()
			} else {
				val, err := strconv.ParseFloat(valStr, 64)
				if err != nil {
					return nil, fmt.Errorf("parsing value at [%d][%d]: %w", i, j, err)
				}
				row[j] = val
			}
		}
		values[i] = row
	}

	return values, nil
}