301 lines
8.2 KiB
Go
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()
|
|
}
|