predictor/internal/api/mapping.go

217 lines
6.4 KiB
Go

package api
import (
"fmt"
"time"
"predictor-refactored/internal/api/async"
"predictor-refactored/internal/engine"
apirest "predictor-refactored/pkg/rest"
)
// normalizeLng folds a longitude into [0, 360) for internal use.
func normalizeLng(lng float64) float64 {
if lng < 0 {
return lng + 360
}
return lng
}
// signedLng converts an internal [0, 360) longitude back to [-180, 180).
func signedLng(lng float64) float64 {
if lng > 180 {
return lng - 360
}
return lng
}
// buildProfile translates an API prediction request into an engine profile
// using the engine's model/constraint registry.
// maxProfileStages bounds the propagator chain length to keep a single
// request's work bounded.
const maxProfileStages = 32
func buildProfile(req *apirest.PredictionV2Request, deps engine.BuildDeps) (engine.Profile, error) {
if len(req.Profile) == 0 {
return engine.Profile{}, fmt.Errorf("profile must contain at least one stage")
}
if len(req.Profile) > maxProfileStages {
return engine.Profile{}, fmt.Errorf("profile has %d stages; maximum is %d", len(req.Profile), maxProfileStages)
}
step := 60.0
tol := 0.01
if o, ok := req.Options.Get(); ok {
step = o.StepSeconds.Or(step)
tol = o.Tolerance.Or(tol)
}
if step <= 0 || step > 3600 {
return engine.Profile{}, fmt.Errorf("options.step_seconds must be in (0, 3600], got %g", step)
}
if tol <= 0 || tol >= 1 {
return engine.Profile{}, fmt.Errorf("options.tolerance must be in (0, 1), got %g", tol)
}
dir := engine.Forward
if req.Direction.Or(apirest.PredictionV2RequestDirectionForward) == apirest.PredictionV2RequestDirectionReverse {
dir = engine.Reverse
}
props := make([]*engine.Propagator, len(req.Profile))
for i, stage := range req.Profile {
if stage.Name == "" {
return engine.Profile{}, fmt.Errorf("stage %d: name is required", i)
}
built, err := engine.BuildModel(toEngineModelSpec(stage.Model), deps)
if err != nil {
return engine.Profile{}, fmt.Errorf("stage %q model: %w", stage.Name, err)
}
constraints, err := toEngineConstraints(stage.Constraints, deps)
if err != nil {
return engine.Profile{}, fmt.Errorf("stage %q: %w", stage.Name, err)
}
props[i] = &engine.Propagator{
Name: stage.Name,
Step: step,
Model: built.Model,
BuildModel: built.Build,
Constraints: constraints,
Tolerance: tol,
}
}
for i, stage := range req.Profile {
idx, ok := stage.FallbackIndex.Get()
if !ok {
continue
}
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]
}
globals, err := toEngineConstraints(req.Globals, deps)
if err != nil {
return engine.Profile{}, fmt.Errorf("globals: %w", err)
}
return engine.Profile{Stages: props, Direction: dir, Globals: globals}, nil
}
func toEngineModelSpec(m apirest.ModelSpec) engine.ModelSpec {
out := engine.ModelSpec{
Type: string(m.Type),
Rate: m.Rate.Or(0),
SeaLevelRate: m.SeaLevelRate.Or(0),
IncludeWind: m.IncludeWind.Or(false),
}
for _, s := range m.Segments {
out.Segments = append(out.Segments, engine.PiecewiseSegmentSpec{
Until: s.Until,
Rate: s.Rate,
Reference: string(s.Reference.Or(apirest.PiecewiseSegmentReferenceAbsolute)),
})
}
return out
}
func toEngineConstraints(specs []apirest.ConstraintSpec, deps engine.BuildDeps) ([]engine.Constraint, error) {
out := make([]engine.Constraint, 0, len(specs))
for i, s := range specs {
c, err := engine.BuildConstraint(toEngineConstraintSpec(s), deps)
if err != nil {
return nil, fmt.Errorf("constraint[%d]: %w", i, err)
}
out = append(out, c)
}
return out, nil
}
func toEngineConstraintSpec(c apirest.ConstraintSpec) engine.ConstraintSpec {
spec := engine.ConstraintSpec{
Type: string(c.Type),
Op: string(c.Op.Or("")),
Limit: c.Limit.Or(0),
Action: string(c.Action.Or(apirest.ConstraintSpecActionStop)),
Mode: string(c.Mode.Or("")),
Label: c.Label.Or(""),
}
for _, v := range c.Vertices {
spec.Vertices = append(spec.Vertices, engine.PolygonVertex{Lat: v.Lat, Lng: v.Lng})
}
return spec
}
// stageResultToAPI maps one engine stage result to the API representation.
func stageResultToAPI(r engine.Result) apirest.StageResult {
out := apirest.StageResult{
Name: r.Propagator,
Outcome: apirest.StageResultOutcome(r.Outcome.String()),
Events: eventsToAPI(r.Events),
}
if r.Constraint != nil {
out.Constraint = apirest.NewOptString(r.ConstraintName)
out.Termination = apirest.NewOptTerminationInfo(apirest.TerminationInfo{
ViolationTime: time.Unix(int64(r.ViolationTime), 0).UTC(),
ViolationState: geoStateToAPI(r.ViolationState),
RefinedTime: time.Unix(int64(r.RefinedTime), 0).UTC(),
RefinedState: geoStateToAPI(r.RefinedState),
})
}
n := r.Path.Len()
out.Trajectory = make([]apirest.TrajectoryPoint, n)
for i := range n {
t, p := r.Path.At(i)
out.Trajectory[i] = apirest.TrajectoryPoint{
Time: time.Unix(int64(t), 0).UTC(),
Latitude: p.Lat,
Longitude: signedLng(p.Lng),
Altitude: p.Altitude,
}
}
return out
}
func geoStateToAPI(s engine.State) apirest.GeoState {
return apirest.GeoState{Lat: s.Lat, Lng: signedLng(s.Lng), Altitude: s.Altitude}
}
func eventsToAPI(in []engine.EventSummary) []apirest.EventSummary {
if len(in) == 0 {
return nil
}
out := make([]apirest.EventSummary, 0, len(in))
for _, e := range in {
out = append(out, apirest.EventSummary{
Type: e.Type,
Count: e.Count,
FirstTime: apirest.NewOptFloat64(e.FirstTime),
LastTime: apirest.NewOptFloat64(e.LastTime),
FirstState: apirest.NewOptGeoState(geoStateToAPI(e.FirstState)),
LastState: apirest.NewOptGeoState(geoStateToAPI(e.LastState)),
Message: apirest.NewOptString(e.Message),
})
}
return out
}
// asyncJobToAPI maps an async job snapshot to the API PredictionJob.
func asyncJobToAPI(info async.JobInfo) *apirest.PredictionJob {
job := &apirest.PredictionJob{
ID: info.ID,
Status: apirest.PredictionJobStatus(info.Status),
CreatedAt: info.CreatedAt,
}
if info.StartedAt != nil {
job.StartedAt = apirest.NewOptDateTime(*info.StartedAt)
}
if info.CompletedAt != nil {
job.CompletedAt = apirest.NewOptDateTime(*info.CompletedAt)
}
if info.Error != "" {
job.Error = apirest.NewOptString(info.Error)
}
if info.Result != nil {
job.Result = apirest.NewOptPredictionV2Response(*info.Result)
}
return job
}