feat: 引入可切换星历模型并扩展天象观测链路

- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
2026-10-15 23:54:15 +08:00
parent 16c62a97d5
commit 044f2f4381
674 changed files with 62983 additions and 8396 deletions
+60
View File
@@ -0,0 +1,60 @@
package geodata
import "math"
// 地球尺度常量 / Earth size constants.
const (
// EarthEquatorialRadiusKM 地球赤道半径 / Earth equatorial radius.
EarthEquatorialRadiusKM = 6378.1366
// EarthMeanRadiusKM 地球平均半径 / Earth mean radius.
EarthMeanRadiusKM = 6371.0088
// EarthWGS84RadiusKM WGS84 椭球长半轴 / WGS84 semi-major axis.
EarthWGS84RadiusKM = 6378.137
// EarthKMPerDegree 大圆每度的近似长度 / approximate great-circle length per degree.
EarthKMPerDegree = 111.195
// WebMercatorMaxLatitude Web Mercator 可投影的纬度上限 / Web Mercator latitude limit.
WebMercatorMaxLatitude = 85.05112878
)
// NormalizeLongitude 把经度归一化到 [-180, 180) / wraps a longitude into [-180, 180).
func NormalizeLongitude(value float64) float64 {
value = math.Mod(value+180, 360)
if value < 0 {
value += 360
}
return value - 180
}
// CentralAngle 两点间的球心角(弧度),参数为度 / haversine central angle in radians.
func CentralAngle(longitude1, latitude1, longitude2, latitude2 float64) float64 {
firstLatitude := latitude1 * math.Pi / 180
secondLatitude := latitude2 * math.Pi / 180
deltaLatitude := secondLatitude - firstLatitude
deltaLongitude := math.Remainder((longitude2-longitude1)*math.Pi/180, 2*math.Pi)
haversine := math.Sin(deltaLatitude/2)*math.Sin(deltaLatitude/2) +
math.Cos(firstLatitude)*math.Cos(secondLatitude)*math.Sin(deltaLongitude/2)*math.Sin(deltaLongitude/2)
return 2 * math.Asin(math.Sqrt(math.Max(0, math.Min(1, haversine))))
}
// EquatorialDistanceKM 以赤道半径计的大圆距离 / great-circle distance on the equatorial-radius sphere.
func EquatorialDistanceKM(first, second GeoPoint) float64 {
return EarthEquatorialRadiusKM * CentralAngle(first.Longitude, first.Latitude, second.Longitude, second.Latitude)
}
// MeanDistanceKM 以平均半径计的大圆距离 / great-circle distance on the mean-radius sphere.
func MeanDistanceKM(first, second GeoPoint) float64 {
return EarthMeanRadiusKM * CentralAngle(first.Longitude, first.Latitude, second.Longitude, second.Latitude)
}
// WebMercatorDistanceKM 两点在 Web Mercator 平面上的距离,经差取短弧 / Web Mercator planar distance.
func WebMercatorDistanceKM(first, second GeoPoint) float64 {
clampLatitude := func(value float64) float64 {
return math.Max(-WebMercatorMaxLatitude, math.Min(WebMercatorMaxLatitude, value))
}
longitude := math.Remainder(second.Longitude-first.Longitude, 360) * math.Pi / 180
firstLatitude := clampLatitude(first.Latitude) * math.Pi / 180
secondLatitude := clampLatitude(second.Latitude) * math.Pi / 180
firstY := math.Log(math.Tan(math.Pi/4 + firstLatitude/2))
secondY := math.Log(math.Tan(math.Pi/4 + secondLatitude/2))
return EarthEquatorialRadiusKM * math.Hypot(longitude, secondY-firstY)
}
+20 -20
View File
@@ -103,7 +103,7 @@ func newVisibleLineworkArcIndex(lines [][]GeoPoint) visibleLineworkArcIndex {
func (index visibleLineworkArcIndex) pointDistanceKM(point GeoPoint) float64 {
if source, ok := index.vertices[visibleLineworkIndexPointKey(point)]; ok {
return geoPointDistanceKM(point, source)
return EquatorialDistanceKM(point, source)
}
pointVector := geoPointVector(point)
minimum := math.Inf(1)
@@ -130,10 +130,10 @@ func (index visibleLineworkArcIndex) edgePointDistanceKM(
matched := false
for _, arcIndex := range arcIndices {
arc := index.arcs[arcIndex]
direct := geoPointDistanceKM(first, arc.start) <= 0.001 &&
geoPointDistanceKM(second, arc.end) <= 0.001
reverse := geoPointDistanceKM(first, arc.end) <= 0.001 &&
geoPointDistanceKM(second, arc.start) <= 0.001
direct := EquatorialDistanceKM(first, arc.start) <= 0.001 &&
EquatorialDistanceKM(second, arc.end) <= 0.001
reverse := EquatorialDistanceKM(first, arc.end) <= 0.001 &&
EquatorialDistanceKM(second, arc.start) <= 0.001
if !direct && !reverse {
continue
}
@@ -151,12 +151,12 @@ func (arc visibleLineworkArc) pointDistanceKM(point GeoPoint, pointVector geoVec
for _, candidate := range []geoVector3{projected, geoVectorScale(projected, -1)} {
if sphericalPointOnArcVectors(candidate, arc.startVector, arc.endVector, arc.arcRadians) {
angle := math.Acos(math.Max(-1, math.Min(1, geoVectorDot(pointVector, candidate))))
return angle * 6378.1366
return angle * EarthEquatorialRadiusKM
}
}
}
}
return math.Min(geoPointDistanceKM(point, arc.start), geoPointDistanceKM(point, arc.end))
return math.Min(EquatorialDistanceKM(point, arc.start), EquatorialDistanceKM(point, arc.end))
}
func visibleLineworkIndexPointKey(point GeoPoint) visibleLineworkPointKey {
@@ -278,7 +278,7 @@ func visibleLineworkPolygons(
// curves, but using that same radius for point-in-face tests makes adjacent
// polar faces overlap and selects the wrong side of a narrow band.
faceToleranceKM := math.Min(5, math.Max(0.5, snapDistanceKM/10))
projectedTolerance := faceToleranceKM / 111.195
projectedTolerance := faceToleranceKM / EarthKMPerDegree
cycleProbes := visibleLineworkFilledCycleProbes(cycles, projectedFill, projectedTolerance)
if len(cycleProbes) > 0 {
for _, probe := range cycleProbes {
@@ -403,7 +403,7 @@ func visibleLineworkProjectedEdgeDistanceKM(first, second GeoPoint) float64 {
latitude := (first.Latitude + second.Latitude) * math.Pi / 360
deltaLongitude := math.Remainder(second.Longitude-first.Longitude, 360) * math.Cos(latitude)
deltaLatitude := second.Latitude - first.Latitude
return math.Hypot(deltaLongitude, deltaLatitude) * 111.195
return math.Hypot(deltaLongitude, deltaLatitude) * EarthKMPerDegree
}
func visibleLineworkBoundarySourceMissDistanceKM(
@@ -516,7 +516,7 @@ func visibleLineworkBoundaryBridge(
if !visibleLineworkBridgeIsSimple(bridge, toleranceKM) {
continue
}
if geoPointDistanceKM(originalStart, bridge[0]) > geoPointDistanceKM(originalStart, bridge[len(bridge)-1]) {
if EquatorialDistanceKM(originalStart, bridge[0]) > EquatorialDistanceKM(originalStart, bridge[len(bridge)-1]) {
reverseGeoPoints(bridge)
}
length := visibleLineworkBridgeLength(bridge)
@@ -554,10 +554,10 @@ func visibleLineworkCrossLineBoundaryBridge(
continue
}
for pointIndex, point := range line {
if distance := geoPointDistanceKM(start, point); distance <= toleranceKM {
if distance := EquatorialDistanceKM(start, point); distance <= toleranceKM {
startLocations = append(startLocations, location{lineIndex, pointIndex, distance})
}
if distance := geoPointDistanceKM(end, point); distance <= toleranceKM {
if distance := EquatorialDistanceKM(end, point); distance <= toleranceKM {
endLocations = append(endLocations, location{lineIndex, pointIndex, distance})
}
}
@@ -574,7 +574,7 @@ func visibleLineworkCrossLineBoundaryBridge(
secondIndices := []int{0, len(secondLine) - 1}
for _, firstIndex := range firstIndices {
for _, secondIndex := range secondIndices {
if geoPointDistanceKM(firstLine[firstIndex], secondLine[secondIndex]) > toleranceKM {
if EquatorialDistanceKM(firstLine[firstIndex], secondLine[secondIndex]) > toleranceKM {
continue
}
firstPath := visibleLineworkLinePath(firstLine, first.pointIndex, firstIndex, start, firstLine[firstIndex])
@@ -609,8 +609,8 @@ func visibleLineworkBridgeIsSimple(points []GeoPoint, toleranceKM float64) bool
}
endpointRepeatTolerance := math.Min(1, toleranceKM/10)
for index := 1; index+1 < len(points); index++ {
if geoPointDistanceKM(points[index], points[0]) <= endpointRepeatTolerance ||
geoPointDistanceKM(points[index], points[len(points)-1]) <= endpointRepeatTolerance {
if EquatorialDistanceKM(points[index], points[0]) <= endpointRepeatTolerance ||
EquatorialDistanceKM(points[index], points[len(points)-1]) <= endpointRepeatTolerance {
return false
}
}
@@ -633,7 +633,7 @@ func visibleLineworkLinePath(
path = append(path, line[index])
}
}
if len(path) == 0 || geoPointDistanceKM(path[len(path)-1], end) > 1e-9 {
if len(path) == 0 || EquatorialDistanceKM(path[len(path)-1], end) > 1e-9 {
path = append(path, end)
}
return path
@@ -642,7 +642,7 @@ func visibleLineworkLinePath(
func visibleLineworkBridgeLength(points []GeoPoint) float64 {
length := 0.0
for index := 1; index < len(points); index++ {
length += geoPointDistanceKM(points[index-1], points[index])
length += EquatorialDistanceKM(points[index-1], points[index])
}
return length
}
@@ -654,7 +654,7 @@ func visibleLineworkNearestVertex(
bestIndex := -1
bestDistance := math.Inf(1)
for index, candidate := range line {
distance := geoPointDistanceKM(point, candidate)
distance := EquatorialDistanceKM(point, candidate)
if distance < bestDistance {
bestIndex = index
bestDistance = distance
@@ -1805,7 +1805,7 @@ func buildVisibleLineworkGraph(
if index == otherIndex || len(other.outgoing) == 0 {
continue
}
if distance := geoPointDistanceKM(point, chart.unproject(other.point)); distance < minimum {
if distance := EquatorialDistanceKM(point, chart.unproject(other.point)); distance < minimum {
first, second, minimum = index, otherIndex, distance
}
}
@@ -1923,7 +1923,7 @@ func visibleLineworkNodeIndex(
bestIndex := -1
bestDistance := math.Inf(1)
for index, node := range nodes {
distance := geoPointDistanceKM(chart.unproject(node.point), target)
distance := EquatorialDistanceKM(chart.unproject(node.point), target)
if distance <= snapDistanceKM && distance < bestDistance {
bestIndex, bestDistance = index, distance
}
+1 -1
View File
@@ -15,7 +15,7 @@ func orthographicTestBorderline(ring []GeoPoint, point GeoPoint) bool {
base := orthographicTestContainsRing(ring, point)
for _, bearing := range []float64{0, 90, 180, 270} {
offset := InterpolateGreatCircle(point, GeoPoint{
Longitude: normalizeLongitude(point.Longitude + 0.05*math.Cos(bearing*rad)),
Longitude: NormalizeLongitude(point.Longitude + 0.05*math.Cos(bearing*rad)),
Latitude: point.Latitude + 0.05*math.Sin(bearing*rad),
}, 0.001)
if orthographicTestContainsRing(ring, offset) != base {
+77 -54
View File
@@ -370,15 +370,10 @@ func polygonUnionRingForPoints(points []polygonUnionPoint) polygonUnionRing {
minY, maxY = math.Min(minY, point.y), math.Max(maxY, point.y)
}
locationEdges := append([]polygonUnionSourceEdge(nil), edges...)
sort.Slice(locationEdges, func(first, second int) bool {
if locationEdges[first].minY != locationEdges[second].minY {
return locationEdges[first].minY < locationEdges[second].minY
}
return locationEdges[first].maxY < locationEdges[second].maxY
})
sort.Sort(polygonUnionEdgesByY(locationEdges))
return polygonUnionRing{
points: points, edges: edges, locationEdges: locationEdges,
locationBins: polygonUnionBuildLocationBins(edges, minY, maxY),
locationBins: polygonUnionBuildLocationBins(locationEdges, minY, maxY),
minX: minX, maxX: maxX, minY: minY, maxY: maxY,
}
}
@@ -394,19 +389,7 @@ func polygonUnionAddIntersections(rings []polygonUnionRing) {
})
}
}
sort.Slice(edges, func(first, second int) bool {
a, b := edges[first], edges[second]
if a.edge.minX != b.edge.minX {
return a.edge.minX < b.edge.minX
}
if a.edge.maxX != b.edge.maxX {
return a.edge.maxX < b.edge.maxX
}
if a.ringIndex != b.ringIndex {
return a.ringIndex < b.ringIndex
}
return a.edgeIndex < b.edgeIndex
})
sort.Sort(polygonUnionEdgeRefsByX(edges))
for firstIndex, first := range edges {
for secondIndex := firstIndex + 1; secondIndex < len(edges); secondIndex++ {
second := edges[secondIndex]
@@ -421,13 +404,13 @@ func polygonUnionAddIntersections(rings []polygonUnionRing) {
if !polygonUnionEdgeBoundsOverlap(first.edge, second.edge) {
continue
}
firstParams, secondParams := polygonUnionSegmentIntersections(
firstParams, firstCount, secondParams, secondCount := polygonUnionSegmentIntersections(
first.edge.start, first.edge.end, second.edge.start, second.edge.end,
)
firstRing := &rings[first.ringIndex]
secondRing := &rings[second.ringIndex]
firstRing.params[first.edgeIndex] = append(firstRing.params[first.edgeIndex], firstParams...)
secondRing.params[second.edgeIndex] = append(secondRing.params[second.edgeIndex], secondParams...)
firstRing.params[first.edgeIndex] = append(firstRing.params[first.edgeIndex], firstParams[:firstCount]...)
secondRing.params[second.edgeIndex] = append(secondRing.params[second.edgeIndex], secondParams[:secondCount]...)
}
}
}
@@ -443,7 +426,7 @@ func polygonUnionEdgesAdjacent(first, second, count int) bool {
func polygonUnionSegmentIntersections(
firstStart, firstEnd, secondStart, secondEnd polygonUnionPoint,
) ([]float64, []float64) {
) (firstParams [2]float64, firstCount int, secondParams [2]float64, secondCount int) {
firstDelta := polygonUnionSubtract(firstEnd, firstStart)
secondDelta := polygonUnionSubtract(secondEnd, secondStart)
offset := polygonUnionSubtract(secondStart, firstStart)
@@ -453,27 +436,29 @@ func polygonUnionSegmentIntersections(
secondParam := polygonUnionCross(offset, firstDelta) / denominator
if firstParam < -polygonUnionEpsilon || firstParam > 1+polygonUnionEpsilon ||
secondParam < -polygonUnionEpsilon || secondParam > 1+polygonUnionEpsilon {
return nil, nil
return firstParams, 0, secondParams, 0
}
return []float64{polygonUnionClampParam(firstParam)}, []float64{polygonUnionClampParam(secondParam)}
firstParams[0] = polygonUnionClampParam(firstParam)
secondParams[0] = polygonUnionClampParam(secondParam)
return firstParams, 1, secondParams, 1
}
if math.Abs(polygonUnionCross(offset, firstDelta)) > polygonUnionEpsilon {
return nil, nil
return firstParams, 0, secondParams, 0
}
firstParams := make([]float64, 0, 2)
secondParams := make([]float64, 0, 2)
for _, point := range []polygonUnionPoint{secondStart, secondEnd} {
for _, point := range [2]polygonUnionPoint{secondStart, secondEnd} {
if value, ok := polygonUnionPointSegmentParam(point, firstStart, firstEnd); ok {
firstParams = append(firstParams, value)
firstParams[firstCount] = value
firstCount++
}
}
for _, point := range []polygonUnionPoint{firstStart, firstEnd} {
for _, point := range [2]polygonUnionPoint{firstStart, firstEnd} {
if value, ok := polygonUnionPointSegmentParam(point, secondStart, secondEnd); ok {
secondParams = append(secondParams, value)
secondParams[secondCount] = value
secondCount++
}
}
return firstParams, secondParams
return firstParams, firstCount, secondParams, secondCount
}
func polygonUnionPointSegmentParam(point, start, end polygonUnionPoint) (float64, bool) {
@@ -551,11 +536,11 @@ func polygonUnionOuterEdges(rings []polygonUnionRing) []polygonUnionEdge {
}
func polygonUnionInsideAnyRing(point polygonUnionPoint, rings []polygonUnionRing, preferred int) bool {
if preferred >= 0 && preferred < len(rings) && polygonUnionRingContainsPoint(point, rings[preferred]) {
if preferred >= 0 && preferred < len(rings) && polygonUnionRingRefContainsPoint(point, &rings[preferred]) {
return true
}
for index, ring := range rings {
if index != preferred && polygonUnionRingContainsPoint(point, ring) {
for index := range rings {
if index != preferred && polygonUnionRingRefContainsPoint(point, &rings[index]) {
return true
}
}
@@ -563,12 +548,16 @@ func polygonUnionInsideAnyRing(point polygonUnionPoint, rings []polygonUnionRing
}
func polygonUnionRingContainsPoint(point polygonUnionPoint, ring polygonUnionRing) bool {
return polygonUnionRingRefContainsPoint(point, &ring)
}
func polygonUnionRingRefContainsPoint(point polygonUnionPoint, ring *polygonUnionRing) bool {
margin := polygonUnionProbeOffset + polygonUnionEpsilon
if point.x < ring.minX-margin || point.x > ring.maxX+margin ||
point.y < ring.minY-margin || point.y > ring.maxY+margin {
return false
}
return polygonUnionPointLocationInRing(point, ring) >= 0
return polygonUnionPointLocationInRingRef(point, ring) >= 0
}
func polygonUnionJoinEdges(edges []polygonUnionEdge) ([][]GeoPoint, error) {
@@ -633,7 +622,7 @@ func polygonUnionJoinEdges(edges []polygonUnionEdge) ([][]GeoPoint, error) {
geographic := make([]GeoPoint, len(points))
for index, point := range points {
geographic[index] = GeoPoint{
Longitude: polygonUnionNormalizeLongitude(point.x),
Longitude: NormalizeLongitude(point.x),
Latitude: point.y,
}
}
@@ -763,30 +752,43 @@ func polygonUnionPointLocationOnEdges(
return -1
}
// polygonUnionBuildLocationBins 要求 sortedEdges 已按 (minY, maxY) 升序,各分箱沿用该顺序。
func polygonUnionBuildLocationBins(
edges []polygonUnionSourceEdge,
sortedEdges []polygonUnionSourceEdge,
minimumY, maximumY float64,
) [][]polygonUnionSourceEdge {
var counts [polygonUnionLocationBinCount]int
total := 0
for _, edge := range sortedEdges {
first := polygonUnionLocationBin(edge.minY-polygonUnionEpsilon, minimumY, maximumY)
last := polygonUnionLocationBin(edge.maxY+polygonUnionEpsilon, minimumY, maximumY)
for index := first; index <= last; index++ {
counts[index]++
}
total += last - first + 1
}
backing := make([]polygonUnionSourceEdge, total)
bins := make([][]polygonUnionSourceEdge, polygonUnionLocationBinCount)
for _, edge := range edges {
offset := 0
for index, count := range counts {
bins[index] = backing[offset : offset : offset+count]
offset += count
}
for _, edge := range sortedEdges {
first := polygonUnionLocationBin(edge.minY-polygonUnionEpsilon, minimumY, maximumY)
last := polygonUnionLocationBin(edge.maxY+polygonUnionEpsilon, minimumY, maximumY)
for index := first; index <= last; index++ {
bins[index] = append(bins[index], edge)
}
}
for index := range bins {
sort.Slice(bins[index], func(first, second int) bool {
if bins[index][first].minY != bins[index][second].minY {
return bins[index][first].minY < bins[index][second].minY
}
return bins[index][first].maxY < bins[index][second].maxY
})
}
return bins
}
func polygonUnionPointLocationInRing(point polygonUnionPoint, ring polygonUnionRing) int {
return polygonUnionPointLocationInRingRef(point, &ring)
}
func polygonUnionPointLocationInRingRef(point polygonUnionPoint, ring *polygonUnionRing) int {
if len(ring.locationBins) != polygonUnionLocationBinCount {
return polygonUnionPointLocationOnEdges(point, ring.locationEdges)
}
@@ -893,10 +895,31 @@ func polygonUnionKey(edge polygonUnionEdge) polygonUnionEdgeKey {
}
}
func polygonUnionNormalizeLongitude(value float64) float64 {
value = math.Mod(value+180, 360)
if value < 0 {
value += 360
type polygonUnionEdgesByY []polygonUnionSourceEdge
func (edges polygonUnionEdgesByY) Len() int { return len(edges) }
func (edges polygonUnionEdgesByY) Swap(i, j int) { edges[i], edges[j] = edges[j], edges[i] }
func (edges polygonUnionEdgesByY) Less(i, j int) bool {
if edges[i].minY != edges[j].minY {
return edges[i].minY < edges[j].minY
}
return value - 180
return edges[i].maxY < edges[j].maxY
}
type polygonUnionEdgeRefsByX []polygonUnionSourceEdgeRef
func (edges polygonUnionEdgeRefsByX) Len() int { return len(edges) }
func (edges polygonUnionEdgeRefsByX) Swap(i, j int) { edges[i], edges[j] = edges[j], edges[i] }
func (edges polygonUnionEdgeRefsByX) Less(i, j int) bool {
a, b := &edges[i], &edges[j]
if a.edge.minX != b.edge.minX {
return a.edge.minX < b.edge.minX
}
if a.edge.maxX != b.edge.maxX {
return a.edge.maxX < b.edge.maxX
}
if a.ringIndex != b.ringIndex {
return a.ringIndex < b.ringIndex
}
return a.edgeIndex < b.edgeIndex
}
+1 -1
View File
@@ -33,7 +33,7 @@ func TestPolygonFragmentsCentredSeamKeepsGeometry(t *testing.T) {
}
for _, point := range fragment {
// 分段用地图窗口坐标表示(居中经线 ±180),折回 -180…180 后再比对。
if longitude := normalizeLongitude180(point.Longitude); longitude < -60-1e-6 || longitude > 60+1e-6 {
if longitude := NormalizeLongitude(point.Longitude); longitude < -60-1e-6 || longitude > 60+1e-6 {
t.Fatalf("fragment longitude %.3f escaped the ring", longitude)
}
if math.Abs(point.Latitude) > 30+0.5 {
+7 -7
View File
@@ -22,7 +22,7 @@ func SphericalCircle(center GeoPoint, radiusDegrees float64, points int) []GeoPo
math.Cos(radius)-math.Sin(latitude)*math.Sin(lat),
)
result[index] = GeoPoint{
Longitude: normalizeLongitude(lon * 180 / math.Pi),
Longitude: NormalizeLongitude(lon * 180 / math.Pi),
Latitude: lat * 180 / math.Pi,
}
}
@@ -53,7 +53,7 @@ func geoVectorPoint(vector geoVector3) GeoPoint {
vector.y /= length
vector.z /= length
return GeoPoint{
Longitude: normalizeLongitude(math.Atan2(vector.y, vector.x) * 180 / math.Pi),
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,
}
}
@@ -103,7 +103,7 @@ func sphericalInterpolate(first, second GeoPoint, fraction float64) GeoPoint {
// 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)),
Longitude: NormalizeLongitude(first.Longitude + fraction*NormalizeLongitude(second.Longitude-first.Longitude)),
Latitude: first.Latitude + fraction*(second.Latitude-first.Latitude),
}
}
@@ -376,12 +376,12 @@ func sphericalPointArcDistanceKM(point, start, end GeoPoint) float64 {
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 angle * EarthEquatorialRadiusKM
}
}
}
}
return math.Min(geoPointDistanceKM(point, start), geoPointDistanceKM(point, end))
return math.Min(EquatorialDistanceKM(point, start), EquatorialDistanceKM(point, end))
}
// JoinPolylineSegments 按最近端点连接无序边界线段 / JoinPolylineSegments joins unordered boundary segments by their nearest
@@ -482,7 +482,7 @@ func ShortestCircleArc(circle []GeoPoint, from, to GeoPoint) []GeoPoint {
// SameGeoPoint 判断两个经纬度点是否在拓扑所需精度内相等 / SameGeoPoint reports whether two longitude/latitude points are equal within
// 地图拓扑辅助函数所需的精度内相等 / the precision needed by the map topology helpers.
func SameGeoPoint(a, b GeoPoint) bool {
return math.Abs(normalizeLongitude(a.Longitude-b.Longitude)) < 1e-9 &&
return math.Abs(NormalizeLongitude(a.Longitude-b.Longitude)) < 1e-9 &&
math.Abs(a.Latitude-b.Latitude) < 1e-9
}
@@ -500,7 +500,7 @@ func nearestGeoPointIndex(points []GeoPoint, target GeoPoint) int {
func angularDistanceDegrees(a, b GeoPoint) float64 {
lat1 := a.Latitude * math.Pi / 180
lat2 := b.Latitude * math.Pi / 180
dLongitude := normalizeLongitude(b.Longitude-a.Longitude) * math.Pi / 180
dLongitude := NormalizeLongitude(b.Longitude-a.Longitude) * math.Pi / 180
cosine := math.Sin(lat1)*math.Sin(lat2) +
math.Cos(lat1)*math.Cos(lat2)*math.Cos(dLongitude)
return math.Acos(math.Max(-1, math.Min(1, cosine))) * 180 / math.Pi
+2 -2
View File
@@ -62,7 +62,7 @@ func absoluteLongitude(value float64) float64 {
func TestSphericalContainmentRejectsAntipodalInterior(t *testing.T) {
for _, center := range []GeoPoint{{}, {Longitude: 179, Latitude: 70}, {Latitude: -90}} {
polygon := SphericalCircle(center, 12, 48)
antipode := GeoPoint{Longitude: normalizeLongitude(center.Longitude + 180), Latitude: -center.Latitude}
antipode := GeoPoint{Longitude: NormalizeLongitude(center.Longitude + 180), Latitude: -center.Latitude}
for _, reversed := range []bool{false, true} {
if reversed {
for first, last := 0, len(polygon)-1; first < last; first, last = first+1, last-1 {
@@ -73,7 +73,7 @@ func TestSphericalContainmentRejectsAntipodalInterior(t *testing.T) {
t.Errorf("center=%+v reversed=%v: direct containment must distinguish the antipode", center, reversed)
}
points := []GeoPoint{center, antipode, polygon[0], {
Longitude: normalizeLongitude(polygon[0].Longitude + 180), Latitude: -polygon[0].Latitude,
Longitude: NormalizeLongitude(polygon[0].Longitude + 180), Latitude: -polygon[0].Latitude,
}}
got := SphericalPolygonsContainPoints([][]GeoPoint{polygon}, points)
for index, want := range []bool{true, false, true, false} {
+12 -22
View File
@@ -106,7 +106,7 @@ func sweepEndpointTracksMonotone(group []openBoundarySweepSample) bool {
if index > 1 {
previousLength := vectorNorm(previousStep)
currentLength := vectorNorm(step)
if math.Min(previousLength, currentLength)*6371.0088 > openBoundaryEndpointStepNoiseKM &&
if math.Min(previousLength, currentLength)*EarthMeanRadiusKM > openBoundaryEndpointStepNoiseKM &&
vectorDot(previousStep, step) < -0.1*previousLength*currentLength {
return false
}
@@ -116,7 +116,7 @@ func sweepEndpointTracksMonotone(group []openBoundarySweepSample) bool {
// tangent chord even while every local step remains forward. Reserve a
// few kilometres for that projection effect; larger gradual reversals
// still invalidate the endpoint envelope.
if progress+openBoundaryEndpointProgressBackKM/6371.0088 < previousProgress {
if progress+openBoundaryEndpointProgressBackKM/EarthMeanRadiusKM < previousProgress {
return false
}
previousProgress = progress
@@ -266,7 +266,7 @@ func OpenBoundarySweepInnerCaps(
caps := make([][]GeoPoint, 0, len(indices))
for _, index := range indices {
boundary := openGeoRing(JoinPolylineSegments(samples[index].Boundaries))
if len(boundary) < 3 || geoPointDistanceKM(boundary[0], boundary[len(boundary)-1]) > maximumGapKM {
if len(boundary) < 3 || EquatorialDistanceKM(boundary[0], boundary[len(boundary)-1]) > maximumGapKM {
continue
}
caps = append(caps, boundary)
@@ -281,7 +281,7 @@ func decimateOpenBoundaryPoints(points []GeoPoint, targetSpacingKM float64) []Ge
result := make([]GeoPoint, 1, len(points))
result[0] = points[0]
for _, point := range points[1 : len(points)-1] {
if geoPointDistanceKM(result[len(result)-1], point) >= targetSpacingKM {
if EquatorialDistanceKM(result[len(result)-1], point) >= targetSpacingKM {
result = append(result, point)
}
}
@@ -330,17 +330,17 @@ func openBoundarySweepGroups(samples []OpenBoundarySweepSample) ([][]openBoundar
}
value := openBoundarySweepSample{boundary: boundary, start: boundary[0], end: boundary[len(boundary)-1]}
if len(current) > 0 {
keep := geoPointDistanceKM(previous.start, value.start) +
geoPointDistanceKM(previous.end, value.end)
reverse := geoPointDistanceKM(previous.start, value.end) +
geoPointDistanceKM(previous.end, value.start)
keep := EquatorialDistanceKM(previous.start, value.start) +
EquatorialDistanceKM(previous.end, value.end)
reverse := EquatorialDistanceKM(previous.start, value.end) +
EquatorialDistanceKM(previous.end, value.start)
if reverse < keep {
reverseSweepGeoPoints(value.boundary)
value.start, value.end = value.end, value.start
}
if math.Max(
geoPointDistanceKM(previous.start, value.start),
geoPointDistanceKM(previous.end, value.end),
EquatorialDistanceKM(previous.start, value.start),
EquatorialDistanceKM(previous.end, value.end),
) > 2000 {
groups = append(groups, current)
current = nil
@@ -567,7 +567,7 @@ func sweepTouchingPolygonPair(
key := sweepTouchingCellKeyFromCoordinates(x+deltaX, y+deltaY, z+deltaZ)
for _, candidate := range cells[key] {
if candidate.polygon <= polygonIndex ||
geoPointDistanceKM(point, candidate.point) > thresholdKM {
EquatorialDistanceKM(point, candidate.point) > thresholdKM {
continue
}
pairs[[2]int{polygonIndex, candidate.polygon}] = struct{}{}
@@ -588,7 +588,7 @@ func sweepTouchingPolygonPair(
}
for _, first := range polygons[firstIndex] {
for _, second := range polygons[secondIndex] {
if !(geoPointDistanceKM(first, second) > thresholdKM) {
if !(EquatorialDistanceKM(first, second) > thresholdKM) {
return firstIndex, secondIndex, GeoPoint{
Longitude: first.Longitude + math.Remainder(second.Longitude-first.Longitude, 360)/2,
Latitude: (first.Latitude + second.Latitude) / 2,
@@ -660,13 +660,3 @@ func reverseSweepGeoPoints(points []GeoPoint) {
points[left], points[right] = points[right], points[left]
}
}
func geoPointDistanceKM(first, second GeoPoint) float64 {
firstLatitude := first.Latitude * math.Pi / 180
secondLatitude := second.Latitude * math.Pi / 180
deltaLatitude := secondLatitude - firstLatitude
deltaLongitude := math.Remainder((second.Longitude-first.Longitude)*math.Pi/180, 2*math.Pi)
haversine := math.Sin(deltaLatitude/2)*math.Sin(deltaLatitude/2) +
math.Cos(firstLatitude)*math.Cos(secondLatitude)*math.Sin(deltaLongitude/2)*math.Sin(deltaLongitude/2)
return 2 * 6378.1366 * math.Asin(math.Sqrt(math.Min(1, haversine)))
}
+6 -22
View File
@@ -80,7 +80,7 @@ func prepareTopologyPoints(points []GeoPoint) ([]GeoPoint, bool) {
normalized := make([]GeoPoint, len(points))
copy(normalized, points)
for index := range normalized {
normalized[index].Longitude = normalizeLongitude(normalized[index].Longitude)
normalized[index].Longitude = NormalizeLongitude(normalized[index].Longitude)
}
return normalized, true
}
@@ -382,17 +382,17 @@ func hemisphereIntersection(a, b GeoPoint) GeoPoint {
firstSign := firstVector.z
secondSign := secondVector.z
if math.Abs(firstSign) <= 1e-15 {
return GeoPoint{Longitude: normalizeLongitude(first.Longitude), Latitude: 0}
return GeoPoint{Longitude: NormalizeLongitude(first.Longitude), Latitude: 0}
}
if math.Abs(secondSign) <= 1e-15 {
return GeoPoint{Longitude: normalizeLongitude(second.Longitude), Latitude: 0}
return GeoPoint{Longitude: NormalizeLongitude(second.Longitude), Latitude: 0}
}
left, right := 0.0, 1.0
for iteration := 0; iteration < 64; iteration++ {
middle := (left + right) / 2
point := sphericalInterpolate(first, second, middle)
if point.Latitude == 0 || right-left <= 1e-13 {
return GeoPoint{Longitude: normalizeLongitude(point.Longitude), Latitude: 0}
return GeoPoint{Longitude: NormalizeLongitude(point.Longitude), Latitude: 0}
}
if point.Latitude*first.Latitude > 0 {
left = middle
@@ -401,7 +401,7 @@ func hemisphereIntersection(a, b GeoPoint) GeoPoint {
}
}
point := sphericalInterpolate(first, second, (left+right)/2)
return GeoPoint{Longitude: normalizeLongitude(point.Longitude), Latitude: 0}
return GeoPoint{Longitude: NormalizeLongitude(point.Longitude), Latitude: 0}
}
func sphericalLongitudeIntersection(a, b GeoPoint, boundary, adjustedLongitude float64) GeoPoint {
@@ -434,14 +434,6 @@ func sphericalLongitudeIntersection(a, b GeoPoint, boundary, adjustedLongitude f
return GeoPoint{Longitude: boundary, Latitude: point.Latitude}
}
func normalizeLongitude(value float64) float64 {
value = math.Mod(value+180, 360)
if value < 0 {
value += 360
}
return value - 180
}
// equirectangularSeamShift 返回把等经纬接缝从 ±180 搬到视图中心对面所需的经度旋转量。
// 视图中心为 0 时返回 0,调用方即可沿用未旋转的原有路径,保证既有输出逐字节不变。
func equirectangularSeamShift(view ClipView) float64 {
@@ -473,7 +465,7 @@ func rotateLongitudes(points []GeoPoint, shift float64) []GeoPoint {
rotated := make([]GeoPoint, len(points))
copy(rotated, points)
for index := range rotated {
rotated[index].Longitude = normalizeLongitude180(rotated[index].Longitude + shift)
rotated[index].Longitude = NormalizeLongitude(rotated[index].Longitude + shift)
}
return rotated
}
@@ -493,11 +485,3 @@ func unshiftLongitudesInPlace(points []GeoPoint, shift float64) {
points[index].Longitude = value
}
}
func normalizeLongitude180(longitude float64) float64 {
value := math.Mod(longitude+180, 360)
if value < 0 {
value += 360
}
return value - 180
}