Add initial project structure with backend and frontend setup
- Created backend structure with Go, including main application logic and API endpoints. - Added Docker support for both development and production environments. - Introduced frontend using Nuxt 3 with Tailwind CSS for styling. - Included configuration files for Docker and environment variables. - Established basic documentation for contributing, development, and deployment processes. - Set up .gitignore and .dockerignore files to manage ignored files in the repository.
This commit is contained in:
269
internal/app/tile.go
Normal file
269
internal/app/tile.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var transparentPNG = []byte{
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
|
||||
0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41,
|
||||
0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
|
||||
0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00,
|
||||
0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
|
||||
0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
type TileData struct {
|
||||
MapID int
|
||||
Coord Coord
|
||||
Zoom int
|
||||
File string
|
||||
Cache int64
|
||||
}
|
||||
|
||||
func (a *App) GetTile(mapid int, c Coord, z int) (td *TileData) {
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
tiles := tx.Bucket([]byte("tiles"))
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
mapb := tiles.Bucket([]byte(strconv.Itoa(mapid)))
|
||||
if mapb == nil {
|
||||
return nil
|
||||
}
|
||||
zoom := mapb.Bucket([]byte(strconv.Itoa(z)))
|
||||
if zoom == nil {
|
||||
return nil
|
||||
}
|
||||
tileraw := zoom.Get([]byte(c.Name()))
|
||||
if tileraw == nil {
|
||||
return nil
|
||||
}
|
||||
json.Unmarshal(tileraw, &td)
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (a *App) SaveTile(mapid int, c Coord, z int, f string, t int64) {
|
||||
a.db.Update(func(tx *bbolt.Tx) error {
|
||||
tiles, err := tx.CreateBucketIfNotExists([]byte("tiles"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mapb, err := tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(mapid)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zoom, err := mapb.CreateBucketIfNotExists([]byte(strconv.Itoa(z)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
td := &TileData{
|
||||
MapID: mapid,
|
||||
Coord: c,
|
||||
Zoom: z,
|
||||
File: f,
|
||||
Cache: t,
|
||||
}
|
||||
raw, err := json.Marshal(td)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.gridUpdates.send(td)
|
||||
return zoom.Put([]byte(c.Name()), raw)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (a *App) reportMerge(from, to int, shift Coord) {
|
||||
a.mergeUpdates.send(&Merge{
|
||||
From: from,
|
||||
To: to,
|
||||
Shift: shift,
|
||||
})
|
||||
}
|
||||
|
||||
type TileCache struct {
|
||||
M, X, Y, Z, T int
|
||||
}
|
||||
|
||||
func (a *App) watchGridUpdates(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if !a.canAccessMap(s) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "text/event-stream")
|
||||
rw.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
rw.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
flusher, ok := rw.(http.Flusher)
|
||||
|
||||
if !ok {
|
||||
http.Error(rw, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c := make(chan *TileData, 1000)
|
||||
mc := make(chan *Merge, 5)
|
||||
|
||||
a.gridUpdates.watch(c)
|
||||
a.mergeUpdates.watch(mc)
|
||||
|
||||
tileCache := make([]TileCache, 0, 100)
|
||||
|
||||
a.db.View(func(tx *bbolt.Tx) error {
|
||||
tiles := tx.Bucket([]byte("tiles"))
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
return tiles.ForEach(func(mk, mv []byte) error {
|
||||
mapb := tiles.Bucket(mk)
|
||||
if mapb == nil {
|
||||
return nil
|
||||
}
|
||||
return mapb.ForEach(func(k, v []byte) error {
|
||||
zoom := mapb.Bucket(k)
|
||||
if zoom == nil {
|
||||
return nil
|
||||
}
|
||||
return zoom.ForEach(func(tk, tv []byte) error {
|
||||
td := TileData{}
|
||||
json.Unmarshal(tv, &td)
|
||||
tileCache = append(tileCache, TileCache{
|
||||
M: td.MapID,
|
||||
X: td.Coord.X,
|
||||
Y: td.Coord.Y,
|
||||
Z: td.Zoom,
|
||||
T: int(td.Cache),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
raw, _ := json.Marshal(tileCache)
|
||||
fmt.Fprint(rw, "data: ")
|
||||
rw.Write(raw)
|
||||
fmt.Fprint(rw, "\n\n")
|
||||
tileCache = tileCache[:0]
|
||||
flusher.Flush()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case e, ok := <-c:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for i := range tileCache {
|
||||
if tileCache[i].M == e.MapID && tileCache[i].X == e.Coord.X && tileCache[i].Y == e.Coord.Y && tileCache[i].Z == e.Zoom {
|
||||
tileCache[i].T = int(e.Cache)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
tileCache = append(tileCache, TileCache{
|
||||
M: e.MapID,
|
||||
X: e.Coord.X,
|
||||
Y: e.Coord.Y,
|
||||
Z: e.Zoom,
|
||||
T: int(e.Cache),
|
||||
})
|
||||
}
|
||||
case e, ok := <-mc:
|
||||
log.Println(e, ok)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
raw, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
log.Println(string(raw))
|
||||
fmt.Fprint(rw, "event: merge\n")
|
||||
fmt.Fprint(rw, "data: ")
|
||||
rw.Write(raw)
|
||||
fmt.Fprint(rw, "\n\n")
|
||||
flusher.Flush()
|
||||
case <-ticker.C:
|
||||
raw, _ := json.Marshal(tileCache)
|
||||
fmt.Fprint(rw, "data: ")
|
||||
rw.Write(raw)
|
||||
fmt.Fprint(rw, "\n\n")
|
||||
tileCache = tileCache[:0]
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tileRegex = regexp.MustCompile("([0-9]+)/([0-9]+)/([-0-9]+)_([-0-9]+).png")
|
||||
|
||||
func (a *App) gridTile(rw http.ResponseWriter, req *http.Request) {
|
||||
s := a.getSession(req)
|
||||
if !a.canAccessMap(s) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
tile := tileRegex.FindStringSubmatch(req.URL.Path)
|
||||
if tile == nil || len(tile) < 5 {
|
||||
http.Error(rw, "invalid path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mapid, err := strconv.Atoi(tile[1])
|
||||
if err != nil {
|
||||
http.Error(rw, "request parsing error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
z, err := strconv.Atoi(tile[2])
|
||||
if err != nil {
|
||||
http.Error(rw, "request parsing error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
x, err := strconv.Atoi(tile[3])
|
||||
if err != nil {
|
||||
http.Error(rw, "request parsing error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
y, err := strconv.Atoi(tile[4])
|
||||
if err != nil {
|
||||
http.Error(rw, "request parsing error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Map frontend Leaflet zoom (1…6) to storage level (0…5): z=6 → 0 (max detail), z=1..5 → same
|
||||
storageZ := z
|
||||
if storageZ == 6 {
|
||||
storageZ = 0
|
||||
}
|
||||
if storageZ < 0 || storageZ > 5 {
|
||||
storageZ = 0
|
||||
}
|
||||
td := a.GetTile(mapid, Coord{X: x, Y: y}, storageZ)
|
||||
if td == nil {
|
||||
rw.Header().Set("Content-Type", "image/png")
|
||||
rw.Header().Set("Cache-Control", "private, max-age=3600")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write(transparentPNG)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "image/png")
|
||||
rw.Header().Set("Cache-Control", "private immutable")
|
||||
http.ServeFile(rw, req, filepath.Join(a.gridStorage, td.File))
|
||||
}
|
||||
Reference in New Issue
Block a user