Add configuration files and update project documentation
- Introduced .editorconfig for consistent coding styles across the project. - Added .golangci.yml for Go linting configuration. - Updated AGENTS.md to clarify project structure and components. - Enhanced CONTRIBUTING.md with Makefile usage for common tasks. - Updated Dockerfiles to use Go 1.24 and improved build instructions. - Refined README.md and deployment documentation for clarity. - Added testing documentation in testing.md for backend and frontend tests. - Introduced Makefile for streamlined development commands and tasks.
This commit is contained in:
162
internal/app/handlers/tile.go
Normal file
162
internal/app/handlers/tile.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app"
|
||||
"github.com/andyleap/hnh-map/internal/app/services"
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
var tileRegex = regexp.MustCompile(`([0-9]+)/([0-9]+)/([-0-9]+)_([-0-9]+)\.png`)
|
||||
|
||||
// WatchGridUpdates is the SSE endpoint for tile updates.
|
||||
func (h *Handlers) WatchGridUpdates(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
s := h.Auth.GetSession(ctx, req)
|
||||
if !h.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 {
|
||||
JSONError(rw, http.StatusInternalServerError, "streaming unsupported", "INTERNAL_ERROR")
|
||||
return
|
||||
}
|
||||
|
||||
c := h.Map.WatchTiles()
|
||||
mc := h.Map.WatchMerges()
|
||||
|
||||
tileCache := h.Map.GetAllTileCache(ctx)
|
||||
|
||||
raw, _ := json.Marshal(tileCache)
|
||||
fmt.Fprint(rw, "data: ")
|
||||
rw.Write(raw)
|
||||
fmt.Fprint(rw, "\n\n")
|
||||
tileCache = tileCache[:0]
|
||||
flusher.Flush()
|
||||
|
||||
ticker := time.NewTicker(app.SSETickInterval)
|
||||
defer ticker.Stop()
|
||||
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, services.TileCache{
|
||||
M: e.MapID,
|
||||
X: e.Coord.X,
|
||||
Y: e.Coord.Y,
|
||||
Z: e.Zoom,
|
||||
T: int(e.Cache),
|
||||
})
|
||||
}
|
||||
case e, ok := <-mc:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
raw, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
slog.Error("failed to marshal merge event", "error", err)
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GridTile serves tile images.
|
||||
func (h *Handlers) GridTile(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
s := h.Auth.GetSession(ctx, req)
|
||||
if !h.canAccessMap(s) {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
tile := tileRegex.FindStringSubmatch(req.URL.Path)
|
||||
if tile == nil || len(tile) < 5 {
|
||||
JSONError(rw, http.StatusBadRequest, "invalid path", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
mapid, err := strconv.Atoi(tile[1])
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "request parsing error", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
z, err := strconv.Atoi(tile[2])
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "request parsing error", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
x, err := strconv.Atoi(tile[3])
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "request parsing error", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
y, err := strconv.Atoi(tile[4])
|
||||
if err != nil {
|
||||
JSONError(rw, http.StatusBadRequest, "request parsing error", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
storageZ := z
|
||||
if storageZ == 6 {
|
||||
storageZ = 0
|
||||
}
|
||||
if storageZ < 0 || storageZ > app.MaxZoomLevel {
|
||||
storageZ = 0
|
||||
}
|
||||
td := h.Map.GetTile(ctx, mapid, app.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(h.Map.GridStorage(), td.File))
|
||||
}
|
||||
Reference in New Issue
Block a user