Files
rond-with-you/internal/fog/mask.go
T
2026-09-18 13:05:49 +08:00

189 lines
5.4 KiB
Go

package fog
import (
"fmt"
"hash/fnv"
"math"
"strconv"
"rond-with-you/internal/geo"
)
// Mask 是一块「对访客隐藏」的圆形区域:中心(**GCJ-02**,与 places 同系)+ 半径(米)。
//
// 迷雾网格是 WGS-84、底图可能是 GCJ-02,所以判定前必须把中心换算到网格坐标,
// 与渲染瓦片时对 GCJ 底图做的纠偏是同一套变换——不然遮罩会整体偏出几百米。
type Mask struct {
ID int64
Name string
Lat float64
Lon float64
RadiusM float64
}
// maskGrid 是遮罩在基网格里的预计算形式。
//
// 网格的纵向边长恒为 CellSideM、横向边长随纬度乘 cos(φ) 收缩,
// 所以「实地上的圆」在网格里是**椭圆**(横向更宽)。半径换算错这一步,
// 高纬度地区会明显偏窄。
type maskGrid struct {
m Mask
gx, gy float64 // 圆心(格坐标,浮点)
rx, ry float64 // 横向 / 纵向半径(格)
x0, x1 int // 包围盒(格),用于快速排除
y0, y1 int
}
func newMaskGrid(m Mask) maskGrid {
wlat, wlon := geo.GCJ02ToWGS84(m.Lat, m.Lon)
gx, gy := mercX(wlon), mercY(wlat)
cos := math.Cos(wlat * math.Pi / 180)
if cos < 1e-6 {
cos = 1e-6
}
ry := m.RadiusM / CellSideM
rx := ry / cos
g := maskGrid{m: m, gx: gx, gy: gy, rx: rx, ry: ry}
g.x0 = int(math.Floor(gx-rx)) - 1
g.x1 = int(math.Ceil(gx+rx)) + 1
g.y0 = int(math.Floor(gy-ry)) - 1
g.y1 = int(math.Ceil(gy+ry)) + 1
return g
}
func (g maskGrid) hitCell(cx, cy int) bool {
if cx < g.x0 || cx > g.x1 || cy < g.y0 || cy > g.y1 {
return false
}
dx := (float64(cx) + 0.5 - g.gx) / g.rx
dy := (float64(cy) + 0.5 - g.gy) / g.ry
return dx*dx+dy*dy <= 1
}
// MaskSet 是一组遮罩的判定集合。零值/nil 表示不遮任何东西。
type MaskSet struct {
grids []maskGrid
}
// NewMaskSet 预计算一组遮罩;传空切片会返回 nil。
func NewMaskSet(masks []Mask) *MaskSet {
if len(masks) == 0 {
return nil
}
s := &MaskSet{grids: make([]maskGrid, 0, len(masks))}
for _, m := range masks {
if m.RadiusM <= 0 {
continue
}
s.grids = append(s.grids, newMaskGrid(m))
}
if len(s.grids) == 0 {
return nil
}
return s
}
// Empty 报告集合里有没有遮罩(nil 安全)。
func (s *MaskSet) Empty() bool { return s == nil || len(s.grids) == 0 }
// Hit 报告某个全球格是否被遮罩盖住。nil 安全,未命中时只花一次包围盒比较。
func (s *MaskSet) Hit(cx, cy int) bool {
if s == nil {
return false
}
for _, g := range s.grids {
if g.hitCell(cx, cy) {
return true
}
}
return false
}
// HitLatLon 按经纬度判定(同样传 GCJ-02),用于地点级的过滤。
func (s *MaskSet) HitLatLon(lat, lon float64) bool {
if s == nil {
return false
}
wlat, wlon := geo.GCJ02ToWGS84(lat, lon)
return s.Hit(CellOf(wlat, wlon))
}
// maskedStats 统计被遮罩盖住的置位格数与面积(m²),用于从「已探索」的总量里扣掉。
//
// 三个要点:
// - 逐格判一次 masks.Hit,而不是按遮罩逐个累加:遮罩可以互相重叠,按遮罩累加会把
// 重叠区域的格子算好几遍(格数扣多,和面积也会对不上);
// - 只遍历遮罩包围盒覆盖到的块(块坐标去重),不扫全库;
// - 面积用 cellAreaM2,与 AreaKm2 同一套算法。
func maskedStats(blocks map[[2]int][]byte, masks *MaskSet) (cells int, areaM2 float64) {
if masks.Empty() {
return 0, 0
}
seen := make(map[[2]int]bool)
for _, g := range masks.grids {
for bx := g.x0 / BlockCells; bx <= g.x1/BlockCells; bx++ {
for by := g.y0 / BlockCells; by <= g.y1/BlockCells; by++ {
key := [2]int{bx, by}
if seen[key] {
continue // 两块遮罩可能盖到同一块,不能算两遍
}
seen[key] = true
bm, ok := blocks[key]
if !ok || len(bm) != bitmapSize {
continue
}
n := 0
for row := 0; row < BlockCells; row++ {
line := bm[row*8 : row*8+8]
for colB, v := range line {
if v == 0 {
continue
}
for bit := 0; bit < 8; bit++ {
if v&(0x80>>bit) == 0 {
continue
}
if masks.Hit(bx*BlockCells+colB*8+bit, by*BlockCells+row) {
n++
}
}
}
}
if n > 0 {
cells += n
areaM2 += float64(n) * cellAreaM2(bx, by)
}
}
}
}
return cells, areaM2
}
// MaskedStats 在读锁内统计被遮罩盖住的部分。调用方本来拿不到块表(它不导出),
// 而走 All() 会为一次统计复制并排序整张块表(上万块),所以这里直接在索引上算。
func (ix *Index) MaskedStats(masks *MaskSet) (cells int, areaM2 float64) {
if masks.Empty() {
return 0, 0
}
ix.mu.RLock()
defer ix.mu.RUnlock()
return maskedStats(ix.blocks, masks)
}
// MaskFingerprint 返回一组遮罩的短指纹,供前端把它拼进迷雾瓦片的 URL。
//
// 遮罩不在 fogIndex 里(每次请求都实时读库),所以服务端改完立刻生效;
// 但访客的瓦片是允许公共缓存的,URL 不变的话浏览器 / CDN 会继续拿缓存里那份
// (最长 1 小时)——设完遮罩看不到变化,很容易被当成「遮罩不生效」。
// 指纹变了 URL 就变,缓存自然失效。
func MaskFingerprint(masks []Mask) string {
if len(masks) == 0 {
return "0"
}
h := fnv.New64a()
for _, m := range masks {
fmt.Fprintf(h, "%d|%.6f|%.6f|%.1f|", m.ID, m.Lat, m.Lon, m.RadiusM)
}
return strconv.FormatUint(h.Sum64(), 16)
}