geocty

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: BSD-2-Clause Imports: 17 Imported by: 0

README

geo-cty-funcs

A Go module providing geographic, solar/astronomical, geodesic, and geometric functions for use in go-cty / HCL2 evaluation contexts.

Installation

go get github.com/tsarna/geo-cty-funcs

Usage

import (
    geocty "github.com/tsarna/geo-cty-funcs"
    "github.com/zclconf/go-cty/cty/function"
)

// Register all functions in an HCL eval context
funcs := geocty.GetGeoFunctions()
// funcs is map[string]function.Function — merge into your eval context

The point Object

A point is a plain cty object with at least lat and lon number attributes (signed decimal degrees). Additional fields (e.g. alt, speed, track, time) are preserved by functions that return derived points. The field names are aligned with the GPSD JSON TPV record, so data from a GPSD source can flow through without translation.

p = {lat = 37.7749, lon = -122.4194}
p2 = {lat = 51.5074, lon = -0.1278, alt = 11.0, speed = 10.0}

IsGeoPoint(cty.Value) error is that shape as a predicate — nil when the value is an object with numeric lat and lon, an error otherwise. A functy host registers it as the open type geopoint, so a .cty annotation or a host variable constraint can require a point while letting the extra fields ride along:

parser.RegisterOpenType("geopoint", geocty.IsGeoPoint)

Signature declarations

The signatures these functions really have are not the ones cty can express, for two reasons. Some fake an optional or type-dispatched argument with a variadic — geo::format's format, the solar functions' offset and time, the union third argument of geo::destination — or vary their result shape, as geo::point does when it merges a base; cty cannot describe these at all. The rest have a fixed arity and a fixed return shape, but their point arguments are objects with arbitrary extras, so their cty parameter type is dynamic — and a dynamic argument poisons cty's return type to dynamic too, hiding the whole return payload (geo::inverse's {distance, bearing, back_bearing} reflects as nothing).

So externs/geo.cty and externs/sky.cty declare every function as a functy //functy:extern declaration — one file per namespace — typing the point arguments as geopoint and spelling out each return shape. They are never compiled and declare nothing callable; they exist so help(), generated documentation, and editor tooling can show the real signatures. Externs() returns them as opaque bytes keyed by filename — this package does not import functy:

parser.RegisterOpenType("geopoint", geocty.IsGeoPoint)
for name, src := range geocty.Externs() {
    parser.RegisterExterns(src, name)
}

A host that is not a functy host can ignore both; the cty Description on every function and parameter is still populated.

Functions

Point Construction and Formatting
Function Signature Description
geo::point geo::point(combined) or geo::point(lat, lon[, base]) Constructs a point from a combined string or separate lat/lon
geo::format geo::format(point[, format]) Formats a point as a string
geo::point(combined[, base]) or geo::point(lat, lon[, base])

A single combined string is parsed by splitting on , (decimal formats) or on the hemisphere-letter boundary (DMS formats). All geo::format output formats round-trip through the single-string form.

When two separate values are given, lat and lon may each be a number (signed decimal degrees) or a string in any of these forms:

  • Signed decimal: "37.7749", "-122.4194"
  • Decimal with hemisphere: "37.7749 N", "N 37.7749", "122.4194W"
  • DMS with degree symbol: "37°46'29.6"N"
  • DMS with ASCII d/D: "37d46'29.6"N"
  • Whitespace-separated DMS: "37 46 29.6 N"

If base is supplied, the result is a copy of base with lat/lon overwritten.

geo::point("37.7749,-122.4194")
geo::point("37°46'29\"N 122°25'9\"W")
geo::point(37.7749, -122.4194)
geo::point("37°46'29\"N", "122°25'9\"W")
geo::point(37.7749, -122.4194, {alt = 11.0, track = 90.0})
geo::point("37.7749,-122.4194", {alt = 11.0})
geo::format(point[, format])
Format Example
"decimal" (default) "37.7749,-122.4194"
"decimal_alt" "37.7749,-122.4194,11" (includes alt if present)
"dms" "37°46'29.6"N 122°25'9.8"W"
"dms_ascii" "37d46'29.6"N 122d25'9.8"W"
"dms_signed" "37°46'29.6" -122°25'9.8""

Solar / Astronomical Time Functions

All four solar functions share the same signature pattern:

f(point)
f(point, offset)     — offset is a duration capsule
f(point, t)          — t is a time capsule
f(point, offset, t)

They return the next occurrence after t (default: now()) of the solar event plus offset (default: zero). If the computed target is in the past, the function advances to the next day. All times are UTC.

In polar regions where the sun does not rise or set for extended periods, the functions search forward day-by-day (up to 400 days) until the event occurs again. sky::solar_noon and sky::solar_midnight require both sunrise and sunset to exist on the relevant day(s); during polar day or polar night they return the first meaningful occurrence after polar conditions end.

