217 lines
6.4 KiB
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
|
|
}
|