1956 lines
62 KiB
Go
1956 lines
62 KiB
Go
|
|
package geodata
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"math"
|
||
|
|
"sort"
|
||
|
|
)
|
||
|
|
|
||
|
|
type visibleLineworkNode struct {
|
||
|
|
point GeoPoint
|
||
|
|
outgoing []visibleLineworkHalfEdge
|
||
|
|
}
|
||
|
|
|
||
|
|
type visibleLineworkEdge struct {
|
||
|
|
points []GeoPoint
|
||
|
|
start, end int
|
||
|
|
}
|
||
|
|
|
||
|
|
type visibleLineworkHalfEdge struct {
|
||
|
|
edge int
|
||
|
|
reverse bool
|
||
|
|
}
|
||
|
|
|
||
|
|
type visibleLineworkBounds struct {
|
||
|
|
minLongitude float64
|
||
|
|
maxLongitude float64
|
||
|
|
minLatitude float64
|
||
|
|
maxLatitude float64
|
||
|
|
}
|
||
|
|
|
||
|
|
// visibleLineworkArcIndex caches the endpoint vectors and great-circle
|
||
|
|
// normals used by the source-boundary audit. The audit may visit thousands of
|
||
|
|
// polygon vertices, but its source segments are fixed for the whole pass.
|
||
|
|
// Caching only these derived values preserves the exact spherical-distance
|
||
|
|
// calculation while avoiding repeated trigonometric conversions in WASM.
|
||
|
|
type visibleLineworkArcIndex struct {
|
||
|
|
arcs []visibleLineworkArc
|
||
|
|
vertices map[visibleLineworkPointKey]GeoPoint
|
||
|
|
edges map[visibleLineworkEdgeKey][]int
|
||
|
|
}
|
||
|
|
|
||
|
|
type visibleLineworkArc struct {
|
||
|
|
start, end GeoPoint
|
||
|
|
startVector geoVector3
|
||
|
|
endVector geoVector3
|
||
|
|
arcRadians float64
|
||
|
|
normal geoVector3
|
||
|
|
normalValid bool
|
||
|
|
}
|
||
|
|
|
||
|
|
type visibleLineworkPointKey struct {
|
||
|
|
longitude int64
|
||
|
|
latitude int64
|
||
|
|
}
|
||
|
|
|
||
|
|
type visibleLineworkEdgeKey struct {
|
||
|
|
first visibleLineworkPointKey
|
||
|
|
second visibleLineworkPointKey
|
||
|
|
}
|
||
|
|
|
||
|
|
const visibleLineworkIndexCoordinateScale = 1e7
|
||
|
|
|
||
|
|
// Coarse historical solar footprints can leave a bounded spherical residual;
|
||
|
|
// larger residuals indicate a planar-union shortcut rather than sampling noise.
|
||
|
|
const visibleLineworkBoundaryMinimumAuditToleranceKM = 100
|
||
|
|
|
||
|
|
func newVisibleLineworkArcIndex(lines [][]GeoPoint) visibleLineworkArcIndex {
|
||
|
|
count := 0
|
||
|
|
for _, line := range lines {
|
||
|
|
if len(line) > 1 {
|
||
|
|
count += len(line) - 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
index := visibleLineworkArcIndex{
|
||
|
|
arcs: make([]visibleLineworkArc, 0, count),
|
||
|
|
vertices: make(map[visibleLineworkPointKey]GeoPoint, count+1),
|
||
|
|
edges: make(map[visibleLineworkEdgeKey][]int, count),
|
||
|
|
}
|
||
|
|
for _, line := range lines {
|
||
|
|
for segment := 1; segment < len(line); segment++ {
|
||
|
|
start, end := line[segment-1], line[segment]
|
||
|
|
startVector := geoPointVector(start)
|
||
|
|
endVector := geoPointVector(end)
|
||
|
|
arcRadians := math.Acos(math.Max(-1, math.Min(1, geoVectorDot(startVector, endVector))))
|
||
|
|
normal, normalValid := geoVectorNormalize(geoVectorCross(startVector, endVector))
|
||
|
|
arcIndex := len(index.arcs)
|
||
|
|
index.arcs = append(index.arcs, visibleLineworkArc{
|
||
|
|
start: start, end: end,
|
||
|
|
startVector: startVector, endVector: endVector,
|
||
|
|
arcRadians: arcRadians,
|
||
|
|
normal: normal, normalValid: normalValid,
|
||
|
|
})
|
||
|
|
startKey := visibleLineworkIndexPointKey(start)
|
||
|
|
endKey := visibleLineworkIndexPointKey(end)
|
||
|
|
index.vertices[startKey] = start
|
||
|
|
index.vertices[endKey] = end
|
||
|
|
edgeKey := visibleLineworkIndexEdgeKey(startKey, endKey)
|
||
|
|
index.edges[edgeKey] = append(index.edges[edgeKey], arcIndex)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return index
|
||
|
|
}
|
||
|
|
|
||
|
|
func (index visibleLineworkArcIndex) pointDistanceKM(point GeoPoint) float64 {
|
||
|
|
if source, ok := index.vertices[visibleLineworkIndexPointKey(point)]; ok {
|
||
|
|
return geoPointDistanceKM(point, source)
|
||
|
|
}
|
||
|
|
pointVector := geoPointVector(point)
|
||
|
|
minimum := math.Inf(1)
|
||
|
|
for _, arc := range index.arcs {
|
||
|
|
minimum = math.Min(minimum, arc.pointDistanceKM(point, pointVector))
|
||
|
|
if minimum == 0 {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return minimum
|
||
|
|
}
|
||
|
|
|
||
|
|
func (index visibleLineworkArcIndex) edgePointDistanceKM(
|
||
|
|
first, second, point GeoPoint,
|
||
|
|
) (float64, bool) {
|
||
|
|
firstKey := visibleLineworkIndexPointKey(first)
|
||
|
|
secondKey := visibleLineworkIndexPointKey(second)
|
||
|
|
arcIndices := index.edges[visibleLineworkIndexEdgeKey(firstKey, secondKey)]
|
||
|
|
if len(arcIndices) == 0 {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
pointVector := geoPointVector(point)
|
||
|
|
minimum := math.Inf(1)
|
||
|
|
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
|
||
|
|
if !direct && !reverse {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
matched = true
|
||
|
|
minimum = math.Min(minimum, arc.pointDistanceKM(point, pointVector))
|
||
|
|
}
|
||
|
|
return minimum, matched
|
||
|
|
}
|
||
|
|
|
||
|
|
func (arc visibleLineworkArc) pointDistanceKM(point GeoPoint, pointVector geoVector3) float64 {
|
||
|
|
if arc.normalValid {
|
||
|
|
projection := geoVectorAdd(pointVector,
|
||
|
|
geoVectorScale(arc.normal, -geoVectorDot(pointVector, arc.normal)))
|
||
|
|
if projected, projectedOK := geoVectorNormalize(projection); projectedOK {
|
||
|
|
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 math.Min(geoPointDistanceKM(point, arc.start), geoPointDistanceKM(point, arc.end))
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkIndexPointKey(point GeoPoint) visibleLineworkPointKey {
|
||
|
|
return visibleLineworkPointKey{
|
||
|
|
longitude: int64(math.Round(point.Longitude * visibleLineworkIndexCoordinateScale)),
|
||
|
|
latitude: int64(math.Round(point.Latitude * visibleLineworkIndexCoordinateScale)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkIndexEdgeKey(
|
||
|
|
first, second visibleLineworkPointKey,
|
||
|
|
) visibleLineworkEdgeKey {
|
||
|
|
if first.longitude > second.longitude ||
|
||
|
|
(first.longitude == second.longitude && first.latitude > second.latitude) {
|
||
|
|
first, second = second, first
|
||
|
|
}
|
||
|
|
return visibleLineworkEdgeKey{first: first, second: second}
|
||
|
|
}
|
||
|
|
|
||
|
|
func sphericalPointOnArcVectors(point, first, second geoVector3, arc float64) bool {
|
||
|
|
if arc <= 1e-14 {
|
||
|
|
return math.Acos(math.Max(-1, math.Min(1, geoVectorDot(first, point)))) <= 1e-9
|
||
|
|
}
|
||
|
|
firstDistance := math.Acos(math.Max(-1, math.Min(1, geoVectorDot(first, point))))
|
||
|
|
secondDistance := math.Acos(math.Max(-1, math.Min(1, geoVectorDot(point, second))))
|
||
|
|
return math.Abs(firstDistance+secondDistance-arc) <= 1e-9
|
||
|
|
}
|
||
|
|
|
||
|
|
// VisibleLineworkPolygons 将球面边界线网多边形化,并仅保留由给定填充多边形覆盖的面。
|
||
|
|
// VisibleLineworkPolygons polygonizes a spherical boundary network and keeps
|
||
|
|
// only faces covered by at least one supplied fill polygon. Boundary line
|
||
|
|
// endpoints within snapDistanceKM are treated as the same physical junction.
|
||
|
|
func VisibleLineworkPolygons(
|
||
|
|
boundaryLines [][]GeoPoint,
|
||
|
|
fillPolygons [][]GeoPoint,
|
||
|
|
coveragePaths [][]GeoPoint,
|
||
|
|
snapDistanceKM float64,
|
||
|
|
) ([][]GeoPoint, error) {
|
||
|
|
return visibleLineworkPolygons(
|
||
|
|
boundaryLines, fillPolygons, coveragePaths, snapDistanceKM, 1.5*snapDistanceKM,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// VisibleLineworkPolygonsWithAuditTolerance 将边界图节点吸附尺度与源填充审计容差分开。
|
||
|
|
// VisibleLineworkPolygonsWithAuditTolerance keeps graph junction snapping and
|
||
|
|
// source-fill auditing on independent scales. This is needed when a boundary
|
||
|
|
// is sampled at a fine spatial cadence but the interior witness is a coarser
|
||
|
|
// sweep: increasing the snap distance would merge nearby physical branches,
|
||
|
|
// while a bounded audit tolerance does not alter the boundary graph.
|
||
|
|
func VisibleLineworkPolygonsWithAuditTolerance(
|
||
|
|
boundaryLines [][]GeoPoint,
|
||
|
|
fillPolygons [][]GeoPoint,
|
||
|
|
coveragePaths [][]GeoPoint,
|
||
|
|
snapDistanceKM, sourceFillToleranceKM float64,
|
||
|
|
) ([][]GeoPoint, error) {
|
||
|
|
return visibleLineworkPolygons(
|
||
|
|
boundaryLines, fillPolygons, coveragePaths, snapDistanceKM, sourceFillToleranceKM,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkPolygons(
|
||
|
|
boundaryLines [][]GeoPoint,
|
||
|
|
fillPolygons [][]GeoPoint,
|
||
|
|
coveragePaths [][]GeoPoint,
|
||
|
|
snapDistanceKM, sourceFillToleranceKM float64,
|
||
|
|
) ([][]GeoPoint, error) {
|
||
|
|
if len(boundaryLines) == 0 {
|
||
|
|
return nil, fmt.Errorf("visible linework has no boundary lines")
|
||
|
|
}
|
||
|
|
if !(snapDistanceKM >= 0) || !(sourceFillToleranceKM >= 0) ||
|
||
|
|
math.IsInf(snapDistanceKM, 0) || math.IsInf(sourceFillToleranceKM, 0) {
|
||
|
|
return nil, fmt.Errorf("visible linework tolerances must be finite and non-negative")
|
||
|
|
}
|
||
|
|
chartInputs := make([][]GeoPoint, 0, len(boundaryLines)+len(fillPolygons))
|
||
|
|
chartInputs = append(chartInputs, boundaryLines...)
|
||
|
|
chartInputs = append(chartInputs, fillPolygons...)
|
||
|
|
chart, ok := newPolygonUnionChart(chartInputs)
|
||
|
|
if !ok {
|
||
|
|
return nil, fmt.Errorf("visible linework has no stable spherical chart")
|
||
|
|
}
|
||
|
|
|
||
|
|
projectedLines := make([][]GeoPoint, 0, len(boundaryLines))
|
||
|
|
for _, source := range boundaryLines {
|
||
|
|
line := projectVisibleLine(chart, source)
|
||
|
|
if len(line) >= 2 {
|
||
|
|
projectedLines = append(projectedLines, line)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
projectedFill := make([][]GeoPoint, 0, len(fillPolygons))
|
||
|
|
for _, source := range fillPolygons {
|
||
|
|
polygon := projectVisibleLine(chart, openGeoRing(source))
|
||
|
|
if len(polygon) >= 3 {
|
||
|
|
projectedFill = append(projectedFill, polygon)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(projectedLines) == 0 || len(projectedFill) == 0 {
|
||
|
|
return nil, fmt.Errorf("visible linework has no usable boundary or fill geometry")
|
||
|
|
}
|
||
|
|
projectedLines = splitVisibleLineworkIntersections(projectedLines)
|
||
|
|
if len(coveragePaths) == 0 {
|
||
|
|
coveragePaths = fillPolygons
|
||
|
|
}
|
||
|
|
projectedCoverage := make([][]GeoPoint, 0, len(coveragePaths))
|
||
|
|
for _, source := range coveragePaths {
|
||
|
|
path := projectVisibleLine(chart, source)
|
||
|
|
if len(path) > 0 {
|
||
|
|
projectedCoverage = append(projectedCoverage, path)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, probe := range visibleLineworkFilledEdgeProbes(projectedLines, projectedFill) {
|
||
|
|
projectedCoverage = append(projectedCoverage, []GeoPoint{probe})
|
||
|
|
}
|
||
|
|
|
||
|
|
nodes, edges, loops := buildVisibleLineworkGraph(projectedLines, chart, snapDistanceKM)
|
||
|
|
cycles := enumerateVisibleLineworkCycles(nodes, edges)
|
||
|
|
cycles = append(cycles, loops...)
|
||
|
|
// Endpoint snapping and face membership have different numerical scales.
|
||
|
|
// A tens-of-kilometres junction snap is useful for joining independently sampled phase
|
||
|
|
// 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
|
||
|
|
cycleProbes := visibleLineworkFilledCycleProbes(cycles, projectedFill, projectedTolerance)
|
||
|
|
if len(cycleProbes) > 0 {
|
||
|
|
for _, probe := range cycleProbes {
|
||
|
|
projectedCoverage = append(projectedCoverage, []GeoPoint{probe})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Audit the selected boundary against the complete source fill, not just
|
||
|
|
// the sparse coverage probes used to choose faces. Otherwise a face can
|
||
|
|
// contain the probes while its outline still cuts across an unsampled part
|
||
|
|
// of the instantaneous footprint.
|
||
|
|
projectedFillSamples := sampleVisibleLineworkFillPaths(projectedFill, 16)
|
||
|
|
fillSamples := visibleLineworkPointPaths(
|
||
|
|
unprojectVisibleLineworkPaths(chart, projectedFillSamples),
|
||
|
|
)
|
||
|
|
selectedCycles := make([][]GeoPoint, 0)
|
||
|
|
if len(cycleProbes) > 0 {
|
||
|
|
priorityCoverage := make([][]GeoPoint, len(cycleProbes))
|
||
|
|
for index, probe := range cycleProbes {
|
||
|
|
priorityCoverage[index] = []GeoPoint{probe}
|
||
|
|
}
|
||
|
|
prioritySelected := selectVisibleLineworkCycles(
|
||
|
|
cycles, priorityCoverage, projectedTolerance,
|
||
|
|
)
|
||
|
|
if visibleLineworkCyclesCoverCoverage(prioritySelected, projectedCoverage, projectedTolerance) {
|
||
|
|
selectedCycles = prioritySelected
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(selectedCycles) == 0 {
|
||
|
|
selectedCycles = selectVisibleLineworkCycles(
|
||
|
|
cycles, projectedCoverage, projectedTolerance,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
selected := make([][]GeoPoint, 0, len(selectedCycles))
|
||
|
|
for _, cycle := range selectedCycles {
|
||
|
|
polygon := make([]GeoPoint, len(cycle))
|
||
|
|
for index, point := range cycle {
|
||
|
|
polygon[index] = chart.unproject(point)
|
||
|
|
}
|
||
|
|
selected = append(selected, polygon)
|
||
|
|
}
|
||
|
|
if len(selected) == 0 {
|
||
|
|
return nil, fmt.Errorf("visible linework produced no covering boundary from %d cycles (coverage=%d cycle_probes=%d misses=%d)",
|
||
|
|
len(cycles), len(projectedCoverage), len(cycleProbes),
|
||
|
|
visibleLineworkCoverageMissCount(cycles, projectedCoverage, projectedTolerance))
|
||
|
|
}
|
||
|
|
// A single simple face is already a polygon. Repeating a boolean union
|
||
|
|
// can lose tiny edges where tangential branches were split into the graph.
|
||
|
|
merged := selected
|
||
|
|
if len(selected) > 1 {
|
||
|
|
var err error
|
||
|
|
merged, err = UnionPolygons(selected)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("visible linework face union: %w", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// The common output already consists of short projected edges. Running the
|
||
|
|
// exact spherical point-to-arc audit on every edge is disproportionately
|
||
|
|
// expensive in TinyGo/WASM, while an artificial polar chord is necessarily
|
||
|
|
// visible as a long projected edge. Keep the precise repair/audit only for
|
||
|
|
// that suspicious case.
|
||
|
|
suspiciousBoundary := visibleLineworkHasLongProjectedEdge(merged, 45) || visibleLineworkHasProjectedPolarBacktrack(merged)
|
||
|
|
if suspiciousBoundary {
|
||
|
|
merged = repairVisibleLineworkBoundaryChords(merged, boundaryLines, 5, 30)
|
||
|
|
// Re-run the exact source audit only if a suspicious edge remains after
|
||
|
|
// repair. Normal authoritative rings have no long edge or polar reversal
|
||
|
|
// at this point, so a full point-to-arc scan would be redundant.
|
||
|
|
if visibleLineworkHasLongProjectedEdge(merged, 45) || visibleLineworkHasProjectedPolarBacktrack(merged) {
|
||
|
|
if sourceMiss := visibleLineworkBoundarySourceMissDistanceKM(merged, boundaryLines); sourceMiss > math.Max(visibleLineworkBoundaryMinimumAuditToleranceKM, 2*snapDistanceKM) {
|
||
|
|
// A planar union can introduce a shortcut while each selected
|
||
|
|
// source cycle is still a valid physical boundary. Preserve those
|
||
|
|
// source cycles as separate polygons rather than falling through
|
||
|
|
// to an endpoint-only sweep that closes through a pole.
|
||
|
|
if selectedMiss := visibleLineworkBoundarySourceMissDistanceKM(selected, boundaryLines); selectedMiss <= math.Max(visibleLineworkBoundaryMinimumAuditToleranceKM, 2*snapDistanceKM) {
|
||
|
|
return selected, nil
|
||
|
|
}
|
||
|
|
return nil, fmt.Errorf("visible linework boundary leaves source linework by %.1f km", sourceMiss)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if miss := SphericalPolygonsPathMissDistanceKM(merged, fillSamples, true); miss > sourceFillToleranceKM {
|
||
|
|
return nil, fmt.Errorf("visible linework boundary misses source fill by %.1f km", miss)
|
||
|
|
}
|
||
|
|
return merged, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkHasProjectedPolarBacktrack(polygons [][]GeoPoint) bool {
|
||
|
|
for _, polygon := range polygons {
|
||
|
|
for index := 1; index+1 < len(polygon); index++ {
|
||
|
|
first, middle, last := polygon[index-1], polygon[index], polygon[index+1]
|
||
|
|
if math.Abs(middle.Latitude) < 70 ||
|
||
|
|
visibleLineworkProjectedEdgeDistanceKM(first, middle) > 40 ||
|
||
|
|
visibleLineworkProjectedEdgeDistanceKM(middle, last) > 40 ||
|
||
|
|
visibleLineworkProjectedEdgeDistanceKM(first, last) > 55 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
longitudeReversal := math.Remainder(middle.Longitude-first.Longitude, 360)*
|
||
|
|
math.Remainder(last.Longitude-middle.Longitude, 360) < 0
|
||
|
|
latitudeReversal := (middle.Latitude-first.Latitude)*(last.Latitude-middle.Latitude) < 0
|
||
|
|
if longitudeReversal || latitudeReversal {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkHasLongProjectedEdge(polygons [][]GeoPoint, maximumKM float64) bool {
|
||
|
|
if maximumKM <= 0 {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
for _, polygon := range polygons {
|
||
|
|
for index := 1; index < len(polygon); index++ {
|
||
|
|
if visibleLineworkProjectedEdgeDistanceKM(polygon[index-1], polygon[index]) > maximumKM {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
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
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkBoundarySourceMissDistanceKM(
|
||
|
|
polygons, boundaryLines [][]GeoPoint,
|
||
|
|
) float64 {
|
||
|
|
sourceArcs := newVisibleLineworkArcIndex(boundaryLines)
|
||
|
|
maximumDistance := 0.0
|
||
|
|
for _, polygon := range polygons {
|
||
|
|
for index, point := range polygon {
|
||
|
|
// Endpoints of a planar shortcut often coincide with real source
|
||
|
|
// vertices. They must not mask a midpoint that leaves the source
|
||
|
|
// boundary by hundreds or thousands of kilometres.
|
||
|
|
edgeMaximum := sourceArcs.pointDistanceKM(point)
|
||
|
|
if index > 0 {
|
||
|
|
midpoint := sphericalInterpolate(polygon[index-1], point, 0.5)
|
||
|
|
midpointDistance, matched := sourceArcs.edgePointDistanceKM(
|
||
|
|
polygon[index-1], point, midpoint,
|
||
|
|
)
|
||
|
|
if !matched {
|
||
|
|
midpointDistance = sourceArcs.pointDistanceKM(midpoint)
|
||
|
|
}
|
||
|
|
edgeMaximum = math.Max(edgeMaximum, midpointDistance)
|
||
|
|
}
|
||
|
|
maximumDistance = math.Max(maximumDistance, edgeMaximum)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return maximumDistance
|
||
|
|
}
|
||
|
|
|
||
|
|
// repairVisibleLineworkBoundaryChords restores a source-boundary run when a
|
||
|
|
// planar union closes a polar face with a shortcut between two vertices on the
|
||
|
|
// same physical curve. The shortcut is topologically valid in the chart but
|
||
|
|
// is not part of the supplied linework and renders as an artificial straight
|
||
|
|
// chord on the map.
|
||
|
|
func repairVisibleLineworkBoundaryChords(
|
||
|
|
polygons, boundaryLines [][]GeoPoint,
|
||
|
|
maxDeviationKM, endpointToleranceKM float64,
|
||
|
|
) [][]GeoPoint {
|
||
|
|
if len(polygons) == 0 || len(boundaryLines) == 0 || maxDeviationKM <= 0 {
|
||
|
|
return polygons
|
||
|
|
}
|
||
|
|
sourceArcs := newVisibleLineworkArcIndex(boundaryLines)
|
||
|
|
result := make([][]GeoPoint, len(polygons))
|
||
|
|
for polygonIndex, ring := range polygons {
|
||
|
|
if len(ring) < 4 {
|
||
|
|
result[polygonIndex] = ring
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
repaired := make([]GeoPoint, 0, len(ring))
|
||
|
|
repaired = append(repaired, ring[0])
|
||
|
|
for index := 1; index < len(ring); index++ {
|
||
|
|
start, end := ring[index-1], ring[index]
|
||
|
|
midpoint := sphericalInterpolate(start, end, 0.5)
|
||
|
|
deviation, matched := sourceArcs.edgePointDistanceKM(start, end, midpoint)
|
||
|
|
if !matched {
|
||
|
|
deviation = sourceArcs.pointDistanceKM(midpoint)
|
||
|
|
}
|
||
|
|
if deviation <= maxDeviationKM {
|
||
|
|
repaired = append(repaired, end)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
bridge, ok := visibleLineworkBoundaryBridge(
|
||
|
|
start, end, boundaryLines, endpointToleranceKM,
|
||
|
|
)
|
||
|
|
if !ok {
|
||
|
|
repaired = append(repaired, end)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
repaired = append(repaired, bridge[1:]...)
|
||
|
|
}
|
||
|
|
result[polygonIndex] = sweepDeduplicateAdjacent(repaired)
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkPointToLinesDistanceKM(
|
||
|
|
point GeoPoint,
|
||
|
|
lines [][]GeoPoint,
|
||
|
|
) float64 {
|
||
|
|
return newVisibleLineworkArcIndex(lines).pointDistanceKM(point)
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkBoundaryBridge(
|
||
|
|
start, end GeoPoint,
|
||
|
|
lines [][]GeoPoint,
|
||
|
|
toleranceKM float64,
|
||
|
|
) ([]GeoPoint, bool) {
|
||
|
|
originalStart := start
|
||
|
|
var best []GeoPoint
|
||
|
|
bestLength := math.Inf(1)
|
||
|
|
for _, line := range lines {
|
||
|
|
if len(line) < 3 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
startIndex, startDistance := visibleLineworkNearestVertex(start, line)
|
||
|
|
endIndex, endDistance := visibleLineworkNearestVertex(end, line)
|
||
|
|
if startDistance > toleranceKM || endDistance > toleranceKM ||
|
||
|
|
startIndex == endIndex || absInt(startIndex-endIndex) < 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if startIndex > endIndex {
|
||
|
|
startIndex, endIndex = endIndex, startIndex
|
||
|
|
start, end = end, start
|
||
|
|
}
|
||
|
|
bridge := make([]GeoPoint, 0, endIndex-startIndex+1)
|
||
|
|
bridge = append(bridge, start)
|
||
|
|
bridge = append(bridge, line[startIndex+1:endIndex]...)
|
||
|
|
bridge = append(bridge, end)
|
||
|
|
if len(bridge) >= 3 {
|
||
|
|
if !visibleLineworkBridgeIsSimple(bridge, toleranceKM) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if geoPointDistanceKM(originalStart, bridge[0]) > geoPointDistanceKM(originalStart, bridge[len(bridge)-1]) {
|
||
|
|
reverseGeoPoints(bridge)
|
||
|
|
}
|
||
|
|
length := visibleLineworkBridgeLength(bridge)
|
||
|
|
if length < bestLength {
|
||
|
|
best, bestLength = bridge, length
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(best) >= 3 {
|
||
|
|
return best, true
|
||
|
|
}
|
||
|
|
return visibleLineworkCrossLineBoundaryBridge(start, end, lines, toleranceKM)
|
||
|
|
}
|
||
|
|
|
||
|
|
// visibleLineworkCrossLineBoundaryBridge restores a source-boundary run whose
|
||
|
|
// endpoints lie on different line strings. Polar phase envelopes commonly
|
||
|
|
// meet a horizon connector at an endpoint; the planar graph can collapse that
|
||
|
|
// junction and leave a long shortcut between the two strings. Stitch only
|
||
|
|
// existing source vertices that are within the same endpoint tolerance, and
|
||
|
|
// choose the shortest resulting source-path so unrelated lines cannot become a
|
||
|
|
// new artificial edge.
|
||
|
|
func visibleLineworkCrossLineBoundaryBridge(
|
||
|
|
start, end GeoPoint,
|
||
|
|
lines [][]GeoPoint,
|
||
|
|
toleranceKM float64,
|
||
|
|
) ([]GeoPoint, bool) {
|
||
|
|
type location struct {
|
||
|
|
lineIndex, pointIndex int
|
||
|
|
distance float64
|
||
|
|
}
|
||
|
|
startLocations := make([]location, 0)
|
||
|
|
endLocations := make([]location, 0)
|
||
|
|
for lineIndex, line := range lines {
|
||
|
|
if len(line) < 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
for pointIndex, point := range line {
|
||
|
|
if distance := geoPointDistanceKM(start, point); distance <= toleranceKM {
|
||
|
|
startLocations = append(startLocations, location{lineIndex, pointIndex, distance})
|
||
|
|
}
|
||
|
|
if distance := geoPointDistanceKM(end, point); distance <= toleranceKM {
|
||
|
|
endLocations = append(endLocations, location{lineIndex, pointIndex, distance})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
var best []GeoPoint
|
||
|
|
bestLength := math.Inf(1)
|
||
|
|
for _, first := range startLocations {
|
||
|
|
for _, second := range endLocations {
|
||
|
|
if first.lineIndex == second.lineIndex {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
firstLine, secondLine := lines[first.lineIndex], lines[second.lineIndex]
|
||
|
|
firstIndices := []int{0, len(firstLine) - 1}
|
||
|
|
secondIndices := []int{0, len(secondLine) - 1}
|
||
|
|
for _, firstIndex := range firstIndices {
|
||
|
|
for _, secondIndex := range secondIndices {
|
||
|
|
if geoPointDistanceKM(firstLine[firstIndex], secondLine[secondIndex]) > toleranceKM {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
firstPath := visibleLineworkLinePath(firstLine, first.pointIndex, firstIndex, start, firstLine[firstIndex])
|
||
|
|
secondPath := visibleLineworkLinePath(secondLine, secondIndex, second.pointIndex, secondLine[secondIndex], end)
|
||
|
|
if len(firstPath) < 2 || len(secondPath) < 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
bridge := append(append([]GeoPoint(nil), firstPath...), secondPath[1:]...)
|
||
|
|
if len(bridge) < 3 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !visibleLineworkBridgeIsSimple(bridge, toleranceKM) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
length := visibleLineworkBridgeLength(bridge)
|
||
|
|
if length < bestLength {
|
||
|
|
best, bestLength = bridge, length
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(best) < 3 {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
return best, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkBridgeIsSimple(points []GeoPoint, toleranceKM float64) bool {
|
||
|
|
if len(points) < 3 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
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 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkLinePath(
|
||
|
|
line []GeoPoint,
|
||
|
|
startIndex, endIndex int,
|
||
|
|
start, end GeoPoint,
|
||
|
|
) []GeoPoint {
|
||
|
|
if startIndex < 0 || startIndex >= len(line) || endIndex < 0 || endIndex >= len(line) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
path := []GeoPoint{start}
|
||
|
|
if startIndex <= endIndex {
|
||
|
|
path = append(path, line[startIndex+1:endIndex+1]...)
|
||
|
|
} else {
|
||
|
|
for index := startIndex - 1; index >= endIndex; index-- {
|
||
|
|
path = append(path, line[index])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(path) == 0 || geoPointDistanceKM(path[len(path)-1], end) > 1e-9 {
|
||
|
|
path = append(path, end)
|
||
|
|
}
|
||
|
|
return path
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkBridgeLength(points []GeoPoint) float64 {
|
||
|
|
length := 0.0
|
||
|
|
for index := 1; index < len(points); index++ {
|
||
|
|
length += geoPointDistanceKM(points[index-1], points[index])
|
||
|
|
}
|
||
|
|
return length
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkNearestVertex(
|
||
|
|
point GeoPoint,
|
||
|
|
line []GeoPoint,
|
||
|
|
) (int, float64) {
|
||
|
|
bestIndex := -1
|
||
|
|
bestDistance := math.Inf(1)
|
||
|
|
for index, candidate := range line {
|
||
|
|
distance := geoPointDistanceKM(point, candidate)
|
||
|
|
if distance < bestDistance {
|
||
|
|
bestIndex = index
|
||
|
|
bestDistance = distance
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return bestIndex, bestDistance
|
||
|
|
}
|
||
|
|
|
||
|
|
func absInt(value int) int {
|
||
|
|
if value < 0 {
|
||
|
|
return -value
|
||
|
|
}
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkFilledEdgeProbes(lines, fillPolygons [][]GeoPoint) []GeoPoint {
|
||
|
|
fillBounds := make([]visibleLineworkBounds, len(fillPolygons))
|
||
|
|
for index, polygon := range fillPolygons {
|
||
|
|
fillBounds[index] = visibleLineworkPolygonBounds(polygon, 0)
|
||
|
|
}
|
||
|
|
probes := make([]GeoPoint, 0, len(lines))
|
||
|
|
for _, line := range lines {
|
||
|
|
if len(line) < 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
segment := (len(line) - 1) / 2
|
||
|
|
start, end := line[segment], line[segment+1]
|
||
|
|
dx := end.Longitude - start.Longitude
|
||
|
|
dy := end.Latitude - start.Latitude
|
||
|
|
length := math.Hypot(dx, dy)
|
||
|
|
if length <= 1e-12 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
midpoint := GeoPoint{
|
||
|
|
Longitude: (start.Longitude + end.Longitude) / 2,
|
||
|
|
Latitude: (start.Latitude + end.Latitude) / 2,
|
||
|
|
}
|
||
|
|
offset := math.Min(0.0025, 0.02*length)
|
||
|
|
for _, side := range []float64{-1, 1} {
|
||
|
|
probe := GeoPoint{
|
||
|
|
Longitude: midpoint.Longitude - side*dy*offset/length,
|
||
|
|
Latitude: midpoint.Latitude + side*dx*offset/length,
|
||
|
|
}
|
||
|
|
for fillIndex, polygon := range fillPolygons {
|
||
|
|
if fillBounds[fillIndex].contains(polygon, probe) &&
|
||
|
|
visibleLineworkContainsWithin(polygon, probe, 0) {
|
||
|
|
probes = append(probes, probe)
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return probes
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkFilledCycleProbes(
|
||
|
|
cycles, fillPolygons [][]GeoPoint,
|
||
|
|
tolerance float64,
|
||
|
|
) []GeoPoint {
|
||
|
|
type candidate struct {
|
||
|
|
polygon []GeoPoint
|
||
|
|
probe GeoPoint
|
||
|
|
area float64
|
||
|
|
bounds visibleLineworkBounds
|
||
|
|
}
|
||
|
|
fillBounds := make([]visibleLineworkBounds, len(fillPolygons))
|
||
|
|
for index, polygon := range fillPolygons {
|
||
|
|
fillBounds[index] = visibleLineworkPolygonBounds(polygon, tolerance)
|
||
|
|
}
|
||
|
|
candidates := make([]candidate, 0, len(cycles))
|
||
|
|
for _, cycle := range cycles {
|
||
|
|
probe, ok := visibleLineworkPolygonCentroid(cycle)
|
||
|
|
if !ok || !visibleLineworkContainsWithin(cycle, probe, tolerance) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
for fillIndex, polygon := range fillPolygons {
|
||
|
|
if fillBounds[fillIndex].contains(polygon, probe) &&
|
||
|
|
visibleLineworkContainsWithin(polygon, probe, tolerance) {
|
||
|
|
candidates = append(candidates, candidate{
|
||
|
|
polygon: cycle,
|
||
|
|
probe: probe,
|
||
|
|
area: math.Abs(visibleLineworkSignedArea(cycle)),
|
||
|
|
bounds: visibleLineworkPolygonBounds(cycle, tolerance),
|
||
|
|
})
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sort.Slice(candidates, func(first, second int) bool {
|
||
|
|
return candidates[first].area < candidates[second].area
|
||
|
|
})
|
||
|
|
selected := make([]candidate, 0, len(candidates))
|
||
|
|
probes := make([]GeoPoint, 0, len(candidates))
|
||
|
|
for _, value := range candidates {
|
||
|
|
coveredBySmallerFace := false
|
||
|
|
for _, smaller := range selected {
|
||
|
|
if smaller.bounds.contains(smaller.polygon, value.probe) &&
|
||
|
|
visibleLineworkContainsWithin(smaller.polygon, value.probe, tolerance) {
|
||
|
|
coveredBySmallerFace = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if coveredBySmallerFace {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
selected = append(selected, value)
|
||
|
|
probes = append(probes, value.probe)
|
||
|
|
}
|
||
|
|
return probes
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkPolygonCentroid(polygon []GeoPoint) (GeoPoint, bool) {
|
||
|
|
if len(polygon) < 3 {
|
||
|
|
return GeoPoint{}, false
|
||
|
|
}
|
||
|
|
area, longitude, latitude := 0.0, 0.0, 0.0
|
||
|
|
for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 {
|
||
|
|
cross := polygon[previous].Longitude*polygon[current].Latitude -
|
||
|
|
polygon[current].Longitude*polygon[previous].Latitude
|
||
|
|
area += cross
|
||
|
|
longitude += (polygon[previous].Longitude + polygon[current].Longitude) * cross
|
||
|
|
latitude += (polygon[previous].Latitude + polygon[current].Latitude) * cross
|
||
|
|
}
|
||
|
|
if math.Abs(area) <= 1e-12 {
|
||
|
|
return GeoPoint{}, false
|
||
|
|
}
|
||
|
|
return GeoPoint{
|
||
|
|
Longitude: longitude / (3 * area),
|
||
|
|
Latitude: latitude / (3 * area),
|
||
|
|
}, true
|
||
|
|
}
|
||
|
|
|
||
|
|
type visibleLineworkSegmentSplit struct {
|
||
|
|
fraction float64
|
||
|
|
point GeoPoint
|
||
|
|
}
|
||
|
|
|
||
|
|
// splitVisibleLineworkIntersections turns interior crossings into graph
|
||
|
|
// nodes. Lunar rise/set curves can cross at a real polar horizon-grazing
|
||
|
|
// site, whereas solar-eclipse linework usually meets only at endpoints.
|
||
|
|
func splitVisibleLineworkIntersections(lines [][]GeoPoint) [][]GeoPoint {
|
||
|
|
if len(lines) < 2 {
|
||
|
|
return lines
|
||
|
|
}
|
||
|
|
splits := make([][][]visibleLineworkSegmentSplit, len(lines))
|
||
|
|
junctions := make([][]bool, len(lines))
|
||
|
|
for lineIndex, line := range lines {
|
||
|
|
if len(line) < 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
splits[lineIndex] = make([][]visibleLineworkSegmentSplit, len(line)-1)
|
||
|
|
junctions[lineIndex] = make([]bool, len(line))
|
||
|
|
}
|
||
|
|
for firstLine := 0; firstLine < len(lines); firstLine++ {
|
||
|
|
for firstSegment := 0; firstSegment+1 < len(lines[firstLine]); firstSegment++ {
|
||
|
|
firstStart := lines[firstLine][firstSegment]
|
||
|
|
firstEnd := lines[firstLine][firstSegment+1]
|
||
|
|
for secondLine := firstLine; secondLine < len(lines); secondLine++ {
|
||
|
|
secondStartSegment := 0
|
||
|
|
if secondLine == firstLine {
|
||
|
|
secondStartSegment = firstSegment + 2
|
||
|
|
}
|
||
|
|
for secondSegment := secondStartSegment; secondSegment+1 < len(lines[secondLine]); secondSegment++ {
|
||
|
|
secondStart := lines[secondLine][secondSegment]
|
||
|
|
secondEnd := lines[secondLine][secondSegment+1]
|
||
|
|
longitudeShift := 360 * math.Round(
|
||
|
|
((firstStart.Longitude+firstEnd.Longitude)-(secondStart.Longitude+secondEnd.Longitude))/720,
|
||
|
|
)
|
||
|
|
shiftedStart := secondStart
|
||
|
|
shiftedEnd := secondEnd
|
||
|
|
shiftedStart.Longitude += longitudeShift
|
||
|
|
shiftedEnd.Longitude += longitudeShift
|
||
|
|
firstFraction, secondFraction, point, ok := visibleLineworkSegmentIntersection(
|
||
|
|
firstStart, firstEnd, shiftedStart, shiftedEnd,
|
||
|
|
)
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
visibleLineworkRecordSplit(
|
||
|
|
splits[firstLine], junctions[firstLine], firstSegment, firstFraction, point,
|
||
|
|
)
|
||
|
|
point.Longitude -= longitudeShift
|
||
|
|
visibleLineworkRecordSplit(
|
||
|
|
splits[secondLine], junctions[secondLine], secondSegment, secondFraction, point,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
result := make([][]GeoPoint, 0, len(lines)*2)
|
||
|
|
for lineIndex, line := range lines {
|
||
|
|
if len(line) < 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
current := []GeoPoint{line[0]}
|
||
|
|
for segmentIndex := 0; segmentIndex+1 < len(line); segmentIndex++ {
|
||
|
|
if segmentIndex > 0 && junctions[lineIndex][segmentIndex] {
|
||
|
|
if len(current) >= 2 {
|
||
|
|
result = append(result, current)
|
||
|
|
}
|
||
|
|
current = []GeoPoint{line[segmentIndex]}
|
||
|
|
}
|
||
|
|
segmentSplits := splits[lineIndex][segmentIndex]
|
||
|
|
sort.Slice(segmentSplits, func(first, second int) bool {
|
||
|
|
return segmentSplits[first].fraction < segmentSplits[second].fraction
|
||
|
|
})
|
||
|
|
for _, split := range segmentSplits {
|
||
|
|
if len(current) == 0 || !SameGeoPoint(current[len(current)-1], split.point) {
|
||
|
|
current = append(current, split.point)
|
||
|
|
}
|
||
|
|
if len(current) >= 2 {
|
||
|
|
result = append(result, current)
|
||
|
|
}
|
||
|
|
current = []GeoPoint{split.point}
|
||
|
|
}
|
||
|
|
if !SameGeoPoint(current[len(current)-1], line[segmentIndex+1]) {
|
||
|
|
current = append(current, line[segmentIndex+1])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(current) >= 2 {
|
||
|
|
result = append(result, current)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkRecordSplit(
|
||
|
|
splits [][]visibleLineworkSegmentSplit,
|
||
|
|
junctions []bool,
|
||
|
|
segment int,
|
||
|
|
fraction float64,
|
||
|
|
point GeoPoint,
|
||
|
|
) {
|
||
|
|
const endpointTolerance = 1e-8
|
||
|
|
if fraction <= endpointTolerance {
|
||
|
|
junctions[segment] = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if fraction >= 1-endpointTolerance {
|
||
|
|
junctions[segment+1] = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
for _, existing := range splits[segment] {
|
||
|
|
if math.Abs(existing.fraction-fraction) <= endpointTolerance {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
splits[segment] = append(splits[segment], visibleLineworkSegmentSplit{
|
||
|
|
fraction: fraction,
|
||
|
|
point: point,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkSegmentIntersection(
|
||
|
|
firstStart, firstEnd, secondStart, secondEnd GeoPoint,
|
||
|
|
) (float64, float64, GeoPoint, bool) {
|
||
|
|
firstX := firstEnd.Longitude - firstStart.Longitude
|
||
|
|
firstY := firstEnd.Latitude - firstStart.Latitude
|
||
|
|
secondX := secondEnd.Longitude - secondStart.Longitude
|
||
|
|
secondY := secondEnd.Latitude - secondStart.Latitude
|
||
|
|
denominator := firstX*secondY - firstY*secondX
|
||
|
|
if math.Abs(denominator) <= 1e-12 {
|
||
|
|
return 0, 0, GeoPoint{}, false
|
||
|
|
}
|
||
|
|
offsetX := secondStart.Longitude - firstStart.Longitude
|
||
|
|
offsetY := secondStart.Latitude - firstStart.Latitude
|
||
|
|
firstFraction := (offsetX*secondY - offsetY*secondX) / denominator
|
||
|
|
secondFraction := (offsetX*firstY - offsetY*firstX) / denominator
|
||
|
|
const intersectionTolerance = 1e-9
|
||
|
|
if firstFraction < -intersectionTolerance || firstFraction > 1+intersectionTolerance ||
|
||
|
|
secondFraction < -intersectionTolerance || secondFraction > 1+intersectionTolerance {
|
||
|
|
// 参数判定只容忍 1e-9,旋转图幅下 T 型交点会被拒绝,这里按垂距回退判定。
|
||
|
|
if fraction, ok := visibleLineworkEndpointOnSegment(secondStart, firstStart, firstEnd); ok {
|
||
|
|
return fraction, 0, secondStart, true
|
||
|
|
}
|
||
|
|
if fraction, ok := visibleLineworkEndpointOnSegment(secondEnd, firstStart, firstEnd); ok {
|
||
|
|
return fraction, 1, secondEnd, true
|
||
|
|
}
|
||
|
|
if fraction, ok := visibleLineworkEndpointOnSegment(firstStart, secondStart, secondEnd); ok {
|
||
|
|
return 0, fraction, firstStart, true
|
||
|
|
}
|
||
|
|
if fraction, ok := visibleLineworkEndpointOnSegment(firstEnd, secondStart, secondEnd); ok {
|
||
|
|
return 1, fraction, firstEnd, true
|
||
|
|
}
|
||
|
|
return 0, 0, GeoPoint{}, false
|
||
|
|
}
|
||
|
|
firstFraction = math.Max(0, math.Min(1, firstFraction))
|
||
|
|
secondFraction = math.Max(0, math.Min(1, secondFraction))
|
||
|
|
return firstFraction, secondFraction, GeoPoint{
|
||
|
|
Longitude: firstStart.Longitude + firstFraction*firstX,
|
||
|
|
Latitude: firstStart.Latitude + firstFraction*firstY,
|
||
|
|
}, true
|
||
|
|
}
|
||
|
|
|
||
|
|
// visibleLineworkTJunctionToleranceDeg 是端点落在另一段内部的垂距容差(度)。
|
||
|
|
const visibleLineworkTJunctionToleranceDeg = 1e-5
|
||
|
|
|
||
|
|
// visibleLineworkEndpointOnSegment 返回端点落在段内部时的投影参数。
|
||
|
|
func visibleLineworkEndpointOnSegment(point, start, end GeoPoint) (float64, bool) {
|
||
|
|
deltaX := end.Longitude - start.Longitude
|
||
|
|
deltaY := end.Latitude - start.Latitude
|
||
|
|
lengthSquared := deltaX*deltaX + deltaY*deltaY
|
||
|
|
if !(lengthSquared > 0) {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
offsetX := point.Longitude - start.Longitude
|
||
|
|
offsetY := point.Latitude - start.Latitude
|
||
|
|
fraction := (offsetX*deltaX + offsetY*deltaY) / lengthSquared
|
||
|
|
if !(fraction > 0 && fraction < 1) {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
distance := math.Hypot(offsetX-fraction*deltaX, offsetY-fraction*deltaY)
|
||
|
|
if !(distance <= visibleLineworkTJunctionToleranceDeg) {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
return fraction, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func selectVisibleLineworkCycles(
|
||
|
|
cycles, coveragePaths [][]GeoPoint,
|
||
|
|
boundaryTolerance float64,
|
||
|
|
) [][]GeoPoint {
|
||
|
|
return selectVisibleLineworkCyclesWithProbeLimit(cycles, coveragePaths, boundaryTolerance, 768)
|
||
|
|
}
|
||
|
|
|
||
|
|
func selectVisibleLineworkCyclesWithProbeLimit(
|
||
|
|
cycles, coveragePaths [][]GeoPoint,
|
||
|
|
boundaryTolerance float64,
|
||
|
|
maximumProbes int,
|
||
|
|
) [][]GeoPoint {
|
||
|
|
type candidate struct {
|
||
|
|
polygon []GeoPoint
|
||
|
|
area float64
|
||
|
|
bounds visibleLineworkBounds
|
||
|
|
index visibleLineworkPolygonIndex
|
||
|
|
indexed bool
|
||
|
|
covers []bool
|
||
|
|
count int
|
||
|
|
}
|
||
|
|
probes := visibleLineworkCoverageProbes(coveragePaths, 8)
|
||
|
|
if len(probes) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
probes = limitVisibleLineworkCoverageProbes(probes, maximumProbes)
|
||
|
|
candidates := make([]candidate, 0, len(cycles))
|
||
|
|
for _, source := range cycles {
|
||
|
|
polygon := append([]GeoPoint(nil), source...)
|
||
|
|
area := visibleLineworkSignedArea(polygon)
|
||
|
|
if math.Abs(area) <= 1e-10 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if area < 0 {
|
||
|
|
reverseSweepGeoPoints(polygon)
|
||
|
|
area = -area
|
||
|
|
}
|
||
|
|
candidates = append(candidates, candidate{
|
||
|
|
polygon: polygon,
|
||
|
|
area: area,
|
||
|
|
bounds: visibleLineworkPolygonBounds(polygon, boundaryTolerance),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
sort.SliceStable(candidates, func(first, second int) bool {
|
||
|
|
return candidates[first].area < candidates[second].area
|
||
|
|
})
|
||
|
|
|
||
|
|
// Most physical line networks have one face containing every footprint
|
||
|
|
// probe. A small subset cheaply rejects impossible faces; candidates that
|
||
|
|
// survive still have to cover the original complete probe set.
|
||
|
|
sentinels := sampleVisibleLineworkProbes(probes, 16)
|
||
|
|
best := -1
|
||
|
|
for candidateIndex := range candidates {
|
||
|
|
value := &candidates[candidateIndex]
|
||
|
|
if best >= 0 && value.area >= candidates[best].area {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !visibleLineworkBoundsContainAll(value.bounds, value.polygon, sentinels) ||
|
||
|
|
!visibleLineworkBoundsContainAll(value.bounds, value.polygon, probes) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !value.indexed {
|
||
|
|
value.index = newVisibleLineworkPolygonIndex(value.polygon)
|
||
|
|
value.indexed = true
|
||
|
|
}
|
||
|
|
if !visibleLineworkContainsAllIndexed(
|
||
|
|
&value.index, value.bounds, sentinels, boundaryTolerance,
|
||
|
|
) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if visibleLineworkContainsAllIndexed(
|
||
|
|
&value.index, value.bounds, probes, boundaryTolerance,
|
||
|
|
) {
|
||
|
|
best = candidateIndex
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if best >= 0 {
|
||
|
|
return [][]GeoPoint{candidates[best].polygon}
|
||
|
|
}
|
||
|
|
|
||
|
|
for candidateIndex := range candidates {
|
||
|
|
value := &candidates[candidateIndex]
|
||
|
|
value.covers = make([]bool, len(probes))
|
||
|
|
for probeIndex, probe := range probes {
|
||
|
|
if !value.bounds.contains(value.polygon, probe) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !value.indexed {
|
||
|
|
value.index = newVisibleLineworkPolygonIndex(value.polygon)
|
||
|
|
value.indexed = true
|
||
|
|
}
|
||
|
|
if visibleLineworkContainsWithinIndexed(&value.index, probe, boundaryTolerance) {
|
||
|
|
value.covers[probeIndex] = true
|
||
|
|
value.count++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
covered := make([]bool, len(probes))
|
||
|
|
remaining := len(probes)
|
||
|
|
selected := make([][]GeoPoint, 0, len(candidates))
|
||
|
|
used := make([]bool, len(candidates))
|
||
|
|
for remaining > 0 {
|
||
|
|
best, bestGain := -1, 0
|
||
|
|
for index, value := range candidates {
|
||
|
|
if used[index] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
gain := 0
|
||
|
|
for probeIndex, contains := range value.covers {
|
||
|
|
if contains && !covered[probeIndex] {
|
||
|
|
gain++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if gain > bestGain || gain == bestGain && gain > 0 &&
|
||
|
|
(best < 0 || value.area < candidates[best].area) {
|
||
|
|
best, bestGain = index, gain
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if best < 0 || bestGain == 0 {
|
||
|
|
// A handful of probes can land exactly on a numerically ambiguous
|
||
|
|
// junction after projection. Keep the already selected faces when
|
||
|
|
// they cover the overwhelming majority of source probes; the miss
|
||
|
|
// audit below still rejects a materially incomplete outline.
|
||
|
|
allowedUncovered := len(probes) / 100
|
||
|
|
if allowedUncovered < 1 {
|
||
|
|
allowedUncovered = 1
|
||
|
|
}
|
||
|
|
if allowedUncovered > 8 {
|
||
|
|
allowedUncovered = 8
|
||
|
|
}
|
||
|
|
if remaining <= allowedUncovered && len(selected) > 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
used[best] = true
|
||
|
|
selected = append(selected, candidates[best].polygon)
|
||
|
|
for probeIndex, contains := range candidates[best].covers {
|
||
|
|
if contains && !covered[probeIndex] {
|
||
|
|
covered[probeIndex] = true
|
||
|
|
remaining--
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return selected
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkBoundsContainAll(
|
||
|
|
bounds visibleLineworkBounds,
|
||
|
|
polygon, probes []GeoPoint,
|
||
|
|
) bool {
|
||
|
|
for _, probe := range probes {
|
||
|
|
if !bounds.contains(polygon, probe) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func sampleVisibleLineworkProbes(probes []GeoPoint, maximumPoints int) []GeoPoint {
|
||
|
|
if maximumPoints < 2 || len(probes) <= maximumPoints {
|
||
|
|
return probes
|
||
|
|
}
|
||
|
|
result := make([]GeoPoint, maximumPoints)
|
||
|
|
for index := range result {
|
||
|
|
probeIndex := index * (len(probes) - 1) / (maximumPoints - 1)
|
||
|
|
result[index] = probes[probeIndex]
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func limitVisibleLineworkCoverageProbes(probes []GeoPoint, maximum int) []GeoPoint {
|
||
|
|
if maximum < 1 || len(probes) <= maximum {
|
||
|
|
return probes
|
||
|
|
}
|
||
|
|
selected := make([]bool, len(probes))
|
||
|
|
reserve := maximum / 4
|
||
|
|
if reserve < 1 {
|
||
|
|
reserve = 1
|
||
|
|
}
|
||
|
|
indices := make([]int, len(probes))
|
||
|
|
for index := range indices {
|
||
|
|
indices[index] = index
|
||
|
|
}
|
||
|
|
sort.SliceStable(indices, func(first, second int) bool {
|
||
|
|
return probes[indices[first]].Latitude > probes[indices[second]].Latitude
|
||
|
|
})
|
||
|
|
for index := 0; index < reserve && index < len(indices); index++ {
|
||
|
|
selected[indices[index]] = true
|
||
|
|
selected[indices[len(indices)-1-index]] = true
|
||
|
|
}
|
||
|
|
remaining := maximum
|
||
|
|
for _, value := range selected {
|
||
|
|
if value {
|
||
|
|
remaining--
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if remaining < 1 {
|
||
|
|
remaining = 1
|
||
|
|
}
|
||
|
|
stride := float64(len(probes)) / float64(remaining)
|
||
|
|
result := make([]GeoPoint, 0, maximum)
|
||
|
|
for index, value := range selected {
|
||
|
|
if value {
|
||
|
|
result = append(result, probes[index])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for cursor := 0.0; len(result) < maximum && int(cursor) < len(probes); cursor += stride {
|
||
|
|
index := int(cursor)
|
||
|
|
if selected[index] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
selected[index] = true
|
||
|
|
result = append(result, probes[index])
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkContainsAll(
|
||
|
|
polygon []GeoPoint,
|
||
|
|
bounds visibleLineworkBounds,
|
||
|
|
probes []GeoPoint,
|
||
|
|
tolerance float64,
|
||
|
|
) bool {
|
||
|
|
for _, probe := range probes {
|
||
|
|
if !bounds.contains(polygon, probe) ||
|
||
|
|
!visibleLineworkContainsWithin(polygon, probe, tolerance) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
const visibleLineworkLatitudeBins = 64
|
||
|
|
|
||
|
|
type visibleLineworkPolygonIndex struct {
|
||
|
|
polygon []GeoPoint
|
||
|
|
minLatitude float64
|
||
|
|
maxLatitude float64
|
||
|
|
bins [][]int
|
||
|
|
marks []uint32
|
||
|
|
generation uint32
|
||
|
|
}
|
||
|
|
|
||
|
|
func newVisibleLineworkPolygonIndex(polygon []GeoPoint) visibleLineworkPolygonIndex {
|
||
|
|
index := visibleLineworkPolygonIndex{polygon: polygon}
|
||
|
|
if len(polygon) == 0 {
|
||
|
|
return index
|
||
|
|
}
|
||
|
|
index.minLatitude, index.maxLatitude = polygon[0].Latitude, polygon[0].Latitude
|
||
|
|
for _, point := range polygon[1:] {
|
||
|
|
index.minLatitude = math.Min(index.minLatitude, point.Latitude)
|
||
|
|
index.maxLatitude = math.Max(index.maxLatitude, point.Latitude)
|
||
|
|
}
|
||
|
|
index.bins = make([][]int, visibleLineworkLatitudeBins)
|
||
|
|
index.marks = make([]uint32, len(polygon))
|
||
|
|
for edge := range polygon {
|
||
|
|
previous := (edge + len(polygon) - 1) % len(polygon)
|
||
|
|
first, second := polygon[previous], polygon[edge]
|
||
|
|
firstBin := index.latitudeBin(math.Min(first.Latitude, second.Latitude))
|
||
|
|
lastBin := index.latitudeBin(math.Max(first.Latitude, second.Latitude))
|
||
|
|
for bin := firstBin; bin <= lastBin; bin++ {
|
||
|
|
index.bins[bin] = append(index.bins[bin], edge)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return index
|
||
|
|
}
|
||
|
|
|
||
|
|
func (index visibleLineworkPolygonIndex) latitudeBin(latitude float64) int {
|
||
|
|
if index.maxLatitude <= index.minLatitude {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
fraction := (latitude - index.minLatitude) / (index.maxLatitude - index.minLatitude)
|
||
|
|
if fraction <= 0 {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
if fraction >= 1 {
|
||
|
|
return visibleLineworkLatitudeBins - 1
|
||
|
|
}
|
||
|
|
return int(fraction * visibleLineworkLatitudeBins)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (index *visibleLineworkPolygonIndex) nextGeneration() uint32 {
|
||
|
|
index.generation++
|
||
|
|
if index.generation == 0 {
|
||
|
|
for mark := range index.marks {
|
||
|
|
index.marks[mark] = 0
|
||
|
|
}
|
||
|
|
index.generation = 1
|
||
|
|
}
|
||
|
|
return index.generation
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkContainsAllIndexed(
|
||
|
|
index *visibleLineworkPolygonIndex,
|
||
|
|
bounds visibleLineworkBounds,
|
||
|
|
probes []GeoPoint,
|
||
|
|
tolerance float64,
|
||
|
|
) bool {
|
||
|
|
for _, probe := range probes {
|
||
|
|
if !bounds.contains(index.polygon, probe) ||
|
||
|
|
!visibleLineworkContainsWithinIndexed(index, probe, tolerance) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkContainsWithinIndexed(
|
||
|
|
index *visibleLineworkPolygonIndex,
|
||
|
|
point GeoPoint,
|
||
|
|
tolerance float64,
|
||
|
|
) bool {
|
||
|
|
if index == nil {
|
||
|
|
return visibleLineworkContainsWithin(nil, point, tolerance)
|
||
|
|
}
|
||
|
|
polygon := index.polygon
|
||
|
|
if len(polygon) < 3 || tolerance <= 0 {
|
||
|
|
return visibleLineworkContainsWithin(polygon, point, tolerance)
|
||
|
|
}
|
||
|
|
point = visibleLineworkPointNearPolygon(polygon, point)
|
||
|
|
if point.Latitude < index.minLatitude-tolerance || point.Latitude > index.maxLatitude+tolerance {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
firstBin := index.latitudeBin(point.Latitude-tolerance) - 2
|
||
|
|
lastBin := index.latitudeBin(point.Latitude+tolerance) + 2
|
||
|
|
if firstBin < 0 {
|
||
|
|
firstBin = 0
|
||
|
|
}
|
||
|
|
if lastBin >= visibleLineworkLatitudeBins {
|
||
|
|
lastBin = visibleLineworkLatitudeBins - 1
|
||
|
|
}
|
||
|
|
generation := index.nextGeneration()
|
||
|
|
inside := false
|
||
|
|
for bin := firstBin; bin <= lastBin; bin++ {
|
||
|
|
for _, edge := range index.bins[bin] {
|
||
|
|
if index.marks[edge] == generation {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
index.marks[edge] = generation
|
||
|
|
current, previous := edge, (edge+len(polygon)-1)%len(polygon)
|
||
|
|
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 >= point.Longitude {
|
||
|
|
inside = !inside
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if inside {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
generation = index.nextGeneration()
|
||
|
|
toleranceSquared := tolerance * tolerance
|
||
|
|
for bin := firstBin; bin <= lastBin; bin++ {
|
||
|
|
for _, edge := range index.bins[bin] {
|
||
|
|
if index.marks[edge] == generation {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
index.marks[edge] = generation
|
||
|
|
current, previous := edge, (edge+len(polygon)-1)%len(polygon)
|
||
|
|
start, end := polygon[previous], polygon[current]
|
||
|
|
if point.Longitude < math.Min(start.Longitude, end.Longitude)-tolerance ||
|
||
|
|
point.Longitude > math.Max(start.Longitude, end.Longitude)+tolerance ||
|
||
|
|
point.Latitude < math.Min(start.Latitude, end.Latitude)-tolerance ||
|
||
|
|
point.Latitude > math.Max(start.Latitude, end.Latitude)+tolerance {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if visibleLineworkPointSegmentDistanceSquared(point, start, end) <= toleranceSquared {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkCyclesCoverCoverage(
|
||
|
|
cycles, coveragePaths [][]GeoPoint,
|
||
|
|
tolerance float64,
|
||
|
|
) bool {
|
||
|
|
return visibleLineworkCoverageMissCount(cycles, coveragePaths, tolerance) == 0
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkCoverageMissCount(
|
||
|
|
cycles, coveragePaths [][]GeoPoint,
|
||
|
|
tolerance float64,
|
||
|
|
) int {
|
||
|
|
if len(cycles) == 0 {
|
||
|
|
return -1
|
||
|
|
}
|
||
|
|
probes := visibleLineworkCoverageProbes(coveragePaths, 8)
|
||
|
|
probes = limitVisibleLineworkCoverageProbes(probes, 768)
|
||
|
|
if len(probes) == 0 {
|
||
|
|
return -1
|
||
|
|
}
|
||
|
|
bounds := make([]visibleLineworkBounds, len(cycles))
|
||
|
|
for index, cycle := range cycles {
|
||
|
|
bounds[index] = visibleLineworkPolygonBounds(cycle, tolerance)
|
||
|
|
}
|
||
|
|
misses := 0
|
||
|
|
for _, probe := range probes {
|
||
|
|
covered := false
|
||
|
|
for index, cycle := range cycles {
|
||
|
|
if bounds[index].contains(cycle, probe) &&
|
||
|
|
visibleLineworkContainsWithin(cycle, probe, tolerance) {
|
||
|
|
covered = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !covered {
|
||
|
|
misses++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return misses
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkPolygonBounds(polygon []GeoPoint, tolerance float64) visibleLineworkBounds {
|
||
|
|
if len(polygon) == 0 {
|
||
|
|
return visibleLineworkBounds{}
|
||
|
|
}
|
||
|
|
bounds := visibleLineworkBounds{
|
||
|
|
minLongitude: polygon[0].Longitude,
|
||
|
|
maxLongitude: polygon[0].Longitude,
|
||
|
|
minLatitude: polygon[0].Latitude,
|
||
|
|
maxLatitude: polygon[0].Latitude,
|
||
|
|
}
|
||
|
|
for _, point := range polygon[1:] {
|
||
|
|
bounds.minLongitude = math.Min(bounds.minLongitude, point.Longitude)
|
||
|
|
bounds.maxLongitude = math.Max(bounds.maxLongitude, point.Longitude)
|
||
|
|
bounds.minLatitude = math.Min(bounds.minLatitude, point.Latitude)
|
||
|
|
bounds.maxLatitude = math.Max(bounds.maxLatitude, point.Latitude)
|
||
|
|
}
|
||
|
|
return visibleLineworkBounds{
|
||
|
|
minLongitude: bounds.minLongitude - tolerance,
|
||
|
|
maxLongitude: bounds.maxLongitude + tolerance,
|
||
|
|
minLatitude: bounds.minLatitude - tolerance,
|
||
|
|
maxLatitude: bounds.maxLatitude + tolerance,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (bounds visibleLineworkBounds) contains(polygon []GeoPoint, point GeoPoint) bool {
|
||
|
|
point = visibleLineworkPointNearPolygon(polygon, point)
|
||
|
|
return point.Longitude >= bounds.minLongitude && point.Longitude <= bounds.maxLongitude &&
|
||
|
|
point.Latitude >= bounds.minLatitude && point.Latitude <= bounds.maxLatitude
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkCoverageProbes(paths [][]GeoPoint, maximumPoints int) []GeoPoint {
|
||
|
|
if maximumPoints < 2 {
|
||
|
|
maximumPoints = 2
|
||
|
|
}
|
||
|
|
probes := make([]GeoPoint, 0, len(paths)*maximumPoints)
|
||
|
|
for _, path := range paths {
|
||
|
|
step := (len(path) + maximumPoints - 1) / maximumPoints
|
||
|
|
if step < 1 {
|
||
|
|
step = 1
|
||
|
|
}
|
||
|
|
for index := 0; index < len(path); index += step {
|
||
|
|
probes = append(probes, path[index])
|
||
|
|
}
|
||
|
|
if len(path) > 0 && (len(path)-1)%step != 0 {
|
||
|
|
probes = append(probes, path[len(path)-1])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return probes
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkContainsWithin(polygon []GeoPoint, point GeoPoint, tolerance float64) bool {
|
||
|
|
if len(polygon) < 3 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
point = visibleLineworkPointNearPolygon(polygon, point)
|
||
|
|
if tolerance <= 0 {
|
||
|
|
return sweepPointInPolygon(polygon, point)
|
||
|
|
}
|
||
|
|
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 >= point.Longitude {
|
||
|
|
inside = !inside
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if inside {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
toleranceSquared := tolerance * tolerance
|
||
|
|
for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 {
|
||
|
|
start, end := polygon[previous], polygon[current]
|
||
|
|
if point.Longitude < math.Min(start.Longitude, end.Longitude)-tolerance ||
|
||
|
|
point.Longitude > math.Max(start.Longitude, end.Longitude)+tolerance ||
|
||
|
|
point.Latitude < math.Min(start.Latitude, end.Latitude)-tolerance ||
|
||
|
|
point.Latitude > math.Max(start.Latitude, end.Latitude)+tolerance {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if visibleLineworkPointSegmentDistanceSquared(point, start, end) <= toleranceSquared {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkPointNearPolygon(polygon []GeoPoint, point GeoPoint) GeoPoint {
|
||
|
|
if len(polygon) == 0 {
|
||
|
|
return point
|
||
|
|
}
|
||
|
|
longitude := point.Longitude
|
||
|
|
for longitude-polygon[0].Longitude > 180 {
|
||
|
|
longitude -= 360
|
||
|
|
}
|
||
|
|
for longitude-polygon[0].Longitude < -180 {
|
||
|
|
longitude += 360
|
||
|
|
}
|
||
|
|
point.Longitude = longitude
|
||
|
|
return point
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkPointSegmentDistanceSquared(point, start, end GeoPoint) float64 {
|
||
|
|
dx := end.Longitude - start.Longitude
|
||
|
|
dy := end.Latitude - start.Latitude
|
||
|
|
denominator := dx*dx + dy*dy
|
||
|
|
if denominator <= 1e-20 {
|
||
|
|
dx = point.Longitude - start.Longitude
|
||
|
|
dy = point.Latitude - start.Latitude
|
||
|
|
return dx*dx + dy*dy
|
||
|
|
}
|
||
|
|
fraction := ((point.Longitude-start.Longitude)*dx + (point.Latitude-start.Latitude)*dy) / denominator
|
||
|
|
fraction = math.Max(0, math.Min(1, fraction))
|
||
|
|
dx = point.Longitude - (start.Longitude + fraction*dx)
|
||
|
|
dy = point.Latitude - (start.Latitude + fraction*dy)
|
||
|
|
return dx*dx + dy*dy
|
||
|
|
}
|
||
|
|
|
||
|
|
func sampleVisibleLineworkFillPaths(paths [][]GeoPoint, maximumPoints int) [][]GeoPoint {
|
||
|
|
if maximumPoints < 3 {
|
||
|
|
maximumPoints = 3
|
||
|
|
}
|
||
|
|
result := make([][]GeoPoint, 0, len(paths))
|
||
|
|
for _, path := range paths {
|
||
|
|
path = openGeoRing(path)
|
||
|
|
if len(path) <= maximumPoints {
|
||
|
|
result = append(result, path)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
indices := make([]int, 0, maximumPoints)
|
||
|
|
seen := make(map[int]bool, maximumPoints)
|
||
|
|
for direction := 0; direction < maximumPoints; direction++ {
|
||
|
|
angle := 2 * math.Pi * float64(direction) / float64(maximumPoints)
|
||
|
|
x, y := math.Cos(angle), math.Sin(angle)
|
||
|
|
bestIndex := 0
|
||
|
|
bestValue := path[0].Longitude*x + path[0].Latitude*y
|
||
|
|
for index := 1; index < len(path); index++ {
|
||
|
|
value := path[index].Longitude*x + path[index].Latitude*y
|
||
|
|
if value > bestValue {
|
||
|
|
bestIndex, bestValue = index, value
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !seen[bestIndex] {
|
||
|
|
seen[bestIndex] = true
|
||
|
|
indices = append(indices, bestIndex)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sort.Ints(indices)
|
||
|
|
sampled := make([]GeoPoint, 0, len(indices))
|
||
|
|
for _, index := range indices {
|
||
|
|
sampled = append(sampled, path[index])
|
||
|
|
}
|
||
|
|
result = append(result, sampled)
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func unprojectVisibleLineworkPaths(chart polygonUnionChart, paths [][]GeoPoint) [][]GeoPoint {
|
||
|
|
result := make([][]GeoPoint, len(paths))
|
||
|
|
for pathIndex, path := range paths {
|
||
|
|
result[pathIndex] = make([]GeoPoint, len(path))
|
||
|
|
for pointIndex, point := range path {
|
||
|
|
result[pathIndex][pointIndex] = chart.unproject(point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkPointPaths(paths [][]GeoPoint) [][]GeoPoint {
|
||
|
|
result := make([][]GeoPoint, 0)
|
||
|
|
for _, path := range paths {
|
||
|
|
for _, point := range path {
|
||
|
|
result = append(result, []GeoPoint{point})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
type visibleLineworkCycleStep struct {
|
||
|
|
edge int
|
||
|
|
reverse bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func enumerateVisibleLineworkCycles(
|
||
|
|
nodes []visibleLineworkNode,
|
||
|
|
edges []visibleLineworkEdge,
|
||
|
|
) [][]GeoPoint {
|
||
|
|
// Walk the face on each side of every edge once. The exterior face of
|
||
|
|
// each component is retained as a candidate for its complete outer band.
|
||
|
|
// Enumerating arbitrary combinations of interior faces grows exponentially.
|
||
|
|
order := make([][]int, len(nodes))
|
||
|
|
position := make([]int, 2*len(edges))
|
||
|
|
halfAngle := make([]float64, 2*len(edges))
|
||
|
|
for index, edge := range edges {
|
||
|
|
// 正向半边的方向取"首点到相邻的第二点",与反向半边的"末点到倒数第二点"对称;
|
||
|
|
// 取首末两点会在折线上给出不同的排序角度,进而改变环枚举结果。
|
||
|
|
first, next := edge.points[0], edge.points[1]
|
||
|
|
previous, last := edge.points[len(edge.points)-2], edge.points[len(edge.points)-1]
|
||
|
|
halfAngle[2*index] = math.Atan2(next.Latitude-first.Latitude, next.Longitude-first.Longitude)
|
||
|
|
halfAngle[2*index+1] = math.Atan2(previous.Latitude-last.Latitude, previous.Longitude-last.Longitude)
|
||
|
|
}
|
||
|
|
for node, value := range nodes {
|
||
|
|
for _, half := range value.outgoing {
|
||
|
|
id := 2 * half.edge
|
||
|
|
if half.reverse {
|
||
|
|
id++
|
||
|
|
}
|
||
|
|
order[node] = append(order[node], id)
|
||
|
|
}
|
||
|
|
sort.SliceStable(order[node], func(i, j int) bool {
|
||
|
|
return halfAngle[order[node][i]] < halfAngle[order[node][j]]
|
||
|
|
})
|
||
|
|
for i, half := range order[node] {
|
||
|
|
position[half] = i
|
||
|
|
}
|
||
|
|
}
|
||
|
|
visited := make([]bool, 2*len(edges))
|
||
|
|
cycles := make([][]GeoPoint, 0, len(edges))
|
||
|
|
seen := make(map[uint64][][]int32)
|
||
|
|
for start := range visited {
|
||
|
|
if visited[start] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
var steps []visibleLineworkCycleStep
|
||
|
|
for half := start; !visited[half]; {
|
||
|
|
visited[half] = true
|
||
|
|
step := visibleLineworkCycleStep{edge: half / 2, reverse: half%2 != 0}
|
||
|
|
if len(steps) > 0 && steps[len(steps)-1].edge == step.edge && steps[len(steps)-1].reverse != step.reverse {
|
||
|
|
steps = steps[:len(steps)-1]
|
||
|
|
} else {
|
||
|
|
steps = append(steps, step)
|
||
|
|
}
|
||
|
|
next := edges[step.edge].end
|
||
|
|
if step.reverse {
|
||
|
|
next = edges[step.edge].start
|
||
|
|
}
|
||
|
|
out := order[next]
|
||
|
|
half = out[(position[half^1]+len(out)-1)%len(out)]
|
||
|
|
}
|
||
|
|
for len(steps) > 1 && steps[0].edge == steps[len(steps)-1].edge && steps[0].reverse != steps[len(steps)-1].reverse {
|
||
|
|
steps = steps[1 : len(steps)-1]
|
||
|
|
}
|
||
|
|
if len(steps) < 1 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
for _, simple := range visibleLineworkSimpleCycles(edges, steps) {
|
||
|
|
if visibleLineworkCycleSeen(seen, simple) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if ring := visibleLineworkCyclePoints(edges, simple); len(ring) >= 3 && math.Abs(visibleLineworkSignedArea(ring)) > 1e-12 {
|
||
|
|
cycles = append(cycles, ring)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return cycles
|
||
|
|
}
|
||
|
|
|
||
|
|
// The exterior face can revisit an articulation node. Separate its lobes so
|
||
|
|
// each candidate is a simple ring, including components joined by a bridge.
|
||
|
|
func visibleLineworkSimpleCycles(edges []visibleLineworkEdge, steps []visibleLineworkCycleStep) [][]visibleLineworkCycleStep {
|
||
|
|
var cycles [][]visibleLineworkCycleStep
|
||
|
|
var path []visibleLineworkCycleStep
|
||
|
|
positions := make(map[int]int)
|
||
|
|
for _, step := range steps {
|
||
|
|
start, end := edges[step.edge].start, edges[step.edge].end
|
||
|
|
if step.reverse {
|
||
|
|
start, end = end, start
|
||
|
|
}
|
||
|
|
positions[start] = len(path)
|
||
|
|
path = append(path, step)
|
||
|
|
if at, ok := positions[end]; ok {
|
||
|
|
cycles = append(cycles, append([]visibleLineworkCycleStep(nil), path[at:]...))
|
||
|
|
for _, removed := range path[at:] {
|
||
|
|
node := edges[removed.edge].start
|
||
|
|
if removed.reverse {
|
||
|
|
node = edges[removed.edge].end
|
||
|
|
}
|
||
|
|
delete(positions, node)
|
||
|
|
}
|
||
|
|
path = path[:at]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return cycles
|
||
|
|
}
|
||
|
|
|
||
|
|
// visibleLineworkCycleSeen 按与方向无关的边集合去重,返回该环是否已出现。
|
||
|
|
func visibleLineworkCycleSeen(seen map[uint64][][]int32, steps []visibleLineworkCycleStep) bool {
|
||
|
|
key := make([]int32, len(steps))
|
||
|
|
for index, step := range steps {
|
||
|
|
key[index] = int32(step.edge)
|
||
|
|
}
|
||
|
|
sort.Slice(key, func(first, second int) bool { return key[first] < key[second] })
|
||
|
|
hash := uint64(14695981039346656037)
|
||
|
|
for _, edge := range key {
|
||
|
|
hash = (hash ^ uint64(uint32(edge))) * 1099511628211
|
||
|
|
}
|
||
|
|
for _, existing := range seen[hash] {
|
||
|
|
if len(existing) != len(key) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
same := true
|
||
|
|
for index, edge := range key {
|
||
|
|
if existing[index] != edge {
|
||
|
|
same = false
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if same {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
seen[hash] = append(seen[hash], key)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkCyclePoints(
|
||
|
|
edges []visibleLineworkEdge,
|
||
|
|
steps []visibleLineworkCycleStep,
|
||
|
|
) []GeoPoint {
|
||
|
|
ring := make([]GeoPoint, 0, len(steps)*4)
|
||
|
|
for _, step := range steps {
|
||
|
|
points := visibleLineworkDirectedPoints(edges[step.edge], step.reverse)
|
||
|
|
if len(ring) == 0 {
|
||
|
|
ring = append(ring, points...)
|
||
|
|
} else {
|
||
|
|
ring = append(ring, points[1:]...)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return sweepDeduplicateAdjacent(openGeoRing(ring))
|
||
|
|
}
|
||
|
|
|
||
|
|
func projectVisibleLine(chart polygonUnionChart, source []GeoPoint) []GeoPoint {
|
||
|
|
if len(source) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
line := make([]GeoPoint, 0, len(source))
|
||
|
|
for sourceIndex, point := range source {
|
||
|
|
projected := chart.project(point)
|
||
|
|
if len(line) > 0 {
|
||
|
|
for projected.Longitude-line[len(line)-1].Longitude > 180 {
|
||
|
|
projected.Longitude -= 360
|
||
|
|
}
|
||
|
|
for projected.Longitude-line[len(line)-1].Longitude < -180 {
|
||
|
|
projected.Longitude += 360
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(line) > 0 {
|
||
|
|
if math.Hypot(projected.Longitude-line[len(line)-1].Longitude, projected.Latitude-line[len(line)-1].Latitude) > 1e-10 {
|
||
|
|
line = appendVisibleProjectedArc(line, chart, source[sourceIndex-1], point, line[len(line)-1], projected, 0)
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if len(line) == 0 || math.Hypot(
|
||
|
|
projected.Longitude-line[len(line)-1].Longitude,
|
||
|
|
projected.Latitude-line[len(line)-1].Latitude,
|
||
|
|
) > 1e-10 {
|
||
|
|
line = append(line, projected)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return line
|
||
|
|
}
|
||
|
|
|
||
|
|
// A great-circle arc becomes curved after rotation into the topology chart.
|
||
|
|
// Bound its projected chord error before intersections and face containment.
|
||
|
|
func appendVisibleProjectedArc(
|
||
|
|
points []GeoPoint,
|
||
|
|
chart polygonUnionChart,
|
||
|
|
first, last, start, end GeoPoint,
|
||
|
|
depth int,
|
||
|
|
) []GeoPoint {
|
||
|
|
if depth < 12 && (math.Abs(end.Longitude-start.Longitude) > 0.25 ||
|
||
|
|
math.Abs(end.Latitude-start.Latitude) > 0.25 ||
|
||
|
|
math.Max(math.Abs(start.Latitude), math.Abs(end.Latitude)) > 75) {
|
||
|
|
middle := sphericalInterpolate(first, last, 0.5)
|
||
|
|
projected := chart.project(middle)
|
||
|
|
projected.Longitude = start.Longitude + math.Remainder(projected.Longitude-start.Longitude, 360)
|
||
|
|
if visibleLineworkPointSegmentDistanceSquared(projected, start, end) > 0.002*0.002 {
|
||
|
|
points = appendVisibleProjectedArc(points, chart, first, middle, start, projected, depth+1)
|
||
|
|
return appendVisibleProjectedArc(points, chart, middle, last, projected, end, depth+1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return append(points, end)
|
||
|
|
}
|
||
|
|
|
||
|
|
func buildVisibleLineworkGraph(
|
||
|
|
lines [][]GeoPoint,
|
||
|
|
chart polygonUnionChart,
|
||
|
|
snapDistanceKM float64,
|
||
|
|
) ([]visibleLineworkNode, []visibleLineworkEdge, [][]GeoPoint) {
|
||
|
|
const commonJunctionKM = 1e-5
|
||
|
|
nodes, edges, loops := buildVisibleLineworkGraphAtTolerance(lines, chart, commonJunctionKM)
|
||
|
|
if snapDistanceKM <= 0 {
|
||
|
|
snapDistanceKM = 1
|
||
|
|
}
|
||
|
|
changed := false
|
||
|
|
for {
|
||
|
|
first, second := -1, -1
|
||
|
|
minimum := snapDistanceKM
|
||
|
|
// Connected intersections are distinct even when physically close.
|
||
|
|
// Only an open endpoint can require the wider sampling snap radius.
|
||
|
|
for index, node := range nodes {
|
||
|
|
if len(node.outgoing) != 1 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
point := chart.unproject(node.point)
|
||
|
|
for otherIndex, other := range nodes {
|
||
|
|
if index == otherIndex || len(other.outgoing) == 0 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if distance := geoPointDistanceKM(point, chart.unproject(other.point)); distance < minimum {
|
||
|
|
first, second, minimum = index, otherIndex, distance
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if first < 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
changed = true
|
||
|
|
for _, half := range nodes[first].outgoing {
|
||
|
|
edge := &edges[half.edge]
|
||
|
|
if edge.start == first {
|
||
|
|
edge.start, edge.points[0] = second, nodes[second].point
|
||
|
|
}
|
||
|
|
if edge.end == first {
|
||
|
|
edge.end, edge.points[len(edge.points)-1] = second, nodes[second].point
|
||
|
|
}
|
||
|
|
nodes[second].outgoing = append(nodes[second].outgoing, half)
|
||
|
|
}
|
||
|
|
nodes[first].outgoing = nil
|
||
|
|
}
|
||
|
|
if !changed {
|
||
|
|
return nodes, edges, loops
|
||
|
|
}
|
||
|
|
joined := make([][]GeoPoint, 0, len(edges)+len(loops))
|
||
|
|
for _, edge := range edges {
|
||
|
|
joined = append(joined, edge.points)
|
||
|
|
}
|
||
|
|
for _, loop := range loops {
|
||
|
|
joined = append(joined, append(append([]GeoPoint(nil), loop...), loop[0]))
|
||
|
|
}
|
||
|
|
// Moving an endpoint can create a crossing on its adjacent segment.
|
||
|
|
// Re-node those intersections before ordering directed edges into faces.
|
||
|
|
return buildVisibleLineworkGraphAtTolerance(splitVisibleLineworkIntersections(joined), chart, commonJunctionKM)
|
||
|
|
}
|
||
|
|
|
||
|
|
func buildVisibleLineworkGraphAtTolerance(
|
||
|
|
lines [][]GeoPoint,
|
||
|
|
chart polygonUnionChart,
|
||
|
|
snapDistanceKM float64,
|
||
|
|
) ([]visibleLineworkNode, []visibleLineworkEdge, [][]GeoPoint) {
|
||
|
|
if snapDistanceKM <= 0 {
|
||
|
|
snapDistanceKM = 1
|
||
|
|
}
|
||
|
|
nodes := make([]visibleLineworkNode, 0, len(lines))
|
||
|
|
edges := make([]visibleLineworkEdge, 0, len(lines))
|
||
|
|
loops := make([][]GeoPoint, 0, 2)
|
||
|
|
for _, source := range lines {
|
||
|
|
line := append([]GeoPoint(nil), source...)
|
||
|
|
start := visibleLineworkNodeIndex(nodes, chart, line[0], snapDistanceKM)
|
||
|
|
if start < 0 {
|
||
|
|
start = len(nodes)
|
||
|
|
nodes = append(nodes, visibleLineworkNode{point: line[0]})
|
||
|
|
}
|
||
|
|
end := visibleLineworkNodeIndex(nodes, chart, line[len(line)-1], snapDistanceKM)
|
||
|
|
if end < 0 {
|
||
|
|
end = len(nodes)
|
||
|
|
nodes = append(nodes, visibleLineworkNode{point: line[len(line)-1]})
|
||
|
|
}
|
||
|
|
line[0] = nodes[start].point
|
||
|
|
line[len(line)-1] = nodes[end].point
|
||
|
|
line = sweepDeduplicateAdjacent(line)
|
||
|
|
if len(line) < 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if start == end && len(openGeoRing(line)) < 3 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
duplicate := false
|
||
|
|
for _, edge := range edges {
|
||
|
|
if len(edge.points) != len(line) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
reverse := edge.start == end && edge.end == start
|
||
|
|
if !reverse && (edge.start != start || edge.end != end) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
same := true
|
||
|
|
for i, point := range line {
|
||
|
|
j := i
|
||
|
|
if reverse {
|
||
|
|
j = len(line) - 1 - i
|
||
|
|
}
|
||
|
|
if !SameGeoPoint(point, edge.points[j]) {
|
||
|
|
same = false
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if same {
|
||
|
|
duplicate = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if duplicate {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
edgeIndex := len(edges)
|
||
|
|
edges = append(edges, visibleLineworkEdge{points: line, start: start, end: end})
|
||
|
|
nodes[start].outgoing = append(nodes[start].outgoing, visibleLineworkHalfEdge{
|
||
|
|
edge: edgeIndex,
|
||
|
|
})
|
||
|
|
nodes[end].outgoing = append(nodes[end].outgoing, visibleLineworkHalfEdge{
|
||
|
|
edge: edgeIndex, reverse: true,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
return nodes, edges, loops
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkNodeIndex(
|
||
|
|
nodes []visibleLineworkNode,
|
||
|
|
chart polygonUnionChart,
|
||
|
|
projected GeoPoint,
|
||
|
|
snapDistanceKM float64,
|
||
|
|
) int {
|
||
|
|
target := chart.unproject(projected)
|
||
|
|
bestIndex := -1
|
||
|
|
bestDistance := math.Inf(1)
|
||
|
|
for index, node := range nodes {
|
||
|
|
distance := geoPointDistanceKM(chart.unproject(node.point), target)
|
||
|
|
if distance <= snapDistanceKM && distance < bestDistance {
|
||
|
|
bestIndex, bestDistance = index, distance
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return bestIndex
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkDirectedPoints(edge visibleLineworkEdge, reverse bool) []GeoPoint {
|
||
|
|
if !reverse {
|
||
|
|
return edge.points
|
||
|
|
}
|
||
|
|
points := make([]GeoPoint, len(edge.points))
|
||
|
|
for index := range edge.points {
|
||
|
|
points[index] = edge.points[len(edge.points)-1-index]
|
||
|
|
}
|
||
|
|
return points
|
||
|
|
}
|
||
|
|
|
||
|
|
func visibleLineworkSignedArea(polygon []GeoPoint) float64 {
|
||
|
|
if len(polygon) < 3 {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
area := 0.0
|
||
|
|
for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 {
|
||
|
|
area += polygon[previous].Longitude*polygon[current].Latitude -
|
||
|
|
polygon[current].Longitude*polygon[previous].Latitude
|
||
|
|
}
|
||
|
|
return area / 2
|
||
|
|
}
|