Function Description
sky::sunrise Next sunrise
sky::sunset Next sunset
sky::solar_noon Next solar noon (midpoint of sunrise and sunset)
sky::solar_midnight Next solar midnight (midpoint of sunset and next sunrise)
time = sky::sunrise(var.location)
time = sky::sunrise(var.location, duration("-30m"))
time = sky::sunset(var.location, duration("-15m"), ctx.scheduled_time)

Sun and Moon Position
Function Signature Returns
sky::sun_position sky::sun_position(point[, t]) {azimuth, altitude} (degrees)
sky::moon_position sky::moon_position(point[, t]) {azimuth, altitude, distance} (degrees, meters)
sky::moon_phase sky::moon_phase([t]) {fraction, phase, angle}
  • azimuth: clockwise from true north (0=N, 90=E, 180=S, 270=W)
  • altitude: degrees above horizon (negative = below)
  • distance: meters to the moon's centre
  • fraction: 0-1, illuminated fraction of the visible disc
  • phase: 0-1, position in lunation cycle (0 = new, 0.5 = full)
  • angle: bright-limb angle in degrees (sign distinguishes waxing/waning)
s = sky::sun_position(var.location)
s.altitude > 0            # true if the sun is up
s.altitude < -6.0         # true if past civil twilight

mp = sky::moon_phase()
mp.phase < 0.5            # true if waxing

Geodesic Functions (WGS-84)
Function Signature Description
geo::inverse geo::inverse(a, b) Distance and bearings between two points
geo::destination geo::destination(origin, bearing, third[, extras]) Point reached by travelling on a bearing
geo::waypoints geo::waypoints(a, b, n) n evenly spaced points along the geodesic
geo::inverse(point_a, point_b)

Returns {distance, bearing, back_bearing}. Distance in meters, bearings in degrees clockwise from north.

r = geo::inverse(var.current, var.destination)
r.distance    # meters
r.bearing     # heading toward destination
geo::destination(origin, bearing, third[, extras])

The third argument selects how far to travel, dispatched on type:

  • Number: distance in meters
  • Duration capsule: travel for that duration at origin.speed (m/s)
  • Time capsule: travel until that time at origin.speed

extras is an optional object merged onto origin before the calculation (extras win on key conflicts). All fields from the effective origin are preserved in the result; only lat/lon (and time for duration/time forms) are updated.

dest = geo::destination(var.location, 90.0, 1000.0)
future = geo::destination(var.vehicle, var.vehicle.track, duration("5m"))
projected = geo::destination({lat = 37.7, lon = -122.4}, 90.0, duration("10m"),
                            {speed = 15.0, time = now()})
geo::waypoints(point_a, point_b, n)

Returns a list of n (>= 2) evenly spaced {lat, lon, track} objects along the geodesic, including both endpoints.

waypoints = geo::waypoints(var.origin, var.destination, 5)

Geometric Functions
Function Signature Description
geo::area geo::area(polygon) Area in square meters
geo::contains geo::contains(polygon, point) Point-in-polygon test
geo::nearest geo::nearest(polygon, point) Nearest point on perimeter
geo::line_intersect geo::line_intersect(line_a, line_b) Intersection points of two polylines

A polygon is a list(point), implicitly closed. Fewer than 3 points is an error.

geo::area(polygon)

Returns area in square meters. Orientation-independent.

geo::contains(polygon, point)

Returns true if point is inside the polygon.

geo::contains(var.home_zone, var.vehicle_position)
geo::nearest(polygon, point)

Returns the closest {lat, lon} on the polygon perimeter (may be mid-edge).

geo::line_intersect(line_a, line_b)

Returns a list of {lat, lon} intersection points. Empty list if no crossings. Each line must have at least 2 points.


Dependencies

Functionality Library
Sunrise/sunset go-sunrise
Sun/moon position, moon phase suncalc
Geodesic (WGS-84) geographiclib-go
Geometry (area, contains, etc.) S2 Geometry

Notes

  • All distances are in meters; all bearings and angles are in degrees.
  • Solar functions return time capsules from time-cty-funcs and accept duration/time capsules as arguments.
  • Geodesic functions use the WGS-84 ellipsoid model for high accuracy.

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

View Source
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.

View Source
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.

View Source
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.

View Source
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).

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
var SunriseFunc = makeSolarFunc(
	"Returns the next sunrise (+ optional offset) after a reference time.",
	solarSunrise,
)

SunriseFunc returns the next sunrise (+ optional offset) after time t.

View Source
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

func Externs() map[string][]byte

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

func GetGeoFunctions() map[string]function.Function

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

func IsGeoPoint(v cty.Value) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL