package v2 import ( "fmt" "predictor-refactored/internal/engine" "predictor-refactored/internal/weather" ) // buildProfile translates a PredictionRequest into an engine.Profile. // // elev may be nil when no terrain dataset is loaded; TerrainContact constraints // will return an error in that case. func buildProfile(req PredictionRequest, field weather.WindField, elev engine.TerrainProvider, warnings *engine.Warnings) (engine.Profile, error) { if len(req.Profile) == 0 { return engine.Profile{}, fmt.Errorf("profile must contain at least one stage") } step := req.Options.StepSeconds if step == 0 { step = 60 } tol := req.Options.Tolerance if tol == 0 { tol = 0.01 } dir := engine.Forward switch req.Direction { case "", "forward": dir = engine.Forward case "reverse": dir = engine.Reverse default: return engine.Profile{}, fmt.Errorf("unknown direction %q", req.Direction) } props := make([]*engine.Propagator, len(req.Profile)) for i, stage := range req.Profile { model, err := buildModel(stage.Model, field, warnings) if err != nil { return engine.Profile{}, fmt.Errorf("stage %q: %w", stage.Name, err) } constraints, err := buildConstraints(stage.Constraints, elev) if err != nil { return engine.Profile{}, fmt.Errorf("stage %q: %w", stage.Name, err) } props[i] = &engine.Propagator{ Name: stage.Name, Step: step, Model: model, Constraints: constraints, Tolerance: tol, } } // Wire fallbacks once all stages exist. for i, stage := range req.Profile { if stage.FallbackIndex == nil { continue } idx := *stage.FallbackIndex if idx < 0 || idx >= len(props) { return engine.Profile{}, fmt.Errorf("stage %q: fallback_index %d out of range", stage.Name, idx) } props[i].Fallback = props[idx] } return engine.Profile{Stages: props, Direction: dir}, nil } func buildModel(spec ModelSpec, field weather.WindField, warnings *engine.Warnings) (engine.Model, error) { var base engine.Model switch spec.Type { case "constant_rate": base = engine.ConstantRate(spec.Rate) case "parachute_descent": if spec.SeaLevelRate <= 0 { return nil, fmt.Errorf("parachute_descent requires positive sea_level_rate") } base = engine.ParachuteDescent(spec.SeaLevelRate) case "piecewise": segs := make([]engine.RateSegment, len(spec.Segments)) for i, s := range spec.Segments { segs[i] = engine.RateSegment{Until: s.Until, Rate: s.Rate} } base = engine.Piecewise(segs) case "wind": if field == nil { return nil, fmt.Errorf("wind model requires a loaded dataset") } return engine.WindTransport(field, warnings), nil default: return nil, fmt.Errorf("unknown model type %q", spec.Type) } if spec.IncludeWind { if field == nil { return nil, fmt.Errorf("include_wind requires a loaded dataset") } return engine.Sum(base, engine.WindTransport(field, warnings)), nil } return base, nil } func buildConstraints(specs []ConstraintSpec, elev engine.TerrainProvider) ([]engine.Constraint, error) { out := make([]engine.Constraint, 0, len(specs)) for _, spec := range specs { action, err := parseAction(spec.Action) if err != nil { return nil, err } var c engine.Constraint switch spec.Type { case "max_altitude": c = engine.MaxAltitude{Limit: spec.Limit, On: action} case "min_altitude": c = engine.MinAltitude{Limit: spec.Limit, On: action} case "max_time": c = engine.MaxTime{Limit: spec.Limit, On: action} case "terrain_contact": if elev == nil { return nil, fmt.Errorf("terrain_contact requires an elevation dataset") } c = engine.TerrainContact{Provider: elev, On: action} default: return nil, fmt.Errorf("unknown constraint type %q", spec.Type) } out = append(out, c) } return out, nil } func parseAction(s string) (engine.Action, error) { switch s { case "", "stop": return engine.ActionStop, nil case "fallback": return engine.ActionFallback, nil case "clip": return engine.ActionClip, nil default: return 0, fmt.Errorf("unknown constraint action %q", s) } }