673 lines
22 KiB
Go
673 lines
22 KiB
Go
|
|
package geodata
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"math"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ErrOpenBoundaryEndpointTrackReversal 表示扫掠边界的端点轨迹发生了物理折返。
|
||
|
|
// ErrOpenBoundaryEndpointTrackReversal reports a physical reversal along a swept boundary endpoint track.
|
||
|
|
var ErrOpenBoundaryEndpointTrackReversal = errors.New("open boundary endpoint track reversal")
|
||
|
|
|
||
|
|
const (
|
||
|
|
openBoundaryEndpointStepNoiseKM = 1.0
|
||
|
|
openBoundaryEndpointProgressBackKM = 5.0
|
||
|
|
)
|
||
|
|
|
||
|
|
// OpenBoundarySweepSample 是用于构造连续扫掠区域的一个边界样本。
|
||
|
|
// OpenBoundarySweepSample is one sampled boundary used to build a continuous
|
||
|
|
// sweep. Closed samples are ignored because they already describe an
|
||
|
|
// instantaneous footprint and are not part of an open non-central band.
|
||
|
|
type OpenBoundarySweepSample struct {
|
||
|
|
Boundaries [][]GeoPoint
|
||
|
|
Closed bool
|
||
|
|
}
|
||
|
|
|
||
|
|
type openBoundarySweepSample struct {
|
||
|
|
boundary []GeoPoint
|
||
|
|
start GeoPoint
|
||
|
|
end GeoPoint
|
||
|
|
}
|
||
|
|
|
||
|
|
// OpenBoundarySweep 将开放边界弧连接为连续扫掠多边形。
|
||
|
|
// OpenBoundarySweep joins open boundary arcs into continuous swept polygons.
|
||
|
|
// The first and last arcs close each group; the matching endpoint tracks form
|
||
|
|
// the two sides. A large endpoint jump starts a new group instead of creating
|
||
|
|
// a false bridge across a branch change.
|
||
|
|
func OpenBoundarySweep(samples []OpenBoundarySweepSample) ([][]GeoPoint, error) {
|
||
|
|
groups, err := openBoundarySweepGroups(samples)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
polygons := make([][]GeoPoint, 0, len(groups))
|
||
|
|
for _, group := range groups {
|
||
|
|
groupPolygons, groupErr := sweepOpenBoundaryGroup(group)
|
||
|
|
if groupErr != nil {
|
||
|
|
return nil, groupErr
|
||
|
|
}
|
||
|
|
polygons = append(polygons, groupPolygons...)
|
||
|
|
}
|
||
|
|
return polygons, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// MonotoneOpenBoundarySweep 连接端点轨迹和中间弧已知单调的开放弧。
|
||
|
|
// MonotoneOpenBoundarySweep joins open arcs whose endpoint tracks and
|
||
|
|
// intermediate arcs are already known to be monotone. It avoids the general
|
||
|
|
// containment audit used by OpenBoundarySweep and is intended for tightly
|
||
|
|
// sampled, event-local contact caps.
|
||
|
|
func MonotoneOpenBoundarySweep(samples []OpenBoundarySweepSample) ([][]GeoPoint, error) {
|
||
|
|
groups, err := openBoundarySweepGroups(samples)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
polygons := make([][]GeoPoint, 0, len(groups))
|
||
|
|
for index, group := range groups {
|
||
|
|
if !sweepEndpointTracksMonotone(group) {
|
||
|
|
return nil, fmt.Errorf("%w in group %d", ErrOpenBoundaryEndpointTrackReversal, index)
|
||
|
|
}
|
||
|
|
outline := sweepOpenBoundaryOutline(group)
|
||
|
|
if len(outline) < 3 {
|
||
|
|
return nil, fmt.Errorf("monotone open boundary sweep group %d has no usable outline", index)
|
||
|
|
}
|
||
|
|
if !sweepOutlineContainsRepresentativeSamples(outline, group) {
|
||
|
|
return nil, fmt.Errorf("monotone open boundary sweep group %d folds outside its endpoint tracks", index)
|
||
|
|
}
|
||
|
|
polygons = append(polygons, outline)
|
||
|
|
}
|
||
|
|
return polygons, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepEndpointTracksMonotone(group []openBoundarySweepSample) bool {
|
||
|
|
if len(group) < 3 {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
for _, endpoint := range []func(openBoundarySweepSample) GeoPoint{
|
||
|
|
func(sample openBoundarySweepSample) GeoPoint { return sample.start },
|
||
|
|
func(sample openBoundarySweepSample) GeoPoint { return sample.end },
|
||
|
|
} {
|
||
|
|
first := geoPointUnitVector(endpoint(group[0]))
|
||
|
|
last := geoPointUnitVector(endpoint(group[len(group)-1]))
|
||
|
|
direction := geoPointTangentDirection(first, last)
|
||
|
|
if vectorNorm(direction) <= 1e-12 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
previousProgress := 0.0
|
||
|
|
previousVector := first
|
||
|
|
var previousStep [3]float64
|
||
|
|
for index := 1; index < len(group); index++ {
|
||
|
|
current := geoPointUnitVector(endpoint(group[index]))
|
||
|
|
step := [3]float64{
|
||
|
|
current[0] - previousVector[0],
|
||
|
|
current[1] - previousVector[1],
|
||
|
|
current[2] - previousVector[2],
|
||
|
|
}
|
||
|
|
if index > 1 {
|
||
|
|
previousLength := vectorNorm(previousStep)
|
||
|
|
currentLength := vectorNorm(step)
|
||
|
|
if math.Min(previousLength, currentLength)*6371.0088 > openBoundaryEndpointStepNoiseKM &&
|
||
|
|
vectorDot(previousStep, step) < -0.1*previousLength*currentLength {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
progress := vectorDot(current, direction)
|
||
|
|
// A long spherical track can deviate slightly from its first-to-last
|
||
|
|
// 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 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
previousProgress = progress
|
||
|
|
previousVector = current
|
||
|
|
previousStep = step
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func geoPointUnitVector(point GeoPoint) [3]float64 {
|
||
|
|
latitude := point.Latitude * math.Pi / 180
|
||
|
|
longitude := point.Longitude * math.Pi / 180
|
||
|
|
cosLatitude := math.Cos(latitude)
|
||
|
|
return [3]float64{
|
||
|
|
cosLatitude * math.Cos(longitude),
|
||
|
|
cosLatitude * math.Sin(longitude),
|
||
|
|
math.Sin(latitude),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func geoPointTangentDirection(first, last [3]float64) [3]float64 {
|
||
|
|
dot := vectorDot(first, last)
|
||
|
|
return [3]float64{
|
||
|
|
last[0] - dot*first[0],
|
||
|
|
last[1] - dot*first[1],
|
||
|
|
last[2] - dot*first[2],
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func vectorDot(first, second [3]float64) float64 {
|
||
|
|
return first[0]*second[0] + first[1]*second[1] + first[2]*second[2]
|
||
|
|
}
|
||
|
|
|
||
|
|
func vectorNorm(value [3]float64) float64 {
|
||
|
|
return math.Sqrt(vectorDot(value, value))
|
||
|
|
}
|
||
|
|
|
||
|
|
// OpenBoundaryEndpointOutlines 返回各开放边界分组的低成本端点轨迹轮廓。
|
||
|
|
// OpenBoundaryEndpointOutlines returns the inexpensive endpoint-track outline
|
||
|
|
// for each open-boundary group. The outlines intentionally omit intermediate
|
||
|
|
// arc bulges, so callers must combine them with the source footprint polygons;
|
||
|
|
// they are not a standalone geometric union.
|
||
|
|
func OpenBoundaryEndpointOutlines(samples []OpenBoundarySweepSample) ([][]GeoPoint, error) {
|
||
|
|
groups, err := openBoundarySweepGroups(samples)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
polygons := make([][]GeoPoint, 0, len(groups))
|
||
|
|
for index, group := range groups {
|
||
|
|
if len(group) < 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
outline := sweepOpenBoundaryOutline(group)
|
||
|
|
if len(outline) < 3 {
|
||
|
|
return nil, fmt.Errorf("open boundary endpoint group %d has no usable outline", index)
|
||
|
|
}
|
||
|
|
polygons = append(polygons, outline)
|
||
|
|
}
|
||
|
|
if len(polygons) == 0 {
|
||
|
|
return nil, fmt.Errorf("open boundary samples contain no endpoint track")
|
||
|
|
}
|
||
|
|
return polygons, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// DecimateOpenBoundarySweepSamples 在保留端点和分组的前提下限制通用带状 union 的采样成本。
|
||
|
|
// DecimateOpenBoundarySweepSamples bounds the cost of the general ribbon
|
||
|
|
// union while retaining every group endpoint and closed-sample separator.
|
||
|
|
func DecimateOpenBoundarySweepSamples(
|
||
|
|
samples []OpenBoundarySweepSample,
|
||
|
|
maximumGroupSamples int,
|
||
|
|
targetSpacingKM float64,
|
||
|
|
) []OpenBoundarySweepSample {
|
||
|
|
if maximumGroupSamples < 2 {
|
||
|
|
maximumGroupSamples = 2
|
||
|
|
}
|
||
|
|
result := make([]OpenBoundarySweepSample, 0, len(samples))
|
||
|
|
flush := func(group []OpenBoundarySweepSample) {
|
||
|
|
if len(group) == 0 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
selected := group
|
||
|
|
if len(group) > maximumGroupSamples {
|
||
|
|
selected = make([]OpenBoundarySweepSample, 0, maximumGroupSamples)
|
||
|
|
last := len(group) - 1
|
||
|
|
lastPosition := -1
|
||
|
|
for index := 0; index < maximumGroupSamples; index++ {
|
||
|
|
position := int(math.Round(float64(index*last) / float64(maximumGroupSamples-1)))
|
||
|
|
if position != lastPosition {
|
||
|
|
selected = append(selected, group[position])
|
||
|
|
lastPosition = position
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, sample := range selected {
|
||
|
|
copySample := OpenBoundarySweepSample{Closed: sample.Closed}
|
||
|
|
copySample.Boundaries = make([][]GeoPoint, len(sample.Boundaries))
|
||
|
|
for index, boundary := range sample.Boundaries {
|
||
|
|
copySample.Boundaries[index] = decimateOpenBoundaryPoints(boundary, targetSpacingKM)
|
||
|
|
}
|
||
|
|
result = append(result, copySample)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
group := make([]OpenBoundarySweepSample, 0, maximumGroupSamples)
|
||
|
|
for _, sample := range samples {
|
||
|
|
if sample.Closed {
|
||
|
|
flush(group)
|
||
|
|
group = group[:0]
|
||
|
|
if len(result) == 0 || !result[len(result)-1].Closed {
|
||
|
|
result = append(result, OpenBoundarySweepSample{Closed: true})
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
group = append(group, sample)
|
||
|
|
}
|
||
|
|
flush(group)
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
// OpenBoundarySweepInnerCaps 闭合紧邻内部闭合阶段的小地平缺口。
|
||
|
|
// OpenBoundarySweepInnerCaps closes the small horizon gaps immediately next
|
||
|
|
// to a closed-sample separator. Those nearly closed arcs overlap the paired
|
||
|
|
// middle band; leaving their gap open can retain a narrow inward notch after
|
||
|
|
// polygon union.
|
||
|
|
func OpenBoundarySweepInnerCaps(
|
||
|
|
samples []OpenBoundarySweepSample,
|
||
|
|
maximumGapKM float64,
|
||
|
|
) [][]GeoPoint {
|
||
|
|
if maximumGapKM <= 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
indices := make([]int, 0, 2)
|
||
|
|
for index := 0; index < len(samples); index++ {
|
||
|
|
if !samples[index].Closed {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if index > 0 && !samples[index-1].Closed {
|
||
|
|
indices = append(indices, index-1)
|
||
|
|
}
|
||
|
|
for index+1 < len(samples) && samples[index+1].Closed {
|
||
|
|
index++
|
||
|
|
}
|
||
|
|
if index+1 < len(samples) && !samples[index+1].Closed {
|
||
|
|
indices = append(indices, index+1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
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 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
caps = append(caps, boundary)
|
||
|
|
}
|
||
|
|
return caps
|
||
|
|
}
|
||
|
|
|
||
|
|
func decimateOpenBoundaryPoints(points []GeoPoint, targetSpacingKM float64) []GeoPoint {
|
||
|
|
if len(points) < 3 || targetSpacingKM <= 0 {
|
||
|
|
return append([]GeoPoint(nil), points...)
|
||
|
|
}
|
||
|
|
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 {
|
||
|
|
result = append(result, point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !SameGeoPoint(result[len(result)-1], points[len(points)-1]) {
|
||
|
|
result = append(result, points[len(points)-1])
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepOutlineContainsRepresentativeSamples(
|
||
|
|
outline []GeoPoint,
|
||
|
|
group []openBoundarySweepSample,
|
||
|
|
) bool {
|
||
|
|
const checksPerArc = 5
|
||
|
|
for _, sample := range group {
|
||
|
|
for check := 0; check < checksPerArc; check++ {
|
||
|
|
index := check * (len(sample.boundary) - 1) / (checksPerArc - 1)
|
||
|
|
if !sweepPointInPolygon(outline, sample.boundary[index]) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func openBoundarySweepGroups(samples []OpenBoundarySweepSample) ([][]openBoundarySweepSample, error) {
|
||
|
|
groups := make([][]openBoundarySweepSample, 0, 2)
|
||
|
|
current := make([]openBoundarySweepSample, 0, len(samples))
|
||
|
|
var previous openBoundarySweepSample
|
||
|
|
for _, source := range samples {
|
||
|
|
// A closed footprint is an instantaneous cap, not an open arc. It
|
||
|
|
// terminates the current open branch so later samples cannot bridge
|
||
|
|
// across a visibility/type transition.
|
||
|
|
if source.Closed {
|
||
|
|
if len(current) > 0 {
|
||
|
|
groups = append(groups, current)
|
||
|
|
current = nil
|
||
|
|
}
|
||
|
|
previous = openBoundarySweepSample{}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
boundary := JoinPolylineSegments(source.Boundaries)
|
||
|
|
boundary = openGeoRing(boundary)
|
||
|
|
if len(boundary) < 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
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)
|
||
|
|
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),
|
||
|
|
) > 2000 {
|
||
|
|
groups = append(groups, current)
|
||
|
|
current = nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
current = append(current, value)
|
||
|
|
previous = value
|
||
|
|
}
|
||
|
|
if len(current) > 0 {
|
||
|
|
groups = append(groups, current)
|
||
|
|
}
|
||
|
|
if len(groups) == 0 {
|
||
|
|
return nil, fmt.Errorf("open boundary samples contain no usable arcs")
|
||
|
|
}
|
||
|
|
return groups, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// sweepOpenBoundaryGroup constructs the union swept by a sequence of open
|
||
|
|
// arcs. Each adjacent pair forms a ribbon whose caps are the two sampled arcs
|
||
|
|
// and whose sides follow their endpoints. Unioning the ribbons retains any
|
||
|
|
// intermediate arc that becomes part of a non-monotone outer boundary; a
|
||
|
|
// convex hull would replace that boundary with visibly incorrect chords.
|
||
|
|
func sweepOpenBoundaryGroup(group []openBoundarySweepSample) ([][]GeoPoint, error) {
|
||
|
|
if len(group) == 0 {
|
||
|
|
return nil, fmt.Errorf("open boundary sweep group is empty")
|
||
|
|
}
|
||
|
|
if len(group) == 1 {
|
||
|
|
return [][]GeoPoint{append([]GeoPoint(nil), group[0].boundary...)}, nil
|
||
|
|
}
|
||
|
|
if outline := sweepOpenBoundaryOutline(group); len(outline) >= 3 && sweepOutlineContainsSamples(outline, group) {
|
||
|
|
return [][]GeoPoint{outline}, nil
|
||
|
|
}
|
||
|
|
ribbons := make([][]GeoPoint, 0, len(group)-1)
|
||
|
|
for index := 1; index < len(group); index++ {
|
||
|
|
first := group[index-1].boundary
|
||
|
|
second := group[index].boundary
|
||
|
|
ring := make([]GeoPoint, 0, len(first)+len(second))
|
||
|
|
ring = append(ring, first...)
|
||
|
|
for pointIndex := len(second) - 1; pointIndex >= 0; pointIndex-- {
|
||
|
|
ring = append(ring, second[pointIndex])
|
||
|
|
}
|
||
|
|
ring = sweepDeduplicateAdjacent(ring)
|
||
|
|
if len(ring) < 3 {
|
||
|
|
return nil, fmt.Errorf("open boundary sweep ribbon %d has no usable boundary", index-1)
|
||
|
|
}
|
||
|
|
ribbons = append(ribbons, ring)
|
||
|
|
}
|
||
|
|
polygons, err := UnionPolygons(ribbons)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("open boundary sweep ribbons: %w", err)
|
||
|
|
}
|
||
|
|
polygons, err = sweepBridgeTouchingPolygons(polygons)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("open boundary sweep touching ribbons: %w", err)
|
||
|
|
}
|
||
|
|
return polygons, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// sweepOpenBoundaryOutline is the simple outer envelope for a non-crossing
|
||
|
|
// sequence of arcs. The two endpoint tracks are the lateral edges of the
|
||
|
|
// swept band; using them directly avoids retaining every overlapping ribbon
|
||
|
|
// edge as a false inward spike near a tangent horizon.
|
||
|
|
func sweepOpenBoundaryOutline(group []openBoundarySweepSample) []GeoPoint {
|
||
|
|
if len(group) < 2 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
first, last := group[0].boundary, group[len(group)-1].boundary
|
||
|
|
if len(first) < 2 || len(last) < 2 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
ring := make([]GeoPoint, 0, len(first)+len(last)+2*len(group))
|
||
|
|
ring = appendUnwrappedSweepPoints(ring, first)
|
||
|
|
for index := 1; index < len(group); index++ {
|
||
|
|
ring = appendUnwrappedSweepPoint(ring, group[index].end)
|
||
|
|
}
|
||
|
|
for index := len(last) - 1; index >= 0; index-- {
|
||
|
|
ring = appendUnwrappedSweepPoint(ring, last[index])
|
||
|
|
}
|
||
|
|
for index := len(group) - 2; index >= 0; index-- {
|
||
|
|
ring = appendUnwrappedSweepPoint(ring, group[index].start)
|
||
|
|
}
|
||
|
|
return sweepDeduplicateAdjacent(ring)
|
||
|
|
}
|
||
|
|
|
||
|
|
func appendUnwrappedSweepPoints(result []GeoPoint, points []GeoPoint) []GeoPoint {
|
||
|
|
for _, point := range points {
|
||
|
|
result = appendUnwrappedSweepPoint(result, point)
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func appendUnwrappedSweepPoint(result []GeoPoint, point GeoPoint) []GeoPoint {
|
||
|
|
if len(result) == 0 {
|
||
|
|
return append(result, point)
|
||
|
|
}
|
||
|
|
longitude := point.Longitude
|
||
|
|
for longitude-result[len(result)-1].Longitude > 180 {
|
||
|
|
longitude -= 360
|
||
|
|
}
|
||
|
|
for longitude-result[len(result)-1].Longitude < -180 {
|
||
|
|
longitude += 360
|
||
|
|
}
|
||
|
|
return append(result, GeoPoint{Longitude: longitude, Latitude: point.Latitude})
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepOutlineContainsSamples(outline []GeoPoint, group []openBoundarySweepSample) bool {
|
||
|
|
if len(outline) < 3 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
for _, sample := range group {
|
||
|
|
for _, point := range sample.boundary {
|
||
|
|
if !sweepPointInPolygon(outline, point) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepPointInPolygon(polygon []GeoPoint, point GeoPoint) bool {
|
||
|
|
if len(polygon) < 3 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
longitude := point.Longitude
|
||
|
|
for longitude-polygon[0].Longitude > 180 {
|
||
|
|
longitude -= 360
|
||
|
|
}
|
||
|
|
for longitude-polygon[0].Longitude < -180 {
|
||
|
|
longitude += 360
|
||
|
|
}
|
||
|
|
probe := GeoPoint{Longitude: longitude, Latitude: point.Latitude}
|
||
|
|
for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 {
|
||
|
|
if sweepPointOnSegment(probe, polygon[previous], polygon[current]) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
inside := false
|
||
|
|
for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 {
|
||
|
|
first, second := polygon[previous], polygon[current]
|
||
|
|
if (first.Latitude > point.Latitude) == (second.Latitude > point.Latitude) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
intersection := first.Longitude +
|
||
|
|
(point.Latitude-first.Latitude)*(second.Longitude-first.Longitude)/(second.Latitude-first.Latitude)
|
||
|
|
if intersection >= longitude {
|
||
|
|
inside = !inside
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return inside
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepPointOnSegment(point, start, end GeoPoint) bool {
|
||
|
|
deltaLongitude := end.Longitude - start.Longitude
|
||
|
|
deltaLatitude := end.Latitude - start.Latitude
|
||
|
|
length := math.Hypot(deltaLongitude, deltaLatitude)
|
||
|
|
if length <= 1e-12 {
|
||
|
|
return math.Hypot(point.Longitude-start.Longitude, point.Latitude-start.Latitude) <= 1e-9
|
||
|
|
}
|
||
|
|
cross := deltaLongitude*(point.Latitude-start.Latitude) -
|
||
|
|
deltaLatitude*(point.Longitude-start.Longitude)
|
||
|
|
if math.Abs(cross) > 1e-9*length {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
dot := (point.Longitude-start.Longitude)*deltaLongitude +
|
||
|
|
(point.Latitude-start.Latitude)*deltaLatitude
|
||
|
|
return dot >= -1e-9*length && dot <= length*length+1e-9*length
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepBridgeTouchingPolygons(polygons [][]GeoPoint) ([][]GeoPoint, error) {
|
||
|
|
const (
|
||
|
|
touchingDistanceKM = 0.01
|
||
|
|
bridgeHalfSizeDeg = 1e-4
|
||
|
|
)
|
||
|
|
for len(polygons) > 1 {
|
||
|
|
firstIndex, secondIndex, touchingPoint, ok := sweepTouchingPolygonPair(polygons, touchingDistanceKM)
|
||
|
|
if !ok {
|
||
|
|
return polygons, nil
|
||
|
|
}
|
||
|
|
bridge := []GeoPoint{
|
||
|
|
{Longitude: touchingPoint.Longitude - bridgeHalfSizeDeg, Latitude: touchingPoint.Latitude - bridgeHalfSizeDeg},
|
||
|
|
{Longitude: touchingPoint.Longitude + bridgeHalfSizeDeg, Latitude: touchingPoint.Latitude - bridgeHalfSizeDeg},
|
||
|
|
{Longitude: touchingPoint.Longitude + bridgeHalfSizeDeg, Latitude: touchingPoint.Latitude + bridgeHalfSizeDeg},
|
||
|
|
{Longitude: touchingPoint.Longitude - bridgeHalfSizeDeg, Latitude: touchingPoint.Latitude + bridgeHalfSizeDeg},
|
||
|
|
}
|
||
|
|
// 输入是并集输出,环之间互不重叠,因此只需合并这一对。
|
||
|
|
merged, err := UnionPolygons([][]GeoPoint{polygons[firstIndex], polygons[secondIndex], bridge})
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if len(merged) >= 2 {
|
||
|
|
return polygons, nil
|
||
|
|
}
|
||
|
|
polygons = sweepReplaceTouchingPolygonPair(polygons, firstIndex, secondIndex, merged)
|
||
|
|
}
|
||
|
|
return polygons, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
const sweepTouchingCellSize = 2e-6
|
||
|
|
|
||
|
|
type sweepTouchingVertex struct {
|
||
|
|
polygon int
|
||
|
|
point GeoPoint
|
||
|
|
}
|
||
|
|
|
||
|
|
// sweepTouchingPolygonPair 返回扫描顺序最前的一对近邻顶点:先按多边形下标,再按顶点下标。
|
||
|
|
func sweepTouchingPolygonPair(
|
||
|
|
polygons [][]GeoPoint,
|
||
|
|
thresholdKM float64,
|
||
|
|
) (int, int, GeoPoint, bool) {
|
||
|
|
cells := make(map[int64][]sweepTouchingVertex)
|
||
|
|
for polygonIndex, polygon := range polygons {
|
||
|
|
for _, point := range polygon {
|
||
|
|
key := sweepTouchingCellKey(geoPointVector(point))
|
||
|
|
cells[key] = append(cells[key], sweepTouchingVertex{polygon: polygonIndex, point: point})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
pairs := make(map[[2]int]struct{})
|
||
|
|
for polygonIndex, polygon := range polygons {
|
||
|
|
for _, point := range polygon {
|
||
|
|
x, y, z := sweepTouchingCellCoordinates(geoPointVector(point))
|
||
|
|
for deltaX := int64(-1); deltaX <= 1; deltaX++ {
|
||
|
|
for deltaY := int64(-1); deltaY <= 1; deltaY++ {
|
||
|
|
for deltaZ := int64(-1); deltaZ <= 1; deltaZ++ {
|
||
|
|
key := sweepTouchingCellKeyFromCoordinates(x+deltaX, y+deltaY, z+deltaZ)
|
||
|
|
for _, candidate := range cells[key] {
|
||
|
|
if candidate.polygon <= polygonIndex ||
|
||
|
|
geoPointDistanceKM(point, candidate.point) > thresholdKM {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
pairs[[2]int{polygonIndex, candidate.polygon}] = struct{}{}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
firstIndex, secondIndex := -1, -1
|
||
|
|
for pair := range pairs {
|
||
|
|
if firstIndex < 0 || pair[0] < firstIndex || (pair[0] == firstIndex && pair[1] < secondIndex) {
|
||
|
|
firstIndex, secondIndex = pair[0], pair[1]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if firstIndex < 0 {
|
||
|
|
return 0, 0, GeoPoint{}, false
|
||
|
|
}
|
||
|
|
for _, first := range polygons[firstIndex] {
|
||
|
|
for _, second := range polygons[secondIndex] {
|
||
|
|
if !(geoPointDistanceKM(first, second) > thresholdKM) {
|
||
|
|
return firstIndex, secondIndex, GeoPoint{
|
||
|
|
Longitude: first.Longitude + math.Remainder(second.Longitude-first.Longitude, 360)/2,
|
||
|
|
Latitude: (first.Latitude + second.Latitude) / 2,
|
||
|
|
}, true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return 0, 0, GeoPoint{}, false
|
||
|
|
}
|
||
|
|
|
||
|
|
// 网格边长 2e-6 弧度,大于 0.01 公里的球面弦长,近邻顶点必落在相邻格。
|
||
|
|
func sweepTouchingCellCoordinates(vector geoVector3) (int64, int64, int64) {
|
||
|
|
const offset = 1 << 19
|
||
|
|
return int64(math.Floor(vector.x/sweepTouchingCellSize)) + offset,
|
||
|
|
int64(math.Floor(vector.y/sweepTouchingCellSize)) + offset,
|
||
|
|
int64(math.Floor(vector.z/sweepTouchingCellSize)) + offset
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepTouchingCellKey(vector geoVector3) int64 {
|
||
|
|
x, y, z := sweepTouchingCellCoordinates(vector)
|
||
|
|
return sweepTouchingCellKeyFromCoordinates(x, y, z)
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepTouchingCellKeyFromCoordinates(x, y, z int64) int64 {
|
||
|
|
return x<<40 | y<<20 | z
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepReplaceTouchingPolygonPair(
|
||
|
|
polygons [][]GeoPoint,
|
||
|
|
first, second int,
|
||
|
|
merged [][]GeoPoint,
|
||
|
|
) [][]GeoPoint {
|
||
|
|
if first > second {
|
||
|
|
first, second = second, first
|
||
|
|
}
|
||
|
|
result := make([][]GeoPoint, 0, len(polygons)-1+len(merged))
|
||
|
|
result = append(result, polygons[:first]...)
|
||
|
|
result = append(result, merged...)
|
||
|
|
result = append(result, polygons[first+1:second]...)
|
||
|
|
result = append(result, polygons[second+1:]...)
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepDeduplicateAdjacent(points []GeoPoint) []GeoPoint {
|
||
|
|
if len(points) < 2 {
|
||
|
|
return points
|
||
|
|
}
|
||
|
|
result := make([]GeoPoint, 0, len(points))
|
||
|
|
for _, point := range points {
|
||
|
|
if len(result) == 0 || !SameGeoPoint(result[len(result)-1], point) {
|
||
|
|
result = append(result, point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(result) > 1 && SameGeoPoint(result[0], result[len(result)-1]) {
|
||
|
|
result = result[:len(result)-1]
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func openGeoRing(points []GeoPoint) []GeoPoint {
|
||
|
|
if len(points) > 1 && SameGeoPoint(points[0], points[len(points)-1]) {
|
||
|
|
return points[:len(points)-1]
|
||
|
|
}
|
||
|
|
return points
|
||
|
|
}
|
||
|
|
|
||
|
|
func reverseSweepGeoPoints(points []GeoPoint) {
|
||
|
|
for left, right := 0, len(points)-1; left < right; left, right = left+1, right-1 {
|
||
|
|
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)))
|
||
|
|
}
|