Files
2026-09-18 13:05:49 +08:00

301 lines
8.2 KiB
Go

package fog
import (
"database/sql"
"errors"
"fmt"
"sync"
"time"
)
// Index 是某个用户迷雾位图的内存索引,按块粒度持有,
// 供瓦片渲染做范围查询;写路径先落库再刷新内存。
type Index struct {
mu sync.RWMutex
blocks map[[2]int][]byte
cells int
// areaM2 是全部置位格的实地面积(m²),写入索引时一并算好。
// 覆盖率统计每次都要这个数,现算得把上万块的位图全扫一遍。
areaM2 float64
ver int64 // 最近一次更新的 unix 秒,用作瓦片缓存版本号
}
// NewIndex 建一个空索引。
func NewIndex() *Index {
return &Index{blocks: map[[2]int][]byte{}}
}
// Load 从库里加载某用户的全部迷雾块。
func (ix *Index) Load(db *sql.DB, userID int64) error {
rows, err := db.Query(`SELECT gbx, gby, bitmap FROM fog_blocks WHERE user_id=$1`, userID)
if err != nil {
return err
}
defer rows.Close()
blocks := map[[2]int][]byte{}
cells, areaM2 := 0, 0.0
for rows.Next() {
var gx, gy int
var bm []byte
if err := rows.Scan(&gx, &gy, &bm); err != nil {
return err
}
blocks[[2]int{gx, gy}] = bm
n := popcountSum(bm)
cells += n
areaM2 += float64(n) * cellAreaM2(gx, gy)
}
if err := rows.Err(); err != nil {
return err
}
ix.mu.Lock()
ix.blocks = blocks
ix.cells = cells
ix.areaM2 = areaM2
ix.ver = time.Now().Unix()
ix.mu.Unlock()
return nil
}
// LoadAll 从库里加载全部迷雾块(站点为单用户展示,公开读路径不区分归属)。
func (ix *Index) LoadAll(db *sql.DB) error {
rows, err := db.Query(`SELECT gbx, gby, bitmap FROM fog_blocks`)
if err != nil {
return err
}
defer rows.Close()
blocks := map[[2]int][]byte{}
cells, areaM2 := 0, 0.0
for rows.Next() {
var gx, gy int
var bm []byte
if err := rows.Scan(&gx, &gy, &bm); err != nil {
return err
}
blocks[[2]int{gx, gy}] = bm
n := popcountSum(bm)
cells += n
areaM2 += float64(n) * cellAreaM2(gx, gy)
}
if err := rows.Err(); err != nil {
return err
}
ix.mu.Lock()
ix.blocks = blocks
ix.cells = cells
ix.areaM2 = areaM2
ix.ver = time.Now().Unix()
ix.mu.Unlock()
return nil
}
// Stats 返回块数与格子数。
func (ix *Index) Stats() (blocks, cells int) {
ix.mu.RLock()
defer ix.mu.RUnlock()
return len(ix.blocks), ix.cells
}
// AreaKm2 返回已探索面积(km²,按块中心纬度做 cos 修正)。写入索引时算好,这里只读。
func (ix *Index) AreaKm2() float64 {
ix.mu.RLock()
defer ix.mu.RUnlock()
return ix.areaM2 / 1e6
}
// Version 返回数据版本号,随每次写入变化。
func (ix *Index) Version() int64 {
ix.mu.RLock()
defer ix.mu.RUnlock()
return ix.ver
}
// View 在读锁内执行渲染查询,避免渲染中途被写换代。
func (ix *Index) View(fn func(blocks map[[2]int][]byte)) {
ix.mu.RLock()
defer ix.mu.RUnlock()
fn(ix.blocks)
}
// Import 把解析出的块写入库:merge 模式按位或合并进已有数据,
// replace 模式先清空该用户全部迷雾再写入。完成后刷新内存索引。
func (ix *Index) Import(db *sql.DB, userID int64, blocks []Block, merge bool) (applied int, err error) {
tx, err := db.Begin()
if err != nil {
return 0, err
}
defer tx.Rollback()
if !merge {
if _, err := tx.Exec(`DELETE FROM fog_blocks WHERE user_id=$1`, userID); err != nil {
return 0, err
}
}
var upsert *sql.Stmt
if upsert, err = tx.Prepare(`INSERT INTO fog_blocks (user_id, gbx, gby, bitmap, updated_at)
VALUES ($1,$2,$3,$4,now())
ON CONFLICT (user_id, gbx, gby) DO UPDATE SET bitmap=$4, updated_at=now()`); err != nil {
return 0, err
}
defer upsert.Close()
for _, b := range blocks {
if merge {
var old []byte
err := tx.QueryRow(`SELECT bitmap FROM fog_blocks WHERE user_id=$1 AND gbx=$2 AND gby=$3`,
userID, b.GX, b.GY).Scan(&old)
switch {
case err == nil && len(old) == len(b.Bitmap):
for i := range b.Bitmap {
b.Bitmap[i] |= old[i]
}
case err != nil && !errors.Is(err, sql.ErrNoRows):
return 0, err
}
}
if _, err := upsert.Exec(userID, b.GX, b.GY, b.Bitmap); err != nil {
return 0, err
}
applied++
}
if err := tx.Commit(); err != nil {
return 0, err
}
if err := ix.Load(db, userID); err != nil {
return applied, err
}
return applied, nil
}
// CellLit 报告某个全局格是否已探索。
func (ix *Index) CellLit(cx, cy int) bool {
ix.mu.RLock()
defer ix.mu.RUnlock()
bm, ok := ix.blocks[[2]int{cx / BlockCells, cy / BlockCells}]
if !ok {
return false
}
col, row := cx%BlockCells, cy%BlockCells
return bm[row*8+col/8]&(0x80>>(col%8)) != 0
}
// All 返回全部块(导出用)。
func (ix *Index) All() []Block {
ix.mu.RLock()
defer ix.mu.RUnlock()
out := make([]Block, 0, len(ix.blocks))
for k, bm := range ix.blocks {
out = append(out, Block{GX: k[0], GY: k[1], Bitmap: bm})
}
sortBlocks(out)
return out
}
// LoadOrNewIndex 返回某用户的迷雾索引;首次访问时从库里加载。
func LoadOrNewIndex(db *sql.DB, userID int64, existing *Index) (*Index, error) {
if existing != nil {
return existing, nil
}
ix := NewIndex()
if err := ix.Load(db, userID); err != nil {
return nil, fmt.Errorf("加载迷雾数据: %w", err)
}
return ix, nil
}
// SaveRawLayers 保存快照里其它图层的原始条目(path -> zlib 流本身,一个字节都不动)。
// replace 为真时先清空(「整体替换」与「清空迷雾」都该把旧的一并带走),
// 否则同名覆盖——后传的快照更完整。
func (ix *Index) SaveRawLayers(db *sql.DB, userID int64, layers map[string][]byte, replace bool) error {
if !replace && len(layers) == 0 {
return nil
}
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if replace {
if _, err := tx.Exec(`DELETE FROM fog_raw_files WHERE user_id=$1`, userID); err != nil {
return err
}
}
for p, data := range layers {
if _, err := tx.Exec(`INSERT INTO fog_raw_files (user_id, path, data) VALUES ($1,$2,$3)
ON CONFLICT (user_id, path) DO UPDATE SET data=EXCLUDED.data`, userID, p, data); err != nil {
return err
}
}
return tx.Commit()
}
// DropRawLayers 丢弃留存的其它图层。清空迷雾时调用(与 fog_blocks 一样是站点级,
// 不按用户区分),否则导出的快照里会留着已经没有对应 Model/* 瓦片的图层条目。
func DropRawLayers(db *sql.DB) error {
_, err := db.Exec(`DELETE FROM fog_raw_files`)
return err
}
// RawLayers 读出留存的非已探索层文件(path -> 原始字节)。
func LoadRawLayers(db *sql.DB, userID int64) (map[string][]byte, error) {
rows, err := db.Query(`SELECT path, data FROM fog_raw_files WHERE user_id=$1`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string][]byte{}
for rows.Next() {
var p string
var data []byte
if err := rows.Scan(&p, &data); err != nil {
return nil, err
}
out[p] = data
}
return out, rows.Err()
}
// ---------- 迷雾遮罩区(对访客隐藏的范围) ----------
// SaveMask 新增或更新一块遮罩(ID 为 0 时新增),返回其 ID。
func SaveMask(db *sql.DB, userID int64, m Mask) (int64, error) {
if m.ID > 0 {
_, err := db.Exec(`UPDATE fog_masks SET name=$1, lat=$2, lon=$3, radius_m=$4 WHERE id=$5 AND user_id=$6`,
m.Name, m.Lat, m.Lon, m.RadiusM, m.ID, userID)
if err != nil {
return 0, err
}
return m.ID, nil
}
var id int64
err := db.QueryRow(`INSERT INTO fog_masks (user_id, name, lat, lon, radius_m) VALUES ($1,$2,$3,$4,$5)
RETURNING id`, userID, m.Name, m.Lat, m.Lon, m.RadiusM).Scan(&id)
return id, err
}
// DeleteMask 删除一块遮罩。
func DeleteMask(db *sql.DB, userID, id int64) error {
_, err := db.Exec(`DELETE FROM fog_masks WHERE id=$1 AND user_id=$2`, id, userID)
return err
}
// LoadMasks 读出全部遮罩。遮罩与迷雾一样是站点级数据(单站长的站点),
// 展示与渲染时不分归属,与 fog_blocks 的 LoadAll 保持一致。
func LoadMasks(db *sql.DB) ([]Mask, error) {
rows, err := db.Query(`SELECT id, COALESCE(name,''), lat, lon, radius_m FROM fog_masks
WHERE radius_m > 0 ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Mask
for rows.Next() {
var m Mask
if err := rows.Scan(&m.ID, &m.Name, &m.Lat, &m.Lon, &m.RadiusM); err != nil {
return nil, err
}
out = append(out, m)
}
return out, rows.Err()
}