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,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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user