first release
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user