chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,183 @@
package files
import (
"bytes"
"image"
"image/color"
"image/png"
"math"
"go.uber.org/zap"
)
// 本文件实现 geo 消息地图缩略图(upload.getWebFile / inputWebFileGeoPointLocation)的
// 本地占位渲染:服务端按坐标确定性合成一张「街区网格 + 定位针」风格的静态图,
// 同一 (lat,long,zoom,w,h,scale) 输入字节级可重现,保证客户端分片续传一致。
// 配置了 Mapbox 代理(maptile_proxy.go)时优先返回真实地图,本渲染降级为故障回退。
const (
mapTileMinEdge = 16
mapTileMaxEdge = 1024
mapTileMinZoom = 13
mapTileMaxZoom = 20
mapTileMaxScale = 3
)
// GeoMapTile 返回一张 w×h(逻辑像素,输出按 scale 放大)的静态地图与 mime。
// 入参越界时按协议约束 clamp(w/h 16-1024、zoom 13-20、scale 1-3),不报错。
// 配置了 Mapbox 代理时优先真实地图(落盘缓存),抓取失败回退确定性占位渲染。
func (s *Service) GeoMapTile(lat, long float64, w, h, zoom, scale int) ([]byte, string) {
w = clampInt(w, mapTileMinEdge, mapTileMaxEdge)
h = clampInt(h, mapTileMinEdge, mapTileMaxEdge)
zoom = clampInt(zoom, mapTileMinZoom, mapTileMaxZoom)
scale = clampInt(scale, 1, mapTileMaxScale)
// nil receiver 合法:占位渲染是纯函数(既有调用方/测试依赖这一点),代理仅在配置后启用。
if s != nil && s.mapTiles != nil {
if data, mime, err := s.mapTiles.tile(lat, long, w, h, zoom, scale); err == nil {
return data, mime
} else if s.log != nil {
s.log.Warn("map tile proxy failed, fallback to placeholder",
zap.Error(err), zap.Float64("lat", lat), zap.Float64("long", long), zap.Int("zoom", zoom))
}
}
pw, ph := w*scale, h*scale
img := image.NewRGBA(image.Rect(0, 0, pw, ph))
background := color.RGBA{R: 0xEB, G: 0xE7, B: 0xDE, A: 0xFF}
park := color.RGBA{R: 0xCF, G: 0xE4, B: 0xC2, A: 0xFF}
road := color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}
roadEdge := color.RGBA{R: 0xD9, G: 0xD3, B: 0xC7, A: 0xFF}
fillRect(img, 0, 0, pw, ph, background)
rng := newMapTileRNG(lat, long, zoom)
// 街区网格:间距与抖动由坐标种子决定,平移随经纬度连续变化,避免所有地点一张脸。
spacing := (48 + int(rng.next()%32)) * scale
offX := int(math.Abs(long*1e4)) % spacing
offY := int(math.Abs(lat*1e4)) % spacing
for x := -offX; x < pw; x += spacing {
major := ((x+offX)/spacing)%3 == int(rng.next()%3)
drawVerticalRoad(img, x+int(rng.next()%uint64(spacing/3)), pw, ph, scale, major, road, roadEdge)
}
for y := -offY; y < ph; y += spacing {
major := ((y+offY)/spacing)%3 == int(rng.next()%3)
drawHorizontalRoad(img, y+int(rng.next()%uint64(spacing/3)), pw, ph, scale, major, road, roadEdge)
}
// 两块「绿地」:取网格内随机街块,铺底色之上、道路之下的视觉层级太复杂,
// 这里直接半覆盖即可(占位图不追求制图精度)。
for i := 0; i < 2; i++ {
bx := int(rng.next() % uint64(pw))
by := int(rng.next() % uint64(ph))
bw := (spacing * 3) / 4
fillRect(img, bx, by, minInt(bx+bw, pw), minInt(by+bw, ph), park)
}
drawCenterPin(img, pw, ph, scale)
var buf bytes.Buffer
_ = png.Encode(&buf, img)
return buf.Bytes(), "image/png"
}
// mapTileRNG 是确定性 xorshift64,种子来自量化坐标与 zoom。
type mapTileRNG struct{ state uint64 }
func newMapTileRNG(lat, long float64, zoom int) *mapTileRNG {
seed := uint64(int64(lat*1e5))*1000003 ^ uint64(int64(long*1e5))*998244353 ^ uint64(zoom)*0x9E3779B97F4A7C15
if seed == 0 {
seed = 0x9E3779B97F4A7C15
}
return &mapTileRNG{state: seed}
}
func (r *mapTileRNG) next() uint64 {
r.state ^= r.state << 13
r.state ^= r.state >> 7
r.state ^= r.state << 17
return r.state
}
func fillRect(img *image.RGBA, x0, y0, x1, y1 int, c color.RGBA) {
bounds := img.Bounds()
x0, y0 = maxInt(x0, bounds.Min.X), maxInt(y0, bounds.Min.Y)
x1, y1 = minInt(x1, bounds.Max.X), minInt(y1, bounds.Max.Y)
for y := y0; y < y1; y++ {
for x := x0; x < x1; x++ {
img.SetRGBA(x, y, c)
}
}
}
func drawVerticalRoad(img *image.RGBA, x, pw, ph, scale int, major bool, road, edge color.RGBA) {
width := 2 * scale
if major {
width = 4 * scale
}
fillRect(img, x-width/2-1, 0, x+width/2+1, ph, edge)
fillRect(img, x-width/2, 0, x+width/2, ph, road)
}
func drawHorizontalRoad(img *image.RGBA, y, pw, ph, scale int, major bool, road, edge color.RGBA) {
width := 2 * scale
if major {
width = 4 * scale
}
fillRect(img, 0, y-width/2-1, pw, y+width/2+1, edge)
fillRect(img, 0, y-width/2, pw, y+width/2, road)
}
// drawCenterPin 在图中心画红色定位针(圆头 + 下尖三角 + 白色内点),中心即坐标点。
func drawCenterPin(img *image.RGBA, pw, ph, scale int) {
pin := color.RGBA{R: 0xE5, G: 0x39, B: 0x35, A: 0xFF}
pinDark := color.RGBA{R: 0xB7, G: 0x1C, B: 0x1C, A: 0xFF}
white := color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}
cx, cy := pw/2, ph/2
headR := 9 * scale
headCY := cy - 14*scale
// 尖角三角形:从圆头两侧收敛到坐标点。
for y := headCY; y <= cy; y++ {
t := float64(y-headCY) / float64(cy-headCY)
half := int(float64(headR) * (1 - t) * 0.82)
for x := cx - half; x <= cx+half; x++ {
img.SetRGBA(x, y, pin)
}
}
// 圆头(带一圈深色描边)。
for dy := -headR - scale; dy <= headR+scale; dy++ {
for dx := -headR - scale; dx <= headR+scale; dx++ {
d2 := dx*dx + dy*dy
switch {
case d2 <= headR*headR:
img.SetRGBA(cx+dx, headCY+dy, pin)
case d2 <= (headR+scale)*(headR+scale):
img.SetRGBA(cx+dx, headCY+dy, pinDark)
}
}
}
innerR := 3 * scale
for dy := -innerR; dy <= innerR; dy++ {
for dx := -innerR; dx <= innerR; dx++ {
if dx*dx+dy*dy <= innerR*innerR {
img.SetRGBA(cx+dx, headCY+dy, white)
}
}
}
}
func clampInt(v, lo, hi int) int {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}