备份与迷雾的差异分析、合并执行,附属数据改挂坐标键

备份(.rondbackup)与迷雾(.fwss)各加一层「先看差异再动手」:

- 迷雾:上传先出差异页,说清叠加能补多少、整体替换会丢多少,并在真实底图上
  用绿/灰/红标出会新增 / 不变 / 会丢的区域
- 备份:与当前数据集按 UUID 逐条比对(rond 每次导出都会重排 Z_PK,编号认不出人),
  新增合入、冲突逐条选、仅站内有逐条勾选删除,全在一个事务里执行;
  合入的行写回 entity_raw,保证再传同一份备份时差异归零
- 地点按坐标对齐:站点的「补全地址」会把地点改名,拿名称当键会让一整个地点的
  到访全变成假冲突
- 附属数据(备注/结论/访客票/图片)从 dataset_id+src_pk 换成坐标键,换数据集不再失联;
  访客票没有 dataset_id,回填按活跃数据集认领
- 修正 contentHash 拿 Z_PK 当指纹的隐患,以及两处「src_pk 跨快照稳定」的错误注释

导出格式(.rondbackup / .fwss)保持原样,往返测试通过。
This commit is contained in:
tamakyi
2026-09-19 16:04:00 +08:00
parent 9e01923244
commit 30c502ce7b
43 changed files with 5968 additions and 170 deletions
+200
View File
@@ -0,0 +1,200 @@
package fog
import (
"image"
"math"
)
// 差异图的三种归属。改这里的色值要同步 templates/admin/fog-preview.html 的图例。
var (
DiffBothColor = [4]byte{126, 134, 146, 148} // 两边都有:导入不影响
DiffGainColor = [4]byte{36, 164, 90, 205} // 只有快照有:叠加会新增
DiffLostColor = [4]byte{214, 56, 56, 210} // 只有库里有:整体替换会丢
)
const (
diffNone uint8 = iota
diffBoth
diffGain
diffLost
)
// RenderDiffTile 渲染一张 256x256 的差异瓦片,供站长在真实底图上核对
// 「这份快照能补上哪里」与「整体替换会丢掉哪里」。
//
// 与 RenderTile 一样按底图坐标系做 WGS84→GCJ02 纠偏;不做描边——三种颜色
// 已经够区分,再描边在粗缩放级下会糊成一片。
//
// 逐格判定只在放大到「一格不小于一像素」(z>=14)时做;更粗的级别下一个块还不到
// 一个像素,按块内占多数的类别整块涂色。绘制顺序 both→gain→lost,让「会新增」
// 和「会丢」压住底色,重叠处不至于被灰吃掉。
func RenderDiffTile(cur, inc map[[2]int][]byte, z, x, y int, crs string) *image.NRGBA {
if z < 0 || z > 22 {
return nil
}
step := WorldCells >> z
scale := 256.0 / float64(step)
ox, oy := float64(x*step), float64(y*step)
layer := make([]uint8, 256*256)
mark := func(px, py, w, h int, kind uint8) {
for r := 0; r < h; r++ {
yy := py + r
if yy < 0 || yy > 255 {
continue
}
row := yy * 256
for c := 0; c < w; c++ {
xx := px + c
if xx < 0 || xx > 255 {
continue
}
layer[row+xx] = kind
}
}
}
// 直接遍历块表而不是全球块范围:低缩放级下范围循环会膨胀到数十亿次空查询。
// GCJ 偏移最多约 700m(74 格),粗裁剪时两侧各放宽两块保证不漏。
const margin = 2 * BlockCells
lo, hi := float64(x*step-margin), float64((x+1)*step+margin)
rlo, rhi := float64(y*step-margin), float64((y+1)*step+margin)
shift := crs == "gcj02"
draw := func(pass uint8, gbx, gby int, cb, ib []byte) {
bx, by := float64(gbx*BlockCells), float64(gby*BlockCells)
if bx+BlockCells <= lo || bx >= hi || by+BlockCells <= rlo || by >= rhi {
return
}
ddx, ddy := 0.0, 0.0
if shift {
ddx, ddy = blockShift(gbx, gby)
}
u0 := (bx + ddx - ox) * scale
v0 := (by + ddy - oy) * scale
if scale >= 1 {
w := int(math.Ceil(scale))
for row := 0; row < BlockCells; row++ {
base := row * 8
for colB := 0; colB < 8; colB++ {
var cbv, ibv byte
if cb != nil {
cbv = cb[base+colB]
}
if ib != nil {
ibv = ib[base+colB]
}
if cbv|ibv == 0 {
continue
}
py := int(math.Floor(v0 + float64(row)*scale))
for bit := 0; bit < 8; bit++ {
m := byte(0x80 >> bit)
kind := cellKind(cbv&m != 0, ibv&m != 0)
if kind != pass {
continue
}
px := int(math.Floor(u0 + float64(colB*8+bit)*scale))
mark(px, py, w, w, kind)
}
}
}
return
}
// 一格不足一像素:整块按多数类涂,三种混在一起的块以数量最多的为准
if dominantKind(blockDiffCounts(cb, ib)) != pass {
return
}
u1 := (bx + BlockCells + ddx - ox) * scale
v1 := (by + BlockCells + ddy - oy) * scale
px0, py0 := int(math.Floor(u0)), int(math.Floor(v0))
px1, py1 := int(math.Ceil(u1)), int(math.Ceil(v1))
if px1-px0 < 1 {
px1 = px0 + 1
}
if py1-py0 < 1 {
py1 = py0 + 1
}
mark(px0, py0, px1-px0, py1-py0, pass)
}
for _, pass := range []uint8{diffBoth, diffGain, diffLost} {
for key, cb := range cur {
draw(pass, key[0], key[1], cb, inc[key])
}
// 只在快照里的块单独走一遍,两边都有的块不再重复画
for key, ib := range inc {
if _, dup := cur[key]; dup {
continue
}
draw(pass, key[0], key[1], nil, ib)
}
}
img := image.NewNRGBA(image.Rect(0, 0, 256, 256))
for i, kind := range layer {
if kind == diffNone {
continue
}
var c [4]byte
switch kind {
case diffBoth:
c = DiffBothColor
case diffGain:
c = DiffGainColor
default:
c = DiffLostColor
}
o := i * 4
img.Pix[o], img.Pix[o+1], img.Pix[o+2], img.Pix[o+3] = c[0], c[1], c[2], c[3]
}
return img
}
func cellKind(inCur, inInc bool) uint8 {
switch {
case inCur && inInc:
return diffBoth
case inInc:
return diffGain
case inCur:
return diffLost
}
return diffNone
}
// blockDiffCounts 数一个块里三类各占多少格。两个位图都可能缺失(只有一边有这个块)。
func blockDiffCounts(cb, ib []byte) (both, gain, lost int) {
n := len(cb)
if len(ib) > n {
n = len(ib)
}
for i := 0; i < n; i++ {
var c, in byte
if i < len(cb) {
c = cb[i]
}
if i < len(ib) {
in = ib[i]
}
both += int(popcount[c&in])
gain += int(popcount[in&^c])
lost += int(popcount[c&^in])
}
return
}
// dominantKind 取三类里占得最多的一类,用来在粗缩放级下决定整块的着色。
func dominantKind(both, gain, lost int) uint8 {
switch {
case gain > 0 && gain >= both && gain >= lost:
return diffGain
case lost > 0 && lost >= both:
return diffLost
case both > 0:
return diffBoth
}
return diffNone
}