216 lines
5.5 KiB
Go
216 lines
5.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"predictor-refactored/internal/prediction"
|
|
api "predictor-refactored/pkg/rest"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var _ api.Handler = (*Handler)(nil)
|
|
|
|
// Handler implements the ogen-generated api.Handler interface.
|
|
type Handler struct {
|
|
svc Service
|
|
log *zap.Logger
|
|
}
|
|
|
|
// New creates a new Handler.
|
|
func New(svc Service, log *zap.Logger) *Handler {
|
|
return &Handler{svc: svc, log: log}
|
|
}
|
|
|
|
// PerformPrediction implements the prediction endpoint.
|
|
func (h *Handler) PerformPrediction(ctx context.Context, params api.PerformPredictionParams) (*api.PredictionResponse, error) {
|
|
if !h.svc.Ready() {
|
|
return nil, newError(http.StatusServiceUnavailable, "no dataset loaded, service is starting up")
|
|
}
|
|
|
|
ds := h.svc.Dataset()
|
|
if ds == nil {
|
|
return nil, newError(http.StatusServiceUnavailable, "dataset unavailable")
|
|
}
|
|
|
|
dsEpoch := float64(ds.DSTime.Unix())
|
|
|
|
// Parse parameters with defaults
|
|
profile := "standard_profile"
|
|
if p, ok := params.Profile.Get(); ok {
|
|
profile = string(p)
|
|
}
|
|
|
|
ascentRate := 5.0
|
|
if v, ok := params.AscentRate.Get(); ok {
|
|
ascentRate = v
|
|
}
|
|
|
|
burstAltitude := 28000.0
|
|
if v, ok := params.BurstAltitude.Get(); ok {
|
|
burstAltitude = v
|
|
}
|
|
|
|
descentRate := 5.0
|
|
if v, ok := params.DescentRate.Get(); ok {
|
|
descentRate = v
|
|
}
|
|
|
|
launchAlt := 0.0
|
|
if v, ok := params.LaunchAltitude.Get(); ok {
|
|
launchAlt = v
|
|
}
|
|
|
|
// Normalize longitude to [0, 360)
|
|
lng := params.LaunchLongitude
|
|
if lng < 0 {
|
|
lng += 360.0
|
|
}
|
|
|
|
launchTime := float64(params.LaunchDatetime.Unix())
|
|
|
|
warnings := &prediction.Warnings{}
|
|
|
|
// Build profile chain
|
|
elev := h.svc.Elevation()
|
|
var stages []prediction.Stage
|
|
switch profile {
|
|
case "standard_profile":
|
|
stages = prediction.StandardProfile(
|
|
ascentRate, burstAltitude, descentRate,
|
|
ds, dsEpoch, warnings, elev)
|
|
case "float_profile":
|
|
floatAlt := 25000.0
|
|
if v, ok := params.FloatAltitude.Get(); ok {
|
|
floatAlt = v
|
|
}
|
|
stopTime := params.LaunchDatetime.Add(24 * time.Hour)
|
|
if v, ok := params.StopDatetime.Get(); ok {
|
|
stopTime = v
|
|
}
|
|
stages = prediction.FloatProfile(
|
|
ascentRate, floatAlt, stopTime,
|
|
ds, dsEpoch, warnings)
|
|
default:
|
|
return nil, newError(http.StatusBadRequest, "unknown profile: "+profile)
|
|
}
|
|
|
|
// Run prediction
|
|
startTime := time.Now().UTC()
|
|
results := prediction.RunPrediction(launchTime, params.LaunchLatitude, lng, launchAlt, stages)
|
|
completeTime := time.Now().UTC()
|
|
|
|
// Build response
|
|
stageNames := []string{"ascent", "descent"}
|
|
if profile == "float_profile" {
|
|
stageNames = []string{"ascent", "float"}
|
|
}
|
|
|
|
var predItems []api.PredictionResponsePredictionItem
|
|
for i, sr := range results {
|
|
stageName := "ascent"
|
|
if i < len(stageNames) {
|
|
stageName = stageNames[i]
|
|
}
|
|
|
|
var stageEnum api.PredictionResponsePredictionItemStage
|
|
switch stageName {
|
|
case "ascent":
|
|
stageEnum = api.PredictionResponsePredictionItemStageAscent
|
|
case "descent":
|
|
stageEnum = api.PredictionResponsePredictionItemStageDescent
|
|
case "float":
|
|
stageEnum = api.PredictionResponsePredictionItemStageFloat
|
|
}
|
|
|
|
var traj []api.PredictionResponsePredictionItemTrajectoryItem
|
|
for _, pt := range sr.Points {
|
|
ptLng := pt.Lng
|
|
if ptLng > 180 {
|
|
ptLng -= 360
|
|
}
|
|
traj = append(traj, api.PredictionResponsePredictionItemTrajectoryItem{
|
|
Datetime: time.Unix(int64(pt.T), 0).UTC(),
|
|
Latitude: pt.Lat,
|
|
Longitude: ptLng,
|
|
Altitude: pt.Alt,
|
|
})
|
|
}
|
|
|
|
predItems = append(predItems, api.PredictionResponsePredictionItem{
|
|
Stage: stageEnum,
|
|
Trajectory: traj,
|
|
})
|
|
}
|
|
|
|
resp := &api.PredictionResponse{
|
|
Prediction: predItems,
|
|
Metadata: api.PredictionResponseMetadata{
|
|
StartDatetime: startTime,
|
|
CompleteDatetime: completeTime,
|
|
},
|
|
}
|
|
|
|
// Echo request
|
|
resp.Request = api.NewOptPredictionResponseRequest(api.PredictionResponseRequest{
|
|
Dataset: api.NewOptString(ds.DSTime.Format("2006-01-02T15:04:05Z")),
|
|
LaunchLatitude: api.NewOptFloat64(params.LaunchLatitude),
|
|
LaunchLongitude: api.NewOptFloat64(params.LaunchLongitude),
|
|
LaunchDatetime: api.NewOptString(params.LaunchDatetime.Format(time.RFC3339)),
|
|
LaunchAltitude: params.LaunchAltitude,
|
|
})
|
|
|
|
// Warnings
|
|
warnMap := warnings.ToMap()
|
|
if len(warnMap) > 0 {
|
|
resp.Warnings = api.NewOptPredictionResponseWarnings(api.PredictionResponseWarnings{})
|
|
}
|
|
|
|
h.log.Info("prediction complete",
|
|
zap.String("profile", profile),
|
|
zap.Int("stages", len(results)),
|
|
zap.Duration("elapsed", completeTime.Sub(startTime)))
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// ReadinessCheck implements the health check endpoint.
|
|
func (h *Handler) ReadinessCheck(ctx context.Context) (*api.ReadinessResponse, error) {
|
|
resp := &api.ReadinessResponse{}
|
|
|
|
if h.svc.Ready() {
|
|
resp.Status = api.ReadinessResponseStatusOk
|
|
if dsTime, ok := h.svc.DatasetTime(); ok {
|
|
resp.DatasetTime = api.NewOptDateTime(dsTime)
|
|
}
|
|
} else {
|
|
resp.Status = api.ReadinessResponseStatusNotReady
|
|
resp.ErrorMessage = api.NewOptString("no dataset loaded")
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// NewError creates an ErrorStatusCode from an error returned by a handler.
|
|
func (h *Handler) NewError(ctx context.Context, err error) *api.ErrorStatusCode {
|
|
if statusErr, ok := err.(*api.ErrorStatusCode); ok {
|
|
return statusErr
|
|
}
|
|
|
|
h.log.Error("unhandled error", zap.Error(err))
|
|
return newError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
func newError(status int, description string) *api.ErrorStatusCode {
|
|
return &api.ErrorStatusCode{
|
|
StatusCode: status,
|
|
Response: api.Error{
|
|
Error: api.ErrorError{
|
|
Type: http.StatusText(status),
|
|
Description: description,
|
|
},
|
|
},
|
|
}
|
|
}
|