diff --git a/pkg/geo/errors.go b/pkg/geo/errors.go index 8bc1d7c55..e719a6655 100644 --- a/pkg/geo/errors.go +++ b/pkg/geo/errors.go @@ -18,9 +18,8 @@ var ( // vertices to define a valid shape. ErrNotEnoughPointsInPolygon = stacktrace.NewErrorWithCode(dsserr.BadRequest, "Not enough points in polygon") - // ErrBadCoordSet indicates that a polygon's coordinates did not form a valid - // singular enclosed area. - ErrBadCoordSet = stacktrace.NewErrorWithCode(dsserr.BadRequest, "Coordinates did not create a well-formed area") + // ErrBadCoord indicates that coordinates are not valid + ErrBadCoord = stacktrace.NewErrorWithCode(dsserr.BadRequest, "Coordinates are invalid") // ErrRadiusMustBeLargerThan0 indicates that a circle with non-positive radius // was specified. diff --git a/pkg/geo/s2.go b/pkg/geo/s2.go index 38d90f869..d3248380e 100644 --- a/pkg/geo/s2.go +++ b/pkg/geo/s2.go @@ -139,7 +139,7 @@ func Covering(points []s2.Point) (s2.CellUnion, error) { // and returns the resulting s2.CellUnion, or else: // * ErrOddNumberOfCoordinatesInAreaString // * ErrNotEnoughPointsInPolygon -// * ErrBadCoordSet +// * ErrBadCoord // // TODO(tvoss): // * Agree and implement a maximum number of points in area @@ -165,13 +165,13 @@ func AreaToCellIDs(area string) (s2.CellUnion, error) { case 0: f, err := strconv.ParseFloat(trimmed, 64) if err != nil { - return nil, stacktrace.Propagate(ErrBadCoordSet, "Unable to parse lat: %s", err.Error()) + return nil, stacktrace.Propagate(ErrBadCoord, "Unable to parse lat: %s", err.Error()) } lat = f case 1: f, err := strconv.ParseFloat(trimmed, 64) if err != nil { - return nil, stacktrace.Propagate(ErrBadCoordSet, "Unable to parse lng: %s", err.Error()) + return nil, stacktrace.Propagate(ErrBadCoord, "Unable to parse lng: %s", err.Error()) } lng = f points = append(points, s2.PointFromLatLng(s2.LatLngFromDegrees(lat, lng))) @@ -181,3 +181,14 @@ func AreaToCellIDs(area string) (s2.CellUnion, error) { } return Covering(points) } + +// NewPointFromDegrees is a convenience function that creates an s2.Point +// from lat lng degree coodrinates, or fails with: +// * ErrBadCoord +func NewPointFromDegrees(lat, lng float64) (s2.Point, error) { + latlng := s2.LatLngFromDegrees(lat, lng) + if !latlng.IsValid() { + return s2.Point{}, stacktrace.Propagate(ErrBadCoord, "Invalid coodrinates [%f, %f]", lat, lng) + } + return s2.PointFromLatLng(latlng), nil +} diff --git a/pkg/models/geo.go b/pkg/models/geo.go index d132c450e..fd39aa759 100644 --- a/pkg/models/geo.go +++ b/pkg/models/geo.go @@ -8,39 +8,6 @@ import ( "github.com/interuss/stacktrace" ) -const ( - // TimeFormatRFC3339 is the string used for RFC3339 - TimeFormatRFC3339 = "RFC3339" - minLat = -90.0 - maxLat = 90.0 - minLng = -180.0 - maxLng = 180.0 - UnitsM = "M" - ReferenceW84 = "W84" -) - -var ( - unitToMeterMultiplicativeFactors = map[unit]float32{ - unitMeter: 1, - } - - altitudeReferenceWGS84 altitudeReference = "W84" - unitMeter unit = "M" -) - -type ( - altitudeReference string - unit string -) - -func (ar altitudeReference) String() string { - return string(ar) -} - -func (u unit) String() string { - return string(u) -} - func float32p(v float32) *float32 { return &v } @@ -108,7 +75,6 @@ func (pcg precomputedCellGeometry) CalculateCovering() (s2.CellUnion, error) { // individual volumes in space and time, or one of these root causes: // * geo.ErrMissingFootprint // * geo.ErrNotEnoughPointsInPolygon -// * geo.ErrBadCoordSet // * geo.ErrRadiusMustBeLargerThan0 func UnionVolumes4D(volumes ...*Volume4D) (*Volume4D, error) { result := &Volume4D{} @@ -193,7 +159,6 @@ func UnionVolumes4D(volumes ...*Volume4D) (*Volume4D, error) { // * geo.ErrMissingSpatialVolume // * geo.ErrMissingFootprint // * geo.ErrNotEnoughPointsInPolygon -// * geo.ErrBadCoordSet // * geo.ErrRadiusMustBeLargerThan0 func (vol4 *Volume4D) CalculateSpatialCovering() (s2.CellUnion, error) { if vol4.SpatialVolume == nil { @@ -205,7 +170,6 @@ func (vol4 *Volume4D) CalculateSpatialCovering() (s2.CellUnion, error) { // CalculateCovering returns the spatial covering of vol3, or one of: // * geo.ErrMissingFootprint // * geo.ErrNotEnoughPointsInPolygon -// * geo.ErrBadCoordSet // * geo.ErrRadiusMustBeLargerThan0 func (vol3 *Volume3D) CalculateCovering() (s2.CellUnion, error) { if vol3.Footprint == nil { @@ -216,7 +180,6 @@ func (vol3 *Volume3D) CalculateCovering() (s2.CellUnion, error) { // CalculateCovering returns the result of invoking gf, with possible errors: // * geo.ErrNotEnoughPointsInPolygon -// * geo.ErrBadCoordSet // * geo.ErrRadiusMustBeLargerThan0 func (gf GeometryFunc) CalculateCovering() (s2.CellUnion, error) { return gf() @@ -224,23 +187,19 @@ func (gf GeometryFunc) CalculateCovering() (s2.CellUnion, error) { // GeoCircle models a circular enclosed area on earth's surface. type GeoCircle struct { - Center LatLngPoint + Center *LatLngPoint RadiusMeter float32 } // CalculateCovering returns the spatial covering of gc. func (gc *GeoCircle) CalculateCovering() (s2.CellUnion, error) { - if (gc.Center.Lat > maxLat) || (gc.Center.Lat < minLat) || (gc.Center.Lng > maxLng) || (gc.Center.Lng < minLng) { - return nil, geo.ErrBadCoordSet - } - if !(gc.RadiusMeter > 0) { return nil, geo.ErrRadiusMustBeLargerThan0 } // TODO: Use an S2 Cap as an inscribed polygon does not fully cover the defined circle return geo.RegionCoverer.Covering(s2.RegularLoop( - s2.PointFromLatLng(s2.LatLngFromDegrees(gc.Center.Lat, gc.Center.Lng)), + gc.Center.point, geo.DistanceMetersToAngle(float64(gc.RadiusMeter)), 20, )), nil @@ -259,15 +218,8 @@ type GeoPolygon struct { // CalculateCovering returns the spatial covering of gp. func (gp *GeoPolygon) CalculateCovering() (s2.CellUnion, error) { var points []s2.Point - if gp == nil { - return nil, geo.ErrBadCoordSet - } for _, v := range gp.Vertices { - // ensure that coordinates passed are actually on earth - if (v.Lat > maxLat) || (v.Lat < minLat) || (v.Lng > maxLng) || (v.Lng < minLng) { - return nil, geo.ErrBadCoordSet - } - points = append(points, s2.PointFromLatLng(s2.LatLngFromDegrees(v.Lat, v.Lng))) + points = append(points, v.point) } if len(points) < 3 { return nil, geo.ErrNotEnoughPointsInPolygon @@ -277,6 +229,15 @@ func (gp *GeoPolygon) CalculateCovering() (s2.CellUnion, error) { // LatLngPoint models a point on the earth's surface. type LatLngPoint struct { - Lat float64 - Lng float64 + point s2.Point +} + +// NewLatLngPoint returns the spatial LatLngPoint, or one of: +// * geo.ErrBadCoord +func NewLatLngPoint(lat, lng float64) (*LatLngPoint, error) { + pt, err := geo.NewPointFromDegrees(lat, lng) + if err != nil { + return nil, err + } + return &LatLngPoint{point: pt}, err } diff --git a/pkg/models/geo_test.go b/pkg/models/geo_test.go index 0b165560d..a04d738b4 100644 --- a/pkg/models/geo_test.go +++ b/pkg/models/geo_test.go @@ -12,21 +12,9 @@ import ( func TestPolygonCovering(t *testing.T) { got, err := (&GeoPolygon{ Vertices: []*LatLngPoint{ - // Stanford - { - Lat: 37.427636, - Lng: -122.170502, - }, - // NASA Ames - { - Lat: 37.408799, - Lng: -122.064069, - }, - // Googleplex - { - Lat: 37.421265, - Lng: -122.086504, - }, + {point: s2.PointFromLatLng(s2.LatLngFromDegrees(37.427636, -122.170502))}, // Stanford + {point: s2.PointFromLatLng(s2.LatLngFromDegrees(37.408799, -122.064069))}, // NASA Ames + {point: s2.PointFromLatLng(s2.LatLngFromDegrees(37.421265, -122.086504))}, // Googleplex }, }).CalculateCovering() diff --git a/pkg/rid/models/api/v1/conversions.go b/pkg/rid/models/api/v1/conversions.go index c3fdd6711..ccdd6fdf9 100644 --- a/pkg/rid/models/api/v1/conversions.go +++ b/pkg/rid/models/api/v1/conversions.go @@ -13,9 +13,11 @@ import ( // FromVolume4D converts RID v1 REST model to business object func FromVolume4D(vol4 *restapi.Volume4D) (*dssmodels.Volume4D, error) { - result := &dssmodels.Volume4D{ - SpatialVolume: FromVolume3D(&vol4.SpatialVolume), + vol3, err := FromVolume3D(&vol4.SpatialVolume) + if err != nil { + return nil, stacktrace.Propagate(err, "Error converting spatial volume") } + result := &dssmodels.Volume4D{SpatialVolume: vol3} if vol4.TimeStart != nil { ts, err := time.Parse(time.RFC3339Nano, *vol4.TimeStart) @@ -37,112 +39,40 @@ func FromVolume4D(vol4 *restapi.Volume4D) (*dssmodels.Volume4D, error) { } // FromVolume3D converts RID v1 REST model to business object -func FromVolume3D(vol3 *restapi.Volume3D) *dssmodels.Volume3D { +func FromVolume3D(vol3 *restapi.Volume3D) (*dssmodels.Volume3D, error) { + p, err := FromGeoPolygon(&vol3.Footprint) + if err != nil { + return nil, stacktrace.Propagate(err, "Error converting polygon") + } return &dssmodels.Volume3D{ - Footprint: FromGeoPolygon(&vol3.Footprint), + Footprint: p, AltitudeLo: (*float32)(vol3.AltitudeLo), AltitudeHi: (*float32)(vol3.AltitudeHi), - } + }, nil } // FromGeoPolygon converts RID v1 REST model to business object -func FromGeoPolygon(footprint *restapi.GeoPolygon) *dssmodels.GeoPolygon { +func FromGeoPolygon(footprint *restapi.GeoPolygon) (*dssmodels.GeoPolygon, error) { result := &dssmodels.GeoPolygon{} for _, ltlng := range footprint.Vertices { - result.Vertices = append(result.Vertices, FromLatLngPoint(<lng)) + v, err := FromLatLngPoint(<lng) + if err != nil { + return nil, stacktrace.Propagate(err, "Error converting polygon vertex") + } + result.Vertices = append(result.Vertices, v) } - return result + return result, nil } // FromLatLngPoint converts RID v1 REST model to business object -func FromLatLngPoint(pt *restapi.LatLngPoint) *dssmodels.LatLngPoint { - return &dssmodels.LatLngPoint{ - Lat: float64(pt.Lat), - Lng: float64(pt.Lng), - } +func FromLatLngPoint(pt *restapi.LatLngPoint) (*dssmodels.LatLngPoint, error) { + return dssmodels.NewLatLngPoint(float64(pt.Lat), float64(pt.Lng)) } // === Business -> RID === -// ToVolume4D converts Volume4D business object to a RID v1 REST model -func ToVolume4D(vol4 *dssmodels.Volume4D) (*restapi.Volume4D, error) { - vol3, err := ToVolume3D(vol4.SpatialVolume) - if err != nil { - return nil, err // No need to Propagate this error as this stack layer does not add useful information - } - - result := &restapi.Volume4D{ - SpatialVolume: *vol3, - } - - if vol4.StartTime != nil { - ts := vol4.StartTime.Format(time.RFC3339Nano) - result.TimeStart = &ts - } - - if vol4.EndTime != nil { - ts := vol4.EndTime.Format(time.RFC3339Nano) - result.TimeEnd = &ts - } - - return result, nil -} - -// ToVolume3D converts Volume3D business object to a RID v1 REST model -func ToVolume3D(vol3 *dssmodels.Volume3D) (*restapi.Volume3D, error) { - if vol3 == nil { - return nil, nil - } - - result := &restapi.Volume3D{} - - if vol3.AltitudeLo != nil { - result.AltitudeLo = (*restapi.Altitude)(vol3.AltitudeLo) - } - - if vol3.AltitudeHi != nil { - result.AltitudeHi = (*restapi.Altitude)(vol3.AltitudeHi) - } - - switch t := vol3.Footprint.(type) { - case nil: - // Empty on purpose - case *dssmodels.GeoPolygon: - result.Footprint = *ToGeoPolygon(t) - default: - return nil, stacktrace.NewError("Unsupported geometry type: %T", vol3.Footprint) - } - - return result, nil -} - -// ToGeoPolygon converts GeoPolygon business object to a RID v1 REST model -func ToGeoPolygon(gp *dssmodels.GeoPolygon) *restapi.GeoPolygon { - if gp == nil { - return nil - } - - result := &restapi.GeoPolygon{} - - for _, pt := range gp.Vertices { - result.Vertices = append(result.Vertices, *ToLatLngPoint(pt)) - } - - return result -} - -// ToLatLngPoint converts latlngpoint business object to a RID v1 REST model -func ToLatLngPoint(pt *dssmodels.LatLngPoint) *restapi.LatLngPoint { - result := &restapi.LatLngPoint{ - Lat: restapi.Latitude(pt.Lat), - Lng: restapi.Longitude(pt.Lng), - } - - return result -} - // ToIdentificationServiceArea converts an IdentificationServiceArea // business object to a RID v1 REST model for API consumption. func ToIdentificationServiceArea(i *ridmodels.IdentificationServiceArea) *restapi.IdentificationServiceArea { diff --git a/pkg/rid/models/api/v2/conversions.go b/pkg/rid/models/api/v2/conversions.go index b84992b0a..20979e0e6 100644 --- a/pkg/rid/models/api/v2/conversions.go +++ b/pkg/rid/models/api/v2/conversions.go @@ -82,7 +82,10 @@ func FromVolume3D(vol3 *restapi.Volume3D) (*dssmodels.Volume3D, error) { if vol3.OutlineCircle != nil { return nil, stacktrace.NewError("Only one of outline_circle or outline_polygon may be specified") } - footprint := FromPolygon(vol3.OutlinePolygon) + footprint, err := FromPolygon(vol3.OutlinePolygon) + if err != nil { + return nil, stacktrace.Propagate(err, "Error parsing outline_polygon for Volume3D") + } result := &dssmodels.Volume3D{ Footprint: footprint, @@ -112,14 +115,18 @@ func FromVolume3D(vol3 *restapi.Volume3D) (*dssmodels.Volume3D, error) { } // FromPolygon converts RID v2 REST model to business object -func FromPolygon(polygon *restapi.Polygon) *dssmodels.GeoPolygon { +func FromPolygon(polygon *restapi.Polygon) (*dssmodels.GeoPolygon, error) { result := &dssmodels.GeoPolygon{} for _, ltlng := range polygon.Vertices { - result.Vertices = append(result.Vertices, FromLatLngPoint(<lng)) + v, err := FromLatLngPoint(<lng) + if err != nil { + return nil, stacktrace.Propagate(err, "Invalid polygon vertex") + } + result.Vertices = append(result.Vertices, v) } - return result + return result, nil } // FromCircle converts RID v2 REST model to business object @@ -133,19 +140,20 @@ func FromCircle(circle *restapi.Circle) (*dssmodels.GeoCircle, error) { if circle.Radius.Units != "M" { return nil, stacktrace.NewError("Only circle radius units of 'M' are acceptable for UTM") } + center, err := FromLatLngPoint(circle.Center) + if err != nil { + return nil, stacktrace.NewError("Invalid `center` from circle") + } result := &dssmodels.GeoCircle{ - Center: *FromLatLngPoint(circle.Center), + Center: center, RadiusMeter: circle.Radius.Value, } return result, nil } // FromLatLngPoint converts RID v2 REST model to business object -func FromLatLngPoint(pt *restapi.LatLngPoint) *dssmodels.LatLngPoint { - return &dssmodels.LatLngPoint{ - Lat: float64(pt.Lat), - Lng: float64(pt.Lng), - } +func FromLatLngPoint(pt *restapi.LatLngPoint) (*dssmodels.LatLngPoint, error) { + return dssmodels.NewLatLngPoint(float64(pt.Lat), float64(pt.Lng)) } // === Business -> RID === @@ -164,16 +172,6 @@ func ToTime(t *time.Time) *restapi.Time { return result } -// ToLatLngPoint converts latlngpoint business object to RID v2 REST model -func ToLatLngPoint(pt *dssmodels.LatLngPoint) *restapi.LatLngPoint { - result := &restapi.LatLngPoint{ - Lat: restapi.Latitude(pt.Lat), - Lng: restapi.Longitude(pt.Lng), - } - - return result -} - // ToIdentificationServiceArea converts an IdentificationServiceArea // business object to RID v2 REST model for API consumption. func ToIdentificationServiceArea(i *ridmodels.IdentificationServiceArea) *restapi.IdentificationServiceArea { diff --git a/pkg/rid/server/v1/server_test.go b/pkg/rid/server/v1/server_test.go index dcb661e73..fe08090ce 100644 --- a/pkg/rid/server/v1/server_test.go +++ b/pkg/rid/server/v1/server_test.go @@ -31,7 +31,11 @@ func mustTimestamp(ts *string) *time.Time { } func mustPolygonToCellIDs(p *restapi.GeoPolygon) s2.CellUnion { - cells, err := apiv1.FromGeoPolygon(p).CalculateCovering() + g, err := apiv1.FromGeoPolygon(p) + if err != nil { + panic(err) + } + cells, err := g.CalculateCovering() if err != nil { panic(err) } diff --git a/pkg/scd/constraints_handler.go b/pkg/scd/constraints_handler.go index 0688176c7..530091aee 100644 --- a/pkg/scd/constraints_handler.go +++ b/pkg/scd/constraints_handler.go @@ -369,10 +369,10 @@ func validateAndReturnConstraintUpsertParams( // Start and end times are required for each volume // The end time may not be in the past - valid.uExtent, err = dssmodels.UnionVolumes4DFromSCDRest( + valid.uExtent, err = scdmodels.UnionVolumes4DFromSCDRest( params.Extents, - dssmodels.WithRequireTimeBounds(), - dssmodels.WithRequireEndTimeAfter(now), + scdmodels.WithRequireTimeBounds(), + scdmodels.WithRequireEndTimeAfter(now), ) if err != nil { return nil, stacktrace.Propagate(err, "Invalid extents") @@ -404,7 +404,7 @@ func (a *Server) QueryConstraintReferences(ctx context.Context, req *restapi.Que } // Parse area of interest to common Volume4D - vol4, err := dssmodels.Volume4DFromSCDRest(aoi) + vol4, err := scdmodels.Volume4DFromSCDRest(aoi) if err != nil { return restapi.QueryConstraintReferencesResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to convert to internal geometry model"))}} diff --git a/pkg/scd/models/constraints.go b/pkg/scd/models/constraints.go index 5f7fdebca..5908d30c0 100644 --- a/pkg/scd/models/constraints.go +++ b/pkg/scd/models/constraints.go @@ -38,14 +38,14 @@ func (c *Constraint) ToRest() *restapi.ConstraintReference { if c.StartTime != nil { result.TimeStart = restapi.Time{ Value: c.StartTime.Format(time.RFC3339Nano), - Format: dssmodels.TimeFormatRFC3339, + Format: TimeFormatRFC3339, } } if c.EndTime != nil { result.TimeEnd = restapi.Time{ Value: c.EndTime.Format(time.RFC3339Nano), - Format: dssmodels.TimeFormatRFC3339, + Format: TimeFormatRFC3339, } } diff --git a/pkg/models/scd_conversions.go b/pkg/scd/models/conversions.go similarity index 50% rename from pkg/models/scd_conversions.go rename to pkg/scd/models/conversions.go index 9685405a0..2d6bfae76 100644 --- a/pkg/models/scd_conversions.go +++ b/pkg/scd/models/conversions.go @@ -4,13 +4,21 @@ import ( "time" restapi "github.com/interuss/dss/pkg/api/scdv1" + dssmodels "github.com/interuss/dss/pkg/models" "github.com/interuss/stacktrace" ) -type Volume4DValidator func(*Volume4D) error +const ( + // TimeFormatRFC3339 is the string used for RFC3339 + TimeFormatRFC3339 = "RFC3339" + UnitsM = "M" + ReferenceW84 = "W84" +) + +type Volume4DValidator func(*dssmodels.Volume4D) error func WithRequireTimeBounds() Volume4DValidator { - return func(v *Volume4D) error { + return func(v *dssmodels.Volume4D) error { if v.StartTime == nil { return stacktrace.NewError("Missing start time") } @@ -22,7 +30,7 @@ func WithRequireTimeBounds() Volume4DValidator { } func WithRequireEndTimeAfter(now time.Time) Volume4DValidator { - return func(v *Volume4D) error { + return func(v *dssmodels.Volume4D) error { if v.EndTime != nil && v.EndTime.Before(now) { return stacktrace.NewError("End time may not be in the past") } @@ -31,7 +39,7 @@ func WithRequireEndTimeAfter(now time.Time) Volume4DValidator { } func WithRequireAltitudeBounds() Volume4DValidator { - return func(v *Volume4D) error { + return func(v *dssmodels.Volume4D) error { if v.SpatialVolume.AltitudeLo == nil { return stacktrace.NewError("Missing lower altitude") } @@ -44,8 +52,8 @@ func WithRequireAltitudeBounds() Volume4DValidator { // UnionVolumes4DFromSCDRest converts a slice of vol4 SCD v1 REST model to a single bounding Volume4D // Validation is applied on the resulting volume union -func UnionVolumes4DFromSCDRest(vol4s []restapi.Volume4D, validators ...Volume4DValidator) (*Volume4D, error) { - volumes := make([]*Volume4D, len(vol4s)) +func UnionVolumes4DFromSCDRest(vol4s []restapi.Volume4D, validators ...Volume4DValidator) (*dssmodels.Volume4D, error) { + volumes := make([]*dssmodels.Volume4D, len(vol4s)) for idx, vol4 := range vol4s { volume, err := Volume4DFromSCDRest(&vol4) if err != nil { @@ -53,7 +61,7 @@ func UnionVolumes4DFromSCDRest(vol4s []restapi.Volume4D, validators ...Volume4DV } volumes[idx] = volume } - union, err := UnionVolumes4D(volumes...) + union, err := dssmodels.UnionVolumes4D(volumes...) if err != nil { return nil, stacktrace.Propagate(err, "Failed to union volumes") } @@ -68,7 +76,7 @@ func UnionVolumes4DFromSCDRest(vol4s []restapi.Volume4D, validators ...Volume4DV } // Volume4DFromSCDRest converts vol4 SCD v1 REST model to a Volume4D -func Volume4DFromSCDRest(vol4 *restapi.Volume4D) (*Volume4D, error) { +func Volume4DFromSCDRest(vol4 *restapi.Volume4D) (*dssmodels.Volume4D, error) { vol3, err := Volume3DFromSCDRest(&vol4.Volume) if err != nil { return nil, err // No need to Propagate this error as this stack layer does not add useful information @@ -96,7 +104,7 @@ func Volume4DFromSCDRest(vol4 *restapi.Volume4D) (*Volume4D, error) { return nil, stacktrace.NewError("Start time cannot be after end time") } - volume := &Volume4D{ + volume := &dssmodels.Volume4D{ SpatialVolume: vol3, StartTime: startTime, EndTime: endTime, @@ -106,7 +114,7 @@ func Volume4DFromSCDRest(vol4 *restapi.Volume4D) (*Volume4D, error) { } // Volume3DFromSCDRest converts a vol3 SCD v1 REST model to a Volume3D -func Volume3DFromSCDRest(vol3 *restapi.Volume3D) (*Volume3D, error) { +func Volume3DFromSCDRest(vol3 *restapi.Volume3D) (*dssmodels.Volume3D, error) { if vol3 == nil { return nil, nil } @@ -119,7 +127,7 @@ func Volume3DFromSCDRest(vol3 *restapi.Volume3D) (*Volume3D, error) { if vol3.AltitudeLower.Reference != ReferenceW84 { return nil, stacktrace.NewError("Invalid lower altitude reference") } - altLo = float32p(float32(vol3.AltitudeLower.Value)) + altLo = new(float32(vol3.AltitudeLower.Value)) } var altHi *float32 @@ -130,161 +138,63 @@ func Volume3DFromSCDRest(vol3 *restapi.Volume3D) (*Volume3D, error) { if vol3.AltitudeUpper.Reference != ReferenceW84 { return nil, stacktrace.NewError("Invalid upper altitude reference") } - altHi = float32p(float32(vol3.AltitudeUpper.Value)) + altHi = new(float32(vol3.AltitudeUpper.Value)) } if altLo != nil && altHi != nil && *altLo > *altHi { return nil, stacktrace.NewError("Lower altitude cannot be greater than upper altitude") } + var ( + footprint dssmodels.Geometry + err error + ) switch { case vol3.OutlineCircle != nil && vol3.OutlinePolygon != nil: - return nil, stacktrace.NewError("Both circle and polygon specified in outline geometry") + err = stacktrace.NewError("Both circle and polygon specified in outline geometry") case vol3.OutlinePolygon != nil: - return &Volume3D{ - Footprint: GeoPolygonFromSCDRest(vol3.OutlinePolygon), - AltitudeLo: altLo, - AltitudeHi: altHi, - }, nil + footprint, err = GeoPolygonFromSCDRest(vol3.OutlinePolygon) case vol3.OutlineCircle != nil: - return &Volume3D{ - Footprint: GeoCircleFromSCDRest(vol3.OutlineCircle), - AltitudeLo: altLo, - AltitudeHi: altHi, - }, nil + footprint, err = GeoCircleFromSCDRest(vol3.OutlineCircle) + } + if err != nil { + return nil, err // No need to Propagate this error as this stack layer does not add useful information } - return &Volume3D{ + return &dssmodels.Volume3D{ + Footprint: footprint, AltitudeLo: altLo, AltitudeHi: altHi, }, nil } // GeoCircleFromSCDRest converts a circle SCD v1 REST model to a GeoCircle -func GeoCircleFromSCDRest(c *restapi.Circle) *GeoCircle { - return &GeoCircle{ - Center: *LatLngPointFromSCDRest(c.Center), - RadiusMeter: unitToMeterMultiplicativeFactors[unit(c.Radius.Units)] * c.Radius.Value, +func GeoCircleFromSCDRest(c *restapi.Circle) (*dssmodels.GeoCircle, error) { + center, err := LatLngPointFromSCDRest(c.Center) + if err != nil { + return nil, stacktrace.Propagate(err, "Invalid circle center") } + return &dssmodels.GeoCircle{ + Center: center, + RadiusMeter: c.Radius.Value, + }, nil } // GeoPolygonFromSCDRest converts a polygon SCD v1 REST model to a GeoPolygon -func GeoPolygonFromSCDRest(p *restapi.Polygon) *GeoPolygon { - result := &GeoPolygon{} +func GeoPolygonFromSCDRest(p *restapi.Polygon) (*dssmodels.GeoPolygon, error) { + result := &dssmodels.GeoPolygon{} for _, ltlng := range p.Vertices { - result.Vertices = append(result.Vertices, LatLngPointFromSCDRest(<lng)) - } - - return result -} - -// LatLngPointFromSCDRest converts a point SCD v1 REST model to a latlngpoint -func LatLngPointFromSCDRest(p *restapi.LatLngPoint) *LatLngPoint { - return &LatLngPoint{ - Lat: float64(p.Lat), - Lng: float64(p.Lng), - } -} - -// ToSCDRest converts the Volume4D to a SCD v1 REST model -func (vol4 *Volume4D) ToSCDRest() *restapi.Volume4D { - - result := &restapi.Volume4D{} - if vol4.SpatialVolume != nil { - result.Volume = *vol4.SpatialVolume.ToSCDRest() - } - - if vol4.StartTime != nil { - result.TimeStart = &restapi.Time{ - Format: TimeFormatRFC3339, - Value: vol4.StartTime.Format(time.RFC3339Nano), - } - } - - if vol4.EndTime != nil { - result.TimeEnd = &restapi.Time{ - Format: TimeFormatRFC3339, - Value: vol4.EndTime.Format(time.RFC3339Nano), - } - } - - return result -} - -// ToSCDRest converts the Volume3D to a SCD v1 REST model -func (vol3 *Volume3D) ToSCDRest() *restapi.Volume3D { - if vol3 == nil { - return nil - } - - result := &restapi.Volume3D{} - - if vol3.AltitudeLo != nil { - result.AltitudeLower = &restapi.Altitude{ - Reference: altitudeReferenceWGS84.String(), - Units: unitMeter.String(), - Value: float64(*vol3.AltitudeLo), - } - } - - if vol3.AltitudeHi != nil { - result.AltitudeUpper = &restapi.Altitude{ - Reference: altitudeReferenceWGS84.String(), - Units: unitMeter.String(), - Value: float64(*vol3.AltitudeHi), + v, err := LatLngPointFromSCDRest(<lng) + if err != nil { + return nil, stacktrace.Propagate(err, "Invalid polygon vertex") } + result.Vertices = append(result.Vertices, v) } - switch t := vol3.Footprint.(type) { - case nil: - // Empty on purpose - case *GeoPolygon: - result.OutlinePolygon = t.ToSCDRest() - case *GeoCircle: - result.OutlineCircle = t.ToSCDRest() - } - - return result + return result, nil } -// ToSCDRest converts the GeoCircle to a SCD v1 REST model -func (gc *GeoCircle) ToSCDRest() *restapi.Circle { - if gc == nil { - return nil - } - - return &restapi.Circle{ - Center: gc.Center.ToSCDRest(), - Radius: &restapi.Radius{ - Units: unitMeter.String(), - Value: gc.RadiusMeter, - }, - } -} - -// ToSCDRest converts the GeoPolygon to a SCD v1 REST model -func (gp *GeoPolygon) ToSCDRest() *restapi.Polygon { - if gp == nil { - return nil - } - - result := &restapi.Polygon{ - Vertices: make([]restapi.LatLngPoint, len(gp.Vertices)), - } - - for _, pt := range gp.Vertices { - result.Vertices = append(result.Vertices, *pt.ToSCDRest()) - } - - return result -} - -// ToSCDRest converts the LatLngPoint to a SCD v1 REST model -func (pt *LatLngPoint) ToSCDRest() *restapi.LatLngPoint { - result := &restapi.LatLngPoint{ - Lat: restapi.Latitude(pt.Lat), - Lng: restapi.Longitude(pt.Lng), - } - - return result +// LatLngPointFromSCDRest converts a point SCD v1 REST model to a latlngpoint +func LatLngPointFromSCDRest(p *restapi.LatLngPoint) (*dssmodels.LatLngPoint, error) { + return dssmodels.NewLatLngPoint(float64(p.Lat), float64(p.Lng)) } diff --git a/pkg/models/scd_conversions_test.go b/pkg/scd/models/conversions_test.go similarity index 86% rename from pkg/models/scd_conversions_test.go rename to pkg/scd/models/conversions_test.go index cab30fbdf..42ee1e9b5 100644 --- a/pkg/models/scd_conversions_test.go +++ b/pkg/scd/models/conversions_test.go @@ -5,6 +5,7 @@ import ( "time" restapi "github.com/interuss/dss/pkg/api/scdv1" + dssmodels "github.com/interuss/dss/pkg/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -30,7 +31,7 @@ func TestUnionVolumes4DFromSCDRest(t *testing.T) { name string validators []Volume4DValidator rest []restapi.Volume4D - want *Volume4D + want *dssmodels.Volume4D wantErr bool }{ { @@ -43,7 +44,7 @@ func TestUnionVolumes4DFromSCDRest(t *testing.T) { {TimeStart: newRestTime(timeStart), TimeEnd: newRestTime(timeMid)}, {TimeStart: newRestTime(timeStart), TimeEnd: newRestTime(timeEnd)}, }, - want: &Volume4D{SpatialVolume: &Volume3D{}, StartTime: &timeStart, EndTime: &timeEnd}, + want: &dssmodels.Volume4D{SpatialVolume: &dssmodels.Volume3D{}, StartTime: &timeStart, EndTime: &timeEnd}, }, { name: "TimeEndExpired", @@ -79,7 +80,7 @@ func TestUnionVolumes4DFromSCDRest(t *testing.T) { {Volume: restapi.Volume3D{AltitudeLower: newRestAlt(altLo), AltitudeUpper: newRestAlt(altMid)}}, {Volume: restapi.Volume3D{AltitudeLower: newRestAlt(altMid), AltitudeUpper: newRestAlt(altHi)}}, }, - want: &Volume4D{SpatialVolume: &Volume3D{AltitudeLo: &altLo, AltitudeHi: &altHi}}, + want: &dssmodels.Volume4D{SpatialVolume: &dssmodels.Volume3D{AltitudeLo: &altLo, AltitudeHi: &altHi}}, }, { name: "MissingLowerAltitude", @@ -122,18 +123,18 @@ func TestVolume4DFromSCDRest(t *testing.T) { testCases := []struct { name string rest *restapi.Volume4D - want *Volume4D + want *dssmodels.Volume4D wantErr bool }{ { name: "Empty", rest: &restapi.Volume4D{}, - want: &Volume4D{SpatialVolume: &Volume3D{}}, + want: &dssmodels.Volume4D{SpatialVolume: &dssmodels.Volume3D{}}, }, { name: "Times", rest: &restapi.Volume4D{TimeStart: newRestTime(start), TimeEnd: newRestTime(end)}, - want: &Volume4D{SpatialVolume: &Volume3D{}, StartTime: &start, EndTime: &end}, + want: &dssmodels.Volume4D{SpatialVolume: &dssmodels.Volume3D{}, StartTime: &start, EndTime: &end}, }, { name: "InvalidTimeStart", @@ -170,42 +171,46 @@ func TestVolume3DFromSCDRest(t *testing.T) { hi := float32(200.0) restInvalid := &restapi.Altitude{Value: 0} + restPoint := &restapi.LatLngPoint{Lat: 1, Lng: 2} + point, err := dssmodels.NewLatLngPoint(1, 2) + require.NoError(t, err) + testCases := []struct { name string rest *restapi.Volume3D - want *Volume3D + want *dssmodels.Volume3D wantErr bool }{ { name: "Empty", rest: &restapi.Volume3D{}, - want: &Volume3D{}, + want: &dssmodels.Volume3D{}, }, { name: "Polygon", rest: &restapi.Volume3D{ OutlinePolygon: &restapi.Polygon{}, }, - want: &Volume3D{ - Footprint: &GeoPolygon{}, + want: &dssmodels.Volume3D{ + Footprint: &dssmodels.GeoPolygon{}, }, }, { name: "Circle", rest: &restapi.Volume3D{ OutlineCircle: &restapi.Circle{ - Center: &restapi.LatLngPoint{}, + Center: restPoint, Radius: &restapi.Radius{}, }, }, - want: &Volume3D{ - Footprint: &GeoCircle{}, + want: &dssmodels.Volume3D{ + Footprint: &dssmodels.GeoCircle{Center: point}, }, }, { name: "Altitudes", rest: &restapi.Volume3D{AltitudeLower: newRestAlt(lo), AltitudeUpper: newRestAlt(hi)}, - want: &Volume3D{AltitudeLo: &lo, AltitudeHi: &hi}, + want: &dssmodels.Volume3D{AltitudeLo: &lo, AltitudeHi: &hi}, }, { name: "InvalidLowerAltitude", diff --git a/pkg/scd/models/operational_intents.go b/pkg/scd/models/operational_intents.go index 8f92d6bb3..f6ef0c2dc 100644 --- a/pkg/scd/models/operational_intents.go +++ b/pkg/scd/models/operational_intents.go @@ -113,14 +113,14 @@ func (o *OperationalIntent) ToRest() *restapi.OperationalIntentReference { if o.StartTime != nil { result.TimeStart = restapi.Time{ Value: o.StartTime.Format(time.RFC3339Nano), - Format: dssmodels.TimeFormatRFC3339, + Format: TimeFormatRFC3339, } } if o.EndTime != nil { result.TimeEnd = restapi.Time{ Value: o.EndTime.Format(time.RFC3339Nano), - Format: dssmodels.TimeFormatRFC3339, + Format: TimeFormatRFC3339, } } diff --git a/pkg/scd/models/subscriptions.go b/pkg/scd/models/subscriptions.go index aa66ecb5f..73b042470 100644 --- a/pkg/scd/models/subscriptions.go +++ b/pkg/scd/models/subscriptions.go @@ -53,14 +53,14 @@ func (s *Subscription) ToRest(dependentOperationalIntents []dssmodels.ID) (*rest if s.StartTime != nil { result.TimeStart = &restapi.Time{ Value: s.StartTime.Format(time.RFC3339Nano), - Format: dssmodels.TimeFormatRFC3339, + Format: TimeFormatRFC3339, } } if s.EndTime != nil { result.TimeEnd = &restapi.Time{ Value: s.EndTime.Format(time.RFC3339Nano), - Format: dssmodels.TimeFormatRFC3339, + Format: TimeFormatRFC3339, } } diff --git a/pkg/scd/operational_intents_handler.go b/pkg/scd/operational_intents_handler.go index f611797d0..44802cf30 100644 --- a/pkg/scd/operational_intents_handler.go +++ b/pkg/scd/operational_intents_handler.go @@ -252,7 +252,7 @@ func (a *Server) QueryOperationalIntentReferences(ctx context.Context, req *rest } // Parse area of interest to common Volume4D - vol4, err := dssmodels.Volume4DFromSCDRest(aoi) + vol4, err := scdmodels.Volume4DFromSCDRest(aoi) if err != nil { return restapi.QueryOperationalIntentReferencesResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Error parsing geometry"))}} @@ -479,11 +479,11 @@ func validateAndReturnOIRUpsertParams( // Start and end times, as well as lower and upper altitudes, are required for each volume // The end time may not be in the past. - valid.uExtent, err = dssmodels.UnionVolumes4DFromSCDRest( + valid.uExtent, err = scdmodels.UnionVolumes4DFromSCDRest( params.Extents, - dssmodels.WithRequireTimeBounds(), - dssmodels.WithRequireAltitudeBounds(), - dssmodels.WithRequireEndTimeAfter(now), + scdmodels.WithRequireTimeBounds(), + scdmodels.WithRequireAltitudeBounds(), + scdmodels.WithRequireEndTimeAfter(now), ) if err != nil { return nil, stacktrace.Propagate(err, "Invalid extents") diff --git a/pkg/scd/subscriptions_handler.go b/pkg/scd/subscriptions_handler.go index 98ab9fed2..d7e2a63d5 100644 --- a/pkg/scd/subscriptions_handler.go +++ b/pkg/scd/subscriptions_handler.go @@ -104,7 +104,7 @@ func (a *Server) PutSubscription(ctx context.Context, manager string, subscripti // Parse extents // If end time is not specified, the value will be chosen automatically by the DSS. // If start time is not specified, it will default to the time the request is processed. - extents, err := dssmodels.Volume4DFromSCDRest(¶ms.Extents) + extents, err := scdmodels.Volume4DFromSCDRest(¶ms.Extents) if err != nil { return nil, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Unable to parse extents") } @@ -378,7 +378,7 @@ func (a *Server) QuerySubscriptions(ctx context.Context, req *restapi.QuerySubsc } // Parse area of interest to common Volume4D - vol4, err := dssmodels.Volume4DFromSCDRest(aoi) + vol4, err := scdmodels.Volume4DFromSCDRest(aoi) if err != nil { return restapi.QuerySubscriptionsResponseSet{Response400: &restapi.ErrorResponse{ Message: dsserr.Handle(ctx, stacktrace.PropagateWithCode(err, dsserr.BadRequest, "Failed to convert to internal geometry model"))}}