// 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 }