fix(input): correct lon mapping
This commit is contained in:
parent
19557f3a62
commit
84d1664b29
20 changed files with 1197 additions and 137 deletions
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
|
|
@ -23,9 +24,45 @@ func (h *Handler) ReadinessCheck(_ context.Context) (*apirest.ReadinessResponse,
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
// fieldFor resolves the dataset a request asked for, or the active one.
|
||||
//
|
||||
// A request naming a dataset that is not stored is refused. The parameter used
|
||||
// to be declared in the spec and never read, so a client asking for a specific
|
||||
// forecast run — an archival date, a reproducible comparison — was silently
|
||||
// served from whatever happened to be loaded instead.
|
||||
func (h *Handler) fieldFor(ctx context.Context, want apirest.OptDateTime) (weather.WindField, error) {
|
||||
if epoch, ok := want.Get(); ok {
|
||||
field, err := h.mgr.FieldFor(ctx, epoch.UTC())
|
||||
if err != nil {
|
||||
return nil, apiError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
return field, nil
|
||||
}
|
||||
field := h.mgr.Active()
|
||||
if field == nil {
|
||||
return nil, apiError(http.StatusServiceUnavailable, "no dataset loaded, service is starting up")
|
||||
}
|
||||
return field, nil
|
||||
}
|
||||
|
||||
// runFailure turns an integration failure into a 400.
|
||||
//
|
||||
// The sampler reporting an error means it had no data — most often a launch time
|
||||
// past the dataset's horizon — so the trajectory is not a forecast. It used to be
|
||||
// discarded, and the run continued with zero wind: the balloon took off and
|
||||
// landed on the same spot, returned as a successful prediction.
|
||||
func runFailure(events *engine.EventSink, field weather.WindField) error {
|
||||
err := events.Err()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return apiError(http.StatusBadRequest,
|
||||
fmt.Sprintf("%s (dataset %s)", err, field.Epoch().UTC().Format(time.RFC3339)))
|
||||
}
|
||||
|
||||
// PerformPredictionV2 implements POST /api/v2/prediction.
|
||||
func (h *Handler) PerformPredictionV2(_ context.Context, req *apirest.PredictionV2Request) (*apirest.PredictionV2Response, error) {
|
||||
resp, err := h.runPredictionV2(req)
|
||||
func (h *Handler) PerformPredictionV2(ctx context.Context, req *apirest.PredictionV2Request) (*apirest.PredictionV2Response, error) {
|
||||
resp, err := h.runPredictionV2(ctx, req)
|
||||
if err == nil {
|
||||
h.metrics.Prediction("v2", resp.CompletedAt.Sub(resp.StartedAt), nil)
|
||||
}
|
||||
|
|
@ -60,23 +97,22 @@ func (h *Handler) CancelPredictionJob(_ context.Context, params apirest.CancelPr
|
|||
|
||||
// runPredictionV2 is the synchronous prediction core, shared by the v2
|
||||
// endpoint and the async worker pool.
|
||||
func (h *Handler) runPredictionV2(req *apirest.PredictionV2Request) (*apirest.PredictionV2Response, error) {
|
||||
func (h *Handler) runPredictionV2(ctx context.Context, req *apirest.PredictionV2Request) (*apirest.PredictionV2Response, error) {
|
||||
// Validate the request shape before checking dataset availability, so a
|
||||
// malformed request is a 400 regardless of startup state.
|
||||
lat := req.Launch.Latitude
|
||||
rawLng := req.Launch.Longitude
|
||||
alt := req.Launch.Altitude.Or(0)
|
||||
if lat < -90 || lat > 90 {
|
||||
return nil, apiError(http.StatusBadRequest, "launch.latitude must be in [-90, 90]")
|
||||
if err := validateLat(lat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rawLng < -180 || rawLng >= 360 {
|
||||
return nil, apiError(http.StatusBadRequest, "launch.longitude must be in [-180, 360)")
|
||||
if err := validateLng(req.Launch.Longitude); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lng := normalizeLng(rawLng)
|
||||
lng := normalizeLng(req.Launch.Longitude)
|
||||
|
||||
field := h.mgr.Active()
|
||||
if field == nil {
|
||||
return nil, apiError(http.StatusServiceUnavailable, "no dataset loaded, service is starting up")
|
||||
field, err := h.fieldFor(ctx, req.Dataset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := engine.NewEventSink()
|
||||
|
|
@ -90,6 +126,9 @@ func (h *Handler) runPredictionV2(req *apirest.PredictionV2Request) (*apirest.Pr
|
|||
started := time.Now().UTC()
|
||||
results := prof.Run(float64(req.Launch.Time.Unix()), engine.State{Lat: lat, Lng: lng, Altitude: alt}, events)
|
||||
completed := time.Now().UTC()
|
||||
if err := runFailure(events, field); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &apirest.PredictionV2Response{
|
||||
Stages: make([]apirest.StageResult, 0, len(results)),
|
||||
|
|
@ -105,10 +144,10 @@ func (h *Handler) runPredictionV2(req *apirest.PredictionV2Request) (*apirest.Pr
|
|||
}
|
||||
|
||||
// PerformPrediction implements GET /api/v1/prediction (Tawhiri-compatible).
|
||||
func (h *Handler) PerformPrediction(_ context.Context, params apirest.PerformPredictionParams) (*apirest.PredictionResponse, error) {
|
||||
field := h.mgr.Active()
|
||||
if field == nil {
|
||||
return nil, apiError(http.StatusServiceUnavailable, "no dataset loaded, service is starting up")
|
||||
func (h *Handler) PerformPrediction(ctx context.Context, params apirest.PerformPredictionParams) (*apirest.PredictionResponse, error) {
|
||||
field, err := h.fieldFor(ctx, params.Dataset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profileKind := "standard_profile"
|
||||
|
|
@ -118,6 +157,12 @@ func (h *Handler) PerformPrediction(_ context.Context, params apirest.PerformPre
|
|||
ascentRate := params.AscentRate.Or(5)
|
||||
descentRate := params.DescentRate.Or(5)
|
||||
launchAlt := params.LaunchAltitude.Or(0)
|
||||
if err := validateLat(params.LaunchLatitude); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateLng(params.LaunchLongitude); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lng := normalizeLng(params.LaunchLongitude)
|
||||
launchTime := float64(params.LaunchDatetime.Unix())
|
||||
|
||||
|
|
@ -142,6 +187,9 @@ func (h *Handler) PerformPrediction(_ context.Context, params apirest.PerformPre
|
|||
started := time.Now().UTC()
|
||||
results := prof.Run(launchTime, engine.State{Lat: params.LaunchLatitude, Lng: lng, Altitude: launchAlt}, events)
|
||||
completed := time.Now().UTC()
|
||||
if err := runFailure(events, field); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.metrics.Prediction(profileKind, completed.Sub(started), nil)
|
||||
|
||||
resp := &apirest.PredictionResponse{
|
||||
|
|
@ -229,9 +277,13 @@ func tawhiriItem(name string, r engine.Result) apirest.PredictionResponsePredict
|
|||
for i := range n {
|
||||
t, p := r.Path.At(i)
|
||||
traj = append(traj, apirest.TawhiriPoint{
|
||||
Datetime: time.Unix(int64(t), 0).UTC(),
|
||||
Latitude: p.Lat,
|
||||
Longitude: signedLng(p.Lng),
|
||||
Datetime: time.Unix(int64(t), 0).UTC(),
|
||||
Latitude: p.Lat,
|
||||
// v1 publishes [0, 360), the convention upstream Tawhiri publishes —
|
||||
// measured: it answers 308.7884 where we used to answer -51.2115 for
|
||||
// the same meridian. v1 exists to be a drop-in, so it matches. v2 keeps
|
||||
// the signed format (mapping.go).
|
||||
Longitude: p.Lng,
|
||||
Altitude: p.Altitude,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue