feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
package geojson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/internal/geodata"
|
||||
"b612.me/astro/mercury"
|
||||
"b612.me/astro/venus"
|
||||
)
|
||||
|
||||
const planetTransitEvent = "planet-transit"
|
||||
|
||||
// 标准大气,与 basic 站心凌日的视地平判定及凌日 SVG 一致。
|
||||
const (
|
||||
planetTransitRefractionPressureHPa = 1010.0
|
||||
planetTransitRefractionTemperatureC = 10.0
|
||||
minimumPlanetTransitIsochroneStep = time.Second
|
||||
)
|
||||
|
||||
// PlanetTransitOptions 是凌日 GeoJSON 导出选项;所有要素都被丢弃时导出失败。
|
||||
// PlanetTransitOptions are the planetary-transit GeoJSON export options; the export fails when every
|
||||
// Feature is dropped.
|
||||
type PlanetTransitOptions struct {
|
||||
// BoundaryPoints 是地平线采样点数,同时决定分区的经度列数 max(360, BoundaryPoints)(上限 720);
|
||||
// 小于等于零时使用 360,其他值限制在 [12, 1440]。
|
||||
// BoundaryPoints is the horizon sample count and also sets the region longitude columns to
|
||||
// max(360, BoundaryPoints), capped at 720. Values <= 0 use 360; others are clamped to [12, 1440].
|
||||
BoundaryPoints int
|
||||
// IsochroneStep 为正时输出一触到四触的站心接触等时线(掠凌没有二触、三触),至少一秒;零值不输出。
|
||||
// A positive IsochroneStep adds topocentric C1-C4 isochrones (no C2/C3 for a grazing transit) and must be
|
||||
// at least one second. The zero value adds none.
|
||||
IsochroneStep time.Duration
|
||||
// TimeMarkers 非空时沿一触到四触的太阳直射点轨迹追加时间标记 Point 要素。
|
||||
// TimeMarkers adds time-marker Point Features along the subsolar track from C1 through C4 when non-nil.
|
||||
TimeMarkers *TimeMarkerOptions
|
||||
// SkipRoles 列出不写进输出的 role;分区三个 role 都跳过时不做分区计算。
|
||||
// SkipRoles lists roles to leave out; skipping all three region roles skips the region computation.
|
||||
SkipRoles []string
|
||||
}
|
||||
|
||||
// planetTransitInput 是水星、金星两种 TransitInfo 的公共视图。
|
||||
type planetTransitInput struct {
|
||||
planet string
|
||||
planetIndex int
|
||||
start time.Time
|
||||
c2 time.Time
|
||||
greatest time.Time
|
||||
c3 time.Time
|
||||
end time.Time
|
||||
hasInternal bool
|
||||
separation float64
|
||||
sunRadius float64
|
||||
planetRad float64
|
||||
}
|
||||
|
||||
// MarshalMercuryTransit 将水星凌日的全球可见分区、一触/四触地平线与可选站心等时线编码为 GeoJSON / encode a Mercury transit as GeoJSON.
|
||||
func (module *Generator) MarshalMercuryTransit(info mercury.TransitInfo, options PlanetTransitOptions) ([]byte, error) {
|
||||
if !info.Valid {
|
||||
return nil, fmt.Errorf("geojson: mercury transit is invalid")
|
||||
}
|
||||
return marshalPlanetTransit(planetTransitInput{
|
||||
planet: "mercury", planetIndex: 1,
|
||||
start: info.Start, c2: info.InternalStart, greatest: info.Greatest, c3: info.InternalEnd, end: info.End,
|
||||
hasInternal: info.HasInternal, separation: info.MinimumSeparationArcsec,
|
||||
sunRadius: info.SunSemidiameterArcsec, planetRad: info.PlanetSemidiameterArcsec,
|
||||
}, options, module)
|
||||
}
|
||||
|
||||
// MarshalVenusTransit 将金星凌日的全球可见分区、一触/四触地平线与可选站心等时线编码为 GeoJSON / encode a Venus transit as GeoJSON.
|
||||
func (module *Generator) MarshalVenusTransit(info venus.TransitInfo, options PlanetTransitOptions) ([]byte, error) {
|
||||
if !info.Valid {
|
||||
return nil, fmt.Errorf("geojson: venus transit is invalid")
|
||||
}
|
||||
return marshalPlanetTransit(planetTransitInput{
|
||||
planet: "venus", planetIndex: 2,
|
||||
start: info.Start, c2: info.InternalStart, greatest: info.Greatest, c3: info.InternalEnd, end: info.End,
|
||||
hasInternal: info.HasInternal, separation: info.MinimumSeparationArcsec,
|
||||
sunRadius: info.SunSemidiameterArcsec, planetRad: info.PlanetSemidiameterArcsec,
|
||||
}, options, module)
|
||||
}
|
||||
|
||||
func validatePlanetTransitInput(input planetTransitInput) error {
|
||||
if input.start.IsZero() || input.greatest.IsZero() || input.end.IsZero() {
|
||||
return fmt.Errorf("geojson: %s transit contact times are required", input.planet)
|
||||
}
|
||||
if !input.hasInternal && (!input.c2.IsZero() || !input.c3.IsZero()) {
|
||||
return fmt.Errorf("geojson: %s transit cannot have internal contact times without internal contacts", input.planet)
|
||||
}
|
||||
ordered := []time.Time{input.start}
|
||||
if input.hasInternal {
|
||||
if input.c2.IsZero() || input.c3.IsZero() {
|
||||
return fmt.Errorf("geojson: %s transit internal contact times are required", input.planet)
|
||||
}
|
||||
ordered = append(ordered, input.c2)
|
||||
}
|
||||
ordered = append(ordered, input.greatest)
|
||||
if input.hasInternal {
|
||||
ordered = append(ordered, input.c3)
|
||||
}
|
||||
ordered = append(ordered, input.end)
|
||||
for index := 1; index < len(ordered); index++ {
|
||||
if !ordered[index-1].Before(ordered[index]) {
|
||||
return fmt.Errorf("geojson: %s transit contact times are not strictly ordered", input.planet)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalPlanetTransit(input planetTransitInput, options PlanetTransitOptions, module *Generator) ([]byte, error) {
|
||||
markerOptions := options.TimeMarkers
|
||||
if markerOptions != nil {
|
||||
if err := validateTimeMarkerOptions(*markerOptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.IsochroneStep < 0 || (options.IsochroneStep > 0 && options.IsochroneStep < minimumPlanetTransitIsochroneStep) {
|
||||
return nil, fmt.Errorf("geojson: transit isochrone step must be zero or at least %s", minimumPlanetTransitIsochroneStep)
|
||||
}
|
||||
if err := validatePlanetTransitInput(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeScale, scaleErr := timeScaleForMarkers(markerOptions)
|
||||
if scaleErr != nil {
|
||||
return nil, scaleErr
|
||||
}
|
||||
// 几何一律用民用时刻,只有写进属性的时刻换时标。
|
||||
label := func(value time.Time) string {
|
||||
if value.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return formatTime(basic.LabelIn(timeScale, value.UTC()))
|
||||
}
|
||||
boundaryPoints := normalizeLunarBoundaryPoints(options.BoundaryPoints)
|
||||
longitudePoints := lunarVisibilityLongitudePoints(boundaryPoints, LunarEclipseOptions{})
|
||||
longitudes := make([]float64, longitudePoints+1)
|
||||
for index := range longitudes {
|
||||
longitudes[index] = -180 + 360*float64(index)/float64(longitudePoints)
|
||||
}
|
||||
refraction := basic.RefractionFromApparentAltitude(0, planetTransitRefractionPressureHPa, planetTransitRefractionTemperatureC)
|
||||
base := map[string]interface{}{"planet": input.planet, "has_internal": input.hasInternal}
|
||||
withBase := func(extra map[string]interface{}) map[string]interface{} {
|
||||
properties := cloneProperties(base)
|
||||
for key, value := range extra {
|
||||
properties[key] = value
|
||||
}
|
||||
return properties
|
||||
}
|
||||
features := make([]feature, 0, 12)
|
||||
|
||||
columnsAt := func(at time.Time) [][]lunarLatitudeInterval {
|
||||
subsolar := solarSubsolarPoint(at, module)
|
||||
columns := make([][]lunarLatitudeInterval, len(longitudes))
|
||||
for index, longitude := range longitudes {
|
||||
columns[index] = planetTransitSunUpIntervals(subsolar, longitude, refraction)
|
||||
}
|
||||
return columns
|
||||
}
|
||||
wantEntire := !skippedLunarRole(options.SkipRoles, "transit-entire")
|
||||
wantSunrise := !skippedLunarRole(options.SkipRoles, "transit-at-sunrise")
|
||||
wantSunset := !skippedLunarRole(options.SkipRoles, "transit-at-sunset")
|
||||
wantRegions := wantEntire || wantSunrise || wantSunset
|
||||
needC1 := wantRegions || !skippedLunarRole(options.SkipRoles, "visible-at-c1")
|
||||
needGreatest := wantRegions
|
||||
needC4 := wantRegions || !skippedLunarRole(options.SkipRoles, "visible-at-c4")
|
||||
var c1Columns, greatestColumns, c4Columns [][]lunarLatitudeInterval
|
||||
if needC1 {
|
||||
c1Columns = columnsAt(input.start)
|
||||
}
|
||||
if needGreatest {
|
||||
greatestColumns = columnsAt(input.greatest)
|
||||
}
|
||||
if needC4 {
|
||||
c4Columns = columnsAt(input.end)
|
||||
}
|
||||
|
||||
for _, contact := range []struct {
|
||||
role, horizonRole string
|
||||
at time.Time
|
||||
columns [][]lunarLatitudeInterval
|
||||
}{
|
||||
{"visible-at-c1", "c1-horizon", input.start, c1Columns},
|
||||
{"visible-at-c4", "c4-horizon", input.end, c4Columns},
|
||||
} {
|
||||
if !skippedLunarRole(options.SkipRoles, contact.role) {
|
||||
value, err := planetTransitRegionGeometry(longitudes, contact.columns)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geojson: %s: %w", contact.role, err)
|
||||
}
|
||||
features = append(features, newFeature(planetTransitEvent, contact.role, value,
|
||||
withBase(map[string]interface{}{"time": label(contact.at)})))
|
||||
}
|
||||
if !skippedLunarRole(options.SkipRoles, contact.horizonRole) {
|
||||
subsolar := solarSubsolarPoint(contact.at, module)
|
||||
antisolar := geodata.GeoPoint{Longitude: geodata.NormalizeLongitude(subsolar.Longitude + 180), Latitude: -subsolar.Latitude}
|
||||
value, err := geoMultiLineGeometry(geodata.SphericalCircle(antisolar, 90-refraction, boundaryPoints), true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geojson: %s: %w", contact.horizonRole, err)
|
||||
}
|
||||
features = append(features, newFeature(planetTransitEvent, contact.horizonRole, value,
|
||||
withBase(map[string]interface{}{"time": label(contact.at)})))
|
||||
}
|
||||
}
|
||||
|
||||
regionFeatures, err := planetTransitRegionFeatures(longitudes, c1Columns, greatestColumns, c4Columns, options.SkipRoles,
|
||||
withBase(map[string]interface{}{
|
||||
"time_start": label(input.start),
|
||||
"time_end": label(input.end),
|
||||
"longitude_points": longitudePoints,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
features = append(features, regionFeatures...)
|
||||
|
||||
if options.IsochroneStep > 0 {
|
||||
isochrones, isoErr := planetTransitIsochroneFeatures(input, options.IsochroneStep, timeScale, options.SkipRoles, withBase, module)
|
||||
if isoErr != nil {
|
||||
return nil, isoErr
|
||||
}
|
||||
features = append(features, isochrones...)
|
||||
}
|
||||
|
||||
greatest := solarSubsolarPoint(input.greatest, module)
|
||||
metadata := withBase(map[string]interface{}{
|
||||
"start": label(input.start),
|
||||
"greatest": label(input.greatest),
|
||||
"end": label(input.end),
|
||||
"minimum_separation_arcsec": input.separation,
|
||||
"sun_semidiameter_arcsec": input.sunRadius,
|
||||
"planet_semidiameter_arcsec": input.planetRad,
|
||||
})
|
||||
if input.hasInternal {
|
||||
metadata["internal_start"], metadata["internal_end"] = label(input.c2), label(input.c3)
|
||||
}
|
||||
features, err = appendPointFeature(features, planetTransitEvent, "greatest",
|
||||
pathSample{Time: basic.LabelIn(timeScale, input.greatest.UTC()), Longitude: greatest.Longitude, Latitude: greatest.Latitude},
|
||||
metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if markerOptions != nil {
|
||||
markers, markerErr := planetTransitTimeMarkerSamples(input.start, input.end, timeScale, *markerOptions, module)
|
||||
if markerErr != nil {
|
||||
return nil, markerErr
|
||||
}
|
||||
features, err = appendTimeMarkerPointFeatures(features, planetTransitEvent, "subsolar-track", markers, markerOptions.Location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return marshalFeatureCollectionWithTimeScale(dropFeaturesByRole(features, options.SkipRoles), timeScale)
|
||||
}
|
||||
|
||||
// planetTransitSunUpIntervals 返回 longitude 子午线上太阳中心在视地平上的纬度区间:
|
||||
// 地点到直射点的角距小于 90°+折射即可见,即 sinδ·sinφ + cosδ·cosΔλ·cosφ > -sin(折射)。
|
||||
func planetTransitSunUpIntervals(subsolar geodata.GeoPoint, longitude, refraction float64) []lunarLatitudeInterval {
|
||||
rad := math.Pi / 180
|
||||
a := math.Sin(subsolar.Latitude * rad)
|
||||
b := math.Cos(subsolar.Latitude*rad) * math.Cos((longitude-subsolar.Longitude)*rad)
|
||||
threshold := -math.Sin(refraction * rad)
|
||||
visible := func(latitude float64) bool {
|
||||
return a*math.Sin(latitude*rad)+b*math.Cos(latitude*rad) > threshold
|
||||
}
|
||||
boundaries := []float64{-90}
|
||||
if amplitude := math.Hypot(a, b); amplitude > 0 && math.Abs(threshold/amplitude) < 1 {
|
||||
// a·sinφ + b·cosφ = R·sin(φ+α),α = atan2(b, a)。
|
||||
alpha := math.Atan2(b, a) / rad
|
||||
base := math.Asin(threshold/amplitude) / rad
|
||||
roots := make([]float64, 0, 2)
|
||||
for _, value := range []float64{base - alpha, 180 - base - alpha} {
|
||||
for _, shift := range []float64{-360, 0, 360} {
|
||||
if root := value + shift; root > -90 && root < 90 {
|
||||
roots = append(roots, root)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(roots) == 2 && roots[0] > roots[1] {
|
||||
roots[0], roots[1] = roots[1], roots[0]
|
||||
}
|
||||
boundaries = append(boundaries, roots...)
|
||||
}
|
||||
boundaries = append(boundaries, 90)
|
||||
intervals := make([]lunarLatitudeInterval, 0, 2)
|
||||
for index := 0; index+1 < len(boundaries); index++ {
|
||||
low, high := boundaries[index], boundaries[index+1]
|
||||
if high-low > 1e-9 && visible((low+high)/2) {
|
||||
intervals = append(intervals, lunarLatitudeInterval{low: low, high: high})
|
||||
}
|
||||
}
|
||||
return lunarUnionLatitudeIntervals(intervals...)
|
||||
}
|
||||
|
||||
func planetTransitRegionGeometry(longitudes []float64, columns [][]lunarLatitudeInterval) (geometry, error) {
|
||||
value, err := lunarVisibilityEnvelopeGeometry(longitudes, columns)
|
||||
if err != nil {
|
||||
return geometry{}, err
|
||||
}
|
||||
return dropDegenerateMultiPolygonRings(value), nil
|
||||
}
|
||||
|
||||
// planetTransitRegionFeatures 按与凌日 SVG 相同的口径分区:全程可见是三刻都在地平上,日出带是四触可见而一触不可见,
|
||||
// 其余凡有一刻可见的都归入日落带(含凌甚前后可见的极区透镜与两端可见、凌甚不可见的高纬带)。
|
||||
func planetTransitRegionFeatures(
|
||||
longitudes []float64,
|
||||
c1, greatest, c4 [][]lunarLatitudeInterval,
|
||||
skip []string,
|
||||
properties map[string]interface{},
|
||||
) ([]feature, error) {
|
||||
wantEntire := !skippedLunarRole(skip, "transit-entire")
|
||||
wantSunrise := !skippedLunarRole(skip, "transit-at-sunrise")
|
||||
wantSunset := !skippedLunarRole(skip, "transit-at-sunset")
|
||||
if !wantEntire && !wantSunrise && !wantSunset {
|
||||
return nil, nil
|
||||
}
|
||||
union := func(groups ...[]lunarLatitudeInterval) []lunarLatitudeInterval {
|
||||
var merged []lunarLatitudeInterval
|
||||
for _, group := range groups {
|
||||
merged = append(merged, group...)
|
||||
}
|
||||
return lunarUnionLatitudeIntervals(merged...)
|
||||
}
|
||||
entire := make([][]lunarLatitudeInterval, len(longitudes))
|
||||
sunrise := make([][]lunarLatitudeInterval, len(longitudes))
|
||||
sunset := make([][]lunarLatitudeInterval, len(longitudes))
|
||||
for index := range longitudes {
|
||||
bothEnds := lunarIntersectLatitudeIntervals(c1[index], c4[index])
|
||||
entire[index] = lunarIntersectLatitudeIntervals(bothEnds, greatest[index])
|
||||
sunrise[index] = lunarSubtractLatitudeIntervals(c4[index], c1[index])
|
||||
sunset[index] = lunarSubtractLatitudeIntervals(union(c1[index], c4[index], greatest[index]),
|
||||
union(entire[index], sunrise[index]))
|
||||
}
|
||||
features := make([]feature, 0, 3)
|
||||
for _, region := range []struct {
|
||||
role string
|
||||
want bool
|
||||
columns [][]lunarLatitudeInterval
|
||||
}{
|
||||
{"transit-entire", wantEntire, entire},
|
||||
{"transit-at-sunrise", wantSunrise, sunrise},
|
||||
{"transit-at-sunset", wantSunset, sunset},
|
||||
} {
|
||||
if !region.want {
|
||||
continue
|
||||
}
|
||||
value, err := planetTransitRegionGeometry(longitudes, region.columns)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geojson: %s: %w", region.role, err)
|
||||
}
|
||||
features = append(features, newFeature(planetTransitEvent, region.role, value, cloneProperties(properties)))
|
||||
}
|
||||
return features, nil
|
||||
}
|
||||
|
||||
// planetTransitIsochroneFeatures 把每个接触的站心等时线编码为一条带 times 的 MultiLineString:
|
||||
// 每段是一条支路,times 与坐标对齐,同一支路上所有点同刻。
|
||||
func planetTransitIsochroneFeatures(
|
||||
input planetTransitInput,
|
||||
step time.Duration,
|
||||
timeScale basic.TimeScale,
|
||||
skip []string,
|
||||
withBase func(map[string]interface{}) map[string]interface{},
|
||||
module *Generator,
|
||||
) ([]feature, error) {
|
||||
result := basic.PlanetTransitResultFromTimes(input.planetIndex, input.start, input.c2, input.greatest, input.c3, input.end)
|
||||
features := make([]feature, 0, 4)
|
||||
for _, contact := range []struct {
|
||||
kind basic.PlanetTransitContactKind
|
||||
role string
|
||||
}{
|
||||
{basic.PlanetTransitC1, "c1-isochrone"},
|
||||
{basic.PlanetTransitC2, "c2-isochrone"},
|
||||
{basic.PlanetTransitC3, "c3-isochrone"},
|
||||
{basic.PlanetTransitC4, "c4-isochrone"},
|
||||
} {
|
||||
if skippedLunarRole(skip, contact.role) {
|
||||
continue
|
||||
}
|
||||
lines := basic.PlanetTransitContactIsochronesWithInstance(result, contact.kind, step, module.inst)
|
||||
coordinates := make([][][]float64, 0, len(lines))
|
||||
times := make([][]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
at := basic.LabelIn(timeScale, basic.JD2DateByZone(line.JD, time.UTC, false))
|
||||
for _, segment := range line.Segments {
|
||||
if len(segment) < 2 {
|
||||
continue
|
||||
}
|
||||
points := make([]pathSample, len(segment))
|
||||
for index, point := range segment {
|
||||
points[index] = pathSample{Time: at, Longitude: point.Longitude, Latitude: point.Latitude}
|
||||
}
|
||||
pieces, err := splitTimedLine(points, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geojson: %s: %w", contact.role, err)
|
||||
}
|
||||
for _, piece := range pieces {
|
||||
if len(piece) < 2 {
|
||||
continue
|
||||
}
|
||||
coordinate := make([][]float64, len(piece))
|
||||
stamp := make([]string, len(piece))
|
||||
for index, point := range piece {
|
||||
if err := validateCoordinate(point.Longitude, point.Latitude); err != nil {
|
||||
return nil, fmt.Errorf("geojson: %s: %w", contact.role, err)
|
||||
}
|
||||
coordinate[index] = []float64{point.Longitude, point.Latitude}
|
||||
stamp[index] = formatTime(point.Time)
|
||||
}
|
||||
coordinates = append(coordinates, coordinate)
|
||||
times = append(times, stamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(coordinates) == 0 {
|
||||
continue
|
||||
}
|
||||
properties := withBase(map[string]interface{}{"step_seconds": step.Seconds(), "times": times})
|
||||
features = append(features, newFeature(planetTransitEvent, contact.role,
|
||||
geometry{Type: "MultiLineString", Coordinates: coordinates}, properties))
|
||||
}
|
||||
return features, nil
|
||||
}
|
||||
|
||||
func planetTransitTimeMarkerSamples(
|
||||
start, end time.Time,
|
||||
scale basic.TimeScale,
|
||||
options TimeMarkerOptions,
|
||||
module *Generator,
|
||||
) ([]pathSample, error) {
|
||||
step, err := normalizeTimeMarkerStep(options.Step)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geojson: transit time markers: %w", err)
|
||||
}
|
||||
location := normalizeTimeMarkerLocation(options.Location)
|
||||
capacity, err := timeMarkerCapacity(start, end, step, location)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geojson: transit time markers: %w", err)
|
||||
}
|
||||
markers := make([]pathSample, 0, capacity)
|
||||
for current := firstTimeMarkerAfter(start, step, location); current.Before(end); current = current.Add(step) {
|
||||
point := solarSubsolarPoint(current, module)
|
||||
markers = append(markers, pathSample{
|
||||
Time: basic.LabelIn(scale, current),
|
||||
Longitude: point.Longitude,
|
||||
Latitude: point.Latitude,
|
||||
})
|
||||
}
|
||||
return markers, nil
|
||||
}
|
||||
Reference in New Issue
Block a user