Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions pkg/geo/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 14 additions & 3 deletions pkg/geo/s2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)))
Expand All @@ -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
}
67 changes: 14 additions & 53 deletions pkg/models/geo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -216,31 +180,26 @@ 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()
}

// 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
Expand All @@ -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
Expand All @@ -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
}
18 changes: 3 additions & 15 deletions pkg/models/geo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
110 changes: 20 additions & 90 deletions pkg/rid/models/api/v1/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(&ltlng))
v, err := FromLatLngPoint(&ltlng)
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 {
Expand Down
Loading