Documentation
¶
Overview ¶
Package geocty provides geographic, solar/astronomical, geodesic, and geometric functions for use in go-cty / HCL2 evaluation contexts.
See GetGeoFunctions for registration.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var GeoAreaFunc = function.New(&function.Spec{ Description: "Returns the area enclosed by a polygon (list of points) in square meters.", Params: []function.Parameter{ {Name: "polygon", Type: cty.DynamicPseudoType, Description: "A list of point objects (numeric lat/lon) forming the ring; at least 3, and it need not be explicitly closed"}, }, Type: function.StaticReturnType(cty.Number), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { loop, err := ctyListToLoop(args[0]) if err != nil { return cty.NilVal, err } areaSr := loop.Area() areaM2 := math.Abs(areaSr) * earthRadiusM * earthRadiusM return cty.NumberFloatVal(areaM2), nil }, })
GeoAreaFunc returns the area enclosed by a polygon in square meters.
var GeoContainsFunc = function.New(&function.Spec{ Description: "Returns true if the point is inside the polygon.", Params: []function.Parameter{ {Name: "polygon", Type: cty.DynamicPseudoType, Description: "A list of point objects (numeric lat/lon) forming the ring; at least 3"}, {Name: "point", Type: cty.DynamicPseudoType, Description: "The point to test (numeric lat and lon)"}, }, Type: function.StaticReturnType(cty.Bool), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { loop, err := ctyListToLoop(args[0]) if err != nil { return cty.NilVal, err } pt, err := ctyToS2Point(args[1]) if err != nil { return cty.NilVal, fmt.Errorf("point: %w", err) } return cty.BoolVal(loop.ContainsPoint(pt)), nil }, })
GeoContainsFunc returns true if point is inside polygon.
var GeoDestinationFunc = function.New(&function.Spec{ Description: "Returns the point reached by travelling from origin on a bearing for a distance, duration, or until a target time.", Params: []function.Parameter{ {Name: "origin", Type: cty.DynamicPseudoType, Description: "The starting point (numeric lat and lon); its extra attributes are carried onto the result"}, {Name: "bearing", Type: cty.Number, Description: "Initial bearing in degrees clockwise from north"}, { Name: "distance_or_duration_or_time", Type: cty.DynamicPseudoType, Description: "How far to travel: a number of meters, a duration (requires origin.speed), or a target time (requires origin.speed and origin.time)", }, }, VarParam: &function.Parameter{ Name: "extras", Type: cty.DynamicPseudoType, Description: "Optional additional objects merged onto origin before travelling (e.g. to supply speed or time); later objects win on key conflicts", }, Type: function.StaticReturnType(cty.DynamicPseudoType), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { if len(args) > 4 { return cty.NilVal, fmt.Errorf("geo_destination takes 3 or 4 arguments, got %d", len(args)) } origin := args[0] if !origin.Type().IsObjectType() { return cty.NilVal, fmt.Errorf("origin must be an object, got %s", origin.Type().FriendlyName()) } var bearing float64 if err := gocty.FromCtyValue(args[1], &bearing); err != nil { return cty.NilVal, fmt.Errorf("bearing: %w", err) } effective := mergeObjects(origin, args[3:]...) effMap := effective.AsValueMap() lat, err := pointCoord(effMap, "lat", axisLat) if err != nil { return cty.NilVal, err } lon, err := pointCoord(effMap, "lon", axisLon) if err != nil { return cty.NilVal, err } third := args[2] var distM float64 var updateTime func(map[string]cty.Value) switch third.Type() { case cty.Number: if err := gocty.FromCtyValue(third, &distM); err != nil { return cty.NilVal, fmt.Errorf("distance: %w", err) } case timecty.DurationCapsuleType: dur, err := timecty.GetDuration(third) if err != nil { return cty.NilVal, err } speed, err := getFloatAttr(effMap, "speed") if err != nil { return cty.NilVal, fmt.Errorf("duration form requires speed: %w", err) } distM = speed * dur.Seconds() updateTime = func(result map[string]cty.Value) { if _, ok := effMap["time"]; ok { origTime, err := timecty.GetTime(effMap["time"]) if err == nil { result["time"] = timecty.NewTimeCapsule(origTime.Add(dur)) } } } case timecty.TimeCapsuleType: targetTime, err := timecty.GetTime(third) if err != nil { return cty.NilVal, err } speed, err := getFloatAttr(effMap, "speed") if err != nil { return cty.NilVal, fmt.Errorf("time form requires speed: %w", err) } origTimeVal, ok := effMap["time"] if !ok { return cty.NilVal, fmt.Errorf("time form requires origin to have a time field") } origTime, err := timecty.GetTime(origTimeVal) if err != nil { return cty.NilVal, err } dur := targetTime.Sub(origTime) if dur < 0 { return cty.NilVal, fmt.Errorf("target_time %v is before origin time %v", targetTime, origTime) } distM = speed * dur.Seconds() updateTime = func(result map[string]cty.Value) { result["time"] = timecty.NewTimeCapsule(targetTime) } default: return cty.NilVal, fmt.Errorf( "third argument must be a number (meters), duration, or time; got %s", third.Type().FriendlyName(), ) } dest := wgs84.DirectCalcLatLon(lat, lon, bearing, distM) result := make(map[string]cty.Value, len(effMap)) for k, v := range effMap { result[k] = v } result["lat"] = cty.NumberFloatVal(dest.LatDeg) result["lon"] = cty.NumberFloatVal(dest.LonDeg) if updateTime != nil { updateTime(result) } return cty.ObjectVal(result), nil }, })
GeoDestinationFunc returns the point reached by travelling from origin on the given bearing. The third argument selects how far to travel, dispatched on its type: number (meters), duration capsule, or time capsule.
var GeoFormatFunc = function.New(&function.Spec{ Description: "Formats a point as a string.", Params: []function.Parameter{ { Name: "point", Type: cty.DynamicPseudoType, Description: "A point object (numeric lat and lon)", }, }, VarParam: &function.Parameter{ Name: "format", Type: cty.String, Description: "Optional format name (at most one): \"decimal\" (default), \"decimal_alt\", \"dms\", \"dms_ascii\", or \"dms_signed\"", }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { if len(args) > 2 { return cty.NilVal, fmt.Errorf("geo_format takes at most 2 arguments") } pt := args[0] if !pt.Type().IsObjectType() { return cty.NilVal, fmt.Errorf("point must be an object, got %s", pt.Type().FriendlyName()) } attrs := pt.AsValueMap() lat, err := pointCoord(attrs, "lat", axisLat) if err != nil { return cty.NilVal, err } lon, err := pointCoord(attrs, "lon", axisLon) if err != nil { return cty.NilVal, err } format := "decimal" if len(args) == 2 { format = args[1].AsString() } s, err := formatPoint(lat, lon, attrs, format) if err != nil { return cty.NilVal, err } return cty.StringVal(s), nil }, })
GeoFormatFunc formats a point as a string using one of the named formats (decimal, decimal_alt, dms, dms_ascii, dms_signed).
var GeoInverseFunc = function.New(&function.Spec{ Description: "Returns distance (meters), bearing, and back_bearing between two points.", Params: []function.Parameter{ {Name: "point_a", Type: cty.DynamicPseudoType, Description: "The origin point (numeric lat and lon)"}, {Name: "point_b", Type: cty.DynamicPseudoType, Description: "The destination point (numeric lat and lon)"}, }, Type: function.StaticReturnType(geoInverseResultType), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { lat1, lon1, err := extractLatLon(args[0]) if err != nil { return cty.NilVal, fmt.Errorf("point_a: %w", err) } lat2, lon2, err := extractLatLon(args[1]) if err != nil { return cty.NilVal, fmt.Errorf("point_b: %w", err) } r := wgs84.InverseCalcDistanceAzimuths(lat1, lon1, lat2, lon2) return cty.ObjectVal(map[string]cty.Value{ "distance": cty.NumberFloatVal(r.DistanceM), "bearing": cty.NumberFloatVal(normalizeAz(r.Azimuth1Deg)), "back_bearing": cty.NumberFloatVal(normalizeAz(r.Azimuth2Deg + 180)), }), nil }, })
GeoInverseFunc solves the inverse geodesic problem: given two points, returns the distance and bearings between them on the WGS-84 ellipsoid.
var GeoLineIntersectFunc = function.New(&function.Spec{ Description: "Returns all intersection points between two polylines.", Params: []function.Parameter{ {Name: "line_a", Type: cty.DynamicPseudoType, Description: "A polyline: a list of point objects (numeric lat/lon), at least 2"}, {Name: "line_b", Type: cty.DynamicPseudoType, Description: "A second polyline: a list of point objects (numeric lat/lon), at least 2"}, }, Type: function.StaticReturnType(lineIntersectResultType), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { lineA, err := ctyListToPoints(args[0]) if err != nil { return cty.NilVal, fmt.Errorf("line_a: %w", err) } lineB, err := ctyListToPoints(args[1]) if err != nil { return cty.NilVal, fmt.Errorf("line_b: %w", err) } if len(lineA) < 2 { return cty.NilVal, fmt.Errorf("line_a must have at least 2 points") } if len(lineB) < 2 { return cty.NilVal, fmt.Errorf("line_b must have at least 2 points") } var intersections []cty.Value for i := 0; i < len(lineA)-1; i++ { for j := 0; j < len(lineB)-1; j++ { a0, a1 := lineA[i], lineA[i+1] b0, b1 := lineB[j], lineB[j+1] if s2.CrossingSign(a0, a1, b0, b1) == s2.Cross { ix := s2.Intersection(a0, a1, b0, b1) intersections = append(intersections, s2PointToCty(ix)) } } } if len(intersections) == 0 { return cty.ListValEmpty(lineIntersectResultType.ElementType()), nil } return cty.ListVal(intersections), nil }, })
GeoLineIntersectFunc returns all intersection points between two polylines.
var GeoNearestFunc = function.New(&function.Spec{ Description: "Returns the nearest point on the polygon perimeter to the given point.", Params: []function.Parameter{ {Name: "polygon", Type: cty.DynamicPseudoType, Description: "A list of point objects (numeric lat/lon) forming the ring; at least 3"}, {Name: "point", Type: cty.DynamicPseudoType, Description: "The point to measure from (numeric lat and lon)"}, }, Type: function.StaticReturnType(cty.Object(map[string]cty.Type{ "lat": cty.Number, "lon": cty.Number, })), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { loop, err := ctyListToLoop(args[0]) if err != nil { return cty.NilVal, err } pt, err := ctyToS2Point(args[1]) if err != nil { return cty.NilVal, fmt.Errorf("point: %w", err) } minDist := s1.InfAngle() var closest s2.Point for i := 0; i < loop.NumEdges(); i++ { edge := loop.Edge(i) projected := s2.Project(pt, edge.V0, edge.V1) dist := pt.Distance(projected) if dist < minDist { minDist = dist closest = projected } } return s2PointToCty(closest), nil }, })
GeoNearestFunc returns the closest point on a polygon's perimeter to the input point. The result is a bare {lat, lon} object.
var GeoPointFunc = function.New(&function.Spec{ Description: "Constructs a point object from lat/lon, optionally merged onto a base object.", Params: []function.Parameter{ { Name: "first", Type: cty.DynamicPseudoType, Description: "Either a combined \"lat,lon\" (or \"DMS DMS\") string, or the latitude as a number or string", }, }, VarParam: &function.Parameter{ Name: "rest", Type: cty.DynamicPseudoType, Description: "The remaining arguments: an optional longitude (with a numeric/string first), and/or a base object whose extra attributes are carried onto the result. See help(\"geo_point\") for the exact forms.", }, Type: func(args []cty.Value) (cty.Type, error) { if len(args) > 3 { return cty.NilType, fmt.Errorf("geo_point takes 1-3 arguments, got %d", len(args)) } attrs := map[string]cty.Type{ "lat": cty.Number, "lon": cty.Number, } base := findBase(args) if base != nil { baseType := base.Type() if !baseType.IsObjectType() { return cty.NilType, fmt.Errorf("base must be an object, got %s", baseType.FriendlyName()) } for k, t := range baseType.AttributeTypes() { if k == "lat" || k == "lon" { continue } attrs[k] = t } } return cty.Object(attrs), nil }, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { var latVal, lonVal cty.Value var err error switch { case len(args) == 1 && args[0].Type() == cty.String: latVal, lonVal, err = parseCombinedString(args[0].AsString()) case len(args) == 2 && args[0].Type() == cty.String && args[1].Type().IsObjectType(): latVal, lonVal, err = parseCombinedString(args[0].AsString()) default: if len(args) < 2 { return cty.NilVal, fmt.Errorf("geo_point requires lat and lon, or a combined coordinate string") } latVal, err = coerceCoord(args[0], axisLat) if err != nil { return cty.NilVal, fmt.Errorf("lat: %w", err) } lonVal, err = coerceCoord(args[1], axisLon) if err != nil { return cty.NilVal, fmt.Errorf("lon: %w", err) } } if err != nil { return cty.NilVal, err } result := map[string]cty.Value{ "lat": latVal, "lon": lonVal, } if base := findBase(args); base != nil { for k, v := range base.AsValueMap() { if k == "lat" || k == "lon" { continue } result[k] = v } } return cty.ObjectVal(result), nil }, })
GeoPointFunc constructs a point object. It accepts three forms:
geo_point(combined_string) — parse "lat,lon" or "DMS DMS" geo_point(combined_string, base) — same, merged onto base geo_point(lat, lon) — separate lat/lon (number or string) geo_point(lat, lon, base) — same, merged onto base
Numbers are treated as signed decimal degrees; strings are parsed as decimal, hemisphere-annotated, or DMS.
var GeoWaypointsFunc = function.New(&function.Spec{ Description: "Returns n evenly spaced points along the geodesic between two points.", Params: []function.Parameter{ {Name: "point_a", Type: cty.DynamicPseudoType, Description: "The start point (numeric lat and lon), returned as the first waypoint"}, {Name: "point_b", Type: cty.DynamicPseudoType, Description: "The end point (numeric lat and lon), returned as the last waypoint"}, {Name: "n", Type: cty.Number, Description: "How many waypoints to return, including both endpoints (must be >= 2)"}, }, Type: function.StaticReturnType(cty.List(cty.Object(map[string]cty.Type{ "lat": cty.Number, "lon": cty.Number, "track": cty.Number, }))), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { lat1, lon1, err := extractLatLon(args[0]) if err != nil { return cty.NilVal, fmt.Errorf("point_a: %w", err) } lat2, lon2, err := extractLatLon(args[1]) if err != nil { return cty.NilVal, fmt.Errorf("point_b: %w", err) } var n int if err := gocty.FromCtyValue(args[2], &n); err != nil { return cty.NilVal, fmt.Errorf("n: %w", err) } if n < 2 { return cty.NilVal, fmt.Errorf("n must be >= 2, got %d", n) } inv := wgs84.InverseCalcDistanceAzimuths(lat1, lon1, lat2, lon2) totalDist := inv.DistanceM azi1 := inv.Azimuth1Deg waypoints := make([]cty.Value, n) for i := 0; i < n; i++ { frac := float64(i) / float64(n-1) dist := totalDist * frac r := wgs84.DirectCalcLatLonAzi(lat1, lon1, azi1, dist) waypoints[i] = cty.ObjectVal(map[string]cty.Value{ "lat": cty.NumberFloatVal(r.LatDeg), "lon": cty.NumberFloatVal(r.LonDeg), "track": cty.NumberFloatVal(normalizeAz(r.AziDeg)), }) } return cty.ListVal(waypoints), nil }, })
GeoWaypointsFunc returns n evenly spaced points along the geodesic from point_a to point_b, including both endpoints.
var MoonPhaseFunc = function.New(&function.Spec{ Description: "Returns the moon's illuminated fraction, phase (0=new, 0.5=full), and bright-limb angle (degrees).", Params: []function.Parameter{}, VarParam: &function.Parameter{ Name: "t", Type: timecty.TimeCapsuleType, Description: "Optional time (at most one); defaults to now. Moon phase is global, so no location is needed", }, Type: function.StaticReturnType(moonPhaseResultType), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { if len(args) > 1 { return cty.NilVal, fmt.Errorf("moon_phase takes 0 or 1 arguments, got %d", len(args)) } t := time.Now() if len(args) == 1 { var err error t, err = timecty.GetTime(args[0]) if err != nil { return cty.NilVal, err } } ill := suncalc.GetMoonIllumination(t) return cty.ObjectVal(map[string]cty.Value{ "fraction": cty.NumberFloatVal(ill.Fraction), "phase": cty.NumberFloatVal(ill.Phase), "angle": cty.NumberFloatVal(radToDeg(ill.Angle)), }), nil }, })
MoonPhaseFunc returns the moon's illumination fraction, phase, and angle.
var MoonPositionFunc = function.New(&function.Spec{ Description: "Returns the moon's azimuth (degrees), altitude (degrees), and distance (meters).", Params: []function.Parameter{ {Name: "point", Type: cty.DynamicPseudoType, Description: "The observer's location (numeric lat and lon)"}, }, VarParam: &function.Parameter{ Name: "t", Type: timecty.TimeCapsuleType, Description: "Optional observation time (at most one); defaults to now", }, Type: function.StaticReturnType(moonPositionResultType), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { if len(args) > 2 { return cty.NilVal, fmt.Errorf("moon_position takes 1 or 2 arguments, got %d", len(args)) } lat, lon, err := extractLatLon(args[0]) if err != nil { return cty.NilVal, err } t := time.Now() if len(args) == 2 { t, err = timecty.GetTime(args[1]) if err != nil { return cty.NilVal, err } } pos := suncalc.GetMoonPosition(t, lat, lon) return cty.ObjectVal(map[string]cty.Value{ "azimuth": cty.NumberFloatVal(southAzToNorthDeg(pos.Azimuth)), "altitude": cty.NumberFloatVal(radToDeg(pos.Altitude)), "distance": cty.NumberFloatVal(pos.Distance * 1000), }), nil }, })
MoonPositionFunc returns the moon's azimuth, altitude, and distance as seen from a point.
var SolarMidnightFunc = makeSolarFunc(
"Returns the next solar midnight (+ optional offset) after a reference time.",
solarMidnight,
)
SolarMidnightFunc returns the next solar midnight (+ optional offset) after time t.
var SolarNoonFunc = makeSolarFunc(
"Returns the next solar noon (+ optional offset) after a reference time.",
solarNoon,
)
SolarNoonFunc returns the next solar noon (+ optional offset) after time t.
var SunPositionFunc = function.New(&function.Spec{ Description: "Returns the sun's azimuth (degrees, clockwise from north) and altitude (degrees above horizon).", Params: []function.Parameter{ {Name: "point", Type: cty.DynamicPseudoType, Description: "The observer's location (numeric lat and lon)"}, }, VarParam: &function.Parameter{ Name: "t", Type: timecty.TimeCapsuleType, Description: "Optional observation time (at most one); defaults to now", }, Type: function.StaticReturnType(sunPositionResultType), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { if len(args) > 2 { return cty.NilVal, fmt.Errorf("sun_position takes 1 or 2 arguments, got %d", len(args)) } lat, lon, err := extractLatLon(args[0]) if err != nil { return cty.NilVal, err } t := time.Now() if len(args) == 2 { t, err = timecty.GetTime(args[1]) if err != nil { return cty.NilVal, err } } pos := suncalc.GetPosition(t, lat, lon) return cty.ObjectVal(map[string]cty.Value{ "azimuth": cty.NumberFloatVal(southAzToNorthDeg(pos.Azimuth)), "altitude": cty.NumberFloatVal(radToDeg(pos.Altitude)), }), nil }, })
SunPositionFunc returns the sun's azimuth and altitude as seen from a point.
var SunriseFunc = makeSolarFunc(
"Returns the next sunrise (+ optional offset) after a reference time.",
solarSunrise,
)
SunriseFunc returns the next sunrise (+ optional offset) after time t.
var SunsetFunc = makeSolarFunc(
"Returns the next sunset (+ optional offset) after a reference time.",
solarSunset,
)
SunsetFunc returns the next sunset (+ optional offset) after time t.
Functions ¶
func Externs ¶ added in v0.3.0
Externs returns the functy `//functy:extern` declarations for the geo functions, keyed by a filename to report in diagnostics. This package does not itself parse them; the bytes are opaque to it.
Every geo function is declared, for one of two reasons. cty can only make a function's *trailing* parameters optional, and only by making them variadic — which erases their names, types, and defaults — so an optional coordinate string, format, offset, or time reads as `...args`; and cty has one signature per function, so those that dispatch on an argument's type (geo::destination's third argument; the solar functions' trailing offset-or-time) or vary their result shape (geo::point merging a base) cannot be described at all.
The rest have a fixed arity and a fixed return shape, but they are declared too: their point arguments are objects with arbitrary extra attributes, so their cty parameter type is `dynamic` — and a dynamic argument poisons cty's return type to `dynamic`, hiding the whole return payload (a distance and bearing, each waypoint's lat/lon/track). Only the declaration can show it, and the `geopoint` shape of the inputs.
There is one file per namespace, because a functy source declares at most one: `geo` (geographic, geodesic, geometric) and `sky` (solar events, celestial position). The declarations lean on the `geopoint` open type (see IsGeoPoint). A host registers both:
parser.RegisterOpenType("geopoint", geocty.IsGeoPoint)
for name, src := range geocty.Externs() {
parser.RegisterExterns(src, name)
}
func GetGeoFunctions ¶
GetGeoFunctions returns all geographic cty functions for registration in an eval context.
The names are namespaced. The geographic, geodesic, and geometric functions — whose bare names (point, area, contains, nearest) would be too generic to expose globally — live under `geo::`. The solar-event and celestial-position functions live under `sky::`. HCL parses `a::b(x)` natively as a single flat map key, so this is a naming choice, not a structural one, and the leaf names drop the redundant prefix the flat names carried.
func IsGeoPoint ¶ added in v0.3.0
IsGeoPoint reports whether v is a geographic point: an object with numeric `lat` and `lon` attributes. Additional attributes (altitude, time, speed, name, …) are allowed, and where a function preserves them they are carried through — this is an open predicate over the two coordinates, not a fixed shape. It returns nil when v qualifies and an error naming what is wrong otherwise.
It is the predicate behind the `geopoint` type a functy host registers, so that a `.cty` annotation, a signature, or a host `var` constraint can name the shape every geo function speaks in:
parser.RegisterOpenType("geopoint", geocty.IsGeoPoint)
The requirement is deliberately narrow — lat and lon, both numbers — because that is exactly what the functions here read; they are all horizontal (great-circle and geodesic on the WGS-84 surface) and none consumes an altitude. It says nothing about dimensionality: a value carrying an altitude is a geopoint, and the altitude rides along. Coordinates supplied as strings are not a geopoint until geo_point() has parsed them into numbers.
Types ¶
This section is empty.