feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
+369
-117
@@ -5,7 +5,8 @@ import "math"
|
||||
// SphericalCircle 返回球面小圆上的等间隔采样点 / SphericalCircle returns evenly spaced points on a small circle on the
|
||||
// 球面小圆;方位角从地理北方顺时针采样 / sphere. Bearings are sampled clockwise from geographic north.
|
||||
func SphericalCircle(center GeoPoint, radiusDegrees float64, points int) []GeoPoint {
|
||||
if points < 3 {
|
||||
// r=0 时采样点重合、r=180 时只剩对跖点副本,都不构成环。
|
||||
if points < 3 || !(radiusDegrees > 0 && radiusDegrees < 180) {
|
||||
return nil
|
||||
}
|
||||
latitude := center.Latitude * math.Pi / 180
|
||||
@@ -28,6 +29,361 @@ func SphericalCircle(center GeoPoint, radiusDegrees float64, points int) []GeoPo
|
||||
return result
|
||||
}
|
||||
|
||||
type geoVector3 struct {
|
||||
x, y, z float64
|
||||
}
|
||||
|
||||
func geoPointVector(point GeoPoint) geoVector3 {
|
||||
latitude := point.Latitude * math.Pi / 180
|
||||
longitude := point.Longitude * math.Pi / 180
|
||||
cosLatitude := math.Cos(latitude)
|
||||
return geoVector3{
|
||||
x: cosLatitude * math.Cos(longitude),
|
||||
y: cosLatitude * math.Sin(longitude),
|
||||
z: math.Sin(latitude),
|
||||
}
|
||||
}
|
||||
|
||||
func geoVectorPoint(vector geoVector3) GeoPoint {
|
||||
length := math.Sqrt(vector.x*vector.x + vector.y*vector.y + vector.z*vector.z)
|
||||
if length == 0 || math.IsNaN(length) || math.IsInf(length, 0) {
|
||||
return GeoPoint{Longitude: math.NaN(), Latitude: math.NaN()}
|
||||
}
|
||||
vector.x /= length
|
||||
vector.y /= length
|
||||
vector.z /= length
|
||||
return GeoPoint{
|
||||
Longitude: normalizeLongitude(math.Atan2(vector.y, vector.x) * 180 / math.Pi),
|
||||
Latitude: math.Asin(math.Max(-1, math.Min(1, vector.z))) * 180 / math.Pi,
|
||||
}
|
||||
}
|
||||
|
||||
func geoVectorDot(first, second geoVector3) float64 {
|
||||
return first.x*second.x + first.y*second.y + first.z*second.z
|
||||
}
|
||||
|
||||
func geoVectorCross(first, second geoVector3) geoVector3 {
|
||||
return geoVector3{
|
||||
x: first.y*second.z - first.z*second.y,
|
||||
y: first.z*second.x - first.x*second.z,
|
||||
z: first.x*second.y - first.y*second.x,
|
||||
}
|
||||
}
|
||||
|
||||
func geoVectorScale(vector geoVector3, scale float64) geoVector3 {
|
||||
return geoVector3{x: vector.x * scale, y: vector.y * scale, z: vector.z * scale}
|
||||
}
|
||||
|
||||
func geoVectorAdd(first, second geoVector3) geoVector3 {
|
||||
return geoVector3{x: first.x + second.x, y: first.y + second.y, z: first.z + second.z}
|
||||
}
|
||||
|
||||
func geoVectorNormalize(vector geoVector3) (geoVector3, bool) {
|
||||
length := math.Sqrt(geoVectorDot(vector, vector))
|
||||
if length <= 1e-15 || math.IsNaN(length) || math.IsInf(length, 0) {
|
||||
return geoVector3{}, false
|
||||
}
|
||||
return geoVectorScale(vector, 1/length), true
|
||||
}
|
||||
|
||||
// InterpolateGreatCircle 在较短大圆弧上按分数插值,分数超出 [0,1] 时沿弧延长 / interpolates the shorter great-circle arc, extending it beyond an endpoint.
|
||||
func InterpolateGreatCircle(first, second GeoPoint, fraction float64) GeoPoint {
|
||||
return sphericalInterpolate(first, second, fraction)
|
||||
}
|
||||
|
||||
func sphericalInterpolate(first, second GeoPoint, fraction float64) GeoPoint {
|
||||
a := geoPointVector(first)
|
||||
b := geoPointVector(second)
|
||||
dot := math.Max(-1, math.Min(1, geoVectorDot(a, b)))
|
||||
if dot > 1-1e-14 {
|
||||
return geoVectorPoint(geoVectorAdd(geoVectorScale(a, 1-fraction), geoVectorScale(b, fraction)))
|
||||
}
|
||||
if dot < -1+1e-14 {
|
||||
// Antipodal endpoints have no unique great circle. The path samplers
|
||||
// never intentionally create one, but retain a finite fallback for
|
||||
// malformed caller input.
|
||||
return GeoPoint{
|
||||
Longitude: normalizeLongitude(first.Longitude + fraction*normalizeLongitude(second.Longitude-first.Longitude)),
|
||||
Latitude: first.Latitude + fraction*(second.Latitude-first.Latitude),
|
||||
}
|
||||
}
|
||||
angle := math.Acos(dot)
|
||||
sine := math.Sin(angle)
|
||||
value := geoVectorAdd(
|
||||
geoVectorScale(a, math.Sin((1-fraction)*angle)/sine),
|
||||
geoVectorScale(b, math.Sin(fraction*angle)/sine),
|
||||
)
|
||||
return geoVectorPoint(value)
|
||||
}
|
||||
|
||||
func sphericalPointOnArc(point, first, second GeoPoint) bool {
|
||||
firstVector := geoPointVector(first)
|
||||
secondVector := geoPointVector(second)
|
||||
pointVector := geoPointVector(point)
|
||||
arc := math.Acos(math.Max(-1, math.Min(1, geoVectorDot(firstVector, secondVector))))
|
||||
if arc <= 1e-14 {
|
||||
return math.Acos(math.Max(-1, math.Min(1, geoVectorDot(firstVector, pointVector)))) <= 1e-9
|
||||
}
|
||||
firstDistance := math.Acos(math.Max(-1, math.Min(1, geoVectorDot(firstVector, pointVector))))
|
||||
secondDistance := math.Acos(math.Max(-1, math.Min(1, geoVectorDot(pointVector, secondVector))))
|
||||
return math.Abs(firstDistance+secondDistance-arc) <= 1e-9
|
||||
}
|
||||
|
||||
// sphericalPolygonContainsOrTouches tests a simple ring using its minor great
|
||||
// circle edges. It is intentionally internal: map encoders still own the
|
||||
// projection-specific winding rules, while topology code needs a seam-free
|
||||
// containment predicate for pole and antimeridian decisions.
|
||||
func sphericalPolygonContainsOrTouches(polygon []GeoPoint, point GeoPoint) bool {
|
||||
if len(polygon) < 3 {
|
||||
return false
|
||||
}
|
||||
if SameGeoPoint(polygon[0], polygon[len(polygon)-1]) {
|
||||
polygon = polygon[:len(polygon)-1]
|
||||
}
|
||||
target := geoPointVector(point)
|
||||
angleSum := 0.0
|
||||
area := 0.0
|
||||
origin := geoPointVector(polygon[0])
|
||||
for index, currentPoint := range polygon {
|
||||
previousPoint := polygon[(index+len(polygon)-1)%len(polygon)]
|
||||
if sphericalPointOnArc(point, previousPoint, currentPoint) {
|
||||
return true
|
||||
}
|
||||
previous := geoPointVector(previousPoint)
|
||||
current := geoPointVector(currentPoint)
|
||||
area += 2 * math.Atan2(geoVectorDot(origin, geoVectorCross(previous, current)),
|
||||
1+geoVectorDot(origin, previous)+geoVectorDot(previous, current)+geoVectorDot(current, origin))
|
||||
previousTangent := geoVectorAdd(previous, geoVectorScale(target, -geoVectorDot(previous, target)))
|
||||
currentTangent := geoVectorAdd(current, geoVectorScale(target, -geoVectorDot(current, target)))
|
||||
previousTangent, previousOK := geoVectorNormalize(previousTangent)
|
||||
currentTangent, currentOK := geoVectorNormalize(currentTangent)
|
||||
if !previousOK || !currentOK {
|
||||
// A vertex and its antipode both have no tangent. Only the vertex
|
||||
// belongs to the ring; acos roundoff can miss it in the arc check.
|
||||
return (!previousOK && geoVectorDot(previous, target) > 0) ||
|
||||
(!currentOK && geoVectorDot(current, target) > 0)
|
||||
}
|
||||
cross := geoVectorCross(previousTangent, currentTangent)
|
||||
angleSum += math.Atan2(geoVectorDot(target, cross), geoVectorDot(previousTangent, currentTangent))
|
||||
}
|
||||
// Tangent winding has opposite signs inside the polygon and its antipodal
|
||||
// image. Match the signed minor area instead of accepting both images.
|
||||
return math.Abs(angleSum) > math.Pi && angleSum*math.Remainder(area, 4*math.Pi) > 0
|
||||
}
|
||||
|
||||
// SphericalPolygonsContainPaths 判断每个路径顶点和加密边中点是否都位于球面多边形内。
|
||||
// SphericalPolygonsContainPaths reports whether every path vertex and every
|
||||
// minor-great-circle edge midpoint lies in or on at least one polygon. When
|
||||
// closePaths is true, the last vertex of each path is also joined to its first.
|
||||
func SphericalPolygonsContainPaths(polygons, paths [][]GeoPoint, closePaths bool) bool {
|
||||
return SphericalPolygonsContainPathsWithinKM(polygons, paths, closePaths, 0)
|
||||
}
|
||||
|
||||
// SphericalPolygonIndex 可复用的球面多边形包含索引 / a reusable containment index for one polygon set.
|
||||
type SphericalPolygonIndex struct {
|
||||
containment sphericalPolygonContainment
|
||||
}
|
||||
|
||||
// NewSphericalPolygonIndex 为多边形集合构建包含索引 / builds a containment index for the polygon set.
|
||||
func NewSphericalPolygonIndex(polygons [][]GeoPoint) *SphericalPolygonIndex {
|
||||
return &SphericalPolygonIndex{containment: newSphericalPolygonContainment(polygons)}
|
||||
}
|
||||
|
||||
// ContainsPoints 返回各点是否位于多边形内,结果与 points 对齐 / reports per-point containment aligned with points.
|
||||
func (index *SphericalPolygonIndex) ContainsPoints(points []GeoPoint) []bool {
|
||||
result := make([]bool, len(points))
|
||||
if index == nil {
|
||||
return result
|
||||
}
|
||||
for pointIndex, point := range points {
|
||||
result[pointIndex] = index.containment.contains(point)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SphericalPolygonsContainPoints 用共享索引批量测试各个独立点 / tests independent points against one shared spherical polygon index.
|
||||
func SphericalPolygonsContainPoints(polygons [][]GeoPoint, points []GeoPoint) []bool {
|
||||
return NewSphericalPolygonIndex(polygons).ContainsPoints(points)
|
||||
}
|
||||
|
||||
// SphericalPolygonsContainPathsWithinKM 是 SphericalPolygonsContainPaths 的容差感知形式。
|
||||
// SphericalPolygonsContainPathsWithinKM is the tolerance-aware form of
|
||||
// SphericalPolygonsContainPaths. It stops at the first probe farther than the
|
||||
// requested distance from every polygon.
|
||||
func SphericalPolygonsContainPathsWithinKM(
|
||||
polygons, paths [][]GeoPoint,
|
||||
closePaths bool,
|
||||
toleranceKM float64,
|
||||
) bool {
|
||||
containment := newSphericalPolygonContainment(polygons)
|
||||
return visitSphericalPathProbes(paths, closePaths, func(point GeoPoint) bool {
|
||||
return containment.missDistanceKM(point) <= toleranceKM
|
||||
})
|
||||
}
|
||||
|
||||
// SphericalPolygonsPathMissDistanceKM 返回路径样本到多边形内部或边界的最大偏离距离。
|
||||
// SphericalPolygonsPathMissDistanceKM returns the greatest distance from a
|
||||
// path probe outside all polygons to the nearest polygon edge. Vertices and
|
||||
// minor-great-circle edge midpoints are probed; a fully contained path returns
|
||||
// zero.
|
||||
func SphericalPolygonsPathMissDistanceKM(polygons, paths [][]GeoPoint, closePaths bool) float64 {
|
||||
containment := newSphericalPolygonContainment(polygons)
|
||||
maximumMiss := 0.0
|
||||
visitSphericalPathProbes(paths, closePaths, func(point GeoPoint) bool {
|
||||
maximumMiss = math.Max(maximumMiss, containment.missDistanceKM(point))
|
||||
return true
|
||||
})
|
||||
return maximumMiss
|
||||
}
|
||||
|
||||
func visitSphericalPathProbes(
|
||||
paths [][]GeoPoint,
|
||||
closePaths bool,
|
||||
visit func(GeoPoint) bool,
|
||||
) bool {
|
||||
for _, path := range paths {
|
||||
path = openGeoRing(path)
|
||||
for _, point := range path {
|
||||
if !visit(point) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
edgeCount := len(path) - 1
|
||||
if closePaths && len(path) > 1 {
|
||||
edgeCount = len(path)
|
||||
}
|
||||
for index := 0; index < edgeCount; index++ {
|
||||
if !visit(sphericalInterpolate(path[index], path[(index+1)%len(path)], 0.5)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type sphericalPolygonContainment struct {
|
||||
polygons [][]GeoPoint
|
||||
projected []polygonUnionRing
|
||||
center geoVector3
|
||||
xAxis geoVector3
|
||||
yAxis geoVector3
|
||||
}
|
||||
|
||||
func newSphericalPolygonContainment(polygons [][]GeoPoint) sphericalPolygonContainment {
|
||||
result := sphericalPolygonContainment{polygons: polygons}
|
||||
center := geoVector3{}
|
||||
for _, polygon := range polygons {
|
||||
for _, point := range openGeoRing(polygon) {
|
||||
center = geoVectorAdd(center, geoPointVector(point))
|
||||
}
|
||||
}
|
||||
var ok bool
|
||||
result.center, ok = geoVectorNormalize(center)
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
reference := geoVector3{z: 1}
|
||||
if math.Abs(geoVectorDot(reference, result.center)) > 0.9 {
|
||||
reference = geoVector3{x: 1}
|
||||
}
|
||||
result.xAxis, ok = geoVectorNormalize(geoVectorCross(reference, result.center))
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
result.yAxis, ok = geoVectorNormalize(geoVectorCross(result.center, result.xAxis))
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
result.projected = make([]polygonUnionRing, len(polygons))
|
||||
for polygonIndex, polygon := range polygons {
|
||||
polygon = openGeoRing(polygon)
|
||||
points := make([]polygonUnionPoint, len(polygon))
|
||||
for pointIndex, point := range polygon {
|
||||
projected, projectedOK := result.project(point)
|
||||
if !projectedOK {
|
||||
result.projected = nil
|
||||
return result
|
||||
}
|
||||
points[pointIndex] = projected
|
||||
}
|
||||
result.projected[polygonIndex] = polygonUnionRingForPoints(points)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (containment sphericalPolygonContainment) project(point GeoPoint) (polygonUnionPoint, bool) {
|
||||
vector := geoPointVector(point)
|
||||
denominator := geoVectorDot(vector, containment.center)
|
||||
if denominator <= 1e-12 {
|
||||
return polygonUnionPoint{}, false
|
||||
}
|
||||
return polygonUnionPoint{
|
||||
x: geoVectorDot(vector, containment.xAxis) / denominator,
|
||||
y: geoVectorDot(vector, containment.yAxis) / denominator,
|
||||
}, true
|
||||
}
|
||||
|
||||
func (containment sphericalPolygonContainment) contains(point GeoPoint) bool {
|
||||
if len(containment.projected) > 0 {
|
||||
if projected, ok := containment.project(point); ok {
|
||||
for _, polygon := range containment.projected {
|
||||
if polygonUnionRingContainsPoint(projected, polygon) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// Every ring is inside this gnomonic hemisphere. A point beyond its
|
||||
// horizon cannot be inside and must not use antipodal tangent winding.
|
||||
return false
|
||||
}
|
||||
for _, polygon := range containment.polygons {
|
||||
if sphericalPolygonContainsOrTouches(polygon, point) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (containment sphericalPolygonContainment) missDistanceKM(point GeoPoint) float64 {
|
||||
if containment.contains(point) {
|
||||
return 0
|
||||
}
|
||||
return containment.sphericalMissDistanceKM(point)
|
||||
}
|
||||
|
||||
func (containment sphericalPolygonContainment) sphericalMissDistanceKM(point GeoPoint) float64 {
|
||||
nearest := math.Inf(1)
|
||||
for _, polygon := range containment.polygons {
|
||||
polygon = openGeoRing(polygon)
|
||||
for index, start := range polygon {
|
||||
end := polygon[(index+1)%len(polygon)]
|
||||
nearest = math.Min(nearest, sphericalPointArcDistanceKM(point, start, end))
|
||||
}
|
||||
}
|
||||
return nearest
|
||||
}
|
||||
|
||||
func sphericalPointArcDistanceKM(point, start, end GeoPoint) float64 {
|
||||
pointVector := geoPointVector(point)
|
||||
startVector := geoPointVector(start)
|
||||
endVector := geoPointVector(end)
|
||||
normal, ok := geoVectorNormalize(geoVectorCross(startVector, endVector))
|
||||
if ok {
|
||||
projection := geoVectorAdd(pointVector, geoVectorScale(normal, -geoVectorDot(pointVector, normal)))
|
||||
if projected, projectedOK := geoVectorNormalize(projection); projectedOK {
|
||||
for _, candidate := range []geoVector3{projected, geoVectorScale(projected, -1)} {
|
||||
candidatePoint := geoVectorPoint(candidate)
|
||||
if sphericalPointOnArc(candidatePoint, start, end) {
|
||||
angle := math.Acos(math.Max(-1, math.Min(1, geoVectorDot(pointVector, candidate))))
|
||||
return angle * 6378.1366
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return math.Min(geoPointDistanceKM(point, start), geoPointDistanceKM(point, end))
|
||||
}
|
||||
|
||||
// JoinPolylineSegments 按最近端点连接无序边界线段 / JoinPolylineSegments joins unordered boundary segments by their nearest
|
||||
// 端点连接;输入线段不会被修改 / endpoints. The input segments are not modified.
|
||||
func JoinPolylineSegments(segments [][]GeoPoint) []GeoPoint {
|
||||
@@ -77,15 +433,25 @@ func JoinPolylineSegments(segments [][]GeoPoint) []GeoPoint {
|
||||
reverseGeoPoints(segment)
|
||||
}
|
||||
if bestPrepend {
|
||||
result = append(segment, result...)
|
||||
result = appendJoinedGeoPoints(segment, result)
|
||||
} else {
|
||||
result = append(result, segment...)
|
||||
result = appendJoinedGeoPoints(result, segment)
|
||||
}
|
||||
used[bestIndex] = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// appendJoinedGeoPoints 拼接两段并丢掉衔接处重合的顶点。
|
||||
func appendJoinedGeoPoints(first, second []GeoPoint) []GeoPoint {
|
||||
result := append([]GeoPoint(nil), first...)
|
||||
start := 0
|
||||
for start < len(second) && len(result) > 0 && SameGeoPoint(result[len(result)-1], second[start]) {
|
||||
start++
|
||||
}
|
||||
return append(result, second[start:]...)
|
||||
}
|
||||
|
||||
// ShortestCircleArc 返回两点之间较短的采样圆弧 / ShortestCircleArc returns the shorter sampled arc from one point to another.
|
||||
func ShortestCircleArc(circle []GeoPoint, from, to GeoPoint) []GeoPoint {
|
||||
if len(circle) == 0 {
|
||||
@@ -120,120 +486,6 @@ func SameGeoPoint(a, b GeoPoint) bool {
|
||||
math.Abs(a.Latitude-b.Latitude) < 1e-9
|
||||
}
|
||||
|
||||
// VisibleHemispherePolygons 返回以指定中心为中心的半球多边形 / VisibleHemispherePolygons returns polygons for the hemisphere centered on
|
||||
// 中心的半球多边形,并裁剪到请求的地图投影 / center, clipped to the requested map projection.
|
||||
func VisibleHemispherePolygons(center GeoPoint, projection Projection, samples int) [][]GeoPoint {
|
||||
if samples < 12 {
|
||||
samples = 12
|
||||
}
|
||||
if projection == ProjectionNorthPolar {
|
||||
return [][]GeoPoint{polarVisibleHemispherePolygon(center, 1, samples/2)}
|
||||
}
|
||||
if projection == ProjectionSouthPolar {
|
||||
return [][]GeoPoint{polarVisibleHemispherePolygon(center, -1, samples/2)}
|
||||
}
|
||||
return equirectangularVisibleHemispherePolygons(center, samples)
|
||||
}
|
||||
|
||||
func equirectangularVisibleHemispherePolygons(center GeoPoint, samples int) [][]GeoPoint {
|
||||
if math.Abs(center.Latitude) < 1e-9 {
|
||||
return equirectangularLongitudeBand(center.Longitude)
|
||||
}
|
||||
polygon := make([]GeoPoint, 0, samples+3)
|
||||
for index := 0; index <= samples; index++ {
|
||||
longitude := -180 + 360*float64(index)/float64(samples)
|
||||
polygon = append(polygon, GeoPoint{
|
||||
Longitude: longitude,
|
||||
Latitude: visibleHorizonLatitude(center, longitude),
|
||||
})
|
||||
}
|
||||
mapEdgeLatitude := math.Copysign(90, center.Latitude)
|
||||
return [][]GeoPoint{append(polygon,
|
||||
GeoPoint{Longitude: 180, Latitude: mapEdgeLatitude},
|
||||
GeoPoint{Longitude: -180, Latitude: mapEdgeLatitude},
|
||||
)}
|
||||
}
|
||||
|
||||
func equirectangularLongitudeBand(centerLongitude float64) [][]GeoPoint {
|
||||
centerLongitude = normalizeLongitude(centerLongitude)
|
||||
start, end := centerLongitude-90, centerLongitude+90
|
||||
var polygons [][]GeoPoint
|
||||
for _, shift := range []float64{-360, 0, 360} {
|
||||
left := math.Max(-180, start+shift)
|
||||
right := math.Min(180, end+shift)
|
||||
if right-left <= 1e-9 {
|
||||
continue
|
||||
}
|
||||
polygons = append(polygons, []GeoPoint{
|
||||
{Longitude: left, Latitude: -90},
|
||||
{Longitude: right, Latitude: -90},
|
||||
{Longitude: right, Latitude: 90},
|
||||
{Longitude: left, Latitude: 90},
|
||||
})
|
||||
}
|
||||
return polygons
|
||||
}
|
||||
|
||||
func polarVisibleHemispherePolygon(center GeoPoint, hemisphere float64, samples int) []GeoPoint {
|
||||
if samples < 6 {
|
||||
samples = 6
|
||||
}
|
||||
if math.Abs(center.Latitude) < 1e-9 {
|
||||
polygon := []GeoPoint{
|
||||
{Longitude: normalizeLongitude(center.Longitude - 90), Latitude: 0},
|
||||
{Longitude: normalizeLongitude(center.Longitude), Latitude: 90 * hemisphere},
|
||||
{Longitude: normalizeLongitude(center.Longitude + 90), Latitude: 0},
|
||||
}
|
||||
return appendPolarVisibilityRim(polygon, center.Longitude, false, samples)
|
||||
}
|
||||
|
||||
centerLongitude := normalizeLongitude(center.Longitude)
|
||||
centerInsideProjection := center.Latitude*hemisphere > 0
|
||||
horizonMidpoint := centerLongitude
|
||||
if centerInsideProjection {
|
||||
horizonMidpoint += 180
|
||||
}
|
||||
polygon := make([]GeoPoint, 0, 2*samples+1)
|
||||
for index := 0; index <= samples; index++ {
|
||||
longitude := horizonMidpoint - 90 + 180*float64(index)/float64(samples)
|
||||
latitude := visibleHorizonLatitude(center, longitude)
|
||||
if latitude*hemisphere < 0 && math.Abs(latitude) < 1e-9 {
|
||||
latitude = 0
|
||||
}
|
||||
polygon = append(polygon, GeoPoint{
|
||||
Longitude: normalizeLongitude(longitude),
|
||||
Latitude: latitude,
|
||||
})
|
||||
}
|
||||
return appendPolarVisibilityRim(polygon, centerLongitude, centerInsideProjection, samples)
|
||||
}
|
||||
|
||||
func appendPolarVisibilityRim(
|
||||
polygon []GeoPoint,
|
||||
centerLongitude float64,
|
||||
centerInsideProjection bool,
|
||||
samples int,
|
||||
) []GeoPoint {
|
||||
for index := 1; index <= samples; index++ {
|
||||
fraction := float64(index) / float64(samples)
|
||||
longitude := centerLongitude + 90 - 180*fraction
|
||||
if centerInsideProjection {
|
||||
longitude = centerLongitude - 90 + 180*fraction
|
||||
}
|
||||
polygon = append(polygon, GeoPoint{
|
||||
Longitude: normalizeLongitude(longitude),
|
||||
Latitude: 0,
|
||||
})
|
||||
}
|
||||
return polygon
|
||||
}
|
||||
|
||||
func visibleHorizonLatitude(center GeoPoint, longitude float64) float64 {
|
||||
declination := center.Latitude * math.Pi / 180
|
||||
deltaLongitude := (longitude - center.Longitude) * math.Pi / 180
|
||||
return math.Atan(-math.Cos(declination)*math.Cos(deltaLongitude)/math.Sin(declination)) * 180 / math.Pi
|
||||
}
|
||||
|
||||
func nearestGeoPointIndex(points []GeoPoint, target GeoPoint) int {
|
||||
bestIndex := 0
|
||||
bestDistance := math.Inf(1)
|
||||
|
||||
Reference in New Issue
Block a user